Skip to content

Commit bbf39ef

Browse files
authored
Merge pull request #50 from incial/dev
Implement optimized user-specific task and meeting retrieval API
2 parents ca4115e + ff93d3f commit bbf39ef

25 files changed

+382
-127
lines changed

client/pages/AdminPerformancePage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,14 @@ export const AdminPerformancePage: React.FC = () => {
137137
}
138138
};
139139

140-
fetchAndCalculate();
140+
let mounted = true;
141+
const loadData = async () => {
142+
if (mounted) {
143+
await fetchAndCalculate();
144+
}
145+
};
146+
loadData();
147+
return () => { mounted = false; };
141148
}, []);
142149

143150
const totalTasks = stats.reduce((acc, s) => acc + s.total, 0);

client/pages/AdminUserManagementPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ export const AdminUserManagementPage: React.FC = () => {
5555
};
5656

5757
useEffect(() => {
58-
fetchUsers();
58+
let mounted = true;
59+
const loadData = async () => {
60+
if (mounted) {
61+
await fetchUsers();
62+
}
63+
};
64+
loadData();
65+
return () => { mounted = false; };
5966
}, []);
6067

6168
const filteredUsers = users.filter(u => {

client/pages/AnalyticsPage.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,25 @@ export const AnalyticsPage: React.FC<AnalyticsPageProps> = ({ title }) => {
2525
const hasPermission = user?.role === 'ROLE_SUPER_ADMIN';
2626

2727
useEffect(() => {
28+
let mounted = true;
2829
const loadData = async () => {
30+
if (!mounted) return;
2931
if (hasPermission) {
3032
try {
3133
const data = await crmApi.getAll();
32-
setEntries(data.crmList);
34+
if (mounted) setEntries(data.crmList);
3335
} catch (error) {
3436
console.error("Failed to fetch analytics data", error);
3537
} finally {
36-
setIsLoading(false);
38+
if (mounted) setIsLoading(false);
3739
}
3840
} else {
39-
setIsLoading(false);
41+
if (mounted) setIsLoading(false);
4042
}
4143
};
4244
loadData();
43-
}, [user, hasPermission]);
45+
return () => { mounted = false; };
46+
}, [hasPermission]);
4447

4548
const handleExport = async (type: 'crm' | 'tasks' | 'performance') => {
4649
showToast(`Generating ${type.toUpperCase()} CSV...`, 'info');

client/pages/CRMPage.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,16 @@ export const CRMPage: React.FC = () => {
4343
finally { setIsLoading(false); }
4444
};
4545

46-
useEffect(() => { fetchData(); }, []);
46+
useEffect(() => {
47+
let mounted = true;
48+
const loadData = async () => {
49+
if (mounted) {
50+
await fetchData();
51+
}
52+
};
53+
loadData();
54+
return () => { mounted = false; };
55+
}, []);
4756

4857
const filteredData = useMemo(() => {
4958
return entries.filter(item => {

client/pages/ClientDetailsPage.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,36 +39,68 @@ export const ClientDetailsPage: React.FC = () => {
3939
});
4040

4141
useEffect(() => {
42+
let mounted = true;
4243
const fetchData = async () => {
44+
if (!mounted) return;
4345
setIsLoading(true);
44-
if (!id) return;
46+
// Validate id parameter before parsing
47+
if (!id || isNaN(parseInt(id))) {
48+
if (mounted) setIsLoading(false);
49+
showToast("Invalid client ID", "error");
50+
return;
51+
}
52+
4553
try {
4654
const [crmData, tasksData, usersData] = await Promise.all([
4755
crmApi.getAll(),
4856
tasksApi.getAll(),
4957
usersApi.getAll()
5058
]);
51-
const foundClient = crmData.crmList.find(c => c.id === parseInt(id));
59+
60+
if (!mounted) return;
61+
62+
// Safely parse and find client
63+
const clientId = parseInt(id);
64+
const foundClient = crmData?.crmList?.find(c => c?.id === clientId);
5265
setClient(foundClient || null);
53-
const clientTasks = tasksData.filter(t => t.companyId === parseInt(id));
66+
67+
// Filter tasks safely
68+
const clientTasks = (tasksData || []).filter(t => t?.companyId === clientId);
5469
setTasks(clientTasks);
70+
71+
// Build user avatar map safely
5572
const uMap: Record<string, string> = {};
56-
usersData.forEach(u => { if (u.avatarUrl) uMap[u.name] = u.avatarUrl; });
73+
(usersData || []).forEach(u => {
74+
if (u?.avatarUrl && u?.name) {
75+
uMap[u.name] = u.avatarUrl;
76+
}
77+
});
5778
setUserAvatarMap(uMap);
58-
} catch (e) { console.error(e); } finally { setIsLoading(false); }
79+
} catch (e) {
80+
console.error("Failed to fetch client data:", e);
81+
if (mounted) showToast("Failed to load client data. Please try again.", "error");
82+
} finally {
83+
if (mounted) setIsLoading(false);
84+
}
5985
};
6086
fetchData();
61-
}, [id]);
87+
return () => { mounted = false; };
88+
}, [id, showToast]);
6289

6390
const filteredBaseTasks = useMemo(() => {
6491
let result = tasks.filter(t => {
65-
const matchesSearch = (t.title || '').toLowerCase().includes(filters.search.toLowerCase());
92+
if (!t) return false;
93+
const matchesSearch = (t.title || '').toLowerCase().includes((filters.search || '').toLowerCase());
6694
const matchesStatus = filters.status === '' || t.status === filters.status;
6795
const matchesPriority = filters.priority === '' || t.priority === filters.priority;
6896
const matchesAssignee = filters.assignedTo === '' || t.assignedTo === filters.assignedTo;
6997
return matchesSearch && matchesStatus && matchesPriority && matchesAssignee;
7098
});
71-
return result.sort((a, b) => new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime());
99+
return result.sort((a, b) => {
100+
const dateA = a?.dueDate ? new Date(a.dueDate).getTime() : 0;
101+
const dateB = b?.dueDate ? new Date(b.dueDate).getTime() : 0;
102+
return dateB - dateA;
103+
});
72104
}, [tasks, filters]);
73105

74106
const { activeTasks, completedTasks } = useMemo(() => {

client/pages/ClientPortalPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@ export const ClientPortalPage: React.FC = () => {
4444
};
4545

4646
useEffect(() => {
47-
fetchData();
48-
}, [user]);
47+
let mounted = true;
48+
const loadData = async () => {
49+
if (mounted) {
50+
await fetchData();
51+
}
52+
};
53+
loadData();
54+
return () => { mounted = false; };
55+
}, [user?.clientCrmId]);
4956

5057
const handleTaskSubmit = async (data: Partial<Task>) => {
5158
try {

client/pages/ClientTrackerPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ export const ClientTrackerPage: React.FC = () => {
2424
});
2525

2626
useEffect(() => {
27+
let mounted = true;
2728
const fetchData = async () => {
29+
if (!mounted) return;
2830
try {
2931
const [crmData, tasksData] = await Promise.all([crmApi.getAll(), tasksApi.getAll()]);
32+
if (!mounted) return;
3033
const activeCompanies = crmData.crmList.filter(c => ['onboarded', 'on progress', 'Quote Sent'].includes(c.status));
3134
setClients(activeCompanies.map(client => {
3235
const clientTasks = tasksData.filter(t => t.companyId === client.id);
@@ -41,10 +44,11 @@ export const ClientTrackerPage: React.FC = () => {
4144
} catch (e) {
4245
console.error(e);
4346
} finally {
44-
setIsLoading(false);
47+
if (mounted) setIsLoading(false);
4548
}
4649
};
4750
fetchData();
51+
return () => { mounted = false; };
4852
}, []);
4953

5054
const handleSort = (key: SortKey) => {

client/pages/CompaniesPage.tsx

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,37 @@ export const CompaniesPage: React.FC = () => {
2424
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
2525
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
2626
const [deleteId, setDeleteId] = useState<number | null>(null);
27+
28+
// Track which tabs have been loaded to avoid duplicate calls
29+
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set(['active']));
2730

2831
const [filters, setFilters] = useState<CompanyFilterState>({
2932
search: '',
3033
status: '',
3134
workType: ''
3235
});
3336

34-
const fetchData = async () => {
37+
const fetchDataForTab = async (tab: 'active' | 'dropped' | 'past') => {
3538
setIsLoading(true);
3639
try {
37-
const data = await companiesApi.getAll();
40+
let data: CRMEntry[];
41+
42+
switch (tab) {
43+
case 'active':
44+
data = await companiesApi.getOnboarded();
45+
break;
46+
case 'dropped':
47+
data = await companiesApi.getClosed();
48+
break;
49+
case 'past':
50+
data = await companiesApi.getDone();
51+
break;
52+
default:
53+
data = [];
54+
}
55+
3856
setCrmEntries(data);
57+
setLoadedTabs(prev => new Set(prev).add(tab));
3958
} catch (error) {
4059
console.error("Failed to fetch data", error);
4160
showToast("Failed to load companies", "error");
@@ -44,29 +63,28 @@ export const CompaniesPage: React.FC = () => {
4463
}
4564
};
4665

66+
// Initial load - fetch only onboarded (active) companies
4767
useEffect(() => {
48-
fetchData();
68+
let mounted = true;
69+
const loadData = async () => {
70+
if (mounted) {
71+
await fetchDataForTab('active');
72+
}
73+
};
74+
loadData();
75+
return () => { mounted = false; };
4976
}, []);
5077

51-
const allCompanies = useMemo(() => {
52-
// Registry now strictly includes those that have passed initial lead stages
53-
return crmEntries.filter(entry => ['onboarded', 'on progress', 'Quote Sent', 'completed', 'drop'].includes(entry.status));
54-
}, [crmEntries]);
55-
56-
const categorizedData = useMemo(() => {
57-
// Onboarded, On Progress, and Quote Sent are considered active Focus nodes in the registry
58-
const active = allCompanies.filter(c => ['onboarded', 'on progress', 'Quote Sent'].includes(c.status));
59-
const dropped = allCompanies.filter(c => c.status === 'drop');
60-
const past = allCompanies.filter(c => c.status === 'completed');
61-
return { active, dropped, past };
62-
}, [allCompanies]);
78+
// Handle tab changes - fetch data only if not already loaded
79+
useEffect(() => {
80+
if (!loadedTabs.has(activeTab)) {
81+
fetchDataForTab(activeTab);
82+
}
83+
}, [activeTab]);
6384

85+
// No need for categorization - data comes pre-filtered from backend
6486
const displayData = useMemo(() => {
65-
let sourceList = categorizedData.active;
66-
if (activeTab === 'dropped') sourceList = categorizedData.dropped;
67-
if (activeTab === 'past') sourceList = categorizedData.past;
68-
69-
let result = sourceList.filter(item => {
87+
let result = crmEntries.filter(item => {
7088
const matchesSearch = filters.search === '' ||
7189
(item.company || '').toLowerCase().includes(filters.search.toLowerCase()) ||
7290
(item.contactName && item.contactName.toLowerCase().includes(filters.search.toLowerCase())) ||
@@ -79,7 +97,7 @@ export const CompaniesPage: React.FC = () => {
7997
});
8098

8199
return result.sort((a, b) => b.id - a.id);
82-
}, [categorizedData, activeTab, filters]);
100+
}, [crmEntries, filters]);
83101

84102
const handleEdit = (company: CRMEntry) => {
85103
setEditingCompany(company);
@@ -100,8 +118,10 @@ export const CompaniesPage: React.FC = () => {
100118
try {
101119
await companiesApi.update(updatedEntry.id, updatedEntry);
102120
showToast("Company details updated", "success");
121+
// Refresh current tab data
122+
await fetchDataForTab(activeTab);
103123
} catch(e) {
104-
fetchData();
124+
await fetchDataForTab(activeTab);
105125
showToast("Failed to update company", "error");
106126
}
107127
}
@@ -118,12 +138,15 @@ export const CompaniesPage: React.FC = () => {
118138
try {
119139
await companiesApi.update(company.id, updatedEntry);
120140
showToast(`Status updated to ${newStatus}`, "success");
121-
// If status changes out of registry criteria, re-fetch to update view
122-
if (!['onboarded', 'on progress', 'Quote Sent', 'completed', 'drop'].includes(newStatus)) {
123-
fetchData();
124-
}
141+
142+
// Status change may move company to different tab
143+
// Clear loaded tabs cache to force refresh when switching tabs
144+
setLoadedTabs(new Set([activeTab]));
145+
146+
// Refresh current tab to reflect changes
147+
await fetchDataForTab(activeTab);
125148
} catch (e) {
126-
fetchData();
149+
await fetchDataForTab(activeTab);
127150
showToast("Failed to update status", "error");
128151
}
129152
};
@@ -170,7 +193,7 @@ export const CompaniesPage: React.FC = () => {
170193
}`}
171194
>
172195
Active Focus
173-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-brand-50 text-brand-600 text-[9px] lg:text-[10px]">{categorizedData.active.length}</span>
196+
{activeTab === 'active' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-brand-50 text-brand-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
174197
</button>
175198
<button
176199
onClick={() => setActiveTab('past')}
@@ -181,7 +204,7 @@ export const CompaniesPage: React.FC = () => {
181204
}`}
182205
>
183206
Historical
184-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 text-[9px] lg:text-[10px]">{categorizedData.past.length}</span>
207+
{activeTab === 'past' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
185208
</button>
186209
<button
187210
onClick={() => setActiveTab('dropped')}
@@ -192,12 +215,12 @@ export const CompaniesPage: React.FC = () => {
192215
}`}
193216
>
194217
Archived
195-
<span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-rose-50 text-rose-600 text-[9px] lg:text-[10px]">{categorizedData.dropped.length}</span>
218+
{activeTab === 'dropped' && <span className="ml-2 px-1.5 lg:px-2 py-0.5 rounded bg-rose-50 text-rose-600 text-[9px] lg:text-[10px]">{crmEntries.length}</span>}
196219
</button>
197220
</div>
198221
</div>
199222

200-
<CompaniesFilters filters={filters} setFilters={setFilters} onRefresh={fetchData} />
223+
<CompaniesFilters filters={filters} setFilters={setFilters} onRefresh={() => fetchDataForTab(activeTab)} />
201224

202225
<div className="pt-4 pb-10 overflow-x-auto">
203226
<CompaniesTable

client/pages/MeetingTrackerPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ export const MeetingTrackerPage: React.FC = () => {
4040
};
4141

4242
useEffect(() => {
43-
fetchData();
43+
let mounted = true;
44+
const loadData = async () => {
45+
if (mounted) {
46+
await fetchData();
47+
}
48+
};
49+
loadData();
50+
return () => { mounted = false; };
4451
}, []);
4552

4653
const { activeMeetings, historyMeetings } = useMemo(() => {

0 commit comments

Comments
 (0)