diff --git a/app/(app)/clients/[id].tsx b/app/(app)/clients/[id].tsx
index e3439ea..8c7d501 100644
--- a/app/(app)/clients/[id].tsx
+++ b/app/(app)/clients/[id].tsx
@@ -268,11 +268,33 @@ export default function ClientDetailScreen() {
No active subscription
) : (
+ {/* Plan type badge */}
+ {sub.type && (
+
+
+ {sub.type}
+
+
+ {sub.status}
+
+
+ )}
-
-
-
+
+
diff --git a/app/(app)/tasks/[id].tsx b/app/(app)/tasks/[id].tsx
index 95e4cbb..1ad6773 100644
--- a/app/(app)/tasks/[id].tsx
+++ b/app/(app)/tasks/[id].tsx
@@ -59,35 +59,40 @@ export default function TicketDetailScreen() {
const [comment, setComment] = useState('');
const [sendingComment, setSendingComment] = useState(false);
- // Prepaid activation payment state
- const [prepaidPayAmount, setPrepaidPayAmount] = useState('');
- const [prepaidPayMethod, setPrepaidPayMethod] = useState('CASH');
- const [prepaidPaying, setPrepaidPaying] = useState(false);
-
const { data: ticket, isLoading, refetch } = useQuery({
queryKey: ['task', id],
queryFn: () => api.get(`/api/v1/tickets/${id}`).then(r => r.data),
staleTime: 0,
});
- // Fetch client with subscriptions when ticket loads (for prepaid gate)
- const { data: clientDetail } = useQuery({
+ // For activation tickets — fetch client (subscription) + first invoice
+ const isActivationTicket = ticket?.type === 'BILLING' && ticket?.subject?.includes('Activation');
+
+ const { data: clientDetail, refetch: refetchClient } = useQuery({
queryKey: ['task-client', ticket?.clientId],
queryFn: () => api.get(`/api/v1/clients/${ticket.clientId}`).then(r => r.data),
- enabled: !!ticket?.clientId && ticket?.type === 'BILLING',
+ enabled: !!ticket?.clientId && isActivationTicket,
+ staleTime: 0,
});
- // Fetch existing payments for this client (to check if first payment done)
- const { data: clientPayments, refetch: refetchPayments } = useQuery({
- queryKey: ['task-client-payments', ticket?.clientId],
- queryFn: () => api.get(`/api/v1/payments?clientId=${ticket.clientId}&limit=5`).then(r => r.data?.data ?? r.data ?? []),
- enabled: !!ticket?.clientId && ticket?.type === 'BILLING',
+ // Fetch invoices for this client (to check if first invoice is PAID)
+ const { data: clientInvoices, refetch: refetchInvoices } = useQuery({
+ queryKey: ['task-client-invoices', ticket?.clientId],
+ queryFn: () => api.get(`/api/v1/invoices?clientId=${ticket.clientId}&limit=5`).then(r => r.data?.data ?? r.data ?? []),
+ enabled: !!ticket?.clientId && isActivationTicket,
+ staleTime: 0,
});
const updateStatus = useMutation({
mutationFn: async (status: TaskStatus) => {
await api.patch(`/api/v1/tickets/${id}`, { status });
- // Log status change as a system comment
+ // If this is an activation ticket being RESOLVED → activate subscription
+ if ((status === 'RESOLVED' || status === 'CLOSED') && isActivationTicket) {
+ const sub = clientDetail?.subscriptions?.[0];
+ if (sub?.id && sub?.status !== 'ACTIVE') {
+ await api.patch(`/api/v1/subscriptions/${sub.id}`, { status: 'ACTIVE' }).catch(() => {});
+ }
+ }
const who = user?.firstName ?? 'Staff';
await api.post(`/api/v1/tickets/${id}/messages`, {
body: `Status changed to ${status.replace('_', ' ')} by ${who}`,
@@ -97,7 +102,10 @@ export default function TicketDetailScreen() {
setShowStatusPicker(false);
await qc.invalidateQueries({ queryKey: ['task', id] });
await qc.invalidateQueries({ queryKey: ['tasks'] });
+ await qc.invalidateQueries({ queryKey: ['clients'] });
await refetch();
+ await refetchClient().catch(() => {});
+ await refetchInvoices().catch(() => {});
},
onError: () => Alert.alert('Error', 'Could not update status.'),
});
@@ -147,7 +155,12 @@ export default function TicketDetailScreen() {
: `Installation confirmed. Location recorded: ${coordStr}`;
await api.post(`/api/v1/tickets/${id}/messages`, { body: note }).catch(() => {});
- // 4. Create follow-up activation ticket (non-fatal if fails for role reasons)
+ // 4. Generate first invoice so it appears in Collect screen
+ if (ticket?.clientId) {
+ await api.post(`/api/v1/invoices/generate/${ticket.clientId}`).catch(() => {});
+ }
+
+ // 5. Create follow-up activation ticket (non-fatal)
await api.post('/api/v1/tickets', {
clientId: ticket?.clientId,
subject: `Account Activation — ${ticket?.client?.firstName ?? ''} ${ticket?.client?.lastName ?? ''}`.trim(),
@@ -210,60 +223,13 @@ export default function TicketDetailScreen() {
const typeBg = TYPE_BG[ticket?.type] ?? '#F1F5F9';
const messages: any[] = ticket?.messages ?? [];
- // Prepaid activation gate
+ // Activation ticket gate — check first invoice paid
const sub = clientDetail?.subscriptions?.[0];
- const isPrepaidActivation =
- ticket?.type === 'BILLING' &&
- ticket?.subject?.includes('Activation') &&
- sub?.type === 'PREPAID' &&
- sub?.status === 'PENDING';
- const hasFirstPayment = Array.isArray(clientPayments) && clientPayments.length > 0;
- const planPrice = Number(sub?.monthlyPrice ?? 0);
-
- const submitPrepaidPayment = async () => {
- const effectiveAmount = prepaidPayAmount || String(planPrice);
- const amt = parseFloat(effectiveAmount);
- if (!amt || amt <= 0) { Alert.alert('Invalid Amount', 'Enter a valid amount.'); return; }
- setPrepaidPaying(true);
- try {
- // 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', {
- clientId: ticket.clientId,
- amount: amt,
- channel: prepaidPayMethod,
- paymentDate: new Date().toISOString(),
- notes: 'First prepaid payment — account activation',
- ...(invoiceId ? { invoiceId } : {}),
- });
- // 2. Activate subscription
- if (sub?.id) {
- await api.patch(`/api/v1/subscriptions/${sub.id}`, { status: 'ACTIVE' }).catch(() => {});
- }
- // 3. Log comment + resolve ticket
- await api.post(`/api/v1/tickets/${id}/messages`, {
- body: `First payment of ₱${amt.toLocaleString()} recorded via ${prepaidPayMethod}. Account activated.`,
- }).catch(() => {});
- await api.patch(`/api/v1/tickets/${id}`, { status: 'RESOLVED' });
-
- await refetch();
- await refetchPayments();
- qc.invalidateQueries({ queryKey: ['tasks'] });
- qc.invalidateQueries({ queryKey: ['clients'] });
- Alert.alert('Account Activated! ✓', `Payment of ₱${amt.toLocaleString()} recorded and account is now ACTIVE.`);
- } catch (e: any) {
- const msg = e?.response?.data?.message ?? 'Could not process payment.';
- Alert.alert('Error', Array.isArray(msg) ? msg.join('\n') : msg);
- } finally {
- setPrepaidPaying(false);
- }
- };
+ const isPrepaid = sub?.type === 'PREPAID';
+ const invoiceList: any[] = Array.isArray(clientInvoices) ? clientInvoices : [];
+ const firstInvoice = invoiceList[0] ?? null;
+ const firstInvoicePaid = firstInvoice?.status === 'PAID' || firstInvoice?.balance === 0;
+ const blockResolve = isActivationTicket && isPrepaid && !firstInvoicePaid;
return (
@@ -357,55 +323,60 @@ export default function TicketDetailScreen() {
) : null}
- {/* ── PREPAID ACTIVATION GATE ── */}
- {isPrepaidActivation && (
-
-
- {hasFirstPayment ? '✓ Payment Received — Ready to Activate' : '⚠️ Prepaid — Payment Required'}
-
-
- {hasFirstPayment
- ? 'First payment has been recorded. Account is now active.'
- : `Collect ₱${planPrice.toLocaleString()} first payment before activating this account.`}
-
-
- {!hasFirstPayment && !isDone && (
+ {/* ── ACTIVATION TICKET BANNER ── */}
+ {isActivationTicket && !isDone && (
+
+ {blockResolve ? (
<>
- {/* Amount */}
- Amount
-
-
- {/* Method */}
- Payment Method
-
- {[['CASH','Cash'],['GCASH','GCash'],['MAYA','Maya'],['BANK_TRANSFER','Bank']].map(([id, label]) => (
- setPrepaidPayMethod(id)}
- style={{ paddingHorizontal: 14, paddingVertical: 8, borderRadius: 20, borderWidth: 1.5,
- borderColor: prepaidPayMethod === id ? '#D97706' : '#FDE68A',
- backgroundColor: prepaidPayMethod === id ? '#FEF3C7' : '#fff' }}
- >
- {label}
-
- ))}
-
-
-
+
+ ⚠️ First Invoice Not Yet Paid
+
+
+ This is a PREPAID account. The first month's invoice must be settled before this account can be activated.
+ {'\n\n'}Go to the Collect screen to record the payment, then come back here to resolve this ticket.
+
+ {firstInvoice && (
+
+
+ Invoice #{firstInvoice.invoiceNumber}
+
+
+ Balance: ₱{Number(firstInvoice.balance ?? firstInvoice.total ?? 0).toLocaleString()}
+
+
+ Status: {firstInvoice.status}
+
+
+ )}
+ >
+ ) : (
+ <>
+
+ ✓ Ready to Activate
+
+
+ {isPrepaid
+ ? 'First invoice has been paid. Tap "Update Status" → Resolved to activate this account.'
+ : 'Postpaid account is ready to activate. Tap "Update Status" → Resolved to activate.'}
+
>
)}
)}
+ {isActivationTicket && isDone && (
+
+ ✅ Account Activated
+
+ Subscription is now ACTIVE.
+
+
+ )}
+
{/* ── INSTALLATION SECTION ── */}
{isInstallation && (
<>
@@ -663,9 +634,13 @@ export default function TicketDetailScreen() {
key={s}
onPress={() => {
if (isActive) return;
- if (isPrepaidActivation && !hasFirstPayment && (s === 'RESOLVED' || s === 'CLOSED')) {
+ if (blockResolve && (s === 'RESOLVED' || s === 'CLOSED')) {
setShowStatusPicker(false);
- Alert.alert('Payment Required', 'This is a PREPAID account. Please collect and record the first payment before resolving this ticket.');
+ Alert.alert(
+ 'Invoice Not Yet Paid',
+ 'This is a PREPAID account. The first month\'s invoice must be paid before activating the account.\n\nGo to Collect screen to record the payment first.',
+ [{ text: 'OK' }]
+ );
return;
}
updateStatus.mutate(s);