import { router } from "expo-router"; import { useState } from "react"; import { Alert, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View, } from "react-native"; import { AppBackground } from "@/components/AppBackground"; import { FilterChip } from "@/components/FilterChip"; import { FloatingActionButton } from "@/components/FloatingActionButton"; import { GlassSurface } from "@/components/GlassSurface"; import { LoadingScreen } from "@/components/LoadingScreen"; import { PageHeader } from "@/components/PageHeader"; import { SwipeableRow } from "@/components/SwipeableRow"; import { StatusBadge } from "@/components/StatusBadge"; import { TabPage } from "@/components/TabPage"; import { TabScrollView } from "@/components/TabScrollView"; import { fonts, spacing } from "@/constants/theme"; import { useAppTheme } from "@/contexts/ThemeContext"; import { formatCurrency, formatDate } from "@/lib/format"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; import { getInvoiceStatus, type InvoiceStatus } from "@/lib/invoice-status"; import { formatTrpcErrorMessage } from "@/lib/trpc-errors"; import { api } from "@/lib/trpc"; const filters: Array<{ label: string; value?: InvoiceStatus | "all" }> = [ { label: "All", value: "all" }, { label: "Draft", value: "draft" }, { label: "Sent", value: "sent" }, { label: "Paid", value: "paid" }, { label: "Overdue", value: "overdue" }, ]; export default function InvoicesScreen() { const { colors } = useAppTheme(); const styles = useThemedStyles(createInvoicesStyles); const [filter, setFilter] = useState<(typeof filters)[number]["value"]>("all"); const utils = api.useUtils(); const invoicesQuery = api.invoices.getAll.useQuery(); const updateStatus = api.invoices.updateStatus.useMutation({ onSuccess: () => { utils.invoices.getAll.invalidate(); utils.dashboard.getStats.invalidate(); }, onError: (err) => Alert.alert("Update failed", err.message), }); const deleteInvoice = api.invoices.delete.useMutation({ onSuccess: () => { utils.invoices.getAll.invalidate(); utils.dashboard.getStats.invalidate(); }, onError: (err) => Alert.alert("Delete failed", err.message), }); if (invoicesQuery.isLoading) { return ; } if (invoicesQuery.error) { return ( Could not load invoices {formatTrpcErrorMessage(invoicesQuery.error)} ); } const invoices = (invoicesQuery.data ?? []).filter((invoice) => { if (filter === "all") return true; return getInvoiceStatus(invoice) === filter; }); function promptStatusChange(invoiceId: string, current: InvoiceStatus) { const options: Array<{ label: string; status: "draft" | "sent" | "paid" }> = []; if (current !== "draft") options.push({ label: "Mark as draft", status: "draft" }); if (current !== "sent" && current !== "overdue") { options.push({ label: "Mark as sent", status: "sent" }); } if (current !== "paid") options.push({ label: "Mark as paid", status: "paid" }); if (options.length === 0) return; Alert.alert("Update status", "Choose a new status", [ ...options.map((option) => ({ text: option.label, onPress: () => { updateStatus.mutate({ id: invoiceId, status: option.status }); }, })), { text: "Cancel", style: "cancel" }, ]); } function confirmDelete(invoiceId: string, label: string) { Alert.alert("Delete invoice?", `Remove ${label}? This cannot be undone.`, [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: () => deleteInvoice.mutate({ id: invoiceId }), }, ]); } return ( } refreshControl={ invoicesQuery.refetch()} tintColor={colors.primary} /> } > {filters.map((item) => ( setFilter(item.value)} /> ))} {invoices.length === 0 ? ( No invoices found Tap + to create your first invoice, or pull to refresh. ) : ( invoices.map((invoice) => { const status = getInvoiceStatus(invoice); const label = `${invoice.invoicePrefix}${invoice.invoiceNumber}`; const actions = [ { key: "open", label: "Open", icon: "open-outline" as const, color: "#fff", backgroundColor: colors.primary, onPress: () => router.push(`/(app)/invoices/${invoice.id}`), }, ...(status === "draft" ? [ { key: "edit", label: "Edit", icon: "create-outline" as const, color: "#fff", backgroundColor: colors.mutedForeground, onPress: () => router.push(`/(app)/invoices/edit/${invoice.id}`), }, { key: "send", label: "Send", icon: "send-outline" as const, color: "#fff", backgroundColor: colors.success, onPress: () => router.push(`/(app)/invoices/send/${invoice.id}`), }, { key: "delete", label: "Delete", icon: "trash-outline" as const, color: "#fff", backgroundColor: colors.destructive, onPress: () => confirmDelete(invoice.id, label), }, ] : [ { key: "status", label: "Status", icon: "flag-outline" as const, color: "#fff", backgroundColor: colors.warning, onPress: () => promptStatusChange(invoice.id, status), }, ]), ]; return ( router.push(`/(app)/invoices/${invoice.id}`)} onLongPress={() => promptStatusChange(invoice.id, status)} > {label} {invoice.client?.name ?? "Client"} {formatCurrency(invoice.totalAmount, invoice.currency)} Due {formatDate(invoice.dueDate)} ); }) )} { Alert.alert("Create invoice", "Choose how to start", [ { text: "Cancel", style: "cancel" }, { text: "With line items", onPress: () => router.push("/(app)/invoices/new"), }, { text: "Blank (for timer)", onPress: () => router.push("/(app)/invoices/new?blank=1"), }, ]); }} /> ); } const createInvoicesStyles = (colors: ThemeColors, _isDark: boolean) => StyleSheet.create({ filterScroll: { flexGrow: 0, marginBottom: spacing.sm, }, filters: { gap: spacing.sm, paddingRight: spacing.md, }, card: {}, cardInner: { padding: spacing.md, gap: spacing.md, }, cardTop: { flexDirection: "row", justifyContent: "space-between", gap: spacing.md, }, cardMeta: { flex: 1, gap: 4, }, invoiceNumber: { fontSize: 16, fontFamily: fonts.bodySemiBold, color: colors.foreground, }, clientName: { color: colors.mutedForeground, fontSize: 14, fontFamily: fonts.body, }, amount: { fontSize: 16, fontFamily: fonts.bodySemiBold, color: colors.foreground, }, cardBottom: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, date: { color: colors.mutedForeground, fontSize: 13, fontFamily: fonts.body, }, empty: { padding: spacing.lg, alignItems: "center", gap: spacing.sm, }, emptyTitle: { fontSize: 18, fontFamily: fonts.bodySemiBold, color: colors.foreground, }, emptyText: { textAlign: "center", color: colors.mutedForeground, fontFamily: fonts.body, lineHeight: 20, }, errorBox: { flex: 1, justifyContent: "center", padding: spacing.lg, gap: spacing.sm, }, errorTitle: { fontSize: 18, fontFamily: fonts.bodySemiBold, color: colors.foreground, }, errorText: { color: colors.mutedForeground, fontFamily: fonts.body, lineHeight: 20, }, });