diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
index a91de1f..f3e2e1c 100644
--- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
@@ -1,19 +1,9 @@
-import { cached } from '@/app/cached-functions'
-import { ExpenseForm } from '@/components/expense-form'
-import {
- deleteExpense,
- getCategories,
- getExpense,
- updateExpense,
-} from '@/lib/api'
+import { EditExpenseForm } from '@/app/groups/[groupId]/expenses/edit-expense-form'
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
-import { expenseFormSchema } from '@/lib/schemas'
import { Metadata } from 'next'
-import { notFound, redirect } from 'next/navigation'
-import { Suspense } from 'react'
export const metadata: Metadata = {
- title: 'Edit expense',
+ title: 'Edit Expense',
}
export default async function EditExpensePage({
@@ -21,35 +11,11 @@ export default async function EditExpensePage({
}: {
params: { groupId: string; expenseId: string }
}) {
- const categories = await getCategories()
- const group = await cached.getGroup(groupId)
- if (!group) notFound()
- const expense = await getExpense(groupId, expenseId)
- if (!expense) notFound()
-
- async function updateExpenseAction(values: unknown, participantId?: string) {
- 'use server'
- const expenseFormValues = expenseFormSchema.parse(values)
- await updateExpense(groupId, expenseId, expenseFormValues, participantId)
- redirect(`/groups/${groupId}`)
- }
-
- async function deleteExpenseAction(participantId?: string) {
- 'use server'
- await deleteExpense(groupId, expenseId, participantId)
- redirect(`/groups/${groupId}`)
- }
-
return (
-
-
-
+
)
}
diff --git a/src/app/groups/[groupId]/expenses/create-expense-form.tsx b/src/app/groups/[groupId]/expenses/create-expense-form.tsx
new file mode 100644
index 0000000..d6cfbed
--- /dev/null
+++ b/src/app/groups/[groupId]/expenses/create-expense-form.tsx
@@ -0,0 +1,45 @@
+'use client'
+import { RuntimeFeatureFlags } from '@/lib/featureFlags'
+import { trpc } from '@/trpc/client'
+import { useRouter } from 'next/navigation'
+import { ExpenseForm } from './expense-form'
+
+export function CreateExpenseForm({
+ groupId,
+ runtimeFeatureFlags,
+}: {
+ groupId: string
+ expenseId?: string
+ runtimeFeatureFlags: RuntimeFeatureFlags
+}) {
+ const { data: groupData } = trpc.groups.get.useQuery({ groupId })
+ const group = groupData?.group
+
+ const { data: categoriesData } = trpc.categories.list.useQuery()
+ const categories = categoriesData?.categories
+
+ const { mutateAsync: createExpenseMutateAsync } =
+ trpc.groups.expenses.create.useMutation()
+
+ const utils = trpc.useUtils()
+ const router = useRouter()
+
+ if (!group || !categories) return null
+
+ return (
+ {
+ await createExpenseMutateAsync({
+ groupId,
+ expenseFormValues,
+ participantId,
+ })
+ utils.groups.expenses.invalidate()
+ router.push(`/groups/${group.id}`)
+ }}
+ runtimeFeatureFlags={runtimeFeatureFlags}
+ />
+ )
+}
diff --git a/src/app/groups/[groupId]/expenses/create/page.tsx b/src/app/groups/[groupId]/expenses/create/page.tsx
index 3ca3b36..daa32dd 100644
--- a/src/app/groups/[groupId]/expenses/create/page.tsx
+++ b/src/app/groups/[groupId]/expenses/create/page.tsx
@@ -1,14 +1,9 @@
-import { cached } from '@/app/cached-functions'
-import { ExpenseForm } from '@/components/expense-form'
-import { createExpense, getCategories } from '@/lib/api'
+import { CreateExpenseForm } from '@/app/groups/[groupId]/expenses/create-expense-form'
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
-import { expenseFormSchema } from '@/lib/schemas'
import { Metadata } from 'next'
-import { notFound, redirect } from 'next/navigation'
-import { Suspense } from 'react'
export const metadata: Metadata = {
- title: 'Create expense',
+ title: 'Create Expense',
}
export default async function ExpensePage({
@@ -16,25 +11,10 @@ export default async function ExpensePage({
}: {
params: { groupId: string }
}) {
- const categories = await getCategories()
- const group = await cached.getGroup(groupId)
- if (!group) notFound()
-
- async function createExpenseAction(values: unknown, participantId?: string) {
- 'use server'
- const expenseFormValues = expenseFormSchema.parse(values)
- await createExpense(expenseFormValues, groupId, participantId)
- redirect(`/groups/${groupId}`)
- }
-
return (
-
-
-
+
)
}
diff --git a/src/app/groups/[groupId]/expenses/edit-expense-form.tsx b/src/app/groups/[groupId]/expenses/edit-expense-form.tsx
new file mode 100644
index 0000000..d762ec4
--- /dev/null
+++ b/src/app/groups/[groupId]/expenses/edit-expense-form.tsx
@@ -0,0 +1,65 @@
+'use client'
+import { RuntimeFeatureFlags } from '@/lib/featureFlags'
+import { trpc } from '@/trpc/client'
+import { useRouter } from 'next/navigation'
+import { ExpenseForm } from './expense-form'
+
+export function EditExpenseForm({
+ groupId,
+ expenseId,
+ runtimeFeatureFlags,
+}: {
+ groupId: string
+ expenseId: string
+ runtimeFeatureFlags: RuntimeFeatureFlags
+}) {
+ const { data: groupData } = trpc.groups.get.useQuery({ groupId })
+ const group = groupData?.group
+
+ const { data: categoriesData } = trpc.categories.list.useQuery()
+ const categories = categoriesData?.categories
+
+ const { data: expenseData } = trpc.groups.expenses.get.useQuery({
+ groupId,
+ expenseId,
+ })
+ const expense = expenseData?.expense
+
+ const { mutateAsync: updateExpenseMutateAsync } =
+ trpc.groups.expenses.update.useMutation()
+ const { mutateAsync: deleteExpenseMutateAsync } =
+ trpc.groups.expenses.delete.useMutation()
+
+ const utils = trpc.useUtils()
+ const router = useRouter()
+
+ if (!group || !categories || !expense) return null
+
+ return (
+ {
+ await updateExpenseMutateAsync({
+ expenseId,
+ groupId,
+ expenseFormValues,
+ participantId,
+ })
+ utils.groups.expenses.invalidate()
+ router.push(`/groups/${group.id}`)
+ }}
+ onDelete={async (participantId) => {
+ await deleteExpenseMutateAsync({
+ expenseId,
+ groupId,
+ participantId,
+ })
+ utils.groups.expenses.invalidate()
+ router.push(`/groups/${group.id}`)
+ }}
+ runtimeFeatureFlags={runtimeFeatureFlags}
+ />
+ )
+}
diff --git a/src/components/expense-form.tsx b/src/app/groups/[groupId]/expenses/expense-form.tsx
similarity index 97%
rename from src/components/expense-form.tsx
rename to src/app/groups/[groupId]/expenses/expense-form.tsx
index 043a160..08f4436 100644
--- a/src/components/expense-form.tsx
+++ b/src/app/groups/[groupId]/expenses/expense-form.tsx
@@ -1,4 +1,3 @@
-'use client'
import { CategorySelector } from '@/components/category-selector'
import { ExpenseDocumentsInput } from '@/components/expense-documents-input'
import { SubmitButton } from '@/components/submit-button'
@@ -33,7 +32,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
-import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
+import { randomId } from '@/lib/api'
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
import { useActiveUser } from '@/lib/hooks'
import {
@@ -42,6 +41,7 @@ import {
expenseFormSchema,
} from '@/lib/schemas'
import { cn } from '@/lib/utils'
+import { AppRouterOutput } from '@/trpc/routers/_app'
import { zodResolver } from '@hookform/resolvers/zod'
import { Save } from 'lucide-react'
import { useTranslations } from 'next-intl'
@@ -50,18 +50,9 @@ import { useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { match } from 'ts-pattern'
-import { DeletePopup } from './delete-popup'
-import { extractCategoryFromTitle } from './expense-form-actions'
-import { Textarea } from './ui/textarea'
-
-export type Props = {
- group: NonNullable>>
- expense?: NonNullable>>
- categories: NonNullable>>
- onSubmit: (values: ExpenseFormValues, participantId?: string) => Promise
- onDelete?: (participantId?: string) => Promise
- runtimeFeatureFlags: RuntimeFeatureFlags
-}
+import { DeletePopup } from '../../../../components/delete-popup'
+import { extractCategoryFromTitle } from '../../../../components/expense-form-actions'
+import { Textarea } from '../../../../components/ui/textarea'
const enforceCurrencyPattern = (value: string) =>
value
@@ -72,7 +63,9 @@ const enforceCurrencyPattern = (value: string) =>
.replace(/#/, '.') // change back # to dot
.replace(/[^-\d.]/g, '') // remove all non-numeric characters
-const getDefaultSplittingOptions = (group: Props['group']) => {
+const getDefaultSplittingOptions = (
+ group: AppRouterOutput['groups']['get']['group'],
+) => {
const defaultValue = {
splitMode: 'EVENLY' as const,
paidFor: group.participants.map(({ id }) => ({
@@ -146,15 +139,23 @@ async function persistDefaultSplittingOptions(
export function ExpenseForm({
group,
- expense,
categories,
+ expense,
onSubmit,
onDelete,
runtimeFeatureFlags,
-}: Props) {
+}: {
+ group: AppRouterOutput['groups']['get']['group']
+ categories: AppRouterOutput['categories']['list']['categories']
+ expense?: AppRouterOutput['groups']['expenses']['get']['expense']
+ onSubmit: (value: ExpenseFormValues, participantId?: string) => Promise
+ onDelete?: (participantId?: string) => Promise
+ runtimeFeatureFlags: RuntimeFeatureFlags
+}) {
const t = useTranslations('ExpenseForm')
const isCreate = expense === undefined
const searchParams = useSearchParams()
+
const getSelectedPayer = (field?: { value: string }) => {
if (isCreate && typeof window !== 'undefined') {
const activeUser = localStorage.getItem(`${group.id}-activeUser`)
diff --git a/src/components/delete-popup.tsx b/src/components/delete-popup.tsx
index 6e7d75a..fc66503 100644
--- a/src/components/delete-popup.tsx
+++ b/src/components/delete-popup.tsx
@@ -1,5 +1,3 @@
-'use client'
-
import { Trash2 } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { AsyncButton } from './async-button'
diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts
index 490eccf..56e7744 100644
--- a/src/lib/hooks.ts
+++ b/src/lib/hooks.ts
@@ -52,12 +52,14 @@ export function useBaseUrl() {
/**
* @returns The active user, or `null` until it is fetched from local storage
*/
-export function useActiveUser(groupId: string) {
+export function useActiveUser(groupId?: string) {
const [activeUser, setActiveUser] = useState(null)
useEffect(() => {
- const activeUser = localStorage.getItem(`${groupId}-activeUser`)
- if (activeUser) setActiveUser(activeUser)
+ if (groupId) {
+ const activeUser = localStorage.getItem(`${groupId}-activeUser`)
+ if (activeUser) setActiveUser(activeUser)
+ }
}, [groupId])
return activeUser
diff --git a/src/trpc/routers/groups/expenses/create.procedure.ts b/src/trpc/routers/groups/expenses/create.procedure.ts
new file mode 100644
index 0000000..1b84ad4
--- /dev/null
+++ b/src/trpc/routers/groups/expenses/create.procedure.ts
@@ -0,0 +1,23 @@
+import { createExpense } from '@/lib/api'
+import { expenseFormSchema } from '@/lib/schemas'
+import { baseProcedure } from '@/trpc/init'
+import { z } from 'zod'
+
+export const createGroupExpenseProcedure = baseProcedure
+ .input(
+ z.object({
+ groupId: z.string().min(1),
+ expenseFormValues: expenseFormSchema,
+ participantId: z.string().optional(),
+ }),
+ )
+ .mutation(
+ async ({ input: { groupId, expenseFormValues, participantId } }) => {
+ const expense = await createExpense(
+ expenseFormValues,
+ groupId,
+ participantId,
+ )
+ return { expenseId: expense.id }
+ },
+ )
diff --git a/src/trpc/routers/groups/expenses/delete.procedure.ts b/src/trpc/routers/groups/expenses/delete.procedure.ts
new file mode 100644
index 0000000..9969cc5
--- /dev/null
+++ b/src/trpc/routers/groups/expenses/delete.procedure.ts
@@ -0,0 +1,16 @@
+import { deleteExpense } from '@/lib/api'
+import { baseProcedure } from '@/trpc/init'
+import { z } from 'zod'
+
+export const deleteGroupExpenseProcedure = baseProcedure
+ .input(
+ z.object({
+ expenseId: z.string().min(1),
+ groupId: z.string().min(1),
+ participantId: z.string().optional(),
+ }),
+ )
+ .mutation(async ({ input: { expenseId, groupId, participantId } }) => {
+ await deleteExpense(groupId, expenseId, participantId)
+ return {}
+ })
diff --git a/src/trpc/routers/groups/expenses/get.procedure.ts b/src/trpc/routers/groups/expenses/get.procedure.ts
new file mode 100644
index 0000000..4dc3b53
--- /dev/null
+++ b/src/trpc/routers/groups/expenses/get.procedure.ts
@@ -0,0 +1,17 @@
+import { getExpense } from '@/lib/api'
+import { baseProcedure } from '@/trpc/init'
+import { TRPCError } from '@trpc/server'
+import { z } from 'zod'
+
+export const getGroupExpenseProcedure = baseProcedure
+ .input(z.object({ groupId: z.string().min(1), expenseId: z.string().min(1) }))
+ .query(async ({ input: { groupId, expenseId } }) => {
+ const expense = await getExpense(groupId, expenseId)
+ if (!expense) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Expense not found',
+ })
+ }
+ return { expense }
+ })
diff --git a/src/trpc/routers/groups/expenses/index.ts b/src/trpc/routers/groups/expenses/index.ts
index d457ade..7f7cb54 100644
--- a/src/trpc/routers/groups/expenses/index.ts
+++ b/src/trpc/routers/groups/expenses/index.ts
@@ -1,6 +1,14 @@
import { createTRPCRouter } from '@/trpc/init'
+import { createGroupExpenseProcedure } from '@/trpc/routers/groups/expenses/create.procedure'
+import { deleteGroupExpenseProcedure } from '@/trpc/routers/groups/expenses/delete.procedure'
+import { getGroupExpenseProcedure } from '@/trpc/routers/groups/expenses/get.procedure'
import { listGroupExpensesProcedure } from '@/trpc/routers/groups/expenses/list.procedure'
+import { updateGroupExpenseProcedure } from '@/trpc/routers/groups/expenses/update.procedure'
export const groupExpensesRouter = createTRPCRouter({
list: listGroupExpensesProcedure,
+ get: getGroupExpenseProcedure,
+ create: createGroupExpenseProcedure,
+ update: updateGroupExpenseProcedure,
+ delete: deleteGroupExpenseProcedure,
})
diff --git a/src/trpc/routers/groups/expenses/update.procedure.ts b/src/trpc/routers/groups/expenses/update.procedure.ts
new file mode 100644
index 0000000..b7f9cc2
--- /dev/null
+++ b/src/trpc/routers/groups/expenses/update.procedure.ts
@@ -0,0 +1,27 @@
+import { updateExpense } from '@/lib/api'
+import { expenseFormSchema } from '@/lib/schemas'
+import { baseProcedure } from '@/trpc/init'
+import { z } from 'zod'
+
+export const updateGroupExpenseProcedure = baseProcedure
+ .input(
+ z.object({
+ expenseId: z.string().min(1),
+ groupId: z.string().min(1),
+ expenseFormValues: expenseFormSchema,
+ participantId: z.string().optional(),
+ }),
+ )
+ .mutation(
+ async ({
+ input: { expenseId, groupId, expenseFormValues, participantId },
+ }) => {
+ const expense = await updateExpense(
+ groupId,
+ expenseId,
+ expenseFormValues,
+ participantId,
+ )
+ return { expenseId: expense.id }
+ },
+ )