fix: [BLOCKER] status RESOLVED race condition - use setQueryData from PATCH response, no refetch race

This commit is contained in:
Nemo
2026-03-24 16:13:36 +08:00
parent a5cac16924
commit 2325d5ad62

View File

@@ -85,8 +85,11 @@ export default function TicketDetailScreen() {
const updateStatus = useMutation({
mutationFn: async ({ status, activationTicket, subId }: { status: string; activationTicket: boolean; subId?: string }) => {
await api.patch(`/api/v1/tickets/${id}`, { status });
// If this is an activation ticket being RESOLVED → activate subscription
// Return the patched ticket so onSuccess can set cache directly
const res = await api.patch(`/api/v1/tickets/${id}`, { status });
const updatedTicket = res.data;
// Activate subscription when resolving an activation ticket
if ((status === 'RESOLVED' || status === 'CLOSED') && activationTicket && subId) {
await api.patch(`/api/v1/subscriptions/${subId}`, { status: 'ACTIVE' }).catch(() => {});
}
@@ -94,15 +97,19 @@ export default function TicketDetailScreen() {
await api.post(`/api/v1/tickets/${id}/messages`, {
body: `Status changed to ${status.replace('_', ' ')} by ${who}`,
}).catch(() => {});
return updatedTicket;
},
onSuccess: async () => {
onSuccess: (updatedTicket) => {
// Directly inject fresh data into cache — avoids race condition with refetch
if (updatedTicket) {
qc.setQueryData(['task', id], (old: any) => ({ ...(old ?? {}), ...updatedTicket }));
}
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(() => {});
// Invalidate list queries in background (non-blocking)
qc.invalidateQueries({ queryKey: ['tasks'] });
qc.invalidateQueries({ queryKey: ['clients'] });
qc.invalidateQueries({ queryKey: ['task-client', updatedTicket?.clientId] });
},
onError: (e: any) => {
const msg = e?.response?.data?.message ?? 'Could not update status.';
@@ -130,8 +137,11 @@ export default function TicketDetailScreen() {
const confirmInstallation = async () => {
setInstConfirming(true);
try {
// 1. Resolve the ticket
await api.patch(`/api/v1/tickets/${id}`, { status: 'RESOLVED' });
// 1. Resolve the ticket — use response to directly update cache
const patchRes = await api.patch(`/api/v1/tickets/${id}`, { status: 'RESOLVED' });
if (patchRes.data) {
qc.setQueryData(['task', id], (old: any) => ({ ...(old ?? {}), ...patchRes.data }));
}
// 2. Update client location with recorded coordinates (skip if GPS unavailable)
if (ticket?.clientId && coords) {
@@ -163,12 +173,9 @@ export default function TicketDetailScreen() {
setInstNotes('');
setCoords(null);
// Invalidate + force refetch so the status shows RESOLVED immediately
await qc.invalidateQueries({ queryKey: ['tasks'] });
await qc.invalidateQueries({ queryKey: ['task', id] });
await refetch();
await qc.invalidateQueries({ queryKey: ['client', ticket?.clientId] });
await qc.invalidateQueries({ queryKey: ['client-tickets', ticket?.clientId] });
// Invalidate list queries in background (cache already updated above)
qc.invalidateQueries({ queryKey: ['tasks'] });
qc.invalidateQueries({ queryKey: ['clients'] });
Alert.alert(
'Installation Complete! ✓',
'Ticket resolved, location recorded, and activation ticket created.',