Skip to content

Commit

Permalink
feat(mobile): [Transaction] add update and delete transaction (#123)
Browse files Browse the repository at this point in the history
Resolves #112
  • Loading branch information
bkdev98 committed Jul 15, 2024
1 parent 82e2d0e commit f6adb56
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 7 deletions.
7 changes: 7 additions & 0 deletions apps/mobile/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export default function AuthenticatedLayout() {
headerShown: false,
}}
/>
<Stack.Screen
name="transaction/[transactionId]"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
<Stack.Screen
name="language"
options={{
Expand Down
106 changes: 106 additions & 0 deletions apps/mobile/app/(app)/transaction/[transactionId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { TransactionForm } from '@/components/transaction/transaction-form'
import { deleteTransaction, updateTransaction } from '@/mutations/transaction'
import { transactionQueries, useTransactionDetail } from '@/queries/transaction'
import { walletQueries } from '@/queries/wallet'
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as Haptics from 'expo-haptics'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { LoaderIcon } from 'lucide-react-native'
import { Alert, View } from 'react-native'

export default function EditRecordScreen() {
const { i18n } = useLingui()
const { transactionId } = useLocalSearchParams<{ transactionId: string }>()
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 (
<View className="flex-1 items-center bg-muted justify-center">
<LoaderIcon className="size-7 animate-spin text-primary" />
</View>
)
}

return (
<TransactionForm
onSubmit={(values) => 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}
/>
)
}
23 changes: 18 additions & 5 deletions apps/mobile/components/transaction/transaction-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -26,12 +26,14 @@ type TransactionFormProps = {
onSubmit: (data: TransactionFormValues) => void
defaultValues?: Partial<TransactionFormValues>
onCancel?: () => void
onDelete?: () => void
}

export const TransactionForm = ({
onSubmit,
defaultValues,
onCancel,
onDelete,
}: TransactionFormProps) => {
const { i18n } = useLingui()

Expand Down Expand Up @@ -65,9 +67,16 @@ export const TransactionForm = ({
>
<View className="flex-row justify-between items-center p-6 pb-0">
<SelectDateField />
<Button size="icon" variant="secondary" onPress={onCancel}>
<XIcon className="size-6 text-primary" />
</Button>
<View className="flex-row items-center gap-4">
{onDelete && (
<Button size="icon" variant="secondary" onPress={onDelete}>
<Trash2Icon className="size-6 text-primary" />
</Button>
)}
<Button size="icon" variant="secondary" onPress={onCancel}>
<XIcon className="size-6 text-primary" />
</Button>
</View>
</View>
<View className="flex-1 items-center justify-center pb-12">
<View className="w-full h-24 justify-end mb-4">
Expand Down Expand Up @@ -108,7 +117,11 @@ export const TransactionForm = ({
</View>
<SubmitButton
onPress={transactionForm.handleSubmit(onSubmit)}
disabled={transactionForm.formState.isLoading || !amount}
disabled={
transactionForm.formState.isLoading ||
!amount ||
!transactionForm.formState.isDirty
}
>
<Text>{t(i18n)`Save`}</Text>
</SubmitButton>
Expand Down
36 changes: 36 additions & 0 deletions apps/mobile/mutations/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
})
}
37 changes: 35 additions & 2 deletions apps/mobile/queries/transaction.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -42,17 +46,46 @@ 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) => {
return lastPage.meta.paginationMeta.after?.toString()
},
})
}

export function useTransactionDetail(transactionId: string) {
return useQuery({
...transactionQueries.detail(transactionId),
})
}

0 comments on commit f6adb56

Please sign in to comment.