mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-13 19:16:13 +01:00
Use tRPC for expense form (#251)
This commit is contained in:
committed by
GitHub
parent
2281316d58
commit
21d0c02687
@@ -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 (
|
||||
<Suspense>
|
||||
<ExpenseForm
|
||||
group={group}
|
||||
expense={expense}
|
||||
categories={categories}
|
||||
onSubmit={updateExpenseAction}
|
||||
onDelete={deleteExpenseAction}
|
||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||
/>
|
||||
</Suspense>
|
||||
<EditExpenseForm
|
||||
groupId={groupId}
|
||||
expenseId={expenseId}
|
||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
45
src/app/groups/[groupId]/expenses/create-expense-form.tsx
Normal file
45
src/app/groups/[groupId]/expenses/create-expense-form.tsx
Normal file
@@ -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 (
|
||||
<ExpenseForm
|
||||
group={group}
|
||||
categories={categories}
|
||||
onSubmit={async (expenseFormValues, participantId) => {
|
||||
await createExpenseMutateAsync({
|
||||
groupId,
|
||||
expenseFormValues,
|
||||
participantId,
|
||||
})
|
||||
utils.groups.expenses.invalidate()
|
||||
router.push(`/groups/${group.id}`)
|
||||
}}
|
||||
runtimeFeatureFlags={runtimeFeatureFlags}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Suspense>
|
||||
<ExpenseForm
|
||||
group={group}
|
||||
categories={categories}
|
||||
onSubmit={createExpenseAction}
|
||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||
/>
|
||||
</Suspense>
|
||||
<CreateExpenseForm
|
||||
groupId={groupId}
|
||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
65
src/app/groups/[groupId]/expenses/edit-expense-form.tsx
Normal file
65
src/app/groups/[groupId]/expenses/edit-expense-form.tsx
Normal file
@@ -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 (
|
||||
<ExpenseForm
|
||||
group={group}
|
||||
expense={expense}
|
||||
categories={categories}
|
||||
onSubmit={async (expenseFormValues, participantId) => {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<Awaited<ReturnType<typeof getGroup>>>
|
||||
expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
|
||||
categories: NonNullable<Awaited<ReturnType<typeof getCategories>>>
|
||||
onSubmit: (values: ExpenseFormValues, participantId?: string) => Promise<void>
|
||||
onDelete?: (participantId?: string) => Promise<void>
|
||||
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<void>
|
||||
onDelete?: (participantId?: string) => Promise<void>
|
||||
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`)
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client'
|
||||
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AsyncButton } from './async-button'
|
||||
|
||||
@@ -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<string | null>(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
|
||||
|
||||
23
src/trpc/routers/groups/expenses/create.procedure.ts
Normal file
23
src/trpc/routers/groups/expenses/create.procedure.ts
Normal file
@@ -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 }
|
||||
},
|
||||
)
|
||||
16
src/trpc/routers/groups/expenses/delete.procedure.ts
Normal file
16
src/trpc/routers/groups/expenses/delete.procedure.ts
Normal file
@@ -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 {}
|
||||
})
|
||||
17
src/trpc/routers/groups/expenses/get.procedure.ts
Normal file
17
src/trpc/routers/groups/expenses/get.procedure.ts
Normal file
@@ -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 }
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
27
src/trpc/routers/groups/expenses/update.procedure.ts
Normal file
27
src/trpc/routers/groups/expenses/update.procedure.ts
Normal file
@@ -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 }
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user