import { router, Stack, useLocalSearchParams } from "expo-router"; import { useEffect, useMemo, useState } from "react"; import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native"; import { AppBackground } from "@/components/AppBackground"; import { InvoiceEditorSectionTabs, type InvoiceEditorSection, } from "@/components/invoices/InvoiceEditorSectionTabs"; import { InvoiceEditorFooter } from "@/components/invoices/InvoiceEditorFooter"; import { InvoicePdfPreview } from "@/components/invoices/InvoicePdfPreview"; import { InvoiceSetupForm } from "@/components/invoices/InvoiceSetupForm"; import { InvoiceTotals } from "@/components/invoices/InvoiceTotals"; 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 { fonts, spacing } from "@/constants/theme"; import { formatCurrency } from "@/lib/format"; import { isRequiredString, isValidTaxRate, validateLineItems, } from "@/lib/form-validation"; import { resolveInvoiceBusinessId } from "@/lib/invoice-business"; import { defaultDueDate, generateInvoiceNumber } from "@/lib/invoice-number"; import { buildPreviewPdfInput } from "@/lib/invoice-pdf-input"; import { useTabBarScrollPadding } from "@/lib/tab-bar-insets"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; import { api } from "@/lib/trpc"; export default function NewInvoiceScreen() { const styles = useThemedStyles(createNewInvoiceStyles); const utils = api.useUtils(); const scrollPadding = useTabBarScrollPadding(); const { blank } = useLocalSearchParams<{ blank?: string }>(); const isBlank = blank === "1" || blank === "true"; const businessesQuery = api.businesses.getAll.useQuery(); const clientsQuery = api.clients.getAll.useQuery(); const [businessId, setBusinessId] = useState(""); const [clientId, setClientId] = useState(""); const [invoiceNumber, setInvoiceNumber] = useState(generateInvoiceNumber); const [issueDate, setIssueDate] = useState(() => new Date()); const [dueDate, setDueDate] = useState(() => defaultDueDate(new Date())); const [notes, setNotes] = useState(""); const [taxRate, setTaxRate] = useState("0"); const [items, setItems] = useState(() => isBlank ? [] : [ { date: new Date(), description: "", hours: "1", rate: "0", }, ], ); const [section, setSection] = useState("setup"); const [error, setError] = useState(null); useEffect(() => { if (businessId || !businessesQuery.data?.length) return; setBusinessId(resolveInvoiceBusinessId(null, businessesQuery.data)); }, [businessId, businessesQuery.data]); const businessOptions = useMemo( () => (businessesQuery.data ?? []).map((business) => ({ label: business.name, value: business.id, })), [businessesQuery.data], ); const clientOptions = useMemo( () => (clientsQuery.data ?? []).map((client) => ({ label: client.name, value: client.id, })), [clientsQuery.data], ); const selectedClient = clientsQuery.data?.find((client) => client.id === clientId); const currency = selectedClient?.currency ?? "USD"; const resolvedBusinessId = resolveInvoiceBusinessId(businessId, businessesQuery.data); useEffect(() => { if (!selectedClient?.defaultHourlyRate) return; setItems((prev) => prev.map((item, index) => index === 0 && (item.rate === "0" || item.rate === "") ? { ...item, rate: String(selectedClient.defaultHourlyRate) } : item, ), ); }, [selectedClient?.defaultHourlyRate, selectedClient?.id]); const createInvoice = api.invoices.create.useMutation({ onSuccess: (invoice) => { void utils.invoices.getAll.invalidate(); void utils.dashboard.getStats.invalidate(); Alert.alert("Invoice created", "Your draft invoice is ready.", [ { text: "View invoice", onPress: () => router.replace(`/(app)/invoices/${invoice.id}`), }, ]); }, onError: (err) => setError(err.message), }); 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 parsedTaxRate = Number(taxRate) || 0; const taxAmount = subtotal * (parsedTaxRate / 100); const total = subtotal + taxAmount; const previewInput = useMemo( () => buildPreviewPdfInput({ invoiceNumber, businessId: resolvedBusinessId, clientId, issueDate, dueDate, taxRate: parsedTaxRate, currency, notes, items, }), [ invoiceNumber, resolvedBusinessId, clientId, issueDate, dueDate, parsedTaxRate, currency, notes, items, ], ); const businessError = resolvedBusinessId ? undefined : "Select a business"; const clientError = clientId ? undefined : "Select a client"; const invoiceNumberError = isRequiredString(invoiceNumber) ? undefined : "Invoice number is required"; const taxError = isValidTaxRate(taxRate) ? undefined : "Tax rate must be between 0 and 100"; const lineItemsError = validateLineItems(items); const canCreate = businessOptions.length > 0 && clientOptions.length > 0 && !businessError && !clientError && !invoiceNumberError && !taxError && !lineItemsError; if (businessesQuery.isLoading || clientsQuery.isLoading) { return ; } function updateItem(index: number, patch: Partial) { setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))); } function addItem() { setItems((prev) => [ ...prev, { date: new Date(), description: "", hours: "1", rate: prev[prev.length - 1]?.rate ?? "0", }, ]); } function removeItem(index: number) { setItems((prev) => prev.filter((_, i) => i !== index)); } function handleCreate() { if (!canCreate) return; setError(null); const parsedItems: Array<{ date: Date; description: string; hours: number; rate: number; }> = []; for (const item of items) { parsedItems.push({ date: item.date, description: item.description.trim(), hours: Number(item.hours), rate: Number(item.rate), }); } createInvoice.mutate({ businessId: resolvedBusinessId, clientId, invoiceNumber: invoiceNumber.trim(), issueDate, dueDate, notes, taxRate: Number(taxRate), currency, items: parsedItems, status: "draft", }); } return ( {section === "preview" ? ( ) : section === "setup" ? ( {clientOptions.length === 0 || businessOptions.length === 0 ? ( {businessOptions.length === 0 ? "Add a business before creating an invoice." : "Add a client before creating an invoice."}