diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index ff5f4cc..5863f89 100644 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -55,6 +55,13 @@ export default function AuthenticatedLayout() { headerShown: false, }} /> + () + const { data: transaction } = useTransactionDetail(transactionId!) + const router = useRouter() + const queryClient = useQueryClient() + const { mutateAsync } = useMutation({ + mutationFn: updateTransaction, + onError(error) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) + Alert.alert(error.message) + }, + onSuccess() { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) + router.back() + }, + async onSettled() { + await queryClient.invalidateQueries({ + queryKey: transactionQueries.all, + }) + await queryClient.invalidateQueries({ + queryKey: walletQueries.list._def, + }) + }, + }) + const { mutateAsync: mutateDelete } = useMutation({ + mutationFn: deleteTransaction, + onMutate() { + router.back() + }, + onError(error) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) + Alert.alert(error.message) + }, + onSuccess() { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) + }, + async onSettled() { + await queryClient.invalidateQueries({ + queryKey: transactionQueries.all, + }) + await queryClient.invalidateQueries({ + queryKey: walletQueries.list._def, + }) + }, + throwOnError: true, + }) + + function handleDelete() { + Haptics.selectionAsync() + Alert.alert( + t( + i18n, + )`This will delete the transaction. Are you sure you want to continue?`, + '', + [ + { + text: t(i18n)`Cancel`, + style: 'cancel', + }, + { + text: t(i18n)`Delete`, + style: 'destructive', + onPress: () => mutateDelete(transactionId!), + }, + ], + ) + } + + if (!transaction) { + return ( + + + + ) + } + + return ( + mutateAsync({ id: transaction.id, data: values })} + onCancel={router.back} + defaultValues={{ + walletAccountId: transaction.walletAccountId, + currency: transaction.currency, + amount: Math.abs(transaction.amount), + date: transaction.date, + note: transaction.note ?? '', + budgetId: transaction.budgetId ?? undefined, + categoryId: transaction.categoryId ?? undefined, + }} + onDelete={handleDelete} + /> + ) +} diff --git a/apps/mobile/components/transaction/transaction-form.tsx b/apps/mobile/components/transaction/transaction-form.tsx index a79e218..99c2b45 100644 --- a/apps/mobile/components/transaction/transaction-form.tsx +++ b/apps/mobile/components/transaction/transaction-form.tsx @@ -5,7 +5,7 @@ import { import { zodResolver } from '@hookform/resolvers/zod' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' -import { LandPlot, XIcon } from 'lucide-react-native' +import { LandPlot, Trash2Icon, XIcon } from 'lucide-react-native' import { Controller, FormProvider, useForm } from 'react-hook-form' import { ScrollView, View } from 'react-native' import Animated, { @@ -26,12 +26,14 @@ type TransactionFormProps = { onSubmit: (data: TransactionFormValues) => void defaultValues?: Partial onCancel?: () => void + onDelete?: () => void } export const TransactionForm = ({ onSubmit, defaultValues, onCancel, + onDelete, }: TransactionFormProps) => { const { i18n } = useLingui() @@ -65,9 +67,16 @@ export const TransactionForm = ({ > - + + {onDelete && ( + + )} + + @@ -108,7 +117,11 @@ export const TransactionForm = ({ {t(i18n)`Save`} diff --git a/apps/mobile/mutations/transaction.ts b/apps/mobile/mutations/transaction.ts index 622821f..8655c36 100644 --- a/apps/mobile/mutations/transaction.ts +++ b/apps/mobile/mutations/transaction.ts @@ -23,3 +23,39 @@ export async function createTransaction(data: TransactionFormValues) { return result } + +export async function updateTransaction({ + id, + data, +}: { + id: string + data: TransactionFormValues +}) { + const hc = await getHonoClient() + const result = await hc.v1.transactions[':transactionId'].$put({ + param: { transactionId: id }, + json: { + ...data, + amount: -data.amount, + }, + }) + + if (result.ok) { + const transaction = await result.json() + return TransactionSchema.merge( + z.object({ + // override Decimal type with number + amount: z.number({ coerce: true }), + }), + ).parse(transaction) + } + + return result +} + +export async function deleteTransaction(id: string) { + const hc = await getHonoClient() + await hc.v1.transactions[':transactionId'].$delete({ + param: { transactionId: id }, + }) +} diff --git a/apps/mobile/queries/transaction.ts b/apps/mobile/queries/transaction.ts index c0652bd..b6b8d3b 100644 --- a/apps/mobile/queries/transaction.ts +++ b/apps/mobile/queries/transaction.ts @@ -1,7 +1,11 @@ import { getHonoClient } from '@/lib/client' import { TransactionPopulatedSchema } from '@6pm/validation' // import { createQueryKeys } from '@lukemorales/query-key-factory' -import { useInfiniteQuery } from '@tanstack/react-query' +import { + useInfiniteQuery, + useQuery, + useQueryClient, +} from '@tanstack/react-query' export type TransactionFilters = { walletAccountId?: string @@ -42,13 +46,36 @@ export const transactionQueries = { } }, }), + detail: (transactionId: string) => ({ + queryKey: ['transaction', { transactionId }], + queryFn: async () => { + const hc = await getHonoClient() + const res = await hc.v1.transactions[':transactionId'].$get({ + param: { transactionId }, + }) + if (!res.ok) { + throw new Error(await res.text()) + } + + const transaction = await res.json() + return TransactionPopulatedSchema.parse(transaction) + }, + }), } export function useTransactions(filters: TransactionFilters) { + const queryClient = useQueryClient() return useInfiniteQuery({ queryKey: transactionQueries.list(filters).queryKey, queryFn: async ({ pageParam = filters.before?.toString() }) => { - return transactionQueries.list(filters).queryFn(pageParam) + const result = await transactionQueries.list(filters).queryFn(pageParam) + result.transactions?.forEach((transaction) => { + queryClient.setQueryData( + transactionQueries.detail(transaction.id).queryKey, + transaction, + ) + }) + return result }, initialPageParam: '', getNextPageParam: (lastPage) => { @@ -56,3 +83,9 @@ export function useTransactions(filters: TransactionFilters) { }, }) } + +export function useTransactionDetail(transactionId: string) { + return useQuery({ + ...transactionQueries.detail(transactionId), + }) +}