Skip to content

Commit 88a3e50

Browse files
dreynowclaude
andcommitted
fix: cancelled flag in useClientDetail, assign error display, remove unused import
- useClientDetail: add cancelled flag to prevent setState on unmounted component - ClientDetailPage: show error message when agent assignment fails - Remove unused ChevronDown import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce2dec1 commit 88a3e50

2 files changed

Lines changed: 24 additions & 9 deletions

File tree

‎apps/observatory/src/hooks/useClientDetail.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,21 @@ export function useClientDetail(clientId: string | undefined) {
7474

7575
const fetchAll = useCallback(async () => {
7676
if (!clientId) return;
77+
let cancelled = false;
7778
setLoading(true);
7879
setError(null);
7980

8081
try {
8182
// Fetch client detail from CP API
8283
const clientResp = await cpFetch(`/v1/clients/${clientId}`);
84+
if (cancelled) return;
8385
if (!clientResp.ok) {
8486
setError(`Failed to load client (${clientResp.status})`);
8587
setLoading(false);
8688
return;
8789
}
8890
const clientData: ClientDetail = await clientResp.json();
91+
if (cancelled) return;
8992
setClient(clientData);
9093

9194
// Fetch QB data if connected (parallel, best-effort)
@@ -94,28 +97,31 @@ export function useClientDetail(clientId: string | undefined) {
9497
if (clientData.quickbooks_connected) {
9598
promises.push(
9699
cpFetch(`/v1/clients/${clientId}/qb/company`)
97-
.then(async (r) => { if (r.ok) setCompany(await r.json()); })
100+
.then(async (r) => { if (!cancelled && r.ok) setCompany(await r.json()); })
98101
.catch(() => {})
99102
);
100103
promises.push(
101104
cpFetch(`/v1/clients/${clientId}/qb/vendors`)
102-
.then(async (r) => { if (r.ok) setVendors(await r.json()); })
105+
.then(async (r) => { if (!cancelled && r.ok) setVendors(await r.json()); })
103106
.catch(() => {})
104107
);
105108
}
106109

107110
// Fetch escalations for this client from Observatory API
108111
promises.push(
109112
apiFetch(`/v1/escalations?client_id=${clientId}&limit=10`)
110-
.then(async (r) => { if (r.ok) setEscalations(await r.json()); })
113+
.then(async (r) => { if (!cancelled && r.ok) setEscalations(await r.json()); })
111114
.catch(() => {})
112115
);
113116

114117
await Promise.all(promises);
115118
} catch (e) {
116-
setError(e instanceof Error ? e.message : 'Network error');
119+
if (!cancelled) setError(e instanceof Error ? e.message : 'Network error');
117120
}
118-
setLoading(false);
121+
if (!cancelled) setLoading(false);
122+
123+
// Return cleanup for useEffect
124+
return () => { cancelled = true; };
119125
}, [clientId]);
120126

121127
useEffect(() => {

‎apps/observatory/src/pages/ClientDetailPage.tsx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
33
import { motion } from 'framer-motion';
44
import {
55
ArrowLeft, Building2, Check, Clock, Users, AlertTriangle, DollarSign,
6-
RefreshCw, Plus, X, Link2, ChevronDown, Shield,
6+
RefreshCw, Plus, X, Link2, Shield,
77
} from 'lucide-react';
88
import { useClientDetail, type ClientAgent, type QbVendor, type Escalation } from '../hooks/useClientDetail';
99
import { DELEGATION_TEMPLATES } from '../lib/constants';
@@ -36,6 +36,7 @@ export const ClientDetailPage: React.FC = () => {
3636
const [assignName, setAssignName] = useState('');
3737
const [assignTemplate, setAssignTemplate] = useState(1); // AP Clerk default
3838
const [assigning, setAssigning] = useState(false);
39+
const [assignError, setAssignError] = useState<string | null>(null);
3940
const [syncing, setSyncing] = useState(false);
4041
const [unassigningAgent, setUnassigningAgent] = useState<string | null>(null);
4142

@@ -67,10 +68,15 @@ export const ClientDetailPage: React.FC = () => {
6768
const handleAssign = async () => {
6869
if (!assignName.trim()) return;
6970
setAssigning(true);
70-
await assignAgent(assignName.trim(), [...template.scopes], 4);
71+
setAssignError(null);
72+
const result = await assignAgent(assignName.trim(), [...template.scopes], 4);
7173
setAssigning(false);
72-
setShowAssign(false);
73-
setAssignName('');
74+
if (result.ok) {
75+
setShowAssign(false);
76+
setAssignName('');
77+
} else {
78+
setAssignError(result.error || 'Failed to assign agent');
79+
}
7480
};
7581

7682
const handleUnassign = async (agentName: string) => {
@@ -227,6 +233,9 @@ export const ClientDetailPage: React.FC = () => {
227233
</div>
228234
<p className="text-[9px] text-[#9C978E] mt-1">{template.description}{template.maxCost ? ` - up to $${template.maxCost.toLocaleString()}` : ''}</p>
229235
</div>
236+
{assignError && (
237+
<p className="text-xs text-[#C23A3A] bg-[#FDF0F0] border border-[#F0C6C6] rounded-md px-3 py-2">{assignError}</p>
238+
)}
230239
<button
231240
onClick={handleAssign}
232241
disabled={!assignName.trim() || assigning}

0 commit comments

Comments
 (0)