fix: 5 bugs - tab height, collect pay modal, invoice picker in record payment, comment body field, dashboard tickets label
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, ScrollView, Alert, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, TextInput, TouchableOpacity, ScrollView, Alert, ActivityIndicator, Modal } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../../services/api';
|
||||
|
||||
const METHODS = [
|
||||
@@ -19,15 +20,26 @@ export default function RecordPaymentScreen() {
|
||||
prefillAccountNumber?: string;
|
||||
}>();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [client, setClient] = useState<any>(null);
|
||||
const [amount, setAmount] = useState('');
|
||||
const [method, setMethod] = useState('CASH');
|
||||
const [search, setSearch] = useState('');
|
||||
const [client, setClient] = useState<any>(null);
|
||||
const [invoice, setInvoice] = useState<any>(null);
|
||||
const [showInvoicePicker, setShowInvoicePicker] = useState(false);
|
||||
const [amount, setAmount] = useState('');
|
||||
const [method, setMethod] = useState('CASH');
|
||||
const [reference, setReference] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
// Auto-fill client if navigated from client detail
|
||||
const { data: clientInvoices } = useQuery({
|
||||
queryKey: ['record-payment-invoices', client?.id],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/api/v1/invoices?clientId=${client.id}&limit=50`);
|
||||
const all: any[] = res.data?.data ?? res.data ?? [];
|
||||
return all.filter((inv: any) => ['SENT', 'PARTIAL', 'OVERDUE'].includes(inv.status) && Number(inv.balance) > 0);
|
||||
},
|
||||
enabled: !!client?.id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (params.prefillClientId && params.prefillName) {
|
||||
setClient({
|
||||
@@ -39,6 +51,9 @@ export default function RecordPaymentScreen() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// When client changes, reset invoice selection
|
||||
useEffect(() => { setInvoice(null); }, [client?.id]);
|
||||
|
||||
const searchClient = async () => {
|
||||
if (!search.trim()) return;
|
||||
setSearching(true);
|
||||
@@ -76,8 +91,9 @@ export default function RecordPaymentScreen() {
|
||||
try {
|
||||
await api.post('/api/v1/payments', {
|
||||
clientId: client.id,
|
||||
invoiceId: invoice?.id ?? undefined,
|
||||
amount: amt,
|
||||
channel: method, // API uses `channel` not `paymentMethod`
|
||||
channel: method,
|
||||
referenceNumber: reference.trim() || undefined,
|
||||
paymentDate: new Date().toISOString(),
|
||||
});
|
||||
@@ -153,6 +169,36 @@ export default function RecordPaymentScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Invoice selection (shown after client selected) */}
|
||||
{client && (
|
||||
<>
|
||||
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>Invoice <Text style={{ fontWeight: '400', color: '#94A3B8' }}>(optional)</Text></Text>
|
||||
{!clientInvoices ? (
|
||||
<ActivityIndicator color="#0891B2" style={{ marginBottom: 16 }} />
|
||||
) : clientInvoices.length === 0 ? (
|
||||
<View style={{ backgroundColor: '#F0FDF4', borderRadius: 14, padding: 14, marginBottom: 16 }}>
|
||||
<Text style={{ fontSize: 15, color: '#166534', fontWeight: '600' }}>✓ No outstanding invoices</Text>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={{ backgroundColor: invoice ? '#ECFEFF' : '#FFF', borderWidth: 1.5, borderColor: invoice ? '#0891B2' : '#E2E8F0', borderRadius: 14, paddingHorizontal: 16, paddingVertical: 14, marginBottom: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
|
||||
onPress={() => setShowInvoicePicker(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View>
|
||||
<Text style={{ fontSize: 16, fontWeight: invoice ? '700' : '400', color: invoice ? '#0891B2' : '#94A3B8' }}>
|
||||
{invoice ? invoice.invoiceNumber : 'Select invoice to apply payment'}
|
||||
</Text>
|
||||
{invoice && (
|
||||
<Text style={{ fontSize: 13, color: '#0891B2', marginTop: 2 }}>Balance: ₱{Number(invoice.balance).toLocaleString()}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={{ color: '#94A3B8', fontSize: 18 }}>▾</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Amount */}
|
||||
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A', marginBottom: 10 }}>Amount (₱)</Text>
|
||||
<TextInput
|
||||
@@ -206,6 +252,41 @@ export default function RecordPaymentScreen() {
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Invoice Picker Modal */}
|
||||
<Modal visible={showInvoicePicker} transparent animationType="slide" onRequestClose={() => setShowInvoicePicker(false)}>
|
||||
<TouchableOpacity style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }} activeOpacity={1} onPress={() => setShowInvoicePicker(false)}>
|
||||
<TouchableOpacity activeOpacity={1} style={{ backgroundColor: '#FFF', borderTopLeftRadius: 28, borderTopRightRadius: 28, padding: 24, maxHeight: '70%' }}>
|
||||
<Text style={{ fontSize: 20, fontWeight: '800', color: '#0F172A', marginBottom: 4 }}>Select Invoice</Text>
|
||||
<Text style={{ fontSize: 15, color: '#64748B', marginBottom: 20 }}>Choose which invoice to apply payment to</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => { setInvoice(null); setShowInvoicePicker(false); }}
|
||||
style={{ padding: 16, borderRadius: 14, marginBottom: 10, backgroundColor: !invoice ? '#ECFEFF' : '#F8FAFC', borderWidth: 1.5, borderColor: !invoice ? '#0891B2' : '#F1F5F9' }}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 16, fontWeight: '600', color: !invoice ? '#0891B2' : '#64748B' }}>No specific invoice (general payment)</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{(clientInvoices ?? []).map((inv: any) => (
|
||||
<TouchableOpacity
|
||||
key={inv.id}
|
||||
onPress={() => { setInvoice(inv); setAmount(String(inv.balance)); setShowInvoicePicker(false); }}
|
||||
style={{ padding: 16, borderRadius: 14, marginBottom: 10, backgroundColor: invoice?.id === inv.id ? '#ECFEFF' : '#F8FAFC', borderWidth: 1.5, borderColor: invoice?.id === inv.id ? '#0891B2' : '#F1F5F9' }}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text style={{ fontSize: 16, fontWeight: '700', color: '#0F172A' }}>{inv.invoiceNumber}</Text>
|
||||
<Text style={{ fontSize: 16, fontWeight: '800', color: '#991B1B' }}>₱{Number(inv.balance).toLocaleString()}</Text>
|
||||
</View>
|
||||
<Text style={{ fontSize: 13, color: '#64748B', marginTop: 3 }}>
|
||||
{inv.dueDate ? `Due: ${new Date(inv.dueDate).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}` : 'No due date'} · {inv.status}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user