mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-06 04:26: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 { EditExpenseForm } from '@/app/groups/[groupId]/expenses/edit-expense-form'
|
||||||
import { ExpenseForm } from '@/components/expense-form'
|
|
||||||
import {
|
|
||||||
deleteExpense,
|
|
||||||
getCategories,
|
|
||||||
getExpense,
|
|
||||||
updateExpense,
|
|
||||||
} from '@/lib/api'
|
|
||||||
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
|
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
|
||||||
import { expenseFormSchema } from '@/lib/schemas'
|
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
|
||||||
import { Suspense } from 'react'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Edit expense',
|
title: 'Edit Expense',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function EditExpensePage({
|
export default async function EditExpensePage({
|
||||||
@@ -21,35 +11,11 @@ export default async function EditExpensePage({
|
|||||||
}: {
|
}: {
|
||||||
params: { groupId: string; expenseId: string }
|
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 (
|
return (
|
||||||
<Suspense>
|
<EditExpenseForm
|
||||||
<ExpenseForm
|
groupId={groupId}
|
||||||
group={group}
|
expenseId={expenseId}
|
||||||
expense={expense}
|
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||||
categories={categories}
|
/>
|
||||||
onSubmit={updateExpenseAction}
|
|
||||||
onDelete={deleteExpenseAction}
|
|
||||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 { CreateExpenseForm } from '@/app/groups/[groupId]/expenses/create-expense-form'
|
||||||
import { ExpenseForm } from '@/components/expense-form'
|
|
||||||
import { createExpense, getCategories } from '@/lib/api'
|
|
||||||
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
|
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
|
||||||
import { expenseFormSchema } from '@/lib/schemas'
|
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
|
||||||
import { Suspense } from 'react'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Create expense',
|
title: 'Create Expense',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ExpensePage({
|
export default async function ExpensePage({
|
||||||
@@ -16,25 +11,10 @@ export default async function ExpensePage({
|
|||||||
}: {
|
}: {
|
||||||
params: { groupId: string }
|
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 (
|
return (
|
||||||
<Suspense>
|
<CreateExpenseForm
|
||||||
<ExpenseForm
|
groupId={groupId}
|
||||||
group={group}
|
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
||||||
categories={categories}
|
/>
|
||||||
onSubmit={createExpenseAction}
|
|
||||||
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 { CategorySelector } from '@/components/category-selector'
|
||||||
import { ExpenseDocumentsInput } from '@/components/expense-documents-input'
|
import { ExpenseDocumentsInput } from '@/components/expense-documents-input'
|
||||||
import { SubmitButton } from '@/components/submit-button'
|
import { SubmitButton } from '@/components/submit-button'
|
||||||
@@ -33,7 +32,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { getCategories, getExpense, getGroup, randomId } from '@/lib/api'
|
import { randomId } from '@/lib/api'
|
||||||
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
|
import { RuntimeFeatureFlags } from '@/lib/featureFlags'
|
||||||
import { useActiveUser } from '@/lib/hooks'
|
import { useActiveUser } from '@/lib/hooks'
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +41,7 @@ import {
|
|||||||
expenseFormSchema,
|
expenseFormSchema,
|
||||||
} from '@/lib/schemas'
|
} from '@/lib/schemas'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { AppRouterOutput } from '@/trpc/routers/_app'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { Save } from 'lucide-react'
|
import { Save } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
@@ -50,18 +50,9 @@ import { useSearchParams } from 'next/navigation'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { match } from 'ts-pattern'
|
import { match } from 'ts-pattern'
|
||||||
import { DeletePopup } from './delete-popup'
|
import { DeletePopup } from '../../../../components/delete-popup'
|
||||||
import { extractCategoryFromTitle } from './expense-form-actions'
|
import { extractCategoryFromTitle } from '../../../../components/expense-form-actions'
|
||||||
import { Textarea } from './ui/textarea'
|
import { Textarea } from '../../../../components/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
|
|
||||||
}
|
|
||||||
|
|
||||||
const enforceCurrencyPattern = (value: string) =>
|
const enforceCurrencyPattern = (value: string) =>
|
||||||
value
|
value
|
||||||
@@ -72,7 +63,9 @@ const enforceCurrencyPattern = (value: string) =>
|
|||||||
.replace(/#/, '.') // change back # to dot
|
.replace(/#/, '.') // change back # to dot
|
||||||
.replace(/[^-\d.]/g, '') // remove all non-numeric characters
|
.replace(/[^-\d.]/g, '') // remove all non-numeric characters
|
||||||
|
|
||||||
const getDefaultSplittingOptions = (group: Props['group']) => {
|
const getDefaultSplittingOptions = (
|
||||||
|
group: AppRouterOutput['groups']['get']['group'],
|
||||||
|
) => {
|
||||||
const defaultValue = {
|
const defaultValue = {
|
||||||
splitMode: 'EVENLY' as const,
|
splitMode: 'EVENLY' as const,
|
||||||
paidFor: group.participants.map(({ id }) => ({
|
paidFor: group.participants.map(({ id }) => ({
|
||||||
@@ -146,15 +139,23 @@ async function persistDefaultSplittingOptions(
|
|||||||
|
|
||||||
export function ExpenseForm({
|
export function ExpenseForm({
|
||||||
group,
|
group,
|
||||||
expense,
|
|
||||||
categories,
|
categories,
|
||||||
|
expense,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
runtimeFeatureFlags,
|
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 t = useTranslations('ExpenseForm')
|
||||||
const isCreate = expense === undefined
|
const isCreate = expense === undefined
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
const getSelectedPayer = (field?: { value: string }) => {
|
const getSelectedPayer = (field?: { value: string }) => {
|
||||||
if (isCreate && typeof window !== 'undefined') {
|
if (isCreate && typeof window !== 'undefined') {
|
||||||
const activeUser = localStorage.getItem(`${group.id}-activeUser`)
|
const activeUser = localStorage.getItem(`${group.id}-activeUser`)
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Trash2 } from 'lucide-react'
|
import { Trash2 } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { AsyncButton } from './async-button'
|
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
|
* @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)
|
const [activeUser, setActiveUser] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeUser = localStorage.getItem(`${groupId}-activeUser`)
|
if (groupId) {
|
||||||
if (activeUser) setActiveUser(activeUser)
|
const activeUser = localStorage.getItem(`${groupId}-activeUser`)
|
||||||
|
if (activeUser) setActiveUser(activeUser)
|
||||||
|
}
|
||||||
}, [groupId])
|
}, [groupId])
|
||||||
|
|
||||||
return activeUser
|
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 { 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 { listGroupExpensesProcedure } from '@/trpc/routers/groups/expenses/list.procedure'
|
||||||
|
import { updateGroupExpenseProcedure } from '@/trpc/routers/groups/expenses/update.procedure'
|
||||||
|
|
||||||
export const groupExpensesRouter = createTRPCRouter({
|
export const groupExpensesRouter = createTRPCRouter({
|
||||||
list: listGroupExpensesProcedure,
|
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