diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/groups/[groupId]/expenses/expense-list.tsx | 177 |
1 files changed, 117 insertions, 60 deletions
diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index 9217123..1a36179 100644 --- a/src/app/groups/[groupId]/expenses/expense-list.tsx +++ b/src/app/groups/[groupId]/expenses/expense-list.tsx @@ -3,7 +3,8 @@ import { CategoryIcon } from '@/app/groups/[groupId]/expenses/category-icon' import { Button } from '@/components/ui/button' import { getGroupExpenses } from '@/lib/api' import { cn } from '@/lib/utils' -import { Participant } from '@prisma/client' +import { Expense, Participant } from '@prisma/client' +import dayjs, { type Dayjs } from 'dayjs' import { ChevronRight } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' @@ -16,6 +17,46 @@ type Props = { groupId: string } +const EXPENSE_GROUPS = { + THIS_WEEK: 'This week', + EARLIER_THIS_MONTH: 'Earlier this month', + LAST_MONTH: 'Last month', + EARLIER_THIS_YEAR: 'Earlier this year', + LAST_YEAR: 'Last year', + OLDER: 'Older', +} + +function getExpenseGroup(date: Dayjs, today: Dayjs) { + if (today.isSame(date, 'week')) { + return EXPENSE_GROUPS.THIS_WEEK + } else if (today.isSame(date, 'month')) { + return EXPENSE_GROUPS.EARLIER_THIS_MONTH + } else if (today.subtract(1, 'month').isSame(date, 'month')) { + return EXPENSE_GROUPS.LAST_MONTH + } else if (today.isSame(date, 'year')) { + return EXPENSE_GROUPS.EARLIER_THIS_YEAR + } else if (today.subtract(1, 'year').isSame(date, 'year')) { + return EXPENSE_GROUPS.LAST_YEAR + } else { + return EXPENSE_GROUPS.OLDER + } +} + +function getGroupedExpensesByDate( + expenses: Awaited<ReturnType<typeof getGroupExpenses>>, +) { + const today = dayjs() + return expenses.reduce( + (result: { [key: string]: Expense[] }, expense: Expense) => { + const expenseGroup = getExpenseGroup(dayjs(expense.expenseDate), today) + result[expenseGroup] = result[expenseGroup] ?? [] + result[expenseGroup].push(expense) + return result + }, + {}, + ) +} + export function ExpenseList({ expenses, currency, @@ -44,67 +85,83 @@ export function ExpenseList({ const getParticipant = (id: string) => participants.find((p) => p.id === id) const router = useRouter() + const groupedExpensesByDate = getGroupedExpensesByDate(expenses) + return expenses.length > 0 ? ( - expenses.map((expense) => ( - <div - key={expense.id} - className={cn( - 'border-t flex justify-between px-4 sm:pr-2 sm:pl-6 py-4 text-sm cursor-pointer hover:bg-accent gap-1 items-stretch', - expense.isReimbursement && 'italic', - )} - onClick={() => { - router.push(`/groups/${groupId}/expenses/${expense.id}/edit`) - }} - > - <CategoryIcon - category={expense.category} - className="w-4 h-4 mr-2 mt-0.5 text-muted-foreground" - /> - <div className="flex-1"> - <div className={cn('mb-1', expense.isReimbursement && 'italic')}> - {expense.title} - </div> - <div className="text-xs text-muted-foreground"> - Paid by <strong>{getParticipant(expense.paidById)?.name}</strong>{' '} - for{' '} - {expense.paidFor.map((paidFor, index) => ( - <Fragment key={index}> - {index !== 0 && <>, </>} - <strong> - { - participants.find((p) => p.id === paidFor.participantId) - ?.name - } - </strong> - </Fragment> - ))} - </div> - </div> - <div className="flex flex-col justify-between items-end"> - <div - className={cn( - 'tabular-nums whitespace-nowrap', - expense.isReimbursement ? 'italic' : 'font-bold', - )} - > - {currency} {(expense.amount / 100).toFixed(2)} - </div> - <div className="text-xs text-muted-foreground"> - {formatDate(expense.expenseDate)} + Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => { + const groupExpenses = groupedExpensesByDate[expenseGroup] + if (!groupExpenses) return null + return ( + <Fragment key={expenseGroup}> + <div className={'border-t text-md pl-3 py-2 font-semibold'}> + {expenseGroup} </div> - </div> - <Button - size="icon" - variant="link" - className="self-center hidden sm:flex" - asChild - > - <Link href={`/groups/${groupId}/expenses/${expense.id}/edit`}> - <ChevronRight className="w-4 h-4" /> - </Link> - </Button> - </div> - )) + {groupExpenses.map((expense: any) => ( + <div + key={expense.id} + className={cn( + 'border-t flex justify-between px-4 sm:pr-2 sm:pl-6 py-4 text-sm cursor-pointer hover:bg-accent gap-1 items-stretch', + expense.isReimbursement && 'italic', + )} + onClick={() => { + router.push(`/groups/${groupId}/expenses/${expense.id}/edit`) + }} + > + <CategoryIcon + category={expense.category} + className="w-4 h-4 mr-2 mt-0.5 text-muted-foreground" + /> + <div className="flex-1"> + <div + className={cn('mb-1', expense.isReimbursement && 'italic')} + > + {expense.title} + </div> + <div className="text-xs text-muted-foreground"> + Paid by{' '} + <strong>{getParticipant(expense.paidById)?.name}</strong> for{' '} + {expense.paidFor.map((paidFor: any, index: number) => ( + <Fragment key={index}> + {index !== 0 && <>, </>} + <strong> + { + participants.find( + (p) => p.id === paidFor.participantId, + )?.name + } + </strong> + </Fragment> + ))} + </div> + </div> + <div className="flex flex-col justify-between items-end"> + <div + className={cn( + 'tabular-nums whitespace-nowrap', + expense.isReimbursement ? 'italic' : 'font-bold', + )} + > + {currency} {(expense.amount / 100).toFixed(2)} + </div> + <div className="text-xs text-muted-foreground"> + {formatDate(expense.expenseDate)} + </div> + </div> + <Button + size="icon" + variant="link" + className="self-center hidden sm:flex" + asChild + > + <Link href={`/groups/${groupId}/expenses/${expense.id}/edit`}> + <ChevronRight className="w-4 h-4" /> + </Link> + </Button> + </div> + ))} + </Fragment> + ) + }) ) : ( <p className="px-6 text-sm py-6"> Your group doesn’t contain any expense yet.{' '} |
