aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrandon Eng <bce25@cornell.edu>2024-01-17 21:42:00 +0700
committerGitHub <noreply@github.com>2024-01-17 09:42:00 -0500
commitff6b84ff8818ee96f1d6cf479f72b2127eddec43 (patch)
treed9d4c3a6293f5844308774fe8f94bd5c09561187
parent6b6d58e95ec018c695e9d865b8136319ad93e825 (diff)
Group expenses (#48)
* Group expenses my date * Group expenses my date * typescript errors * prettier * getExpenseGroup * update logic to use dayjs * clean up
-rw-r--r--components.json2
-rw-r--r--compose.yaml4
-rw-r--r--package-lock.json6
-rw-r--r--package.json1
-rw-r--r--src/app/groups/[groupId]/expenses/expense-list.tsx177
5 files changed, 127 insertions, 63 deletions
diff --git a/components.json b/components.json
index 48c34e4..fd5076f 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 86a4656..9f28da6 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 bf150ff..fa7d4c9 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 26c8c79..29bb41d 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 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.{' '}