import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useMemo, useState } from "react"; import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppBackground } from "@/components/AppBackground"; import { LineItemEditor, type EditableLineItem } from "@/components/invoices/LineItemEditor"; import { LoadingScreen } from "@/components/LoadingScreen"; import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; import { DateTimeField } from "@/components/ui/DateTimeField"; import { Input } from "@/components/ui/Input"; import { fonts, spacing } from "@/constants/theme"; import { useAppTheme } from "@/contexts/ThemeContext"; import { useNativeTabBarHeight, useTabBarScrollPadding } from "@/lib/tab-bar-insets"; import { formatCurrency } from "@/lib/format"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; import { api } from "@/lib/trpc"; export default function InvoiceEditScreen() { const { colors } = useAppTheme(); const styles = useThemedStyles(createInvoiceEditStyles); const { id } = useLocalSearchParams<{ id: string }>(); const utils = api.useUtils(); const insets = useSafeAreaInsets(); const tabBarHeight = useNativeTabBarHeight(); const scrollPadding = useTabBarScrollPadding(); const invoiceQuery = api.invoices.getById.useQuery( { id: id ?? "" }, { enabled: Boolean(id) }, ); const [notes, setNotes] = useState(""); const [dueDate, setDueDate] = useState(() => new Date()); const [items, setItems] = useState([]); const [expandedIndex, setExpandedIndex] = useState(null); const [error, setError] = useState(null); const [footerHeight, setFooterHeight] = useState(0); useEffect(() => { const invoice = invoiceQuery.data; if (!invoice) return; setNotes(invoice.notes ?? ""); setDueDate(new Date(invoice.dueDate)); setItems( invoice.items.map((item) => ({ id: item.id, date: new Date(item.date), description: item.description, hours: String(item.hours), rate: String(item.rate), })), ); setExpandedIndex(null); }, [invoiceQuery.data]); const updateInvoice = api.invoices.update.useMutation({ onSuccess: () => { void utils.invoices.getById.invalidate({ id: id ?? "" }); void utils.invoices.getAll.invalidate(); void utils.dashboard.getStats.invalidate(); Alert.alert("Saved", "Invoice updated", [ { text: "OK", onPress: () => router.back() }, ]); }, onError: (err) => setError(err.message), }); const invoice = invoiceQuery.data; const subtotal = useMemo( () => items.reduce((sum, item) => { const hours = Number(item.hours) || 0; const rate = Number(item.rate) || 0; return sum + hours * rate; }, 0), [items], ); const taxRate = invoice?.taxRate ?? 0; const taxAmount = subtotal * (taxRate / 100); const total = subtotal + taxAmount; const currency = invoice?.currency ?? "USD"; if (!id) { return ; } if (invoiceQuery.isLoading) { return ; } if (!invoice) { return ; } function updateItem(index: number, patch: Partial) { setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))); } function addItem() { const nextIndex = items.length; setItems((prev) => [ ...prev, { date: new Date(), description: "", hours: "1", rate: prev[prev.length - 1]?.rate ?? "0", }, ]); setExpandedIndex(nextIndex); } function removeItem(index: number) { if (items.length <= 1) { Alert.alert("Cannot remove", "An invoice needs at least one line item."); return; } setItems((prev) => prev.filter((_, i) => i !== index)); setExpandedIndex((current) => { if (current === null) return null; if (current === index) return null; return current > index ? current - 1 : current; }); } function handleSave() { setError(null); const parsedItems: Array<{ date: Date; description: string; hours: number; rate: number; }> = []; for (const item of items) { const hours = Number(item.hours); const rate = Number(item.rate); if (!item.description.trim()) { setError("Each line needs a description"); return; } if (Number.isNaN(hours) || hours < 0) { setError("Hours must be a valid number"); return; } if (Number.isNaN(rate) || rate < 0) { setError("Rate must be a valid number"); return; } parsedItems.push({ date: item.date, description: item.description.trim(), hours, rate, }); } updateInvoice.mutate({ id, notes, dueDate, items: parsedItems, }); } return ( {invoice.invoicePrefix} {invoice.invoiceNumber} {invoice.client?.name ?? "Client"} {items.map((item, index) => ( setExpandedIndex((current) => (current === index ? null : index)) } onChange={(patch) => updateItem(index, patch)} onRemove={() => removeItem(index)} /> ))} + Add line {taxRate > 0 ? ( ) : null} {error ? {error} : null} setFooterHeight(event.nativeEvent.layout.height)} style={[ styles.footer, { bottom: tabBarHeight, paddingBottom: Math.max(insets.bottom, spacing.sm), }, ]} >