diff --git a/components.json b/components.json index 48c34e4c..fd5076fb 100644 --- a/components.json +++ b/components.json @@ -13,4 +13,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/compose.yaml b/compose.yaml index 86a4656c..9f28da64 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,7 +8,7 @@ services: depends_on: db: condition: service_healthy - + db: image: postgres:latest ports: @@ -18,7 +18,7 @@ services: volumes: - /var/lib/postgresql/data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 5 diff --git a/package-lock.json b/package-lock.json index bf150ffb..fa7d4c99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@tailwindcss/typography": "^0.5.10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "dayjs": "^1.11.10", "lucide-react": "^0.290.0", "nanoid": "^5.0.4", "next": "^14.0.4", @@ -2583,6 +2584,11 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 26c8c791..29bb41d6 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tailwindcss/typography": "^0.5.10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "dayjs": "^1.11.10", "lucide-react": "^0.290.0", "nanoid": "^5.0.4", "next": "^14.0.4", diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx index 92171236..1a361791 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>, +) { + 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) => ( -
{ - router.push(`/groups/${groupId}/expenses/${expense.id}/edit`) - }} - > - -
-
- {expense.title} -
-
- Paid by {getParticipant(expense.paidById)?.name}{' '} - for{' '} - {expense.paidFor.map((paidFor, index) => ( - - {index !== 0 && <>, } - - { - participants.find((p) => p.id === paidFor.participantId) - ?.name - } - - - ))} -
-
-
-
- {currency} {(expense.amount / 100).toFixed(2)} -
-
- {formatDate(expense.expenseDate)} + Object.values(EXPENSE_GROUPS).map((expenseGroup: string) => { + const groupExpenses = groupedExpensesByDate[expenseGroup] + if (!groupExpenses) return null + return ( + +
+ {expenseGroup}
-
- -
- )) + {groupExpenses.map((expense: any) => ( +
{ + router.push(`/groups/${groupId}/expenses/${expense.id}/edit`) + }} + > + +
+
+ {expense.title} +
+
+ Paid by{' '} + {getParticipant(expense.paidById)?.name} for{' '} + {expense.paidFor.map((paidFor: any, index: number) => ( + + {index !== 0 && <>, } + + { + participants.find( + (p) => p.id === paidFor.participantId, + )?.name + } + + + ))} +
+
+
+
+ {currency} {(expense.amount / 100).toFixed(2)} +
+
+ {formatDate(expense.expenseDate)} +
+
+ +
+ ))} + + ) + }) ) : (

Your group doesn’t contain any expense yet.{' '}