aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/groups/[groupId]/expenses/expense-list.tsx177
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.{' '}