EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Reports.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
5import './Reports.css';
6import { apiFetch } from '../lib/api';
7
8
9function Reports() {
10 // selectedTenantId should come from props/context, not localStorage
11 const [selectedTenantId, setSelectedTenantId] = useState('');
12 // No need for separate root detection, handled by TenantSelector
13 const [activeReport, setActiveReport] = useState('revenue');
14 const [revenueData, setRevenueData] = useState(null);
15 const [ticketData, setTicketData] = useState(null);
16 const [customerData, setCustomerData] = useState(null);
17 const [agentData, setAgentData] = useState(null);
18 const [agingData, setAgingData] = useState(null);
19 const [loading, setLoading] = useState(false);
20 const [error, setError] = useState(null);
21
22 const fetchReport = async (reportType) => {
23 setLoading(true);
24 setError(null);
25 try {
26 let url = `/reports/${reportType}`;
27 url += `?tenant_id=${selectedTenantId}`;
28 const response = await apiFetch(url);
29 if (!response.ok) throw new Error(`Failed to load ${reportType} report`);
30 const result = await response.json();
31
32 switch(reportType) {
33 case 'revenue': setRevenueData(result); break;
34 case 'tickets': setTicketData(result); break;
35 case 'customers': setCustomerData(result); break;
36 case 'agents': setAgentData(result); break;
37 case 'invoice-aging': setAgingData(result); break;
38 }
39 } catch (err) {
40 setError(err.message);
41 } finally {
42 setLoading(false);
43 }
44 };
45
46 useEffect(() => {
47 fetchReport(activeReport);
48 }, [activeReport, selectedTenantId]);
49
50 const renderRevenueReport = () => {
51 if (!revenueData) return null;
52
53 const totalRevenue = revenueData.reduce((sum, row) => sum + parseFloat(row.total_revenue || 0), 0);
54 const totalPaid = revenueData.reduce((sum, row) => sum + parseFloat(row.paid_revenue || 0), 0);
55 const totalUnpaid = revenueData.reduce((sum, row) => sum + parseFloat(row.unpaid_revenue || 0), 0);
56
57 return (
58 <div className="report-content">
59 <h2>Revenue Report - Last 12 Months</h2>
60
61 <div className="report-cards">
62 <div className="report-card">
63 <span className="material-symbols-outlined">payments</span>
64 <div className="card-content">
65 <h3>${totalRevenue.toFixed(2)}</h3>
66 <p>Total Revenue</p>
67 </div>
68 </div>
69 <div className="report-card success">
70 <span className="material-symbols-outlined">check_circle</span>
71 <div className="card-content">
72 <h3>${totalPaid.toFixed(2)}</h3>
73 <p>Paid</p>
74 </div>
75 </div>
76 <div className="report-card warning">
77 <span className="material-symbols-outlined">pending</span>
78 <div className="card-content">
79 <h3>${totalUnpaid.toFixed(2)}</h3>
80 <p>Unpaid</p>
81 </div>
82 </div>
83 </div>
84
85 <div className="report-table">
86 <table>
87 <thead>
88 <tr>
89 <th>Month</th>
90 <th>Invoices</th>
91 <th>Total Revenue</th>
92 <th>Paid</th>
93 <th>Unpaid</th>
94 </tr>
95 </thead>
96 <tbody>
97 {revenueData.map(row => (
98 <tr key={row.month}>
99 <td>{row.month}</td>
100 <td>{row.invoice_count}</td>
101 <td>${parseFloat(row.total_revenue || 0).toFixed(2)}</td>
102 <td className="success">${parseFloat(row.paid_revenue || 0).toFixed(2)}</td>
103 <td className="warning">${parseFloat(row.unpaid_revenue || 0).toFixed(2)}</td>
104 </tr>
105 ))}
106 </tbody>
107 </table>
108 </div>
109 </div>
110 );
111 };
112
113 const renderTicketReport = () => {
114 if (!ticketData) return null;
115
116 const totalTickets = ticketData.byStatus?.reduce((sum, s) => sum + parseInt(s.count), 0) || 0;
117
118 return (
119 <div className="report-content">
120 <h2>Ticket Statistics</h2>
121
122 <div className="report-cards">
123 <div className="report-card">
124 <span className="material-symbols-outlined">confirmation_number</span>
125 <div className="card-content">
126 <h3>{totalTickets}</h3>
127 <p>Total Tickets</p>
128 </div>
129 </div>
130 </div>
131
132 <div className="report-grid">
133 <div className="report-section">
134 <h3>By Status</h3>
135 <table>
136 <thead>
137 <tr>
138 <th>Status</th>
139 <th>Count</th>
140 </tr>
141 </thead>
142 <tbody>
143 {ticketData.byStatus?.map(row => (
144 <tr key={row.status}>
145 <td><span className={`status-badge ${row.status}`}>{row.status}</span></td>
146 <td>{row.count}</td>
147 </tr>
148 ))}
149 </tbody>
150 </table>
151 </div>
152
153 <div className="report-section">
154 <h3>By Priority</h3>
155 <table>
156 <thead>
157 <tr>
158 <th>Priority</th>
159 <th>Count</th>
160 </tr>
161 </thead>
162 <tbody>
163 {ticketData.byPriority?.map(row => (
164 <tr key={row.priority}>
165 <td><span className={`priority-badge ${row.priority}`}>{row.priority}</span></td>
166 <td>{row.count}</td>
167 </tr>
168 ))}
169 </tbody>
170 </table>
171 </div>
172 </div>
173
174 <div className="report-table">
175 <h3>Monthly Trend - Last 12 Months</h3>
176 <table>
177 <thead>
178 <tr>
179 <th>Month</th>
180 <th>Total Created</th>
181 <th>Closed</th>
182 <th>Close Rate</th>
183 </tr>
184 </thead>
185 <tbody>
186 {ticketData.monthlyTrend?.map(row => {
187 const closeRate = row.total > 0 ? ((row.closed / row.total) * 100).toFixed(1) : 0;
188 return (
189 <tr key={row.month}>
190 <td>{row.month}</td>
191 <td>{row.total}</td>
192 <td>{row.closed}</td>
193 <td>{closeRate}%</td>
194 </tr>
195 );
196 })}
197 </tbody>
198 </table>
199 </div>
200 </div>
201 );
202 };
203
204 const renderCustomerReport = () => {
205 if (!customerData) return null;
206
207 return (
208 <div className="report-content">
209 <h2>Customer Overview</h2>
210
211 <div className="report-grid">
212 <div className="report-section">
213 <h3>Top Customers by Revenue</h3>
214 <table>
215 <thead>
216 <tr>
217 <th>Customer</th>
218 <th>Invoices</th>
219 <th>Total Revenue</th>
220 </tr>
221 </thead>
222 <tbody>
223 {customerData.topByRevenue?.map(row => (
224 <tr key={row.customer_id}>
225 <td>{row.customer_name}</td>
226 <td>{row.invoice_count}</td>
227 <td>${parseFloat(row.total_revenue || 0).toFixed(2)}</td>
228 </tr>
229 ))}
230 </tbody>
231 </table>
232 </div>
233
234 <div className="report-section">
235 <h3>Top Customers by Tickets</h3>
236 <table>
237 <thead>
238 <tr>
239 <th>Customer</th>
240 <th>Total Tickets</th>
241 <th>Open Tickets</th>
242 </tr>
243 </thead>
244 <tbody>
245 {customerData.topByTickets?.map(row => (
246 <tr key={row.customer_id}>
247 <td>{row.customer_name}</td>
248 <td>{row.ticket_count}</td>
249 <td className={row.open_tickets > 0 ? 'warning' : ''}>{row.open_tickets}</td>
250 </tr>
251 ))}
252 </tbody>
253 </table>
254 </div>
255 </div>
256 </div>
257 );
258 };
259
260 const renderAgentReport = () => {
261 if (!agentData) return null;
262
263 const totalAgents = agentData.agents?.length || 0;
264 const onlineAgents = agentData.statusSummary?.find(s => s.status === 'online')?.count || 0;
265 const offlineAgents = agentData.statusSummary?.find(s => s.status === 'offline')?.count || 0;
266
267 return (
268 <div className="report-content">
269 <h2>Agent Status Report</h2>
270
271 <div className="report-cards">
272 <div className="report-card">
273 <span className="material-symbols-outlined">people</span>
274 <div className="card-content">
275 <h3>{totalAgents}</h3>
276 <p>Total Agents</p>
277 </div>
278 </div>
279 <div className="report-card success">
280 <span className="material-symbols-outlined">check_circle</span>
281 <div className="card-content">
282 <h3>{onlineAgents}</h3>
283 <p>Online</p>
284 </div>
285 </div>
286 <div className="report-card">
287 <span className="material-symbols-outlined">cancel</span>
288 <div className="card-content">
289 <h3>{offlineAgents}</h3>
290 <p>Offline</p>
291 </div>
292 </div>
293 </div>
294
295 <div className="report-table">
296 <table>
297 <thead>
298 <tr>
299 <th>Agent</th>
300 <th>Tenant</th>
301 <th>Status</th>
302 <th>Last Seen</th>
303 <th>Assigned Tickets</th>
304 <th>Open Tickets</th>
305 </tr>
306 </thead>
307 <tbody>
308 {agentData.agents?.map(agent => (
309 <tr key={agent.agent_id}>
310 <td>{agent.agent_name}</td>
311 <td>{agent.tenant_name}</td>
312 <td>
313 <span className={`status-badge ${agent.status}`}>{agent.status}</span>
314 </td>
315 <td>{agent.last_seen ? new Date(agent.last_seen).toLocaleString() : 'Never'}</td>
316 <td>{agent.assigned_tickets}</td>
317 <td className={agent.open_tickets > 0 ? 'warning' : ''}>{agent.open_tickets}</td>
318 </tr>
319 ))}
320 </tbody>
321 </table>
322 </div>
323 </div>
324 );
325 };
326
327 const renderAgingReport = () => {
328 if (!agingData) return null;
329
330 const { summary, invoices } = agingData;
331
332 return (
333 <div className="report-content">
334 <h2>Invoice Aging Report</h2>
335
336 <div className="aging-summary">
337 <div className="aging-bucket">
338 <h4>Current</h4>
339 <p className="count">{summary.current.count} invoices</p>
340 <p className="amount">${summary.current.total.toFixed(2)}</p>
341 </div>
342 <div className="aging-bucket warning-light">
343 <h4>1-30 Days</h4>
344 <p className="count">{summary.overdue1_30.count} invoices</p>
345 <p className="amount">${summary.overdue1_30.total.toFixed(2)}</p>
346 </div>
347 <div className="aging-bucket warning">
348 <h4>31-60 Days</h4>
349 <p className="count">{summary.overdue31_60.count} invoices</p>
350 <p className="amount">${summary.overdue31_60.total.toFixed(2)}</p>
351 </div>
352 <div className="aging-bucket error-light">
353 <h4>61-90 Days</h4>
354 <p className="count">{summary.overdue61_90.count} invoices</p>
355 <p className="amount">${summary.overdue61_90.total.toFixed(2)}</p>
356 </div>
357 <div className="aging-bucket error">
358 <h4>90+ Days</h4>
359 <p className="count">{summary.overdue90plus.count} invoices</p>
360 <p className="amount">${summary.overdue90plus.total.toFixed(2)}</p>
361 </div>
362 </div>
363
364 <div className="report-table">
365 <h3>Outstanding Invoices</h3>
366 <table>
367 <thead>
368 <tr>
369 <th>Invoice #</th>
370 <th>Customer</th>
371 <th>Issued</th>
372 <th>Due</th>
373 <th>Amount</th>
374 <th>Days Overdue</th>
375 <th>Status</th>
376 </tr>
377 </thead>
378 <tbody>
379 {invoices?.map(inv => (
380 <tr key={inv.invoice_id} className={inv.days_overdue > 0 ? 'overdue' : ''}>
381 <td>{inv.invoice_number}</td>
382 <td>{inv.customer_name}</td>
383 <td>{new Date(inv.date_issued).toLocaleDateString()}</td>
384 <td>{new Date(inv.date_due).toLocaleDateString()}</td>
385 <td>${parseFloat(inv.total_amount).toFixed(2)}</td>
386 <td className={inv.days_overdue > 0 ? 'error' : ''}>{inv.days_overdue}</td>
387 <td><span className={`status-badge ${inv.payment_status}`}>{inv.payment_status}</span></td>
388 </tr>
389 ))}
390 </tbody>
391 </table>
392 </div>
393 </div>
394 );
395 };
396
397 return (
398 <MainLayout>
399 <div className="reports-page">
400 <div className="page-header">
401 <h1>Reports & Analytics</h1>
402 </div>
403
404 <div className="report-tabs">
405 <button
406 className={`report-tab ${activeReport === 'revenue' ? 'active' : ''}`}
407 onClick={() => setActiveReport('revenue')}
408 >
409 <span className="material-symbols-outlined">payments</span>
410 Revenue
411 </button>
412 <button
413 className={`report-tab ${activeReport === 'tickets' ? 'active' : ''}`}
414 onClick={() => setActiveReport('tickets')}
415 >
416 <span className="material-symbols-outlined">confirmation_number</span>
417 Tickets
418 </button>
419 <button
420 className={`report-tab ${activeReport === 'customers' ? 'active' : ''}`}
421 onClick={() => setActiveReport('customers')}
422 >
423 <span className="material-symbols-outlined">business</span>
424 Customers
425 </button>
426 <button
427 className={`report-tab ${activeReport === 'agents' ? 'active' : ''}`}
428 onClick={() => setActiveReport('agents')}
429 >
430 <span className="material-symbols-outlined">support_agent</span>
431 Agents
432 </button>
433 <button
434 className={`report-tab ${activeReport === 'invoice-aging' ? 'active' : ''}`}
435 onClick={() => setActiveReport('invoice-aging')}
436 >
437 <span className="material-symbols-outlined">schedule</span>
438 Invoice Aging
439 </button>
440 </div>
441
442 {loading && (
443 <div className="loading-state">
444 <span className="material-symbols-outlined spinning">refresh</span>
445 Loading report...
446 </div>
447 )}
448
449 {error && (
450 <div className="error-alert">
451 <span className="material-symbols-outlined">error</span>
452 {error}
453 </div>
454 )}
455
456 {!loading && !error && (
457 <>
458 {activeReport === 'revenue' && renderRevenueReport()}
459 {activeReport === 'tickets' && renderTicketReport()}
460 {activeReport === 'customers' && renderCustomerReport()}
461 {activeReport === 'agents' && renderAgentReport()}
462 {activeReport === 'invoice-aging' && renderAgingReport()}
463 </>
464 )}
465 </div>
466 </MainLayout>
467 );
468}
469
470export default Reports;