import React, { useState, useEffect, useMemo } from "react"; import { View, Pressable, TextInput, StyleSheet, ActivityIndicator, ScrollView, Modal, Dimensions, } from "react-native"; import { useColorScheme } from "nativewind"; import * as DocumentPicker from "expo-document-picker"; import { Text } from "@/components/ui/text"; import { ChevronDown, Upload, X, FileText, Plus, Link2, Search, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { api, BASE_URL } from "@/lib/api"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { FormFlow } from "@/components/FormFlow"; import { getPlaceholderColor } from "@/lib/colors"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); type DeclarationType = "VAT" | "WITHHOLDING_TAX"; type Period = "MONTHLY" | "QUARTERLY" | "YEARLY" | ""; type FileEntry = { uri: string; name: string; type: string }; const DECLARATION_TYPES: SelectOption[] = [ { label: "VAT", value: "VAT" }, { label: "Withholding Tax", value: "WITHHOLDING_TAX" }, ]; const PERIODS: SelectOption[] = [ { label: "Monthly", value: "MONTHLY" }, { label: "Quarterly", value: "QUARTERLY" }, { label: "Semi-Annual", value: "SEMI_ANNUAL" }, { label: "Annual", value: "ANNUAL" }, { label: "One-Time", value: "ONE_TIME" }, ]; const S = StyleSheet.create({ input: { paddingHorizontal: 12, paddingVertical: 12, fontSize: 12, fontWeight: "500", borderRadius: 6, borderWidth: 1, textAlignVertical: "center", }, }); function useInputColors() { const { colorScheme } = useColorScheme(); const dark = colorScheme === "dark"; return { bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)", border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)", text: dark ? "#f1f5f9" : "#0f172a", placeholder: "rgba(100,116,139,0.45)", }; } function Field({ label, value, onChangeText, placeholder, numeric = false, flex, multiline = false, required = false, }: { label: string; value: string; onChangeText: (v: string) => void; placeholder: string; numeric?: boolean; flex?: number; multiline?: boolean; required?: boolean; }) { const c = useInputColors(); return ( {label} {required && *} ); } function PickerField({ label, value, onPress, required = false, }: { label: string; value: string; onPress: () => void; required?: boolean; }) { const c = useInputColors(); return ( {label} {required && *} {value} ); } export default function CreateDeclarationScreen() { const nav = useSirouRouter(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const c = useInputColors(); const [step, setStep] = useState(0); const [submitting, setSubmitting] = useState(false); // Step 1: Declaration Detail const [declarationNumber, setDeclarationNumber] = useState(""); const [type, setType] = useState("VAT"); const [title, setTitle] = useState(""); const [period, setPeriod] = useState(""); const [periodStart, setPeriodStart] = useState(""); const [periodEnd, setPeriodEnd] = useState(""); // Step 2: Tax Info const [tin, setTin] = useState(""); const [taxAccountNumber, setTaxAccountNumber] = useState(""); const [taxCentre, setTaxCentre] = useState(""); const [selectedInvoices, setSelectedInvoices] = useState([]); // Step 3: Files const [declarationFile, setDeclarationFile] = useState( null, ); const [receiptFiles, setReceiptFiles] = useState([]); const [suggestedFilename, setSuggestedFilename] = useState(""); // Step 4: Notes const [notes, setNotes] = useState(""); const [dueDate, setDueDate] = useState(""); // Pickers & Modals const [showTypePicker, setShowTypePicker] = useState(false); const [showPeriodPicker, setShowPeriodPicker] = useState(false); const [showInvoicePicker, setShowInvoicePicker] = useState(false); const [invoices, setInvoices] = useState([]); const [invoiceSearch, setInvoiceSearch] = useState(""); const [loadingInvoices, setLoadingInvoices] = useState(false); const steps = [ { key: "details", label: "Declaration Detail" }, { key: "tax", label: "Tax Info" }, { key: "files", label: "Files" }, { key: "notes", label: "Notes" }, { key: "review", label: "Review" }, ]; const openInvoicePicker = async () => { setLoadingInvoices(true); try { const response = await api.invoices.getAll({ query: { limit: 50 } }); setInvoices(Array.isArray(response) ? response : response.data || []); setInvoiceSearch(""); setShowInvoicePicker(true); } catch { toast.error("Error", "Failed to fetch invoices"); } finally { setLoadingInvoices(false); } }; const toggleInvoice = (inv: any) => { setSelectedInvoices((prev) => prev.find((i) => i.id === inv.id) ? prev.filter((i) => i.id !== inv.id) : [...prev, inv], ); }; const filteredInvoices = useMemo(() => { if (!invoiceSearch) return invoices; const q = invoiceSearch.toLowerCase(); return invoices.filter( (inv) => (inv.invoiceNumber || "").toLowerCase().includes(q) || (inv.customerName || "").toLowerCase().includes(q), ); }, [invoices, invoiceSearch]); const pickFile = async () => { try { const result = await DocumentPicker.getDocumentAsync({ type: "application/pdf", copyToCacheDirectory: true, }); if (!result.canceled && result.assets?.length > 0) { const asset = result.assets[0]; setDeclarationFile({ uri: asset.uri, name: asset.name, type: asset.mimeType || "application/pdf", }); } } catch { toast.warning("Picker", "Could not open document picker"); } }; const pickReceipts = async () => { try { const result = await DocumentPicker.getDocumentAsync({ type: "*/*", copyToCacheDirectory: true, multiple: true, }); if (!result.canceled && result.assets?.length > 0) { setReceiptFiles((prev) => [ ...prev, ...result.assets.map((a: any) => ({ uri: a.uri, name: a.name, type: a.mimeType || "application/octet-stream", })), ]); } } catch { toast.warning("Picker", "Could not open document picker"); } }; const handleNext = () => { if (step === 0) { if (!declarationNumber.trim()) { toast.error("Validation", "Declaration number is required"); return; } if (!title.trim()) { toast.error("Validation", "Title is required"); return; } if (!periodStart.trim()) { toast.error("Validation", "Period start is required"); return; } if (!periodEnd.trim()) { toast.error("Validation", "Period end is required"); return; } } setStep(step + 1); }; const handleSubmit = async () => { setSubmitting(true); try { const { accessToken } = useAuthStore.getState(); const payload: Record = { declarationNumber: declarationNumber.trim(), type, period, title: title.trim(), periodStart: new Date(periodStart).toISOString(), periodEnd: new Date(periodEnd).toISOString(), }; if (tin.trim()) payload.tin = tin.trim(); if (taxAccountNumber.trim()) payload.taxAccountNumber = taxAccountNumber.trim(); if (taxCentre.trim()) payload.taxCentre = taxCentre.trim(); if (notes.trim()) payload.notes = notes.trim(); if (dueDate.trim()) payload.dueDate = new Date(dueDate).toISOString(); if (suggestedFilename.trim()) payload.suggestedFilename = suggestedFilename.trim(); if (selectedInvoices.length > 0) { payload.invoiceIds = selectedInvoices.map((i) => i.id); } const hasFiles = declarationFile !== null || receiptFiles.length > 0; if (hasFiles) { const formData = new FormData(); formData.append("data", JSON.stringify(payload)); if (declarationFile) { formData.append("file", { uri: declarationFile.uri, name: declarationFile.name, type: declarationFile.type, } as any); } receiptFiles.forEach((rf) => { formData.append("receipts", { uri: rf.uri, name: rf.name, type: rf.type, } as any); }); const response = await fetch(`${BASE_URL}declarations`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "multipart/form-data", }, body: formData, }); if (!response.ok) throw new Error(await response.text()); } else { await api.declarations.create({ body: payload }); } toast.success("Success", "Declaration created"); nav.back(); } catch (error: any) { console.error("[CreateDeclaration] Error:", error); toast.error( "Error", error?.response?.data?.message || error?.data?.message || error?.message || "Failed to create declaration", ); } finally { setSubmitting(false); } }; return ( setStep(step - 1)} onComplete={handleSubmit} loading={submitting} completeLabel="Create Declaration" > {step === 0 && ( Declaration Detail setShowTypePicker(true)} /> setShowPeriodPicker(true)} /> )} {step === 1 && ( Tax Info {/* Link Invoices */} Link Invoices {loadingInvoices ? ( ) : ( <> 0 ? c.text : c.placeholder, }} numberOfLines={1} > {selectedInvoices.length > 0 ? `${selectedInvoices.length} invoice${selectedInvoices.length > 1 ? "s" : ""} linked` : "Select invoices to link (optional)"} )} )} {step === 2 && ( File Uploads MoR Declaration PDF {declarationFile ? ( {declarationFile.name} setDeclarationFile(null)}> ) : ( Tap to select PDF )} Payment Receipts {receiptFiles.length > 0 && ( {receiptFiles.map((rf, idx) => ( {rf.name} setReceiptFiles((p) => p.filter((_, i) => i !== idx), ) } > ))} )} Add Receipt )} {step === 3 && ( )} {step === 4 && ( Summary {dueDate ? : null} {tin ? : null} {taxAccountNumber ? ( ) : null} {taxCentre ? ( ) : null} {selectedInvoices.length > 0 ? ( 1 ? "s" : ""}`} /> ) : null} {notes && ( )} {declarationFile && ( )} {receiptFiles.length > 0 && ( Receipts ({receiptFiles.length}) {receiptFiles.map((rf, i) => ( {i + 1}. {rf.name} ))} )} )} {/* Invoice Picker Bottom Sheet */} setShowInvoicePicker(false)} > setShowInvoicePicker(false)} > e.stopPropagation()} > Link Invoices setShowInvoicePicker(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > {filteredInvoices.map((inv: any) => { const selected = selectedInvoices.find( (i) => i.id === inv.id, ); return ( toggleInvoice(inv)} className={`flex-row items-center py-3 px-3 rounded-[8px] mb-1 ${ selected ? "bg-primary/10" : "" }`} > {inv.customerName || "Unknown"} #{inv.invoiceNumber} · {inv.currency}{" "} {Number( inv.amount?.value || inv.amount || 0, ).toLocaleString()} {selected && ( )} ); })} {filteredInvoices.length === 0 && ( No invoices found )} setShowTypePicker(false)} > {DECLARATION_TYPES.map((opt) => ( { setType(v as DeclarationType); setShowTypePicker(false); }} /> ))} setShowPeriodPicker(false)} > {PERIODS.map((opt) => ( { setPeriod(v as Period); setShowPeriodPicker(false); }} /> ))} ); } function SummaryRow({ label, value }: { label: string; value: string }) { return ( {label} {value} ); }