import { View, Text, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator, Linking, Alert, TextInput, Modal, KeyboardAvoidingView, Platform } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router } from 'expo-router'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { api } from '../../../services/api'; // ── Pay Invoice Modal ────────────────────────────────────────────────────────── function PayInvoiceModal({ invoice, onClose, onSuccess }: { invoice: any; onClose: () => void; onSuccess: () => void }) { const [amount, setAmount] = useState(''); const [method, setMethod] = useState('CASH'); const [ref, setRef] = useState(''); const [loading, setLoading] = useState(false); const METHODS = ['CASH', 'GCASH', 'MAYA', 'BANK']; const submit = async () => { const amt = Number(amount); if (!amt || amt <= 0) return Alert.alert('Required', 'Enter a valid amount.'); if (amt > Number(invoice.balance)) { Alert.alert('Over Payment', `Amount exceeds balance of ₱${Number(invoice.balance).toLocaleString()}`); return; } setLoading(true); try { await api.post('/api/v1/payments', { clientId: invoice.clientId, invoiceId: invoice.id, amount: amt, channel: method, referenceNumber: ref.trim() || undefined, paymentDate: new Date().toISOString(), }); Alert.alert('Payment Recorded! ✓', `₱${amt.toLocaleString()} applied to ${invoice.invoiceNumber}`); onSuccess(); } catch (e: any) { const msg = e?.response?.data?.message; Alert.alert('Error', Array.isArray(msg) ? msg.join('\n') : msg ?? 'Payment failed.'); } finally { setLoading(false); } }; return ( Record Payment {invoice.client?.firstName} {invoice.client?.lastName} · {invoice.client?.accountNumber} {invoice.invoiceNumber} · Balance: ₱{Number(invoice.balance).toLocaleString()} Amount (₱) Method {METHODS.map(m => ( setMethod(m)} style={{ flex: 1, borderRadius: 12, paddingVertical: 12, alignItems: 'center', marginHorizontal: 3, backgroundColor: method === m ? '#0891B2' : '#F1F5F9' }} activeOpacity={0.7} > {m} ))} Reference # (optional) 0 ? '#059669' : '#CBD5E1', borderRadius: 14, paddingVertical: 18, alignItems: 'center' }} onPress={submit} disabled={loading} activeOpacity={0.8} > {loading ? : Confirm Payment} ); } export default function CollectScreen() { const [search, setSearch] = useState(''); const [payingInvoice, setPayingInvoice] = useState(null); const qc = useQueryClient(); const { data: invoices, isLoading, isRefetching, refetch } = useQuery({ queryKey: ['unpaid-invoices'], queryFn: async () => { const res = await api.get('/api/v1/invoices?limit=100'); const all: any[] = res.data?.data ?? res.data ?? []; const unpaid = all.filter((inv: any) => ['SENT', 'PARTIAL', 'OVERDUE'].includes(inv.status) && Number(inv.balance) > 0 ); // Sort: overdue first, then by due date ascending unpaid.sort((a: any, b: any) => { const today = new Date().getTime(); const da = a.dueDate ? new Date(a.dueDate).getTime() : Infinity; const db = b.dueDate ? new Date(b.dueDate).getTime() : Infinity; const aOverdue = da < today; const bOverdue = db < today; if (aOverdue && !bOverdue) return -1; if (!aOverdue && bOverdue) return 1; return da - db; }); return unpaid; }, }); const filtered = (invoices ?? []).filter((inv: any) => { if (!search.trim()) return true; const q = search.toLowerCase(); const name = `${inv.client?.firstName ?? ''} ${inv.client?.lastName ?? ''}`.toLowerCase(); const acct = inv.client?.accountNumber?.toLowerCase() ?? ''; const num = inv.invoiceNumber?.toLowerCase() ?? ''; return name.includes(q) || acct.includes(q) || num.includes(q); }); const today = new Date(); const navigate = (inv: any) => { const lat = inv.client?.lat; const lng = inv.client?.lng; if (!lat || !lng) { Alert.alert('No Location', `${inv.client?.firstName} ${inv.client?.lastName} has no recorded location yet.\n\nLocation is set during installation confirmation.`); return; } const name = encodeURIComponent(`${inv.client?.firstName} ${inv.client?.lastName}`); Alert.alert( '📍 Navigate to Client', `${inv.client?.firstName} ${inv.client?.lastName}\n${inv.client?.address ?? ''}`, [ { text: 'Google Maps', onPress: () => Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&destination_place_id=${name}`) }, { text: 'Waze', onPress: () => Linking.openURL(`waze://?ll=${lat},${lng}&navigate=yes`) }, { text: 'Cancel', style: 'cancel' }, ] ); }; const totalUnremitted = filtered.reduce((s: number, inv: any) => s + Number(inv.balance ?? 0), 0); return ( {/* Header */} Collect Unpaid invoices · sorted by due date {/* Action Buttons */} router.push('/(app)/payments/record')} activeOpacity={0.8} > + Record Payment router.push('/(app)/remittances')} activeOpacity={0.8} > 📋 Remittances {/* Search */} {search.length > 0 && ( setSearch('')} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}> × )} {isLoading ? ( ) : ( } > {/* Summary banner */} {filtered.length > 0 && ( {filtered.length} unpaid invoice{filtered.length !== 1 ? 's' : ''} Total outstanding ₱{totalUnremitted.toLocaleString()} )} {filtered.length === 0 ? ( {search ? 'No results found' : 'All invoices paid! 🎉'} ) : ( filtered.map((inv: any) => { const dueDate = inv.dueDate ? new Date(inv.dueDate) : null; const isOverdue = dueDate && dueDate < today; const daysLeft = dueDate ? Math.ceil((dueDate.getTime() - today.getTime()) / 86400000) : null; const hasLocation = !!(inv.client?.lat && inv.client?.lng); return ( setPayingInvoice(inv)} activeOpacity={0.8} style={{ backgroundColor: '#FFF', borderRadius: 16, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: isOverdue ? '#FCA5A5' : '#F1F5F9', borderLeftWidth: 4, borderLeftColor: isOverdue ? '#DC2626' : '#F59E0B', }} > {/* Client + amount */} {inv.client?.firstName} {inv.client?.lastName} {inv.client?.accountNumber} · {inv.invoiceNumber} ₱{Number(inv.balance).toLocaleString()} {inv.status === 'PARTIAL' && ( PARTIAL )} {/* Due date + navigate */} {isOverdue ? `⚠️ Overdue ${Math.abs(daysLeft ?? 0)}d` : daysLeft !== null ? `Due in ${daysLeft}d` : 'No due date'} navigate(inv)} style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: hasLocation ? '#ECFEFF' : '#F1F5F9', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 7, }} activeOpacity={0.7} > {hasLocation ? '📍 Navigate' : '📍 No location'} ); }) )} )} {payingInvoice && ( setPayingInvoice(null)} onSuccess={() => { setPayingInvoice(null); qc.invalidateQueries({ queryKey: ['unpaid-invoices'] }); }} /> )} ); }