import { router } from "expo-router"; import { useMemo, useState } from "react"; import { Alert, StyleSheet, Text, View } from "react-native"; import { AppBackground } from "@/components/AppBackground"; import { defaultExpenseFormState, ExpenseFormFields, type ExpenseFormState, } from "@/components/expenses/ExpenseFormFields"; import { ReceiptItemSelector } from "@/components/expenses/ReceiptItemSelector"; import { PageHeader } from "@/components/PageHeader"; import { TabPage } from "@/components/TabPage"; import { TabScrollView } from "@/components/TabScrollView"; import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; import { fonts, spacing } from "@/constants/theme"; import { useAppTheme } from "@/contexts/ThemeContext"; import { scanReceiptImage, type PickedReceiptImage, type ReceiptScanResult, } from "@/lib/receipt-scan"; import { api } from "@/lib/trpc"; type ReceiptSplitDraft = Pick< ReceiptScanResult, "items" | "subtotal" | "tax" | "total" >; export default function NewExpenseScreen() { const { colors } = useAppTheme(); const utils = api.useUtils(); const businessesQuery = api.businesses.getAll.useQuery(); const clientsQuery = api.clients.getAll.useQuery(); const businesses = businessesQuery.data ?? []; const clients = clientsQuery.data ?? []; const defaultBusinessId = useMemo( () => businesses.find((business) => business.isDefault)?.id ?? businesses[0]?.id ?? "", [businesses], ); const [form, setForm] = useState(() => defaultExpenseFormState(), ); const [scanning, setScanning] = useState(false); const [pendingReceipt, setPendingReceipt] = useState(null); const [receiptSplit, setReceiptSplit] = useState( null, ); const createExpense = api.expenses.create.useMutation(); const uploadReceipt = api.expenses.uploadReceipt.useMutation(); const suggest = api.expenses.suggestFromReceiptText.useMutation(); async function runScan(fromCamera: boolean) { setScanning(true); try { const result = await scanReceiptImage( fromCamera, { description: form.description, amountText: form.amountText, date: form.date, }, (input) => suggest.mutateAsync(input), ); if (!result) return; setForm((current) => ({ ...current, description: result.description, amountText: result.amountText, date: result.date, notes: result.ocrText, businessId: current.businessId || defaultBusinessId, })); setPendingReceipt(result.image); setReceiptSplit( result.items.length > 0 ? { items: result.items, subtotal: result.subtotal, tax: result.tax, total: result.total, } : null, ); Alert.alert( "Receipt scanned", result.items.length > 0 ? "Select the items this person owes, then apply the split amount." : result.amountText ? `Filled amount $${result.amountText}${result.description ? ` from ${result.description}` : ""}. Review and save.` : "Review the fields and enter the total before saving.", ); } finally { setScanning(false); } } async function handleSave() { const amount = Number(form.amountText); if (!form.description.trim()) { Alert.alert("Description required", "Enter what this expense was for."); return; } if (!Number.isFinite(amount) || amount <= 0) { Alert.alert("Amount required", "Enter a valid amount."); return; } try { const expense = await createExpense.mutateAsync({ description: form.description.trim(), amount, date: form.date, currency: "USD", category: form.category || undefined, clientId: form.clientId || undefined, businessId: form.businessId || defaultBusinessId || undefined, billable: form.billable, reimbursable: form.reimbursable, taxDeductible: form.taxDeductible, notes: form.notes.trim() || undefined, }); if (pendingReceipt) { await uploadReceipt.mutateAsync({ expenseId: expense.id, filename: pendingReceipt.filename, mimeType: pendingReceipt.mimeType, data: pendingReceipt.base64, }); } await utils.expenses.getAll.invalidate(); router.replace(`/(app)/more/expenses/${expense.id}` as never); } catch (err) { Alert.alert( "Could not save expense", err instanceof Error ? err.message : "Try again.", ); } } return ( } keyboardShouldPersistTaps="handled" >