diff --git a/apps/mobile/app/(app)/(tabs)/_layout.tsx b/apps/mobile/app/(app)/(tabs)/_layout.tsx index f534b150..937b7fa0 100644 --- a/apps/mobile/app/(app)/(tabs)/_layout.tsx +++ b/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -1,12 +1,14 @@ +import { Button } from '@/components/ui/button' import { useColorScheme } from '@/hooks/useColorScheme' import { theme } from '@/lib/theme' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' -import { Tabs } from 'expo-router' +import { Link, Tabs } from 'expo-router' import { + // BarChartBigIcon, CogIcon, LandPlotIcon, - ScanTextIcon, + PlusIcon, WalletIcon, } from 'lucide-react-native' @@ -47,16 +49,23 @@ export default function TabLayout() { headerTitle: t(i18n)`Budgets`, tabBarShowLabel: false, tabBarIcon: ({ color }) => , + headerRight: () => ( + + + + ), }} /> - , + tabBarIcon: ({ color }) => , }} - /> + /> */} ({ + resolver: zodResolver(zTransactionFormValues), + defaultValues: { + walletAccountId: transaction?.walletAccountId, + currency: transaction?.currency, + amount: Math.abs(transaction?.amount ?? 0), + date: transaction?.date, + note: transaction?.note ?? '', + budgetId: transaction?.budgetId ?? undefined, + categoryId: transaction?.categoryId ?? undefined, + }, + }) + const { mutateAsync } = useMutation({ mutationFn: updateTransaction, onError(error) { @@ -89,17 +109,9 @@ export default function EditRecordScreen() { 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/app/(app)/transaction/new-record.tsx b/apps/mobile/app/(app)/transaction/new-record.tsx index ab3e1861..641a13f9 100644 --- a/apps/mobile/app/(app)/transaction/new-record.tsx +++ b/apps/mobile/app/(app)/transaction/new-record.tsx @@ -1,24 +1,50 @@ import { toast } from '@/components/common/toast' +import { Scanner } from '@/components/transaction/scanner' import { TransactionForm } from '@/components/transaction/transaction-form' import { createTransaction } from '@/mutations/transaction' import { transactionQueries } from '@/queries/transaction' import { useWallets, walletQueries } from '@/queries/wallet' -import { zUpdateTransaction } from '@6pm/validation' +import { + type TransactionFormValues, + zTransactionFormValues, + zUpdateTransaction, +} from '@6pm/validation' +import { zodResolver } from '@hookform/resolvers/zod' 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 { useRef } from 'react' +import { useForm } from 'react-hook-form' import { Alert, View } from 'react-native' +import PagerView from 'react-native-pager-view' export default function NewRecordScreen() { - const router = useRouter() - const params = useLocalSearchParams() - const defaultValues = zUpdateTransaction.parse(params) - const { data: walletAccounts } = useWallets() const { i18n } = useLingui() + const ref = useRef(null) const queryClient = useQueryClient() + const router = useRouter() + const { data: walletAccounts } = useWallets() + const defaultWallet = walletAccounts?.[0] + + const params = useLocalSearchParams() + const parsedParams = zUpdateTransaction.parse(params) + const defaultValues = { + date: new Date(), + amount: 0, + currency: 'USD', + note: '', + walletAccountId: defaultWallet?.id, + ...parsedParams, + } + + const transactionForm = useForm({ + resolver: zodResolver(zTransactionFormValues), + defaultValues, + }) + const { mutateAsync } = useMutation({ mutationFn: createTransaction, onError(error) { @@ -42,8 +68,6 @@ export default function NewRecordScreen() { }, }) - const defaultWallet = walletAccounts?.[0] - if (!defaultWallet) { return ( @@ -53,14 +77,39 @@ export default function NewRecordScreen() { } return ( - + + + { + ref.current?.setPage(1) + }} + /> + ref.current?.setScrollEnabled(false)} + onScanResult={(result) => { + transactionForm.reset( + { + ...defaultValues, + ...result, + }, + { + keepDefaultValues: false, + }, + ) + ref.current?.setScrollEnabled(true) + ref.current?.setPage(0) + }} + /> + + ) } diff --git a/apps/mobile/app/(app)/(tabs)/scanner.tsx b/apps/mobile/components/transaction/scanner.tsx similarity index 88% rename from apps/mobile/app/(app)/(tabs)/scanner.tsx rename to apps/mobile/components/transaction/scanner.tsx index 1a150481..7a4a66b3 100644 --- a/apps/mobile/app/(app)/(tabs)/scanner.tsx +++ b/apps/mobile/components/transaction/scanner.tsx @@ -4,6 +4,7 @@ import { ScanningOverlay } from '@/components/scanner/scanning-overlay' import { Button } from '@/components/ui/button' import { Text } from '@/components/ui/text' import { getAITransactionData } from '@/mutations/transaction' +import type { UpdateTransaction } from '@6pm/validation' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { useMutation } from '@tanstack/react-query' @@ -11,7 +12,6 @@ import { type CameraType, CameraView, useCameraPermissions } from 'expo-camera' import * as Haptics from 'expo-haptics' import { SaveFormat, manipulateAsync } from 'expo-image-manipulator' import * as ImagePicker from 'expo-image-picker' -import { useRouter } from 'expo-router' import { CameraIcon, ImagesIcon, @@ -22,6 +22,7 @@ import { cssInterop } from 'nativewind' import { useRef, useState } from 'react' import { Alert } from 'react-native' import { ImageBackground, View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' cssInterop(CameraView, { className: { @@ -29,16 +30,24 @@ cssInterop(CameraView, { }, }) -export default function ScannerScreen() { +type ScannerProps = { + onScanStart?: () => void + onScanResult: (result: UpdateTransaction) => void +} + +export function Scanner({ onScanStart, onScanResult }: ScannerProps) { const camera = useRef(null) - const router = useRouter() const [facing, setFacing] = useState('back') const [permission, requestPermission] = useCameraPermissions() const [imageUri, setImageUri] = useState(null) const { i18n } = useLingui() + const { bottom } = useSafeAreaInsets() const { mutateAsync } = useMutation({ mutationFn: getAITransactionData, + onMutate() { + onScanStart?.() + }, onError(error) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) Alert.alert(error.message ?? t(i18n)`Cannot extract transaction data`) @@ -46,11 +55,7 @@ export default function ScannerScreen() { }, onSuccess(result) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) - router.push({ - pathname: '/transaction/new-record', - // biome-ignore lint/suspicious/noExplicitAny: - params: result as any, - }) + onScanResult(result) setImageUri(null) }, }) @@ -131,6 +136,7 @@ export default function ScannerScreen() { {/* */} @@ -140,7 +146,7 @@ export default function ScannerScreen() { - {onDelete && ( + {onDelete ? ( + ) : ( + )} - - +