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'] });
}}
/>
)}
);
}