fix: 5 issues - leads 500, prepaid amount/invoice, install RESOLVED, dup tickets, confirm fail

This commit is contained in:
Nemo
2026-03-24 14:52:40 +08:00
parent 16cf1c0616
commit 882e416a40
3 changed files with 40 additions and 32 deletions

View File

@@ -151,25 +151,26 @@ export default function NewClientScreen() {
startDate: new Date().toISOString(), startDate: new Date().toISOString(),
}); });
// 3. Create installation ticket // 3. Fetch the auto-created installation ticket (API creates one on client creation)
const ticketRes = await api.post('/api/v1/tickets', { let ticket: any = null;
clientId: client.id, try {
subject: `New Installation — ${firstName.trim()} ${lastName.trim()}`, const ticketsRes = await api.get(`/api/v1/tickets?clientId=${client.id}&type=INSTALLATION&limit=1`);
type: 'INSTALLATION', const items = ticketsRes.data?.data ?? ticketsRes.data ?? [];
priority: 'NORMAL', ticket = items[0] ?? null;
// Pass subType so activation ticket knows to gate on payment for PREPAID } catch {}
...(subType === 'PREPAID' ? { priority: 'HIGH' } : {}),
});
const ticket = ticketRes.data;
Alert.alert( Alert.alert(
'Client Onboarded! 🎉', 'Client Onboarded! 🎉',
`${firstName} ${lastName} has been registered.\n\nAn installation ticket has been created.`, `${firstName} ${lastName} has been registered.\n\nAn installation ticket has been created.`,
[{ [{
text: 'View Ticket', text: ticket ? 'View Ticket' : 'View Client',
onPress: () => { onPress: () => {
router.replace('/(app)/clients'); if (ticket) {
setTimeout(() => router.push(`/(app)/tasks/${ticket.id}`), 300); router.replace('/(app)/clients');
setTimeout(() => router.push(`/(app)/tasks/${ticket.id}`), 300);
} else {
router.replace('/(app)/clients');
}
}, },
}, { }, {
text: 'Done', text: 'Done',

View File

@@ -72,14 +72,13 @@ export default function LeadDetailScreen() {
}); });
const client = clientRes.data; const client = clientRes.data;
// Create installation ticket // Fetch the auto-created installation ticket (API creates one on client creation)
const ticketRes = await api.post('/api/v1/tickets', { let ticket: any = null;
clientId: client.id, try {
subject: `New Installation — ${lead.firstName} ${lead.lastName !== '—' ? lead.lastName : ''}`.trim(), const tRes = await api.get(`/api/v1/tickets?clientId=${client.id}&type=INSTALLATION&limit=1`);
type: 'INSTALLATION', const items = tRes.data?.data ?? tRes.data ?? [];
priority: 'NORMAL', ticket = items[0] ?? null;
}); } catch {}
const ticket = ticketRes.data;
// Mark lead as CONVERTED // Mark lead as CONVERTED
await api.patch(`/api/v1/leads/${id}`, { await api.patch(`/api/v1/leads/${id}`, {

View File

@@ -147,13 +147,12 @@ export default function TicketDetailScreen() {
: `Installation confirmed. Location recorded: ${coordStr}`; : `Installation confirmed. Location recorded: ${coordStr}`;
await api.post(`/api/v1/tickets/${id}/messages`, { body: note }).catch(() => {}); await api.post(`/api/v1/tickets/${id}/messages`, { body: note }).catch(() => {});
// 4. Create follow-up activation ticket // 4. Create follow-up activation ticket (non-fatal if fails for role reasons)
await api.post('/api/v1/tickets', { await api.post('/api/v1/tickets', {
clientId: ticket?.clientId, clientId: ticket?.clientId,
subject: `Account Activation — ${ticket?.client?.firstName ?? ''} ${ticket?.client?.lastName ?? ''}`.trim(), subject: `Account Activation — ${ticket?.client?.firstName ?? ''} ${ticket?.client?.lastName ?? ''}`.trim(),
type: 'BILLING', type: 'BILLING',
priority: 'NORMAL', priority: 'NORMAL',
description: `Follow-up after installation confirmed. Please activate the client account and generate the first invoice.\n\nInstallation ref: ${id}\nLocation: ${coordStr}`,
}).catch(() => {}); }).catch(() => {});
setInstNotes(''); setInstNotes('');
@@ -222,17 +221,26 @@ export default function TicketDetailScreen() {
const planPrice = Number(sub?.monthlyPrice ?? 0); const planPrice = Number(sub?.monthlyPrice ?? 0);
const submitPrepaidPayment = async () => { const submitPrepaidPayment = async () => {
const amt = parseFloat(prepaidPayAmount); const effectiveAmount = prepaidPayAmount || String(planPrice);
const amt = parseFloat(effectiveAmount);
if (!amt || amt <= 0) { Alert.alert('Invalid Amount', 'Enter a valid amount.'); return; } if (!amt || amt <= 0) { Alert.alert('Invalid Amount', 'Enter a valid amount.'); return; }
setPrepaidPaying(true); setPrepaidPaying(true);
try { try {
// 1. Record payment // 1. Generate first invoice for client
let invoiceId: string | undefined;
try {
const invRes = await api.post(`/api/v1/invoices/generate/${ticket.clientId}`);
invoiceId = invRes.data?.id;
} catch {}
// 2. Record payment (link to invoice if available)
await api.post('/api/v1/payments', { await api.post('/api/v1/payments', {
clientId: ticket.clientId, clientId: ticket.clientId,
amount: amt, amount: amt,
channel: prepaidPayMethod, channel: prepaidPayMethod,
paymentDate: new Date().toISOString(), paymentDate: new Date().toISOString(),
notes: 'First prepaid payment — account activation', notes: 'First prepaid payment — account activation',
...(invoiceId ? { invoiceId } : {}),
}); });
// 2. Activate subscription // 2. Activate subscription
if (sub?.id) { if (sub?.id) {
@@ -366,11 +374,11 @@ export default function TicketDetailScreen() {
{/* Amount */} {/* Amount */}
<Text style={{ fontSize: 13, fontWeight: '600', color: '#92400E', marginBottom: 6 }}>Amount</Text> <Text style={{ fontSize: 13, fontWeight: '600', color: '#92400E', marginBottom: 6 }}>Amount</Text>
<TextInput <TextInput
value={prepaidPayAmount || String(planPrice)} value={prepaidPayAmount}
onChangeText={setPrepaidPayAmount} onChangeText={setPrepaidPayAmount}
keyboardType="decimal-pad" keyboardType="decimal-pad"
style={{ borderWidth: 1.5, borderColor: '#FDE68A', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 20, fontWeight: '700', color: '#92400E', marginBottom: 12, backgroundColor: '#fff' }} style={{ borderWidth: 1.5, borderColor: '#FDE68A', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 20, fontWeight: '700', color: '#92400E', marginBottom: 12, backgroundColor: '#fff' }}
placeholder={String(planPrice)} placeholder={planPrice ? String(planPrice) : '0.00'}
/> />
{/* Method */} {/* Method */}
@@ -388,7 +396,7 @@ export default function TicketDetailScreen() {
</View> </View>
<SlideToConfirm <SlideToConfirm
label={`Slide to collect ₱${parseFloat(prepaidPayAmount || String(planPrice)).toLocaleString()} & activate`} label={`Slide to collect ₱${parseFloat(prepaidPayAmount || String(planPrice) || '0').toLocaleString()} & activate`}
color="#D97706" color="#D97706"
onConfirm={submitPrepaidPayment} onConfirm={submitPrepaidPayment}
disabled={prepaidPaying} disabled={prepaidPaying}