3import { useState, useEffect } from 'react';
4import { Icon } from '@/contexts/IconContext';
5import { apiClient } from '@/lib/client/apiClient';
10 StorePerformanceChart,
11 TellerPerformanceChart,
13} from '@/components/Charts';
16 * Analytics Dashboard - Updated to use apiClient with new OpenAPI routes
18 * This page now leverages the comprehensive OpenAPI routes implementation:
19 * - Uses type-safe apiClient methods instead of raw fetch calls
20 * - Properly handles OpenAPI response format: { success, data: { data: { Entity: [...] } } }
21 * - Fetches real data from Customers, Products, and Locations endpoints
24interface DashboardData {
46export default function AnalyticsDashboard() {
47 const [dateRange, setDateRange] = useState('today');
48 const [loading, setLoading] = useState(true);
49 const [dashboardData, setDashboardData] = useState<DashboardData>({
54 const [products, setProducts] = useState<Product[]>([]);
55 const [customers, setCustomers] = useState<Customer[]>([]);
56 const [lowStockProducts, setLowStockProducts] = useState<Array<{
60 locations: Array<{ locid: number; locationName: string; qty: number }>
62 const [salesHourly, setSalesHourly] = useState<Array<{ hour: string; sales: number; amount: number }>>([]);
63 const [salesDaily, setSalesDaily] = useState<Array<{ time: string; sales: number }>>([]);
64 const [paymentMethods, setPaymentMethods] = useState<Array<{ method: string; amount: number; percentage: number }>>([]);
65 const [storePerformance, setStorePerformance] = useState<Array<{ store: string; sales: number; amount: number }>>([]);
66 const [tellerPerformance, setTellerPerformance] = useState<Array<{ teller: string; sales: number; amount: number }>>([]);
72 const loadHourlyStats = async () => {
74 const today = new Date();
75 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
76 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
78 const params = new URLSearchParams({
79 startDate: startOfDay.toISOString().split('T')[0],
80 endDate: endOfDay.toISOString().split('T')[0]
83 const response = await fetch(`/api/v1/stats/hourly-sales?${params}`);
89 const data = await response.json();
90 const sales = data.APPD || [];
92 if (sales.length === 0) {
97 const hourCounts: { [key: number]: { count: number; total: number } } = {};
99 sales.forEach((item: any) => {
100 const timestamp = item.f131 || item.f132 || item.CompletedDt;
103 const date = new Date(timestamp);
104 const hour = date.getHours();
106 if (!hourCounts[hour]) {
107 hourCounts[hour] = { count: 0, total: 0 };
109 hourCounts[hour].count += 1;
110 hourCounts[hour].total += parseFloat(item.f120 || item.f110 || 0);
112 // Skip invalid dates
117 const hourlyArray = Object.entries(hourCounts)
118 .map(([hourStr, data]) => {
119 const hour = parseInt(hourStr);
120 let hourLabel: string;
121 if (hour === 0) hourLabel = '12AM';
122 else if (hour < 12) hourLabel = `${hour}AM`;
123 else if (hour === 12) hourLabel = '12PM';
124 else hourLabel = `${hour - 12}PM`;
129 amount: Math.round(data.total)
133 const getHour = (label: string) => {
134 const isPM = label.includes('PM');
135 const num = parseInt(label);
136 if (num === 12) return isPM ? 12 : 0;
137 return isPM ? num + 12 : num;
139 return getHour(a.hour) - getHour(b.hour);
142 setSalesHourly(hourlyArray);
144 console.error('Error loading hourly stats:', error);
149 const loadDailyStats = async () => {
151 const today = new Date();
152 const weekAgo = new Date(today);
153 weekAgo.setDate(weekAgo.getDate() - 7);
155 const params = new URLSearchParams({
156 startDate: weekAgo.toISOString().split('T')[0],
157 endDate: today.toISOString().split('T')[0]
160 const response = await fetch(`/api/v1/stats/daily-sales?${params}`);
166 const data = await response.json();
167 const days = data.APPD || [];
169 if (days.length === 0) {
174 const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
175 const dayMap: { [key: string]: { count: number; total: number } } = {
176 'Mon': { count: 0, total: 0 },
177 'Tue': { count: 0, total: 0 },
178 'Wed': { count: 0, total: 0 },
179 'Thu': { count: 0, total: 0 },
180 'Fri': { count: 0, total: 0 },
181 'Sat': { count: 0, total: 0 },
182 'Sun': { count: 0, total: 0 }
185 days.forEach((item: any) => {
186 const dayIndex = item.f100 || item.DayOfWeek;
187 if (dayIndex !== undefined && dayIndex >= 0 && dayIndex < 7) {
188 const dayName = dayNames[dayIndex];
189 dayMap[dayName].count += parseInt(item.f107) || 0;
190 dayMap[dayName].total += parseFloat(item.f120) || 0;
194 const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
195 const dailyArray = weekDays.map(day => ({
197 sales: dayMap[day].count
200 setSalesDaily(dailyArray);
202 console.error('Error loading daily stats:', error);
207 const loadPaymentStats = async () => {
209 const today = new Date();
210 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
211 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
213 const params = new URLSearchParams({
214 startDate: startOfDay.toISOString().split('T')[0],
215 endDate: endOfDay.toISOString().split('T')[0]
218 const response = await fetch(`/api/v1/stats/today-payment?${params}`);
220 setPaymentMethods([]);
224 const result = await response.json();
225 if (!result.success || !result.data || result.data.length === 0) {
226 setPaymentMethods([]);
230 const paymentTotals: { [key: string]: number } = {};
231 result.data.forEach((storeData: any) => {
232 for (let i = 300; i <= 330; i++) {
233 const amount = parseFloat(storeData[`f${i}`]) || 0;
235 const nameField = `f${i + 200}`;
236 const methodName = storeData[nameField] || `Payment ${i - 299}`;
237 if (methodName && methodName !== 'Unknown' && methodName !== 'Other') {
238 paymentTotals[methodName] = (paymentTotals[methodName] || 0) + amount;
244 const totalAmount = Object.values(paymentTotals).reduce((sum, amt) => sum + amt, 0);
245 const paymentArray = Object.entries(paymentTotals)
246 .map(([method, amount]) => ({
248 amount: Math.round(amount),
249 percentage: totalAmount > 0 ? Math.round((amount / totalAmount) * 100) : 0
251 .filter(p => p.amount > 0)
252 .sort((a, b) => b.amount - a.amount)
255 setPaymentMethods(paymentArray);
257 console.error('Error loading payment stats:', error);
258 setPaymentMethods([]);
262 const loadTellerStats = async () => {
264 const today = new Date();
265 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
266 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
268 const params = new URLSearchParams({
269 startDate: startOfDay.toISOString().split('T')[0],
270 endDate: endOfDay.toISOString().split('T')[0]
273 const response = await fetch(`/api/v1/stats/teller-performance?${params}`);
275 setTellerPerformance([]);
279 const result = await response.json();
280 if (!result.success || !result.data || result.data.length === 0) {
281 setTellerPerformance([]);
285 const tellerArray = result.data
286 .map((item: any) => ({
287 teller: item.f106 || 'Unknown',
288 sales: parseInt(item.f107) || 0,
289 amount: Math.round(parseFloat(item.f120) || 0)
291 .filter((t: any) => t.teller !== 'Unknown' && t.amount > 0)
292 .sort((a: any, b: any) => b.amount - a.amount)
295 setTellerPerformance(tellerArray);
297 console.error('Error loading teller stats:', error);
298 setTellerPerformance([]);
302 const loadTopProducts = async () => {
304 const today = new Date();
305 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
306 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
308 const params = new URLSearchParams({
309 startDate: startOfDay.toISOString().split('T')[0],
310 endDate: endOfDay.toISOString().split('T')[0]
313 const response = await fetch(`/api/v1/stats/top-products?${params}`);
318 const data = await response.json();
319 const products = data.APPD || [];
321 if (products.length > 0) {
322 const productArray = products
323 .map((item: any) => ({
324 Name: item.f2000 || 'Unknown Product',
325 StockLevel: parseInt(item.f107) || 0,
326 SellPrice: parseFloat(item.f120) || 0
328 .filter((p: any) => p.SellPrice > 0)
329 .sort((a: any, b: any) => b.SellPrice - a.SellPrice)
332 setProducts(productArray);
335 console.error('Error loading top products:', error);
339 const loadStorePerformance = async () => {
341 const today = new Date();
342 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
343 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
345 const params = new URLSearchParams({
346 startDate: startOfDay.toISOString().split('T')[0],
347 endDate: endOfDay.toISOString().split('T')[0]
350 const response = await fetch(`/api/v1/stats/store-performance?${params}`);
352 setStorePerformance([]);
356 const data = await response.json();
357 const stores = data.APPD || [];
359 if (stores.length === 0) {
360 setStorePerformance([]);
364 const storeArray = stores
365 .map((item: any) => ({
366 store: item.f2000 || 'Unknown Store',
367 sales: parseInt(item.f107) || 0,
368 amount: Math.round(parseFloat(item.f120) || 0)
370 .filter((s: any) => s.amount > 0)
371 .sort((a: any, b: any) => b.amount - a.amount)
374 setStorePerformance(storeArray);
376 console.error('Error loading store performance:', error);
377 setStorePerformance([]);
381 async function loadDashboardData() {
385 // Fetch real data using apiClient (leverages new OpenAPI routes)
391 ] = await Promise.allSettled([
392 apiClient.getCustomers({ limit: 10000, source: 'openapi' }),
393 apiClient.getProducts({ limit: 10000, source: 'openapi' }),
394 apiClient.getLocations(),
395 apiClient.getSalesTotals()
398 // Process customers - apiClient returns { success, data } where data has the structure
399 let customerCount = 0;
400 let customersData: Customer[] = [];
401 if (customersResult.status === 'fulfilled' && customersResult.value.success) {
402 const apiData = customersResult.value.data as any;
403 const customerData = apiData?.data?.Customer;
404 if (Array.isArray(customerData)) {
405 customerCount = customerData.length;
406 customersData = customerData;
410 // Process products - apiClient returns { success, data } where data has the structure
411 let productCount = 0;
412 let productsData: Product[] = [];
413 if (productsResult.status === 'fulfilled' && productsResult.value.success) {
414 const apiData = productsResult.value.data as any;
415 const productData = apiData?.data?.Product;
416 if (Array.isArray(productData)) {
417 productCount = productData.length;
418 productsData = productData;
422 // Process locations - apiClient returns { success, data } where data has the structure
423 let locationCount = 0;
424 if (locationsResult.status === 'fulfilled' && locationsResult.value.success) {
425 const apiData = locationsResult.value.data as any;
426 const locationData = apiData?.data?.Location;
427 if (Array.isArray(locationData)) {
428 locationCount = locationData.length;
432 console.log('Dashboard data loaded:', {
433 customers: customerCount,
434 products: productCount,
435 locations: locationCount
438 // Calculate low stock products from products data (stock level <= 15)
439 const lowStock = productsData.filter(p => (p.StockLevel || 0) <= 15 && (p.StockLevel || 0) > 0)
443 description: p.Name || 'Unknown Product',
444 totalQty: p.StockLevel || 0,
447 setLowStockProducts(lowStock);
450 customers: customerCount,
451 products: productCount,
452 locations: locationCount
455 // Load real chart data from new endpoints
456 await loadHourlyStats();
457 await loadDailyStats();
458 await loadPaymentStats();
459 await loadTellerStats();
460 await loadTopProducts();
461 await loadStorePerformance();
463 setProducts(productsData);
464 setCustomers(customersData);
467 console.error('Failed to load dashboard data:', error);
474 <div className="p-6">
476 <div className="mb-6">
477 <div className="flex justify-between items-center">
479 <h1 className="text-3xl font-bold mb-2 flex items-center gap-2">Analytics Dashboard <Icon name="analytics" /></h1>
480 <p className="text-muted">Comprehensive sales analytics and insights</p>
482 <div className="flex gap-2">
485 onChange={(e) => setDateRange(e.target.value)}
486 className="px-3 py-2 border border-border rounded-md text-sm"
488 <option value="today">Today</option>
489 <option value="week">This Week</option>
490 <option value="month">This Month</option>
491 <option value="quarter">This Quarter</option>
497 {/* Key Metrics - Using Real Data */}
498 <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
499 <div className="bg-surface rounded-lg shadow p-6">
500 <div className="flex items-center">
501 <div className="text-3xl mr-4"><Icon name="people" className="text-brand" /></div>
503 <div className="text-sm text-muted">Total Customers</div>
504 <div className="text-2xl font-bold text-brand">
505 {loading ? '...' : dashboardData.customers}
507 <div className="text-xs text-muted/70">From database</div>
512 <div className="bg-surface rounded-lg shadow p-6">
513 <div className="flex items-center">
514 <div className="text-3xl mr-4"><Icon name="inventory_2" className="text-brand" /></div>
516 <div className="text-sm text-muted">Total Products</div>
517 <div className="text-2xl font-bold text-brand">
518 {loading ? '...' : dashboardData.products}
520 <div className="text-xs text-muted/70">From database</div>
525 <div className="bg-surface rounded-lg shadow p-6">
526 <div className="flex items-center">
527 <div className="text-3xl mr-4"><Icon name="store" className="text-brand" /></div>
529 <div className="text-sm text-muted">Active Locations</div>
530 <div className="text-2xl font-bold text-success">
531 {loading ? '...' : dashboardData.locations}
533 <div className="text-xs text-muted/70">From database</div>
538 <div className="bg-surface rounded-lg shadow p-6">
539 <div className="flex items-center">
540 <div className="text-3xl mr-4"><Icon name="api" className="text-brand" /></div>
542 <div className="text-sm text-muted">Data Source</div>
543 <div className="text-lg font-bold text-info">OpenAPI</div>
544 <div className="text-xs text-success"><Icon name="check_circle" className="text-xs" /> Live data</div>
550 {/* Data Status Notice */}
551 <div className="bg-success/10 border border-success/30 rounded-lg p-4 mb-8">
552 <div className="flex items-start">
553 <div className="text-2xl mr-3"><Icon name="check_circle" className="text-success" /></div>
555 <h3 className="font-semibold text-success mb-1">Real-Time Data Active</h3>
556 <p className="text-sm text-success/90">
557 Displaying <strong>real data</strong> from your system.
558 Customer count: <strong>{dashboardData.customers}</strong>,
559 Product count: <strong>{dashboardData.products}</strong>,
560 Location count: <strong>{dashboardData.locations}</strong>
566 {/* Main Charts Grid */}
567 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
569 {/* Sales Performance by Hour */}
570 <div className="bg-surface rounded-lg shadow">
571 <div className="p-4 border-b">
572 <h2 className="text-lg font-semibold">Sales Performance by Hour</h2>
574 <div className="p-4 h-80">
575 <SalesHourlyChart data={salesHourly} />
579 {/* Weekly Sales Trend */}
580 <div className="bg-surface rounded-lg shadow">
581 <div className="p-4 border-b">
582 <h2 className="text-lg font-semibold">Weekly Sales Trend</h2>
584 <div className="p-4 h-80">
585 <SalesOverTimeChart timeframe="day" data={salesDaily} />
589 {/* Payment Methods Distribution */}
590 <div className="bg-surface rounded-lg shadow">
591 <div className="p-4 border-b">
592 <h2 className="text-lg font-semibold">Payment Methods Distribution</h2>
594 <div className="p-4 h-80">
595 <PaymentMethodsChart data={paymentMethods} />
599 {/* Top Products Performance */}
600 <div className="bg-surface rounded-lg shadow">
601 <div className="p-4 border-b">
602 <h2 className="text-lg font-semibold">Top Products (By Stock Level)</h2>
604 <div className="p-4 h-80">
605 <TopProductsChart data={products.slice(0, 10).map(p => ({
606 name: p.Name || 'Unknown Product',
607 quantity: p.StockLevel || 0,
608 value: (p.SellPrice || 0) * (p.StockLevel || 0)
613 {/* Store Performance Comparison */}
614 <div className="bg-surface rounded-lg shadow">
615 <div className="p-4 border-b">
616 <h2 className="text-lg font-semibold">Store Performance Comparison</h2>
618 <div className="p-4 h-80">
619 <StorePerformanceChart data={storePerformance} />
623 {/* Staff Performance */}
624 <div className="bg-surface rounded-lg shadow">
625 <div className="p-4 border-b">
626 <h2 className="text-lg font-semibold">Staff Performance</h2>
628 <div className="p-4 h-80">
629 <TellerPerformanceChart data={tellerPerformance} />
634 {/* Additional Insights */}
635 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
637 {/* Top Selling Categories */}
638 <div className="bg-surface rounded-lg shadow">
639 <div className="p-4 border-b">
640 <h2 className="text-lg font-semibold">Product Statistics</h2>
642 <div className="p-4">
643 <div className="space-y-3">
644 <div className="space-y-1">
645 <div className="flex justify-between text-sm">
646 <span className="text-text">Total Products</span>
647 <span className="text-text font-semibold">{dashboardData.products}</span>
649 <div className="w-full bg-surface-2 rounded-full h-2">
651 className="bg-brand h-2 rounded-full"
652 style={{ width: '100%' }}
657 <div className="space-y-1">
658 <div className="flex justify-between text-sm">
659 <span className="text-text">Products with Stock</span>
660 <span className="text-text font-semibold">
661 {products.filter(p => (p.StockLevel || 0) > 0).length}
664 <div className="w-full bg-surface-2 rounded-full h-2">
666 className="bg-success h-2 rounded-full"
668 width: `${dashboardData.products > 0
669 ? (products.filter(p => (p.StockLevel || 0) > 0).length / dashboardData.products * 100)
674 <div className="text-xs text-muted">
675 {dashboardData.products > 0
676 ? `${((products.filter(p => (p.StockLevel || 0) > 0).length / dashboardData.products) * 100).toFixed(0)}%`
681 <div className="space-y-1">
682 <div className="flex justify-between text-sm">
683 <span className="text-text">Total Stock Value</span>
684 <span className="text-text font-semibold">
685 ${products.reduce((sum, p) =>
686 sum + ((p.SellPrice || 0) * (p.StockLevel || 0)), 0
690 <div className="w-full bg-surface-2 rounded-full h-2">
692 className="bg-brand h-2 rounded-full"
693 style={{ width: '75%' }}
696 <div className="text-xs text-muted">Estimated retail value</div>
702 {/* Customer Insights */}
703 <div className="bg-surface rounded-lg shadow">
704 <div className="p-4 border-b">
705 <h2 className="text-lg font-semibold">Customer Insights</h2>
707 <div className="p-4">
708 <div className="space-y-4">
709 <div className="text-center">
710 <div className="text-2xl font-bold text-brand">{dashboardData.customers}</div>
711 <div className="text-sm text-muted">Total Customers</div>
712 <div className="text-xs text-success">Real-time count</div>
715 <div className="text-center">
716 <div className="text-2xl font-bold text-info">
717 {customers.filter(c => c.Email).length}
719 <div className="text-sm text-muted">With Email</div>
720 <div className="text-xs text-muted">
721 {dashboardData.customers > 0
722 ? `${((customers.filter(c => c.Email).length / dashboardData.customers) * 100).toFixed(0)}%`
727 <div className="text-center">
728 <div className="text-2xl font-bold text-success">
729 {customers.filter(c => c.Phone || c.Mobile).length}
731 <div className="text-sm text-muted">With Phone</div>
732 <div className="text-xs text-muted">
733 {dashboardData.customers > 0
734 ? `${((customers.filter(c => c.Phone || c.Mobile).length / dashboardData.customers) * 100).toFixed(0)}%`
742 {/* Inventory Alerts */}
743 <div className="bg-surface rounded-lg shadow">
744 <div className="p-4 border-b">
745 <h2 className="text-lg font-semibold">Inventory Status - Low Stock Alerts</h2>
746 <p className="text-xs text-muted mt-1">Products with stock ≤ 15 units across all locations</p>
748 <div className="p-4">
749 <div className="space-y-3">
750 {lowStockProducts.map((product, idx) => {
751 const stock = product.totalQty;
752 const status = stock === 0 ? 'out' : stock <= 5 ? 'critical' : stock <= 15 ? 'low' : 'ok';
754 <div key={idx} className="border rounded-lg p-3">
755 <div className="flex justify-between items-start mb-2">
756 <div className="flex-1">
757 <div className="text-sm font-medium text-text">
758 {(product.description || 'Unknown').substring(0, 40)}
759 {(product.description || '').length > 40 ? '...' : ''}
761 <div className="text-xs text-muted mt-1">
762 Product ID: {product.pid} | Total Stock: {stock} units
765 <div className={`px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap ml-2 ${
766 status === 'out' ? 'bg-red-100 text-red-800' :
767 status === 'critical' ? 'bg-orange-100 text-orange-800' :
768 status === 'low' ? 'bg-yellow-100 text-yellow-800' :
769 'bg-green-100 text-green-800'
771 {status === 'out' ? '⚫ OUT' :
772 status === 'critical' ? '🔴 CRITICAL' :
773 status === 'low' ? '🟡 LOW' : '🟢 OK'}
776 {product.locations.length > 0 && (
777 <div className="mt-2 pt-2 border-t">
778 <div className="text-xs text-muted mb-1">Stock by location:</div>
779 <div className="grid grid-cols-2 gap-1">
780 {product.locations.slice(0, 4).map((loc, locIdx) => (
781 <div key={locIdx} className="text-xs text-muted">
782 {loc.locationName}: <span className="font-medium">{loc.qty}</span>
791 {lowStockProducts.length === 0 && (
792 <div className="text-sm text-muted text-center py-4">
793 ✅ No low stock alerts - All products well stocked!
802 <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6">
803 <h2 className="text-lg font-semibold mb-4">📋 Data Summary & Insights</h2>
804 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
805 <div className="bg-surface rounded-lg p-4">
806 <h3 className="font-medium text-text mb-2">📊 Current Status</h3>
807 <ul className="text-sm text-muted space-y-1">
808 <li>• {dashboardData.customers} customers in database</li>
809 <li>• {dashboardData.products} products catalogued</li>
810 <li>• {products.filter(p => (p.StockLevel || 0) > 0).length} products in stock</li>
811 <li>• {customers.filter(c => c.Email).length} customers with email</li>
814 <div className="bg-surface rounded-lg p-4">
815 <h3 className="font-medium text-text mb-2">💡 Quick Insights</h3>
816 <ul className="text-sm text-muted space-y-1">
817 <li>• Low stock items: {lowStockProducts.length}</li>
818 <li>• Out of stock: {lowStockProducts.filter(p => p.totalQty === 0).length}</li>
819 <li>• Inventory value: ${products.reduce((sum, p) =>
820 sum + ((p.SellPrice || 0) * (p.StockLevel || 0)), 0
822 <li>• Active locations: {dashboardData.locations}</li>