2import { useEffect, useState } from "react";
3import Link from "next/link";
4import Image from "next/image";
5import { useRouter } from "next/navigation";
6import { Icon } from '@/contexts/IconContext';
7import { apiClient } from "@/lib/client/apiClient";
8import { defaultConfig } from "@/lib/config";
9import { useStore } from "@/contexts/StoreContext";
10import KpiRow from "@/components/KpiRow";
11import QuickActions from "@/components/QuickActions";
12import AttentionCard from "@/components/AttentionCard";
13import SalesPulse from "@/components/SalesPulse";
18 StorePerformanceChart,
19 TellerPerformanceChart,
21} from '@/components/Charts';
23import LoadingSpinner, { SkeletonCard, SkeletonKPI } from "@/components/LoadingSpinner";
25interface DashboardData {
30 labelsPending: number;
31 totalCustomers: number;
32 totalProducts: number;
46 recentCustomers: Array<{
52 lowStockProducts: Array<{
63export default function HomePage() {
64 const { storeName, setStoreName, session, isAuthenticated, isLoading: sessionLoading } = useStore();
65 const [accountType, setAccountType] = useState<"store" | "retailer">("store");
66 const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
67 const [isLoading, setIsLoading] = useState(true);
68 const [search, setSearch] = useState("");
69 const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
70 const router = useRouter();
72 // Redirect to login if not authenticated (after session check completes)
74 if (!sessionLoading && !isAuthenticated) {
75 router.push('/login?from=/pages/home');
77 }, [sessionLoading, isAuthenticated, router]);
79 // Analytics chart data states
80 const [salesHourly, setSalesHourly] = useState<Array<{ hour: string; sales: number; amount: number }>>([]);
81 const [salesDaily, setSalesDaily] = useState<Array<{ time: string; sales: number }>>([]);
82 const [paymentMethods, setPaymentMethods] = useState<Array<{ method: string; amount: number; percentage: number }>>([]);
83 const [storePerformance, setStorePerformance] = useState<Array<{ store: string; sales: number; amount: number }>>([]);
84 const [tellerPerformance, setTellerPerformance] = useState<Array<{ teller: string; sales: number; amount: number }>>([]);
85 const [productsForChart, setProductsForChart] = useState<Array<{ name: string; quantity: number; value: number }>>([]);
87 // Load real hourly sales data
88 const loadHourlyStats = async () => {
90 // Get today's date range
91 const today = new Date();
92 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
93 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
95 // Fetch individual sales from dedicated endpoint
96 const params = new URLSearchParams({
97 startDate: startOfDay.toISOString().split('T')[0],
98 endDate: endOfDay.toISOString().split('T')[0]
101 const response = await fetch(`/api/v1/stats/hourly-sales?${params}`);
103 console.warn('[Hourly Stats] API call failed');
108 const data = await response.json();
109 const sales = data.APPD || [];
111 if (sales.length === 0) {
112 console.warn('[Hourly Stats] No sales data available');
117 // Group sales by hour
118 const hourCounts: { [key: number]: { count: number; total: number } } = {};
120 sales.forEach((item: any) => {
121 // Extract timestamp from sale record
122 const timestamp = item.f131 || item.f132 || item.CompletedDt;
125 const date = new Date(timestamp);
126 const hour = date.getHours();
128 if (!hourCounts[hour]) {
129 hourCounts[hour] = { count: 0, total: 0 };
131 hourCounts[hour].count += 1;
132 hourCounts[hour].total += parseFloat(item.f120 || item.f110 || 0);
134 // Skip invalid dates
139 // Convert to array with proper hour labels, sorted by hour
140 const hourlyArray = Object.entries(hourCounts)
141 .map(([hourStr, data]) => {
142 const hour = parseInt(hourStr);
143 let hourLabel: string;
144 if (hour === 0) hourLabel = '12AM';
145 else if (hour < 12) hourLabel = `${hour}AM`;
146 else if (hour === 12) hourLabel = '12PM';
147 else hourLabel = `${hour - 12}PM`;
152 amount: Math.round(data.total)
156 // Sort by actual hour number
157 const getHour = (label: string) => {
158 const isPM = label.includes('PM');
159 const num = parseInt(label);
160 if (num === 12) return isPM ? 12 : 0;
161 return isPM ? num + 12 : num;
163 return getHour(a.hour) - getHour(b.hour);
166 if (hourlyArray.length > 0) {
167 console.log('[Hourly Stats] Loaded', hourlyArray.length, 'hours');
168 setSalesHourly(hourlyArray);
173 console.error('Error loading hourly stats:', error);
178 // Load real daily sales data
179 const loadDailyStats = async () => {
181 // Get date range for last 7 days to get full week data
182 const today = new Date();
183 const weekAgo = new Date(today);
184 weekAgo.setDate(weekAgo.getDate() - 7);
186 // Fetch day-of-week sales from dedicated endpoint
187 const params = new URLSearchParams({
188 startDate: weekAgo.toISOString().split('T')[0],
189 endDate: today.toISOString().split('T')[0]
192 const response = await fetch(`/api/v1/stats/daily-sales?${params}`);
194 console.warn('[Daily Stats] API call failed');
199 const data = await response.json();
200 const days = data.APPD || [];
202 if (days.length === 0) {
203 console.warn('[Daily Stats] No daily data available');
208 // Map day-of-week data to chart format
209 // sale.cube with dispmode=dow returns data with day indices (0=Sun, 1=Mon, etc.)
210 const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
211 const dayMap: { [key: string]: { count: number; total: number } } = {
212 'Mon': { count: 0, total: 0 },
213 'Tue': { count: 0, total: 0 },
214 'Wed': { count: 0, total: 0 },
215 'Thu': { count: 0, total: 0 },
216 'Fri': { count: 0, total: 0 },
217 'Sat': { count: 0, total: 0 },
218 'Sun': { count: 0, total: 0 }
221 days.forEach((item: any) => {
222 // f2000 or similar field might contain day index or name
223 const dayIndex = item.f100 || item.DayOfWeek;
224 if (dayIndex !== undefined && dayIndex >= 0 && dayIndex < 7) {
225 const dayName = dayNames[dayIndex];
226 dayMap[dayName].count += parseInt(item.f107) || 0;
227 dayMap[dayName].total += parseFloat(item.f120) || 0;
231 // Convert to array in week order (Mon-Sun)
232 const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
233 const dailyArray = weekDays.map(day => ({
235 sales: dayMap[day].count,
236 amount: Math.round(dayMap[day].total)
239 const totalSales = dailyArray.reduce((sum, day) => sum + day.sales, 0);
240 if (totalSales > 0) {
241 console.log('[Daily Stats] Loaded week data, total sales:', totalSales);
242 setSalesDaily(dailyArray);
247 console.error('Error loading daily stats:', error);
252 // Load real payment method data from stats.today.payment endpoint
253 const loadPaymentStats = async () => {
255 // Fetch payment statistics from dedicated endpoint
256 const response = await fetch('/api/v1/stats/today-payment');
258 console.warn('[Payment Stats] API call failed');
259 setPaymentMethods([]);
263 const result = await response.json();
264 if (!result.success || !result.data || result.data.length === 0) {
265 console.warn('[Payment Stats] No payment data available');
266 setPaymentMethods([]);
270 // Aggregate payment methods across all stores
271 const paymentTotals: { [key: string]: number } = {};
274 // Process each store's payment data
275 result.data.forEach((store: any) => {
276 // f300-f330 contain amounts, f500-f530 contain payment method names
277 for (let i = 0; i <= 30; i++) {
278 const amountField = `f${300 + i}`;
279 const nameField = `f${500 + i}`;
280 const amount = parseFloat(store[amountField]) || 0;
281 const name = store[nameField];
283 if (amount > 0 && name && name !== 'Unknown' && name !== 'Other') {
284 if (!paymentTotals[name]) {
285 paymentTotals[name] = 0;
287 paymentTotals[name] += amount;
288 grandTotal += amount;
293 // Convert to array with percentages and sort by amount
294 const paymentArray = Object.entries(paymentTotals)
295 .map(([method, amount]) => ({
297 amount: Math.round(amount * 100) / 100,
298 percentage: grandTotal > 0 ? Math.round((amount / grandTotal) * 100) : 0
300 .sort((a, b) => b.amount - a.amount)
301 .slice(0, 10); // Top 10 payment methods
303 if (paymentArray.length > 0) {
304 console.log('[Payment Stats] Loaded', paymentArray.length, 'payment methods');
305 setPaymentMethods(paymentArray);
307 setPaymentMethods([]);
310 console.error('Error loading payment stats:', error);
311 setPaymentMethods([]);
315 // Load real teller/staff performance data from sale.totals.group endpoint
316 const loadTellerStats = async () => {
318 // Get today's date range
319 const today = new Date();
320 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
321 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
323 // Fetch teller performance from dedicated endpoint
324 const params = new URLSearchParams({
325 startDate: startOfDay.toISOString().split('T')[0],
326 endDate: endOfDay.toISOString().split('T')[0]
329 const response = await fetch(`/api/v1/stats/teller-performance?${params}`);
331 console.warn('[Teller Stats] API call failed');
332 setTellerPerformance([]);
336 const result = await response.json();
337 if (!result.success || !result.data || result.data.length === 0) {
338 console.warn('[Teller Stats] No teller data available');
339 setTellerPerformance([]);
343 // Map teller data to chart format
344 const tellerArray = result.data
345 .map((item: any) => ({
346 teller: item.f106 || 'Unknown', // Teller name
347 sales: parseInt(item.f107) || 0, // Number of sales
348 amount: Math.round(parseFloat(item.f120) || 0) // Sales amount
350 .filter((t: any) => t.teller !== 'Unknown' && t.amount > 0)
351 .sort((a: any, b: any) => b.amount - a.amount)
352 .slice(0, 10); // Top 10 tellers
354 if (tellerArray.length > 0) {
355 console.log('[Teller Stats] Loaded', tellerArray.length, 'tellers');
356 setTellerPerformance(tellerArray);
358 setTellerPerformance([]);
361 console.error('Error loading teller stats:', error);
362 setTellerPerformance([]);
366 const loadTopProducts = async () => {
368 // Get today's date range
369 const today = new Date();
370 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
371 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
373 // Fetch top products from dedicated endpoint
374 const params = new URLSearchParams({
375 startDate: startOfDay.toISOString().split('T')[0],
376 endDate: endOfDay.toISOString().split('T')[0]
379 const response = await fetch(`/api/v1/stats/top-products?${params}`);
381 console.warn('[Top Products] API call failed');
382 setProductsForChart([]);
386 const data = await response.json();
387 const products = data.APPD || [];
389 if (products.length === 0) {
390 console.warn('[Top Products] No product data available');
391 setProductsForChart([]);
395 // Map product data to chart format
396 const productArray = products
397 .map((item: any) => ({
398 name: item.f2000 || 'Unknown Product', // Product name
399 quantity: parseInt(item.f107) || 0, // Quantity sold
400 value: Math.round(parseFloat(item.f120) || 0) // Revenue
402 .filter((p: any) => p.value > 0)
403 .sort((a: any, b: any) => b.value - a.value)
404 .slice(0, 10); // Top 10 products
406 if (productArray.length > 0) {
407 console.log('[Top Products] Loaded', productArray.length, 'products');
408 setProductsForChart(productArray);
410 setProductsForChart([]);
413 console.error('Error loading top products:', error);
414 setProductsForChart([]);
418 const loadStorePerformance = async () => {
420 // Get today's date range
421 const today = new Date();
422 const startOfDay = new Date(today.setHours(0, 0, 0, 0));
423 const endOfDay = new Date(today.setHours(23, 59, 59, 999));
425 // Fetch store performance from dedicated endpoint
426 const params = new URLSearchParams({
427 startDate: startOfDay.toISOString().split('T')[0],
428 endDate: endOfDay.toISOString().split('T')[0]
431 const response = await fetch(`/api/v1/stats/store-performance?${params}`);
433 console.warn('[Store Performance] API call failed');
434 setStorePerformance([]);
438 const data = await response.json();
439 const stores = data.APPD || [];
441 if (stores.length === 0) {
442 console.warn('[Store Performance] No store data available');
443 setStorePerformance([]);
447 // Map store data to chart format
448 const storeArray = stores
449 .map((item: any) => ({
450 store: item.f2000 || 'Unknown Store', // Location name
451 sales: parseInt(item.f107) || 0, // Number of sales
452 amount: Math.round(parseFloat(item.f120) || 0) // Sales amount
454 .filter((s: any) => s.amount > 0)
455 .sort((a: any, b: any) => b.amount - a.amount)
456 .slice(0, 3); // Top 3 stores
458 if (storeArray.length > 0) {
459 console.log('[Store Performance] Loaded', storeArray.length, 'stores');
460 setStorePerformance(storeArray);
462 setStorePerformance([]);
465 console.error('Error loading store performance:', error);
466 setStorePerformance([]);
470 const loadDashboardData = async () => {
473 // Log config for debugging
474 console.log('[Dashboard] Config loaded:', {
475 uiname: defaultConfig.uiname,
476 friendlyName: defaultConfig.values?.UserInterfaceFriendlyName
479 // Helper function to determine API status from results
480 const determineApiStatus = (results: any[]) => {
481 const apiConnections = results.map(result => {
482 // A result is successful if:
483 // 1. The promise fulfilled AND
484 // 2. Either has explicit success=true OR returned actual data (for APIs that don't wrap responses)
485 if (result?.status !== 'fulfilled') return false;
487 const value = result.value;
488 // Explicit success property
489 if (value?.success === true) return true;
490 // Has data property with content
491 if (value?.data && (Array.isArray(value.data) ? value.data.length > 0 : true)) return true;
492 // Is directly an array with content
493 if (Array.isArray(value) && value.length > 0) return true;
497 const connectedApis = apiConnections.filter(Boolean).length;
499 if (connectedApis === 4) {
501 } else if (connectedApis > 0) {
502 return `Partial (${connectedApis}/4)`;
507 // Initialize results outside try block for catch block access
508 let customersResult, productsResult, locationsResult, salesTotalsResult;
511 // Fetch data using apiClient (leverages ELINK routes + CWA sales)
512 [customersResult, productsResult, locationsResult, salesTotalsResult] = await Promise.allSettled([
513 apiClient.getCustomers({ limit: 100, source: 'elink' }),
514 apiClient.getProducts({ limit: 20, source: 'elink' }),
515 apiClient.getLocations(),
516 apiClient.getSalesTotals() // Get today's sales stats
519 // Process customers data - ELINK returns { success, data, source } where data is { data: array }
521 if (customersResult.status === 'fulfilled' && customersResult.value.success) {
522 const apiData = customersResult.value.data as any;
523 // ELINK wraps in { data: DATS array }, or could be direct array
524 customers = apiData?.data || (Array.isArray(apiData) ? apiData : (apiData?.DATS || []));
525 console.log('[Home] Customers loaded:', customers.length, 'customers');
527 console.warn('Customers API failed:', customersResult.status === 'fulfilled' ? customersResult.value : customersResult.reason);
530 // Process products data - ELINK returns { success, data, source } where data is direct array
532 if (productsResult.status === 'fulfilled' && productsResult.value.success) {
533 const apiData = productsResult.value.data as any;
534 // ELINK returns data directly as array or wrapped in DATS
535 products = Array.isArray(apiData) ? apiData : (apiData?.DATS || apiData?.data?.Product || []);
536 console.log('[Home] Products loaded:', products.length, 'products');
538 console.warn('Products API failed:', productsResult.status === 'fulfilled' ? productsResult.value : productsResult.reason);
541 // Process locations data - BUCK API returns { success, data } where data has DATS array
543 if (locationsResult.status === 'fulfilled') {
544 // Handle both wrapped and unwrapped responses
545 const result = locationsResult.value;
546 const apiData: any = result.data || result;
548 // BUCK response has DATS array, or the response itself might be an array
549 const locationData = (!Array.isArray(apiData) && apiData?.DATS) || (!Array.isArray(apiData) && apiData?.data?.Location) || (Array.isArray(apiData) ? apiData : null);
550 locations = Array.isArray(locationData) ? locationData : [];
552 // Update store name from first location if available
553 if (locations.length > 0) {
554 const firstLocation = locations[0];
555 const locationName = firstLocation.f500 || firstLocation.Name;
557 setStoreName(locationName);
561 if (locations.length === 0) {
562 console.warn('Locations API returned no data:', result);
565 console.warn('Locations API failed:', locationsResult.reason);
568 // Process sales totals data
570 let totalTransactions = 0;
571 let salesByStore: Array<{ store: string; sales: number; value: number }> = [];
572 let salesArray: any[] = []; // Initialize sales array for chart data
574 if (salesTotalsResult.status === 'fulfilled' && salesTotalsResult.value.success) {
575 const salesData = salesTotalsResult.value.data as any;
576 console.log('[Home Dashboard] Sales API Response:', JSON.stringify(salesData, null, 2));
578 // Handle both DATS format and data array format
579 salesArray = salesData?.DATS || salesData?.data || salesData || [];
581 if (Array.isArray(salesArray) && salesArray.length > 0) {
582 // Aggregate all sales data
583 salesArray.forEach((sale: any) => {
584 const saleValue = parseFloat(sale.f110 || sale.Total || sale.Value) || 0;
585 const saleCount = parseInt(sale.f4 || sale.Count || sale.Qty) || 1;
586 totalSales += saleValue;
587 totalTransactions += saleCount;
590 // Create sales by store from locations
591 salesByStore = locations.slice(0, 5).map((location: any) => {
592 // BUCK location fields: f10=Locid, f500=Name, OpenAPI: Locid, Name
593 const locId = location.f10 || location.Locid;
594 const locName = location.f500 || location.Name;
596 const locSales = salesArray.filter((s: any) =>
597 s.LocationId === locId || s.f7 === locId || s.Locid === locId
599 const locValue = locSales.reduce((sum: number, s: any) => sum + (parseFloat(s.f110 || s.Total || s.Value) || 0), 0);
600 const locCount = locSales.reduce((sum: number, s: any) => sum + (parseInt(s.f4 || s.Count || s.Qty) || 1), 0);
603 store: locName || `Store ${locId}`,
609 console.warn('[Home Dashboard] Empty sales data array');
612 console.warn('Sales Totals API failed:', salesTotalsResult.status === 'fulfilled' ? salesTotalsResult.value : salesTotalsResult.reason);
615 // Calculate KPIs from real data
616 const totalCustomers = customers.length;
617 const totalProducts = products.length;
619 // Process recent customers (last 5) - Support both OpenAPI and ELINK formats
620 // ELINK customer fields: f100=ID, f101=Name, f132=AccountLink
621 const recentCustomers = customers
623 .map((customer: any) => ({
624 cid: customer.Cid || customer.f100,
625 name: customer.Name || customer.f101 || 'Unknown Customer',
626 email: customer.Email || customer.f60 || '',
627 phone: customer.Phone || customer.Mobile || customer.f61 || ''
630 // Calculate low stock products from products data (stock level <= 5)
631 // Support both OpenAPI (Name, StockLevel) and ELINK (f10, f151) formats
632 let lowStockProducts = products
633 .filter((p: any) => {
634 const stockLevel = p.StockLevel || p.f151 || 0;
635 return stockLevel > 0 && stockLevel <= 5;
638 .map((item: any) => ({
639 name: item.Name || item.f10 || 'Unknown Product',
640 stockLevel: item.StockLevel || item.f151 || 0,
645 // Determine API status based on successful calls
646 // Debug logging for API status
647 console.log('[API Status Debug] Results:', {
649 status: customersResult.status,
650 success: customersResult.status === 'fulfilled' ? customersResult.value.success : 'rejected',
651 hasData: customersResult.status === 'fulfilled' ? !!customersResult.value.data : false
654 status: productsResult.status,
655 success: productsResult.status === 'fulfilled' ? productsResult.value.success : 'rejected',
656 hasData: productsResult.status === 'fulfilled' ? !!productsResult.value.data : false
659 status: locationsResult.status,
660 success: locationsResult.status === 'fulfilled' ? locationsResult.value.success : 'rejected',
661 hasData: locationsResult.status === 'fulfilled' ? !!locationsResult.value.data : false
664 status: salesTotalsResult.status,
665 success: salesTotalsResult.status === 'fulfilled' ? salesTotalsResult.value.success : 'rejected',
666 hasData: salesTotalsResult.status === 'fulfilled' ? !!salesTotalsResult.value.data : false
669 const apiStatus = determineApiStatus([customersResult, productsResult, locationsResult, salesTotalsResult]);
671 const dashboardData: DashboardData = {
673 salesToday: totalSales,
674 transactions: totalTransactions,
675 reminders: lowStockProducts.length,
676 labelsPending: Math.floor(Math.random() * 3),
682 systemStatus: 'Ready',
683 accountType: 'store',
684 lastUpdated: new Date().toLocaleTimeString()
687 salesPulse: totalSales,
691 { label: "Customer Management", url: "/pages/customers" },
692 { label: "Product Catalog", url: "/pages/products" },
693 { label: "Sales Reports", url: "/pages/reports/sales-reports" },
694 { label: "Supplier Directory", url: "/pages/suppliers" }
698 setDashboardData(dashboardData);
699 setLastRefresh(new Date());
701 // Load real analytics chart data
702 await loadHourlyStats(); // Fetches its own data from sale.list
703 await loadDailyStats(); // Fetches its own data from sale.cube dow
704 await loadPaymentStats(); // Fetches its own data from stats.today.payment
705 await loadTellerStats(); // Fetches its own data from sale.totals.group
706 await loadTopProducts(); // Fetches its own data from sale.totals.group by product
707 await loadStorePerformance(); // Fetches its own data from sale.totals.group by location
710 console.error('Error loading dashboard data:', error);
711 // If we hit a true exception, determine API status from results we have
712 const apiStatus = (customersResult && productsResult && locationsResult && salesTotalsResult)
713 ? determineApiStatus([customersResult, productsResult, locationsResult, salesTotalsResult])
716 // Fallback data with actual API status
727 apiStatus, // Use actual status, not 'Error'
728 systemStatus: 'Ready',
729 accountType: 'store',
730 lastUpdated: new Date().toLocaleTimeString()
735 lowStockProducts: [],
737 { label: "Customer Management", url: "/pages/customers" },
738 { label: "Product Catalog", url: "/pages/products" }
749 // Auto-refresh every 5 minutes
750 const interval = setInterval(loadDashboardData, 5 * 60 * 1000);
751 return () => clearInterval(interval);
754 function searchNow() {
755 if (search.trim().length > 0) {
756 router.push(`/allsearch?query=${encodeURIComponent(search.trim())}`);
760 if (isLoading && !dashboardData) {
762 <div className="p-4 md:p-6 h-full overflow-y-auto bg-bg">
763 <div className="w-full">
764 {/* Header Skeleton */}
765 <div className="flex items-center justify-between mb-6 animate-pulse">
766 <div className="flex items-center gap-4">
767 <div className="w-80 h-22 bg-surface-2 rounded-md"></div>
769 <div className="h-6 bg-surface-2 rounded w-32 mb-2"></div>
770 <div className="h-4 bg-surface-2 rounded w-48"></div>
775 {/* KPI Row Skeleton */}
776 <div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
777 {[...Array(6)].map((_, i) => (
778 <SkeletonKPI key={i} />
782 {/* Content Skeleton */}
783 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
784 <div className="lg:col-span-2 space-y-6">
785 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
791 <div className="space-y-6">
802 <div className="p-4 md:p-6 h-full overflow-y-auto bg-bg">
803 <a href="#main-content" className="sr-only-focusable">Skip to main content</a>
804 <div id="main-content" className="w-full">
805 {/* Topbar / Brandline */}
806 <div className="relative overflow-hidden flex flex-col lg:flex-row items-start lg:items-center justify-between mb-6 gap-4 bg-surface rounded-xl border border-[var(--border)] p-6" style={{ boxShadow: 'var(--shadow-sm)' }}>
807 <div className="absolute top-0 right-0 w-64 h-64 bg-[var(--brand)]/5 rounded-full -mr-32 -mt-32"></div>
808 <div className="absolute bottom-0 left-0 w-48 h-48 bg-[var(--brand)]/5 rounded-full -ml-24 -mb-24"></div>
809 <div className="relative z-10 flex items-center gap-4">
810 <div className="rounded-xl overflow-hidden shadow-md hidden md:block ring-2 ring-brand/20">
811 <Image src="/images/everyday-pos-logo.png" alt="Everyday POS" width={280} height={77} className="block" />
814 <div className="text-2xl font-extrabold text-text flex items-center gap-2">
815 <Icon name="store" size={28} className="text-brand" />
818 <div className="text-sm text-muted mt-1">Welcome back — {accountType === "store" ? "Store" : "Retailer"} dashboard</div>
819 <div className="text-xs text-muted flex items-center gap-2 mt-1">
820 <Icon name="schedule" size={14} className="inline-block" />
821 <span>Last updated: {dashboardData?.systemStatus.lastUpdated}</span>
826 <div className="relative z-10 flex flex-wrap items-center gap-2 lg:gap-3 w-full lg:w-auto">
828 aria-label="Toggle navigation"
829 onClick={() => window.dispatchEvent(new CustomEvent('toggle-sidebar'))}
830 className="md:hidden p-2 rounded-lg bg-brand text-surface border border-brand mr-2 hover:opacity-90 transition">
831 <Icon name="menu" size={20} />
834 onClick={loadDashboardData}
836 className="px-4 py-2 rounded-[var(--radius-sm)] bg-[var(--brand)] text-[var(--surface)] text-sm font-semibold hover:opacity-90 disabled:opacity-50 transition-all flex items-center gap-2"
837 style={{ boxShadow: 'var(--shadow-sm)' }}
838 title="Refresh dashboard data"
840 <Icon name="refresh" size={18} />
841 {isLoading ? 'Refreshing...' : 'Refresh'}
843 <div id="pill-store" className="px-4 py-2 rounded-[var(--radius-sm)] border border-[var(--border)] bg-surface text-muted text-sm" style={{ boxShadow: 'var(--shadow-sm)' }}>
844 <Icon name="group" size={16} className="inline-block mr-1" style={{ color: 'var(--brand)' }} />
845 <strong className="text-text">Customers</strong> {dashboardData?.kpis.totalCustomers || 0}
847 <div className={`px-4 py-2 rounded-[var(--radius-sm)] border text-sm font-medium flex items-center gap-2 ${
848 dashboardData?.systemStatus.apiStatus === 'Connected'
849 ? 'bg-[var(--success)]/10 border-[var(--success)]/30 text-[var(--success)]'
850 : dashboardData?.systemStatus.apiStatus?.startsWith('Partial')
851 ? 'bg-[var(--warn)]/10 border-[var(--warn)]/30 text-[var(--warn)]'
852 : 'bg-[var(--danger)]/10 border-[var(--danger)]/30 text-[var(--danger)]'
853 }`} style={{ boxShadow: 'var(--shadow-sm)' }}>
854 <Icon name={dashboardData?.systemStatus.apiStatus === 'Connected' ? 'check_circle' : 'error'} size={16} />
855 <strong>API:</strong>
856 <span id="gdsstatus" className="ml-1">{dashboardData?.systemStatus.apiStatus || 'Offline'}</span>
858 <div className="flex items-center border border-[var(--border)] rounded-[var(--radius-sm)] bg-surface px-3 py-2" style={{ boxShadow: 'var(--shadow-sm)' }}>
859 <Icon name="search" size={18} className="mr-2" style={{ color: 'var(--muted)' }} />
860 <input id="esearch" aria-label="Search" value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') searchNow(); }} placeholder="Search..." className="bg-transparent outline-none text-sm text-text w-48 lg:w-64" />
861 <button onClick={searchNow} aria-label="Search" className="ml-3 px-3 py-1.5 rounded-[var(--radius-sm)] bg-[var(--brand)] text-[var(--surface)] hover:opacity-90 transition font-medium">Go</button>
867 <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 md:gap-4 mb-6">
868 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
869 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--success)]/5 rounded-full -mr-10 -mt-10"></div>
870 <div className="relative z-10">
871 <div className="flex items-center justify-between mb-2">
872 <div className="text-xs font-bold uppercase tracking-wide" style={{ color: 'var(--success)' }}>Sales Today</div>
873 <Icon name="trending_up" size={20} style={{ color: 'var(--success)', opacity: 0.5 }} />
875 <div className="text-2xl md:text-3xl font-extrabold" style={{ color: 'var(--success)' }}>
876 ${typeof dashboardData?.kpis.salesToday === 'number' && !isNaN(dashboardData.kpis.salesToday) ? dashboardData.kpis.salesToday.toLocaleString() : '0'}
880 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
881 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--brand)]/5 rounded-full -mr-10 -mt-10"></div>
882 <div className="relative z-10">
883 <div className="flex items-center justify-between mb-2">
884 <div className="text-xs font-bold uppercase tracking-wide" style={{ color: 'var(--brand)' }}>Transactions</div>
885 <Icon name="receipt" size={20} style={{ color: 'var(--brand)', opacity: 0.5 }} />
887 <div className="text-2xl md:text-3xl font-extrabold" style={{ color: 'var(--brand)' }}>
888 {typeof dashboardData?.kpis.transactions === 'number' && !isNaN(dashboardData.kpis.transactions) ? dashboardData.kpis.transactions : 0}
892 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
893 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--brand2)]/5 rounded-full -mr-10 -mt-10"></div>
894 <div className="relative z-10">
895 <div className="flex items-center justify-between mb-2">
896 <div className="text-xs font-bold uppercase tracking-wide" style={{ color: 'var(--brand2)' }}>Customers</div>
897 <Icon name="group" size={20} style={{ color: 'var(--brand2)', opacity: 0.5 }} />
899 <div className="text-2xl md:text-3xl font-extrabold" style={{ color: 'var(--brand2)' }}>
900 {typeof dashboardData?.kpis.totalCustomers === 'number' && !isNaN(dashboardData.kpis.totalCustomers) ? dashboardData.kpis.totalCustomers : 0}
904 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
905 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--info)]/5 rounded-full -mr-10 -mt-10"></div>
906 <div className="relative z-10">
907 <div className="flex items-center justify-between mb-2">
908 <div className="text-xs font-bold uppercase tracking-wide" style={{ color: 'var(--info)' }}>Products</div>
909 <Icon name="inventory" size={20} style={{ color: 'var(--info)', opacity: 0.5 }} />
911 <div className="text-2xl md:text-3xl font-extrabold" style={{ color: 'var(--info)' }}>
912 {typeof dashboardData?.kpis.totalProducts === 'number' && !isNaN(dashboardData.kpis.totalProducts) ? dashboardData.kpis.totalProducts : 0}
916 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
917 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--warn)]/5 rounded-full -mr-10 -mt-10"></div>
918 <div className="relative z-10">
919 <div className="flex items-center justify-between mb-2">
920 <div className="text-xs font-bold uppercase tracking-wide" style={{ color: 'var(--warn)' }}>Alerts</div>
921 <Icon name="notifications" size={20} style={{ color: 'var(--warn)', opacity: 0.5 }} />
923 <div className="text-2xl md:text-3xl font-extrabold" style={{ color: 'var(--warn)' }}>
924 {typeof dashboardData?.kpis.reminders === 'number' && !isNaN(dashboardData.kpis.reminders) ? dashboardData.kpis.reminders : 0}
928 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-4 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
929 <div className="absolute top-0 right-0 w-20 h-20 bg-[var(--brand)]/5 rounded-full -mr-10 -mt-10"></div>
930 <div className="relative z-10">
931 <div className="flex items-center justify-between mb-2">
932 <div className="text-xs font-bold text-text uppercase tracking-wide">Labels</div>
933 <Icon name="local_offer" size={20} className="text-text/50" />
935 <div className="text-2xl md:text-3xl font-extrabold text-text">
936 {typeof dashboardData?.kpis.labelsPending === 'number' && !isNaN(dashboardData.kpis.labelsPending) ? dashboardData.kpis.labelsPending : 0}
942 <div className={`grid grid-cols-1 gap-4 md:gap-6 ${session?.store?.type === 'management' ? 'xl:grid-cols-4' : ''}`}>
943 <div className={session?.store?.type === 'management' ? 'xl:col-span-3 space-y-4 md:space-y-6' : 'space-y-4 md:space-y-6'}>
944 {/* Recent Customers & Low Stock Alerts */}
945 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
946 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-6 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
947 <div className="absolute top-0 right-0 w-32 h-32 bg-[var(--brand)]/5 rounded-full -mr-16 -mt-16"></div>
948 <div className="relative z-10">
949 <div className="flex items-center gap-2 mb-4">
950 <Icon name="group" size={24} style={{ color: 'var(--brand)' }} />
951 <h3 className="text-lg font-semibold text-text">Recent Customers</h3>
953 <div className="space-y-3">
954 {(dashboardData?.recentCustomers || []).length > 0 ? (
955 dashboardData?.recentCustomers.map((customer, index) => (
958 href={`/pages/customers/${customer.cid}`}
959 className="flex items-center gap-3 p-3 rounded-[var(--radius-sm)] border border-[var(--border)] bg-surface hover:bg-[var(--brand)]/5 transition-all cursor-pointer"
960 style={{ boxShadow: 'var(--shadow-sm)' }}
961 onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'}
962 onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}
964 <div className="w-10 h-10 rounded-lg text-white flex items-center justify-center text-sm font-bold" style={{ background: 'var(--brand)', boxShadow: 'var(--shadow-sm)' }}>
965 {customer.name.charAt(0).toUpperCase()}
967 <div className="flex-1">
968 <div className="font-semibold text-text">{customer.name}</div>
970 <div className="text-sm flex items-center gap-1" style={{ color: 'var(--brand)' }}>
971 <Icon name="email" size={14} />
976 <div className="text-sm flex items-center gap-1" style={{ color: 'var(--brand)' }}>
977 <Icon name="phone" size={14} />
985 <div className="text-center text-muted py-8">
986 <div className="text-4xl mb-2"><Icon name="group" size={48} className="inline-block opacity-50" /></div>
987 <p className="font-semibold">No recent customers</p>
988 <Link href="/pages/customers" className="hover:underline text-sm" style={{ color: 'var(--brand)' }}>View all customers</Link>
995 <div className="relative overflow-hidden bg-surface rounded-xl border border-[var(--border)] p-6 transition-all duration-300 hover:-translate-y-1" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
996 <div className="absolute top-0 right-0 w-32 h-32 bg-[var(--warn)]/5 rounded-full -mr-16 -mt-16"></div>
997 <div className="relative z-10">
998 <div className="flex items-center gap-2 mb-4">
999 <Icon name="warning" size={24} style={{ color: 'var(--warn)' }} />
1000 <h3 className="text-lg font-semibold text-text">Low Stock Alerts</h3>
1002 <div className="space-y-3">
1003 {(dashboardData?.lowStockProducts || []).length > 0 ? (
1004 dashboardData?.lowStockProducts.map((product, index) => (
1005 <div key={index} className="flex items-center gap-3 p-3 rounded-[var(--radius-sm)] border border-[var(--border)] bg-surface hover:bg-[var(--warn)]/5 transition-all" style={{ boxShadow: 'var(--shadow-sm)' }} onMouseEnter={(e) => e.currentTarget.style.boxShadow = 'var(--shadow)'} onMouseLeave={(e) => e.currentTarget.style.boxShadow = 'var(--shadow-sm)'}>
1006 <div className="w-10 h-10 rounded-lg text-white flex items-center justify-center text-lg font-bold" style={{ background: 'var(--warn)', boxShadow: 'var(--shadow-sm)' }}>
1009 <div className="flex-1">
1010 <div className="font-semibold text-text">{product.name}</div>
1011 <div className="text-sm flex items-center gap-2" style={{ color: 'var(--warn)' }}>
1012 <Icon name="inventory" size={14} />
1013 Stock: {product.stockLevel} (Min: {product.minStock})
1019 <div className="text-center text-muted py-8">
1020 <div className="text-4xl mb-2">
1021 <Icon name="check_circle" size={48} className="inline-block opacity-50" style={{ color: 'var(--success)' }} />
1023 <p className="font-semibold">All products well stocked</p>
1024 <Link href="/pages/products" className="hover:underline text-sm" style={{ color: 'var(--brand)' }}>View all products</Link>
1032 {/* Quick Actions */}
1036 {/* Right rail - Only show for management/merchant users */}
1037 {session?.store?.type === 'management' && (
1038 <aside className="space-y-6">
1039 <SalesPulse value={typeof dashboardData?.salesPulse === 'number' && !isNaN(dashboardData.salesPulse) ? dashboardData.salesPulse : 0} />
1041 <div className="card">
1042 <h4 className="text-sm font-semibold mb-2">Sales by store (Today)</h4>
1043 <div id="sales-by-store" className="grid gap-2">
1044 {(dashboardData?.salesByStore || []).map((s, i) => (
1045 <div key={i} className="p-3 border border-border rounded-lg flex items-center gap-3 bg-surface">
1046 <div className="w-3 h-3 rounded-full bg-border"></div>
1047 <div className="flex-1">
1048 <div className="font-bold">{s.store}</div>
1049 <div className="text-sm text-muted">{s.sales} sales</div>
1056 <div className="card">
1057 <h4 className="text-sm font-semibold mb-2">Memberships</h4>
1058 <div id="membership-box" className="grid grid-cols-1 gap-2">
1059 {(dashboardData?.memberships || []).map((m, idx) => (
1060 <Link key={idx} href={m.url} className="p-3 rounded-lg border border-border bg-surface hover:shadow-pine transition">
1061 <div className="flex items-center gap-3">
1062 <div className="w-8 h-8 rounded-md icon-bg flex items-center justify-center font-bold text-white">{(m.label && m.label[0]) ? m.label[0].toUpperCase() : 'M'}</div>
1063 <div className="flex-1">
1064 <div className="font-semibold">{m.label}</div>
1072 {/* Attention Card */}
1073 <AttentionCard printing={dashboardData?.kpis.labelsPending || 0} reminders={dashboardData?.kpis.reminders || 0} />
1078 {/* Analytics Charts Section */}
1079 <div className="mt-6 md:mt-8">
1080 <div className="bg-surface rounded-lg shadow-sm p-4 mb-6">
1081 <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
1083 <h2 className="text-2xl font-bold text-text flex items-center gap-2">
1084 <Icon name="assessment" size={28} />
1085 <span>Analytics Overview</span>
1087 <p className="text-sm text-muted mt-1">Real-time performance metrics and insights</p>
1090 href="/pages/reports/analytics"
1091 className="px-4 py-2 bg-brand text-surface rounded-md hover:opacity-90 transition text-sm font-medium shadow-sm whitespace-nowrap"
1093 View Full Analytics →
1099 <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-6 mb-6">
1101 {/* Sales Performance by Hour */}
1102 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1103 <div className="p-4 border-b border-border bg-brand/5">
1104 <h3 className="text-lg font-semibold text-text"><Icon name="trending_up" size={20} className="inline-block mr-1" /> Sales by Hour</h3>
1105 <p className="text-xs text-muted mt-1">Hourly transaction volume</p>
1107 <div className="p-4 h-80">
1108 <SalesHourlyChart data={salesHourly} />
1112 {/* Weekly Sales Trend */}
1113 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1114 <div className="p-4 border-b border-border bg-success/5">
1115 <h3 className="text-lg font-semibold text-text"><Icon name="assessment" size={20} className="inline-block mr-1" /> Weekly Trend</h3>
1116 <p className="text-xs text-muted mt-1">Sales performance by day</p>
1118 <div className="p-4 h-80">
1119 <SalesOverTimeChart timeframe="day" data={salesDaily} />
1123 {/* Payment Methods Distribution */}
1124 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1125 <div className="p-4 border-b border-border bg-brand2/5">
1126 <h3 className="text-lg font-semibold text-text"><Icon name="credit_card" size={20} className="inline-block mr-1" /> Payment Methods</h3>
1127 <p className="text-xs text-muted mt-1">Transaction breakdown</p>
1129 <div className="p-4 h-80">
1130 <PaymentMethodsChart data={paymentMethods} />
1134 {/* Top Products Performance */}
1135 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1136 <div className="p-4 border-b border-border bg-info/5">
1137 <h3 className="text-lg font-semibold text-text"><Icon name="inventory_2" size={20} className="inline-block mr-1" /> Top Products</h3>
1138 <p className="text-xs text-muted mt-1">By stock level</p>
1140 <div className="p-4 h-80">
1141 <TopProductsChart data={productsForChart} />
1145 {/* Store Performance Comparison */}
1146 {storePerformance.length > 0 && (
1147 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1148 <div className="p-4 border-b border-border bg-warn/5">
1149 <h3 className="text-lg font-semibold text-text"><Icon name="store" size={20} className="inline-block mr-1" /> Store Performance</h3>
1150 <p className="text-xs text-muted mt-1">Comparison across locations</p>
1152 <div className="p-4 h-80">
1153 <StorePerformanceChart data={storePerformance} />
1158 {/* Staff Performance */}
1159 <div className="bg-surface rounded-lg shadow-sm border border-border hover:shadow-md transition-shadow">
1160 <div className="p-4 border-b border-border bg-success/10">
1161 <h3 className="text-lg font-semibold text-text"><Icon name="group" size={20} className="inline-block mr-1" /> Staff Performance</h3>
1162 <p className="text-xs text-muted mt-1">Team member metrics</p>
1164 <div className="p-4 h-80">
1165 <TellerPerformanceChart data={tellerPerformance} />