EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1'use client';
2
3import { useState, useEffect } from 'react';
4import { Icon } from '@/contexts/IconContext';
5import { apiClient } from '@/lib/client/apiClient';
6import {
7 SalesHourlyChart,
8 PaymentMethodsChart,
9 TopProductsChart,
10 StorePerformanceChart,
11 TellerPerformanceChart,
12 SalesOverTimeChart
13} from '@/components/Charts';
14
15/**
16 * Analytics Dashboard - Updated to use apiClient with new OpenAPI routes
17 *
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
22 */
23
24interface DashboardData {
25 customers: number;
26 products: number;
27 locations: number;
28}
29
30interface Product {
31 Pid?: number;
32 Name?: string;
33 Barcode?: string;
34 SellPrice?: number;
35 StockLevel?: number;
36}
37
38interface Customer {
39 Cid?: number;
40 Name?: string;
41 Phone?: string;
42 Mobile?: string;
43 Email?: string;
44}
45
46export default function AnalyticsDashboard() {
47 const [dateRange, setDateRange] = useState('today');
48 const [loading, setLoading] = useState(true);
49 const [dashboardData, setDashboardData] = useState<DashboardData>({
50 customers: 0,
51 products: 0,
52 locations: 0
53 });
54 const [products, setProducts] = useState<Product[]>([]);
55 const [customers, setCustomers] = useState<Customer[]>([]);
56 const [lowStockProducts, setLowStockProducts] = useState<Array<{
57 pid: number;
58 description: string;
59 totalQty: number;
60 locations: Array<{ locid: number; locationName: string; qty: number }>
61 }>>([]);
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 }>>([]);
67
68 useEffect(() => {
69 loadDashboardData();
70 }, []);
71
72 const loadHourlyStats = async () => {
73 try {
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));
77
78 const params = new URLSearchParams({
79 startDate: startOfDay.toISOString().split('T')[0],
80 endDate: endOfDay.toISOString().split('T')[0]
81 });
82
83 const response = await fetch(`/api/v1/stats/hourly-sales?${params}`);
84 if (!response.ok) {
85 setSalesHourly([]);
86 return;
87 }
88
89 const data = await response.json();
90 const sales = data.APPD || [];
91
92 if (sales.length === 0) {
93 setSalesHourly([]);
94 return;
95 }
96
97 const hourCounts: { [key: number]: { count: number; total: number } } = {};
98
99 sales.forEach((item: any) => {
100 const timestamp = item.f131 || item.f132 || item.CompletedDt;
101 if (timestamp) {
102 try {
103 const date = new Date(timestamp);
104 const hour = date.getHours();
105
106 if (!hourCounts[hour]) {
107 hourCounts[hour] = { count: 0, total: 0 };
108 }
109 hourCounts[hour].count += 1;
110 hourCounts[hour].total += parseFloat(item.f120 || item.f110 || 0);
111 } catch (e) {
112 // Skip invalid dates
113 }
114 }
115 });
116
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`;
125
126 return {
127 hour: hourLabel,
128 sales: data.count,
129 amount: Math.round(data.total)
130 };
131 })
132 .sort((a, b) => {
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;
138 };
139 return getHour(a.hour) - getHour(b.hour);
140 });
141
142 setSalesHourly(hourlyArray);
143 } catch (error) {
144 console.error('Error loading hourly stats:', error);
145 setSalesHourly([]);
146 }
147 };
148
149 const loadDailyStats = async () => {
150 try {
151 const today = new Date();
152 const weekAgo = new Date(today);
153 weekAgo.setDate(weekAgo.getDate() - 7);
154
155 const params = new URLSearchParams({
156 startDate: weekAgo.toISOString().split('T')[0],
157 endDate: today.toISOString().split('T')[0]
158 });
159
160 const response = await fetch(`/api/v1/stats/daily-sales?${params}`);
161 if (!response.ok) {
162 setSalesDaily([]);
163 return;
164 }
165
166 const data = await response.json();
167 const days = data.APPD || [];
168
169 if (days.length === 0) {
170 setSalesDaily([]);
171 return;
172 }
173
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 }
183 };
184
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;
191 }
192 });
193
194 const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
195 const dailyArray = weekDays.map(day => ({
196 time: day,
197 sales: dayMap[day].count
198 }));
199
200 setSalesDaily(dailyArray);
201 } catch (error) {
202 console.error('Error loading daily stats:', error);
203 setSalesDaily([]);
204 }
205 };
206
207 const loadPaymentStats = async () => {
208 try {
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));
212
213 const params = new URLSearchParams({
214 startDate: startOfDay.toISOString().split('T')[0],
215 endDate: endOfDay.toISOString().split('T')[0]
216 });
217
218 const response = await fetch(`/api/v1/stats/today-payment?${params}`);
219 if (!response.ok) {
220 setPaymentMethods([]);
221 return;
222 }
223
224 const result = await response.json();
225 if (!result.success || !result.data || result.data.length === 0) {
226 setPaymentMethods([]);
227 return;
228 }
229
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;
234 if (amount > 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;
239 }
240 }
241 }
242 });
243
244 const totalAmount = Object.values(paymentTotals).reduce((sum, amt) => sum + amt, 0);
245 const paymentArray = Object.entries(paymentTotals)
246 .map(([method, amount]) => ({
247 method,
248 amount: Math.round(amount),
249 percentage: totalAmount > 0 ? Math.round((amount / totalAmount) * 100) : 0
250 }))
251 .filter(p => p.amount > 0)
252 .sort((a, b) => b.amount - a.amount)
253 .slice(0, 10);
254
255 setPaymentMethods(paymentArray);
256 } catch (error) {
257 console.error('Error loading payment stats:', error);
258 setPaymentMethods([]);
259 }
260 };
261
262 const loadTellerStats = async () => {
263 try {
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));
267
268 const params = new URLSearchParams({
269 startDate: startOfDay.toISOString().split('T')[0],
270 endDate: endOfDay.toISOString().split('T')[0]
271 });
272
273 const response = await fetch(`/api/v1/stats/teller-performance?${params}`);
274 if (!response.ok) {
275 setTellerPerformance([]);
276 return;
277 }
278
279 const result = await response.json();
280 if (!result.success || !result.data || result.data.length === 0) {
281 setTellerPerformance([]);
282 return;
283 }
284
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)
290 }))
291 .filter((t: any) => t.teller !== 'Unknown' && t.amount > 0)
292 .sort((a: any, b: any) => b.amount - a.amount)
293 .slice(0, 10);
294
295 setTellerPerformance(tellerArray);
296 } catch (error) {
297 console.error('Error loading teller stats:', error);
298 setTellerPerformance([]);
299 }
300 };
301
302 const loadTopProducts = async () => {
303 try {
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));
307
308 const params = new URLSearchParams({
309 startDate: startOfDay.toISOString().split('T')[0],
310 endDate: endOfDay.toISOString().split('T')[0]
311 });
312
313 const response = await fetch(`/api/v1/stats/top-products?${params}`);
314 if (!response.ok) {
315 return;
316 }
317
318 const data = await response.json();
319 const products = data.APPD || [];
320
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
327 }))
328 .filter((p: any) => p.SellPrice > 0)
329 .sort((a: any, b: any) => b.SellPrice - a.SellPrice)
330 .slice(0, 10);
331
332 setProducts(productArray);
333 }
334 } catch (error) {
335 console.error('Error loading top products:', error);
336 }
337 };
338
339 const loadStorePerformance = async () => {
340 try {
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));
344
345 const params = new URLSearchParams({
346 startDate: startOfDay.toISOString().split('T')[0],
347 endDate: endOfDay.toISOString().split('T')[0]
348 });
349
350 const response = await fetch(`/api/v1/stats/store-performance?${params}`);
351 if (!response.ok) {
352 setStorePerformance([]);
353 return;
354 }
355
356 const data = await response.json();
357 const stores = data.APPD || [];
358
359 if (stores.length === 0) {
360 setStorePerformance([]);
361 return;
362 }
363
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)
369 }))
370 .filter((s: any) => s.amount > 0)
371 .sort((a: any, b: any) => b.amount - a.amount)
372 .slice(0, 3);
373
374 setStorePerformance(storeArray);
375 } catch (error) {
376 console.error('Error loading store performance:', error);
377 setStorePerformance([]);
378 }
379 };
380
381 async function loadDashboardData() {
382 try {
383 setLoading(true);
384
385 // Fetch real data using apiClient (leverages new OpenAPI routes)
386 const [
387 customersResult,
388 productsResult,
389 locationsResult,
390 salesTotalsResult
391 ] = await Promise.allSettled([
392 apiClient.getCustomers({ limit: 10000, source: 'openapi' }),
393 apiClient.getProducts({ limit: 10000, source: 'openapi' }),
394 apiClient.getLocations(),
395 apiClient.getSalesTotals()
396 ]);
397
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;
407 }
408 }
409
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;
419 }
420 }
421
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;
429 }
430 }
431
432 console.log('Dashboard data loaded:', {
433 customers: customerCount,
434 products: productCount,
435 locations: locationCount
436 });
437
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)
440 .slice(0, 10)
441 .map(p => ({
442 pid: p.Pid || 0,
443 description: p.Name || 'Unknown Product',
444 totalQty: p.StockLevel || 0,
445 locations: []
446 }));
447 setLowStockProducts(lowStock);
448
449 setDashboardData({
450 customers: customerCount,
451 products: productCount,
452 locations: locationCount
453 });
454
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();
462
463 setProducts(productsData);
464 setCustomers(customersData);
465
466 } catch (error) {
467 console.error('Failed to load dashboard data:', error);
468 } finally {
469 setLoading(false);
470 }
471 }
472
473 return (
474 <div className="p-6">
475 {/* Header */}
476 <div className="mb-6">
477 <div className="flex justify-between items-center">
478 <div>
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>
481 </div>
482 <div className="flex gap-2">
483 <select
484 value={dateRange}
485 onChange={(e) => setDateRange(e.target.value)}
486 className="px-3 py-2 border border-border rounded-md text-sm"
487 >
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>
492 </select>
493 </div>
494 </div>
495 </div>
496
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>
502 <div>
503 <div className="text-sm text-muted">Total Customers</div>
504 <div className="text-2xl font-bold text-brand">
505 {loading ? '...' : dashboardData.customers}
506 </div>
507 <div className="text-xs text-muted/70">From database</div>
508 </div>
509 </div>
510 </div>
511
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>
515 <div>
516 <div className="text-sm text-muted">Total Products</div>
517 <div className="text-2xl font-bold text-brand">
518 {loading ? '...' : dashboardData.products}
519 </div>
520 <div className="text-xs text-muted/70">From database</div>
521 </div>
522 </div>
523 </div>
524
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>
528 <div>
529 <div className="text-sm text-muted">Active Locations</div>
530 <div className="text-2xl font-bold text-success">
531 {loading ? '...' : dashboardData.locations}
532 </div>
533 <div className="text-xs text-muted/70">From database</div>
534 </div>
535 </div>
536 </div>
537
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>
541 <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>
545 </div>
546 </div>
547 </div>
548 </div>
549
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>
554 <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>
561 </p>
562 </div>
563 </div>
564 </div>
565
566 {/* Main Charts Grid */}
567 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
568
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>
573 </div>
574 <div className="p-4 h-80">
575 <SalesHourlyChart data={salesHourly} />
576 </div>
577 </div>
578
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>
583 </div>
584 <div className="p-4 h-80">
585 <SalesOverTimeChart timeframe="day" data={salesDaily} />
586 </div>
587 </div>
588
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>
593 </div>
594 <div className="p-4 h-80">
595 <PaymentMethodsChart data={paymentMethods} />
596 </div>
597 </div>
598
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>
603 </div>
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)
609 }))} />
610 </div>
611 </div>
612
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>
617 </div>
618 <div className="p-4 h-80">
619 <StorePerformanceChart data={storePerformance} />
620 </div>
621 </div>
622
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>
627 </div>
628 <div className="p-4 h-80">
629 <TellerPerformanceChart data={tellerPerformance} />
630 </div>
631 </div>
632 </div>
633
634 {/* Additional Insights */}
635 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
636
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>
641 </div>
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>
648 </div>
649 <div className="w-full bg-surface-2 rounded-full h-2">
650 <div
651 className="bg-brand h-2 rounded-full"
652 style={{ width: '100%' }}
653 ></div>
654 </div>
655 </div>
656
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}
662 </span>
663 </div>
664 <div className="w-full bg-surface-2 rounded-full h-2">
665 <div
666 className="bg-success h-2 rounded-full"
667 style={{
668 width: `${dashboardData.products > 0
669 ? (products.filter(p => (p.StockLevel || 0) > 0).length / dashboardData.products * 100)
670 : 0}%`
671 }}
672 ></div>
673 </div>
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)}%`
677 : '0%'} in stock
678 </div>
679 </div>
680
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
687 ).toFixed(0)}
688 </span>
689 </div>
690 <div className="w-full bg-surface-2 rounded-full h-2">
691 <div
692 className="bg-brand h-2 rounded-full"
693 style={{ width: '75%' }}
694 ></div>
695 </div>
696 <div className="text-xs text-muted">Estimated retail value</div>
697 </div>
698 </div>
699 </div>
700 </div>
701
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>
706 </div>
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>
713 </div>
714 <hr />
715 <div className="text-center">
716 <div className="text-2xl font-bold text-info">
717 {customers.filter(c => c.Email).length}
718 </div>
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)}%`
723 : '0%'}
724 </div>
725 </div>
726 <hr />
727 <div className="text-center">
728 <div className="text-2xl font-bold text-success">
729 {customers.filter(c => c.Phone || c.Mobile).length}
730 </div>
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)}%`
735 : '0%'}
736 </div>
737 </div>
738 </div>
739 </div>
740 </div>
741
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>
747 </div>
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';
753 return (
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 ? '...' : ''}
760 </div>
761 <div className="text-xs text-muted mt-1">
762 Product ID: {product.pid} | Total Stock: {stock} units
763 </div>
764 </div>
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'
770 }`}>
771 {status === 'out' ? '⚫ OUT' :
772 status === 'critical' ? '🔴 CRITICAL' :
773 status === 'low' ? '🟡 LOW' : '🟢 OK'}
774 </div>
775 </div>
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>
783 </div>
784 ))}
785 </div>
786 </div>
787 )}
788 </div>
789 );
790 })}
791 {lowStockProducts.length === 0 && (
792 <div className="text-sm text-muted text-center py-4">
793 ✅ No low stock alerts - All products well stocked!
794 </div>
795 )}
796 </div>
797 </div>
798 </div>
799 </div>
800
801 {/* Action Items */}
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>
812 </ul>
813 </div>
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
821 ).toFixed(0)}</li>
822 <li>• Active locations: {dashboardData.locations}</li>
823 </ul>
824 </div>
825 </div>
826 </div>
827 </div>
828 );
829 }