1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
6import { apiFetch } from '../lib/api';
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);
22 const fetchReport = async (reportType) => {
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();
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;
40 setError(err.message);
47 fetchReport(activeReport);
48 }, [activeReport, selectedTenantId]);
50 const renderRevenueReport = () => {
51 if (!revenueData) return null;
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);
58 <div className="report-content">
59 <h2>Revenue Report - Last 12 Months</h2>
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>
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>
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>
85 <div className="report-table">
91 <th>Total Revenue</th>
97 {revenueData.map(row => (
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>
113 const renderTicketReport = () => {
114 if (!ticketData) return null;
116 const totalTickets = ticketData.byStatus?.reduce((sum, s) => sum + parseInt(s.count), 0) || 0;
119 <div className="report-content">
120 <h2>Ticket Statistics</h2>
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>
132 <div className="report-grid">
133 <div className="report-section">
143 {ticketData.byStatus?.map(row => (
144 <tr key={row.status}>
145 <td><span className={`status-badge ${row.status}`}>{row.status}</span></td>
153 <div className="report-section">
163 {ticketData.byPriority?.map(row => (
164 <tr key={row.priority}>
165 <td><span className={`priority-badge ${row.priority}`}>{row.priority}</span></td>
174 <div className="report-table">
175 <h3>Monthly Trend - Last 12 Months</h3>
180 <th>Total Created</th>
186 {ticketData.monthlyTrend?.map(row => {
187 const closeRate = row.total > 0 ? ((row.closed / row.total) * 100).toFixed(1) : 0;
192 <td>{row.closed}</td>
193 <td>{closeRate}%</td>
204 const renderCustomerReport = () => {
205 if (!customerData) return null;
208 <div className="report-content">
209 <h2>Customer Overview</h2>
211 <div className="report-grid">
212 <div className="report-section">
213 <h3>Top Customers by Revenue</h3>
219 <th>Total Revenue</th>
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>
234 <div className="report-section">
235 <h3>Top Customers by Tickets</h3>
240 <th>Total Tickets</th>
241 <th>Open Tickets</th>
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>
260 const renderAgentReport = () => {
261 if (!agentData) return null;
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;
268 <div className="report-content">
269 <h2>Agent Status Report</h2>
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>
279 <div className="report-card success">
280 <span className="material-symbols-outlined">check_circle</span>
281 <div className="card-content">
282 <h3>{onlineAgents}</h3>
286 <div className="report-card">
287 <span className="material-symbols-outlined">cancel</span>
288 <div className="card-content">
289 <h3>{offlineAgents}</h3>
295 <div className="report-table">
303 <th>Assigned Tickets</th>
304 <th>Open Tickets</th>
308 {agentData.agents?.map(agent => (
309 <tr key={agent.agent_id}>
310 <td>{agent.agent_name}</td>
311 <td>{agent.tenant_name}</td>
313 <span className={`status-badge ${agent.status}`}>{agent.status}</span>
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>
327 const renderAgingReport = () => {
328 if (!agingData) return null;
330 const { summary, invoices } = agingData;
333 <div className="report-content">
334 <h2>Invoice Aging Report</h2>
336 <div className="aging-summary">
337 <div className="aging-bucket">
339 <p className="count">{summary.current.count} invoices</p>
340 <p className="amount">${summary.current.total.toFixed(2)}</p>
342 <div className="aging-bucket warning-light">
344 <p className="count">{summary.overdue1_30.count} invoices</p>
345 <p className="amount">${summary.overdue1_30.total.toFixed(2)}</p>
347 <div className="aging-bucket warning">
349 <p className="count">{summary.overdue31_60.count} invoices</p>
350 <p className="amount">${summary.overdue31_60.total.toFixed(2)}</p>
352 <div className="aging-bucket error-light">
354 <p className="count">{summary.overdue61_90.count} invoices</p>
355 <p className="amount">${summary.overdue61_90.total.toFixed(2)}</p>
357 <div className="aging-bucket error">
359 <p className="count">{summary.overdue90plus.count} invoices</p>
360 <p className="amount">${summary.overdue90plus.total.toFixed(2)}</p>
364 <div className="report-table">
365 <h3>Outstanding Invoices</h3>
374 <th>Days Overdue</th>
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>
399 <div className="reports-page">
400 <div className="page-header">
401 <h1>Reports & Analytics</h1>
404 <div className="report-tabs">
406 className={`report-tab ${activeReport === 'revenue' ? 'active' : ''}`}
407 onClick={() => setActiveReport('revenue')}
409 <span className="material-symbols-outlined">payments</span>
413 className={`report-tab ${activeReport === 'tickets' ? 'active' : ''}`}
414 onClick={() => setActiveReport('tickets')}
416 <span className="material-symbols-outlined">confirmation_number</span>
420 className={`report-tab ${activeReport === 'customers' ? 'active' : ''}`}
421 onClick={() => setActiveReport('customers')}
423 <span className="material-symbols-outlined">business</span>
427 className={`report-tab ${activeReport === 'agents' ? 'active' : ''}`}
428 onClick={() => setActiveReport('agents')}
430 <span className="material-symbols-outlined">support_agent</span>
434 className={`report-tab ${activeReport === 'invoice-aging' ? 'active' : ''}`}
435 onClick={() => setActiveReport('invoice-aging')}
437 <span className="material-symbols-outlined">schedule</span>
443 <div className="loading-state">
444 <span className="material-symbols-outlined spinning">refresh</span>
450 <div className="error-alert">
451 <span className="material-symbols-outlined">error</span>
456 {!loading && !error && (
458 {activeReport === 'revenue' && renderRevenueReport()}
459 {activeReport === 'tickets' && renderTicketReport()}
460 {activeReport === 'customers' && renderCustomerReport()}
461 {activeReport === 'agents' && renderAgentReport()}
462 {activeReport === 'invoice-aging' && renderAgingReport()}
470export default Reports;