From 45ee9cdba4e6e7c9ac918859b89d2b2ed38d4571 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Thu, 11 Jan 2024 21:38:30 +0000 Subject: Assign categories to expenses (#28) * add expense categories * set category to Payment for reimbursements * Insert categories as part of the migration * Display category groups --------- Co-authored-by: Sebastien Castiel --- .../[groupId]/expenses/[expenseId]/edit/page.tsx | 4 +- src/app/groups/[groupId]/expenses/create/page.tsx | 5 +- src/components/expense-form.tsx | 64 +++++++++++++++++++++- src/lib/api.ts | 11 +++- src/lib/schemas.ts | 1 + src/scripts/migrate.ts | 1 + 6 files changed, 78 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx index 188d08f..bb32da0 100644 --- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx +++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx @@ -1,5 +1,5 @@ import { ExpenseForm } from '@/components/expense-form' -import { deleteExpense, getExpense, getGroup, updateExpense } from '@/lib/api' +import { deleteExpense, getExpense, getCategories, getGroup, updateExpense } from '@/lib/api' import { expenseFormSchema } from '@/lib/schemas' import { Metadata } from 'next' import { notFound, redirect } from 'next/navigation' @@ -13,6 +13,7 @@ export default async function EditExpensePage({ }: { params: { groupId: string; expenseId: string } }) { + const categories = await getCategories() const group = await getGroup(groupId) if (!group) notFound() const expense = await getExpense(groupId, expenseId) @@ -35,6 +36,7 @@ export default async function EditExpensePage({ diff --git a/src/app/groups/[groupId]/expenses/create/page.tsx b/src/app/groups/[groupId]/expenses/create/page.tsx index e603e59..405db26 100644 --- a/src/app/groups/[groupId]/expenses/create/page.tsx +++ b/src/app/groups/[groupId]/expenses/create/page.tsx @@ -1,5 +1,5 @@ import { ExpenseForm } from '@/components/expense-form' -import { createExpense, getGroup } from '@/lib/api' +import { createExpense, getGroup, getCategories } from '@/lib/api' import { expenseFormSchema } from '@/lib/schemas' import { Metadata } from 'next' import { notFound, redirect } from 'next/navigation' @@ -13,6 +13,7 @@ export default async function ExpensePage({ }: { params: { groupId: string } }) { + const categories = await getCategories() const group = await getGroup(groupId) if (!group) notFound() @@ -23,5 +24,5 @@ export default async function ExpensePage({ redirect(`/groups/${groupId}`) } - return + return } diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx index d14d72e..d00211a 100644 --- a/src/components/expense-form.tsx +++ b/src/components/expense-form.tsx @@ -28,14 +28,17 @@ import { Input } from '@/components/ui/input' import { Select, SelectContent, + SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' -import { getExpense, getGroup } from '@/lib/api' +import { getCategories, getExpense, getGroup } from '@/lib/api' import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas' import { cn } from '@/lib/utils' import { zodResolver } from '@hookform/resolvers/zod' +import { Category } from '@prisma/client' import { Save, Trash2 } from 'lucide-react' import { useSearchParams } from 'next/navigation' import { useForm } from 'react-hook-form' @@ -44,11 +47,18 @@ import { match } from 'ts-pattern' export type Props = { group: NonNullable>> expense?: NonNullable>> + categories: NonNullable>> onSubmit: (values: ExpenseFormValues) => Promise onDelete?: () => Promise } -export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { +export function ExpenseForm({ + group, + expense, + categories, + onSubmit, + onDelete, +}: Props) { const isCreate = expense === undefined const searchParams = useSearchParams() const getSelectedPayer = (field?: { value: string }) => { @@ -67,6 +77,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { title: expense.title, expenseDate: expense.expenseDate ?? new Date(), amount: String(expense.amount / 100) as unknown as number, // hack + category: expense.categoryId, paidBy: expense.paidById, paidFor: expense.paidFor.map(({ participantId, shares }) => ({ participant: participantId, @@ -82,6 +93,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { amount: String( (Number(searchParams.get('amount')) || 0) / 100, ) as unknown as number, // hack + category: 1, // category with Id 1 is Payment paidBy: searchParams.get('from') ?? undefined, paidFor: [ searchParams.get('to') @@ -95,6 +107,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { title: '', expenseDate: new Date(), amount: 0, + category: 0, // category with Id 0 is General paidFor: [], paidBy: getSelectedPayer(), isReimbursement: false, @@ -102,6 +115,14 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { }, }) + const categoriesByGroup = categories.reduce>( + (acc, category) => ({ + ...acc, + [category.grouping]: [...(acc[category.grouping] ?? []), category], + }), + {}, + ) + return (
onSubmit(values))}> @@ -138,7 +159,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { name="expenseDate" render={({ field }) => ( - Expense Date + Expense date + ( + + Category + + + Select the expense category. + + + + )} + /> +