From 45ee9cdba4e6e7c9ac918859b89d2b2ed38d4571 Mon Sep 17 00:00:00 2001
From: Chris Johnston <christopher_johnston@me.com>
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 <sebastien@castiel.me>
---
 src/components/expense-form.tsx | 64 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 61 insertions(+), 3 deletions(-)

(limited to 'src/components/expense-form.tsx')

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<Awaited<ReturnType<typeof getGroup>>>
   expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
+  categories: NonNullable<Awaited<ReturnType<typeof getCategories>>>
   onSubmit: (values: ExpenseFormValues) => Promise<void>
   onDelete?: () => Promise<void>
 }
 
-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<Record<string, Category[]>>(
+    (acc, category) => ({
+      ...acc,
+      [category.grouping]: [...(acc[category.grouping] ?? []), category],
+    }),
+    {},
+  )
+
   return (
     <Form {...form}>
       <form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
@@ -138,7 +159,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
               name="expenseDate"
               render={({ field }) => (
                 <FormItem className="sm:order-1">
-                  <FormLabel>Expense Date</FormLabel>
+                  <FormLabel>Expense date</FormLabel>
                   <FormControl>
                     <Input
                       className="date-base"
@@ -201,6 +222,43 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
               )}
             />
 
+            <FormField
+              control={form.control}
+              name="category"
+              render={({ field }) => (
+                <FormItem className="order-3 sm:order-2">
+                  <FormLabel>Category</FormLabel>
+                  <Select
+                    onValueChange={field.onChange}
+                    defaultValue={field.value.toString()}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="Select a category" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {Object.keys(categoriesByGroup).map((group) => (
+                        <SelectGroup key={group}>
+                          <SelectLabel className="-ml-6">{group}</SelectLabel>
+                          {categoriesByGroup[group].map(({ id, name }) => (
+                            <SelectItem
+                              key={id.toString()}
+                              value={id.toString()}
+                            >
+                              {name}
+                            </SelectItem>
+                          ))}
+                        </SelectGroup>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                  <FormDescription>
+                    Select the expense category.
+                  </FormDescription>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
             <FormField
               control={form.control}
               name="paidBy"
-- 
cgit v1.3

