From c704ceed1d723ae1901b542647b4cc1d216235b5 Mon Sep 17 00:00:00 2001 From: g Date: Mon, 5 Jan 2026 21:55:55 +0000 Subject: [PATCH] Add year selection to admin analytics and split product table AdminAnalytics now supports selecting historical years and updates available years dynamically from the API. The product table is refactored to display enabled and disabled products in separate tables, improving clarity. Minor formatting and code style improvements are also included. --- components/admin/AdminAnalytics.tsx | 757 +++++++++++++----- .../layout/NotificationProviderWrapper.tsx | 1 + components/tables/product-table.tsx | 287 ++++--- models/products.ts | 6 +- public/git-info.json | 4 +- 5 files changed, 726 insertions(+), 329 deletions(-) diff --git a/components/admin/AdminAnalytics.tsx b/components/admin/AdminAnalytics.tsx index 3aa294b..ced1333 100644 --- a/components/admin/AdminAnalytics.tsx +++ b/components/admin/AdminAnalytics.tsx @@ -1,18 +1,58 @@ "use client"; import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Button } from "@/components/ui/button"; -import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart, - TrendingUp, TrendingDown, DollarSign, Package, Trophy } from "lucide-react"; +import { + AlertCircle, + BarChart, + RefreshCw, + Users, + ShoppingCart, + TrendingUp, + TrendingDown, + DollarSign, + Package, + Trophy, +} from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { fetchClient } from "@/lib/api-client"; -import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts'; +import { + BarChart as RechartsBarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + LineChart, + Line, + ComposedChart, +} from "recharts"; import { formatGBP } from "@/utils/format"; interface AnalyticsData { + meta?: { + year: number; + currentYear: number; + availableYears: number[]; + range: string; + isCurrentYear: boolean; + }; vendors?: { total?: number; newToday?: number; @@ -41,7 +81,12 @@ interface AnalyticsData { total?: number; today?: number; thisWeek?: number; - dailyRevenue?: { date: string; amount: number; orders?: number; avgOrderValue?: number }[]; + dailyRevenue?: { + date: string; + amount: number; + orders?: number; + avgOrderValue?: number; + }[]; }; engagement?: { totalChats?: number; @@ -64,20 +109,45 @@ interface AnalyticsData { } export default function AdminAnalytics() { - const [analyticsData, setAnalyticsData] = useState(null); + const [analyticsData, setAnalyticsData] = useState( + null, + ); const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState("7days"); + const [selectedYear, setSelectedYear] = useState( + new Date().getFullYear(), + ); + const [availableYears, setAvailableYears] = useState([ + new Date().getFullYear(), + ]); const [errorMessage, setErrorMessage] = useState(null); const [refreshing, setRefreshing] = useState(false); const [showDebug, setShowDebug] = useState(false); + const currentYear = new Date().getFullYear(); + const isViewingCurrentYear = selectedYear === currentYear; + const fetchAnalyticsData = async () => { try { setLoading(true); setErrorMessage(null); - - const data = await fetchClient(`/admin/analytics?range=${dateRange}`); + + // Build query params - include year if not current year + const params = new URLSearchParams(); + params.set("range", dateRange); + if (selectedYear !== currentYear) { + params.set("year", selectedYear.toString()); + } + + const data = await fetchClient( + `/admin/analytics?${params.toString()}`, + ); setAnalyticsData(data); + + // Update available years from response if provided + if (data.meta?.availableYears && data.meta.availableYears.length > 0) { + setAvailableYears(data.meta.availableYears); + } } catch (error) { console.error("Error fetching analytics data:", error); setErrorMessage("Failed to load analytics data. Please try again."); @@ -89,7 +159,7 @@ export default function AdminAnalytics() { useEffect(() => { fetchAnalyticsData(); - }, [dateRange]); + }, [dateRange, selectedYear]); const handleRefresh = () => { setRefreshing(true); @@ -105,64 +175,97 @@ export default function AdminAnalytics() { } // Helper to transform data for recharts - const transformChartData = (data: Array<{ date: string; [key: string]: any }>, valueKey: string = "count") => { + const transformChartData = ( + data: Array<{ date: string; [key: string]: any }>, + valueKey: string = "count", + ) => { if (!data || data.length === 0) return []; - - return data.map(item => { + + return data.map((item) => { const dateStr = item.date; // Parse YYYY-MM-DD format - const parts = dateStr.split('-'); - const date = parts.length === 3 - ? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])) - : new Date(dateStr); - + const parts = dateStr.split("-"); + const date = + parts.length === 3 + ? new Date( + parseInt(parts[0]), + parseInt(parts[1]) - 1, + parseInt(parts[2]), + ) + : new Date(dateStr); + // Format with day of week: "Mon, Nov 21" - const dayOfWeek = date.toLocaleDateString('en-GB', { weekday: 'short' }); - const monthDay = date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }); - + const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" }); + const monthDay = date.toLocaleDateString("en-GB", { + month: "short", + day: "numeric", + }); + return { date: dateStr, formattedDate: `${dayOfWeek}, ${monthDay}`, value: Number(item[valueKey]) || 0, - [valueKey]: Number(item[valueKey]) || 0 + [valueKey]: Number(item[valueKey]) || 0, }; }); }; // Helper to combine orders and revenue data for dual-axis chart - const combineOrdersAndRevenue = (orders: Array<{ date: string; count: number }>, revenue: Array<{ date: string; amount: number; orders?: number; avgOrderValue?: number }>) => { + const combineOrdersAndRevenue = ( + orders: Array<{ date: string; count: number }>, + revenue: Array<{ + date: string; + amount: number; + orders?: number; + avgOrderValue?: number; + }>, + ) => { if (!orders || orders.length === 0) return []; - + // Create a map of revenue and AOV by date for quick lookup - const revenueMap = new Map(); + const revenueMap = new Map< + string, + { amount: number; avgOrderValue: number } + >(); if (revenue && revenue.length > 0) { - revenue.forEach(r => { - revenueMap.set(r.date, { + revenue.forEach((r) => { + revenueMap.set(r.date, { amount: r.amount || 0, - avgOrderValue: r.avgOrderValue || 0 + avgOrderValue: r.avgOrderValue || 0, }); }); } - - return orders.map(order => { + + return orders.map((order) => { const dateStr = order.date; - const parts = dateStr.split('-'); - const date = parts.length === 3 - ? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])) - : new Date(dateStr); - + const parts = dateStr.split("-"); + const date = + parts.length === 3 + ? new Date( + parseInt(parts[0]), + parseInt(parts[1]) - 1, + parseInt(parts[2]), + ) + : new Date(dateStr); + // Format with day of week: "Mon, Nov 21" - const dayOfWeek = date.toLocaleDateString('en-GB', { weekday: 'short' }); - const monthDay = date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }); - - const revenueData = revenueMap.get(dateStr) || { amount: 0, avgOrderValue: 0 }; - + const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" }); + const monthDay = date.toLocaleDateString("en-GB", { + month: "short", + day: "numeric", + }); + + const revenueData = revenueMap.get(dateStr) || { + amount: 0, + avgOrderValue: 0, + }; + return { date: dateStr, formattedDate: `${dayOfWeek}, ${monthDay}`, orders: order.count || 0, revenue: revenueData.amount, - avgOrderValue: revenueData.avgOrderValue + avgOrderValue: revenueData.avgOrderValue, }; }); }; @@ -172,34 +275,47 @@ export default function AdminAnalytics() { if (active && payload && payload.length) { const data = payload[0].payload; const dataKey = payload[0].dataKey; - const isDualAxis = data.orders !== undefined && data.revenue !== undefined; - + const isDualAxis = + data.orders !== undefined && data.revenue !== undefined; + // Determine if this is a currency amount or a count // transformChartData creates both 'value' and the original key (count/amount) // So we check the original key to determine the type - const isAmount = dataKey === 'amount' || dataKey === 'revenue' || - (dataKey === 'value' && data.amount !== undefined && data.count === undefined); - + const isAmount = + dataKey === "amount" || + dataKey === "revenue" || + (dataKey === "value" && + data.amount !== undefined && + data.count === undefined); + return (
-

{data.formattedDate || label}

+

+ {data.formattedDate || label} +

{isDualAxis ? (

Orders: {data.orders}

- Revenue: {formatGBP(data.revenue)} + Revenue:{" "} + {formatGBP(data.revenue)}

{data.avgOrderValue !== undefined && data.avgOrderValue > 0 && (

- Avg Order Value: {formatGBP(data.avgOrderValue)} + Avg Order Value:{" "} + + {formatGBP(data.avgOrderValue)} +

)}
) : (

- {isAmount ? formatGBP(data.value || data.amount || 0) : `${data.value || data.count || 0}`} + {isAmount + ? formatGBP(data.value || data.amount || 0) + : `${data.value || data.count || 0}`}

)}
@@ -209,18 +325,28 @@ export default function AdminAnalytics() { }; // Trend indicator component for metric cards - const TrendIndicator = ({ current, previous }: { current: number, previous: number }) => { + const TrendIndicator = ({ + current, + previous, + }: { + current: number; + previous: number; + }) => { if (!current || !previous) return null; - + const percentChange = ((current - previous) / previous) * 100; - + if (Math.abs(percentChange) < 0.1) return null; - + return ( -
= 0 ? 'text-green-500' : 'text-red-500'}`}> - {percentChange >= 0 ? - : - } +
= 0 ? "text-green-500" : "text-red-500"}`} + > + {percentChange >= 0 ? ( + + ) : ( + + )} {Math.abs(percentChange).toFixed(1)}%
); @@ -228,55 +354,70 @@ export default function AdminAnalytics() { // Format currency const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-GB', { - style: 'currency', - currency: 'GBP', - maximumFractionDigits: 0 + return new Intl.NumberFormat("en-GB", { + style: "currency", + currency: "GBP", + maximumFractionDigits: 0, }).format(value); }; // Calculate best month from daily data (for YTD view) const calculateBestMonth = () => { - if (dateRange !== 'ytd' || !analyticsData?.revenue?.dailyRevenue || analyticsData.revenue.dailyRevenue.length === 0) { + if ( + dateRange !== "ytd" || + !analyticsData?.revenue?.dailyRevenue || + analyticsData.revenue.dailyRevenue.length === 0 + ) { return null; } // Group daily revenue by month - const monthlyTotals = new Map(); - - analyticsData.revenue.dailyRevenue.forEach(day => { + const monthlyTotals = new Map< + string, + { revenue: number; orders: number } + >(); + + analyticsData.revenue.dailyRevenue.forEach((day) => { const date = new Date(day.date); - const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 }; monthlyTotals.set(monthKey, { revenue: current.revenue + (day.amount || 0), - orders: current.orders + orders: current.orders, }); }); // Also group orders by month if (analyticsData.orders?.dailyOrders) { - analyticsData.orders.dailyOrders.forEach(day => { + analyticsData.orders.dailyOrders.forEach((day) => { const date = new Date(day.date); - const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; - const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 }; + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + const current = monthlyTotals.get(monthKey) || { + revenue: 0, + orders: 0, + }; monthlyTotals.set(monthKey, { revenue: current.revenue, - orders: current.orders + (day.count || 0) + orders: current.orders + (day.count || 0), }); }); } // Find the month with highest revenue - let bestMonth: { month: string; revenue: number; orders: number } | null = null; + let bestMonth: { month: string; revenue: number; orders: number } | null = + null; monthlyTotals.forEach((totals, monthKey) => { if (!bestMonth || totals.revenue > bestMonth.revenue) { - const [year, month] = monthKey.split('-'); - const monthName = new Date(parseInt(year), parseInt(month) - 1, 1).toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }); + const [year, month] = monthKey.split("-"); + const monthName = new Date( + parseInt(year), + parseInt(month) - 1, + 1, + ).toLocaleDateString("en-GB", { month: "long", year: "numeric" }); bestMonth = { month: monthName, revenue: totals.revenue, - orders: totals.orders + orders: totals.orders, }; } }); @@ -295,51 +436,87 @@ export default function AdminAnalytics() { {errorMessage} )} - +
-

Dashboard Analytics

-

Overview of your marketplace performance

+

+ Dashboard Analytics + {!isViewingCurrentYear && ( + + ({selectedYear}) + + )} +

+

+ {isViewingCurrentYear + ? "Overview of your marketplace performance" + : `Historical data for ${selectedYear}`} +

- +
- + + {/* Date range selector - only show options for current year */} + - - - -
- + {showDebug && analyticsData && ( @@ -351,43 +528,66 @@ export default function AdminAnalytics() {
Orders:
-
Total: {analyticsData?.orders?.total || 'N/A'}
-
Today: {analyticsData?.orders?.totalToday || 'N/A'}
-
Daily Orders Array Length: {analyticsData?.orders?.dailyOrders?.length || 0}
+
Total: {analyticsData?.orders?.total || "N/A"}
+
Today: {analyticsData?.orders?.totalToday || "N/A"}
+
+ Daily Orders Array Length:{" "} + {analyticsData?.orders?.dailyOrders?.length || 0} +
First 3 Daily Orders:
-                    {JSON.stringify(analyticsData?.orders?.dailyOrders?.slice(0, 3), null, 2)}
+                    {JSON.stringify(
+                      analyticsData?.orders?.dailyOrders?.slice(0, 3),
+                      null,
+                      2,
+                    )}
                   
- +
Revenue:
-
Total: {analyticsData?.revenue?.total || 'N/A'}
-
Today: {analyticsData?.revenue?.today || 'N/A'}
-
Daily Revenue Array Length: {analyticsData?.revenue?.dailyRevenue?.length || 0}
+
Total: {analyticsData?.revenue?.total || "N/A"}
+
Today: {analyticsData?.revenue?.today || "N/A"}
+
+ Daily Revenue Array Length:{" "} + {analyticsData?.revenue?.dailyRevenue?.length || 0} +
First 3 Daily Revenue:
-                    {JSON.stringify(analyticsData?.revenue?.dailyRevenue?.slice(0, 3), null, 2)}
+                    {JSON.stringify(
+                      analyticsData?.revenue?.dailyRevenue?.slice(0, 3),
+                      null,
+                      2,
+                    )}
                   
- +
Vendors:
-
Total: {analyticsData?.vendors?.total || 'N/A'}
-
Daily Growth Array Length: {analyticsData?.vendors?.dailyGrowth?.length || 0}
+
Total: {analyticsData?.vendors?.total || "N/A"}
+
+ Daily Growth Array Length:{" "} + {analyticsData?.vendors?.dailyGrowth?.length || 0} +
First 3 Daily Growth:
-                    {JSON.stringify(analyticsData?.vendors?.dailyGrowth?.slice(0, 3), null, 2)}
+                    {JSON.stringify(
+                      analyticsData?.vendors?.dailyGrowth?.slice(0, 3),
+                      null,
+                      2,
+                    )}
                   
- +
- Full JSON Response + + Full JSON Response +
                   {JSON.stringify(analyticsData, null, 2)}
                 
@@ -396,7 +596,7 @@ export default function AdminAnalytics() { )} - + {/* Best Month Card (only show for YTD) */} {bestMonth && ( @@ -407,14 +607,24 @@ export default function AdminAnalytics() {
-
Best Month (YTD)
-
{bestMonth.month}
+
+ Best Month (YTD) +
+
+ {bestMonth.month} +
-
Revenue
-
{formatCurrency(bestMonth.revenue)}
-
{bestMonth.orders.toLocaleString()} orders
+
+ Revenue +
+
+ {formatCurrency(bestMonth.revenue)} +
+
+ {bestMonth.orders.toLocaleString()} orders +
@@ -426,46 +636,59 @@ export default function AdminAnalytics() {
- Total Orders + + Total Orders +
- {analyticsData?.orders?.total?.toLocaleString() || '0'} + {analyticsData?.orders?.total?.toLocaleString() || "0"}
Today: {analyticsData?.orders?.totalToday || 0} -
- + {loading || refreshing ? (
- ) : analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? ( + ) : analyticsData?.orders?.dailyOrders && + analyticsData.orders.dailyOrders.length > 0 ? (
- + } />
) : ( -
No chart data available
+
+ No chart data available +
)}
- + {/* Revenue Card */}
- Total Revenue + + Total Revenue +
@@ -474,32 +697,43 @@ export default function AdminAnalytics() { {formatCurrency(analyticsData?.revenue?.total || 0)}
- Today: {formatCurrency(analyticsData?.revenue?.today || 0)} - + Today: {formatCurrency(analyticsData?.revenue?.today || 0)} + +
- + {loading || refreshing ? (
- ) : analyticsData?.revenue?.dailyRevenue && analyticsData.revenue.dailyRevenue.length > 0 ? ( + ) : analyticsData?.revenue?.dailyRevenue && + analyticsData.revenue.dailyRevenue.length > 0 ? (
- + } />
) : ( -
No chart data available
+
+ No chart data available +
)}
- + {/* Vendors Card */} @@ -510,39 +744,50 @@ export default function AdminAnalytics() {
- {analyticsData?.vendors?.total?.toLocaleString() || '0'} + {analyticsData?.vendors?.total?.toLocaleString() || "0"}
Active: {analyticsData?.vendors?.active || 0} - Stores: {analyticsData?.vendors?.activeStores || 0} + + Stores: {analyticsData?.vendors?.activeStores || 0} +
New Today: {analyticsData?.vendors?.newToday || 0} -
- + {loading || refreshing ? (
- ) : analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? ( + ) : analyticsData?.vendors?.dailyGrowth && + analyticsData.vendors.dailyGrowth.length > 0 ? (
- + } />
) : ( -
No chart data available
+
+ No chart data available +
)}
- + {/* Products Card */} @@ -553,7 +798,7 @@ export default function AdminAnalytics() {
- {analyticsData?.products?.total?.toLocaleString() || '0'} + {analyticsData?.products?.total?.toLocaleString() || "0"}
New This Week: {analyticsData?.products?.recent || 0} @@ -561,19 +806,20 @@ export default function AdminAnalytics() {
- + Orders Vendors - + Order Trends - Daily order volume and revenue processed over the selected time period + Daily order volume and revenue processed over the selected time + period @@ -581,43 +827,79 @@ export default function AdminAnalytics() {
-

Loading chart data...

+

+ Loading chart data... +

- ) : analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? ( + ) : analyticsData?.orders?.dailyOrders && + analyticsData.orders.dailyOrders.length > 0 ? (
- - - - `£${(value / 1000).toFixed(0)}k`} - label={{ value: 'Revenue / AOV', angle: 90, position: 'insideRight' }} + tickFormatter={(value) => + `£${(value / 1000).toFixed(0)}k` + } + label={{ + value: "Revenue / AOV", + angle: 90, + position: "insideRight", + }} /> } /> - - - + + +
@@ -626,30 +908,40 @@ export default function AdminAnalytics() { No order data available for the selected time period )} - + {/* Calculate totals for the selected period */} - {analyticsData?.orders?.dailyOrders && analyticsData?.revenue?.dailyRevenue && ( -
-
-
Total Revenue
-
- {formatCurrency( - analyticsData.revenue.dailyRevenue.reduce((sum, day) => sum + (day.amount || 0), 0) - )} + {analyticsData?.orders?.dailyOrders && + analyticsData?.revenue?.dailyRevenue && ( +
+
+
+ Total Revenue +
+
+ {formatCurrency( + analyticsData.revenue.dailyRevenue.reduce( + (sum, day) => sum + (day.amount || 0), + 0, + ), + )} +
+
+
+
+ Total Orders +
+
+ {analyticsData.orders.dailyOrders + .reduce((sum, day) => sum + (day.count || 0), 0) + .toLocaleString()} +
-
-
Total Orders
-
- {analyticsData.orders.dailyOrders.reduce((sum, day) => sum + (day.count || 0), 0).toLocaleString()} -
-
-
- )} + )} - + @@ -663,16 +955,25 @@ export default function AdminAnalytics() {
-

Loading chart data...

+

+ Loading chart data... +

- ) : analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? ( + ) : analyticsData?.vendors?.dailyGrowth && + analyticsData.vendors.dailyGrowth.length > 0 ? (
- + - } /> - +
@@ -689,54 +994,76 @@ export default function AdminAnalytics() { No vendor data available for the selected time period
)} - +
Total Vendors
-
{analyticsData?.vendors?.total?.toLocaleString() || '0'}
+
+ {analyticsData?.vendors?.total?.toLocaleString() || "0"} +
Active Vendors
-
{analyticsData?.vendors?.active?.toLocaleString() || '0'}
+
+ {analyticsData?.vendors?.active?.toLocaleString() || "0"} +
Active Stores
-
{analyticsData?.vendors?.activeStores?.toLocaleString() || '0'}
+
+ {analyticsData?.vendors?.activeStores?.toLocaleString() || + "0"} +
New This Week
-
{analyticsData?.vendors?.newThisWeek?.toLocaleString() || '0'}
+
+ {analyticsData?.vendors?.newThisWeek?.toLocaleString() || + "0"} +
{/* Top Vendors by Revenue */} - {analyticsData?.vendors?.topVendors && analyticsData.vendors.topVendors.length > 0 && ( -
-

Top Vendors by Revenue

-
- {analyticsData.vendors.topVendors.map((vendor, index) => ( -
-
-
- {index + 1} + {analyticsData?.vendors?.topVendors && + analyticsData.vendors.topVendors.length > 0 && ( +
+

+ Top Vendors by Revenue +

+
+ {analyticsData.vendors.topVendors.map((vendor, index) => ( +
+
+
+ {index + 1} +
+
+
+ {vendor.vendorName} +
+
+ {vendor.orderCount} orders +
+
-
-
{vendor.vendorName}
-
{vendor.orderCount} orders
+
+
+ {formatCurrency(vendor.totalRevenue)} +
-
-
{formatCurrency(vendor.totalRevenue)}
-
-
- ))} + ))} +
-
- )} + )}
); -} \ No newline at end of file +} diff --git a/components/layout/NotificationProviderWrapper.tsx b/components/layout/NotificationProviderWrapper.tsx index b2dc122..a32b34d 100644 --- a/components/layout/NotificationProviderWrapper.tsx +++ b/components/layout/NotificationProviderWrapper.tsx @@ -32,3 +32,4 @@ export function NotificationProviderWrapper({ children }: NotificationProviderWr return <>{children}; } + diff --git a/components/tables/product-table.tsx b/components/tables/product-table.tsx index 04096fd..dcdbb3e 100644 --- a/components/tables/product-table.tsx +++ b/components/tables/product-table.tsx @@ -1,5 +1,20 @@ -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator, Copy } from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Edit, + Trash, + AlertTriangle, + CheckCircle, + AlertCircle, + Calculator, + Copy, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Product } from "@/models/products"; import { Badge } from "@/components/ui/badge"; @@ -24,128 +39,182 @@ const ProductTable = ({ onDelete, onToggleEnabled, onProfitAnalysis, - getCategoryNameById + getCategoryNameById, }: ProductTableProps) => { + // Separate enabled and disabled products + const enabledProducts = products.filter((p) => p.enabled !== false); + const disabledProducts = products.filter((p) => p.enabled === false); - const sortedProducts = [...products].sort((a, b) => { - const categoryNameA = getCategoryNameById(a.category); - const categoryNameB = getCategoryNameById(b.category); - return categoryNameA.localeCompare(categoryNameB); - }); + const sortByCategory = (productList: Product[]) => { + return [...productList].sort((a, b) => { + const categoryNameA = getCategoryNameById(a.category); + const categoryNameB = getCategoryNameById(b.category); + return categoryNameA.localeCompare(categoryNameB); + }); + }; + + const sortedEnabledProducts = sortByCategory(enabledProducts); + const sortedDisabledProducts = sortByCategory(disabledProducts); const getStockIcon = (product: Product) => { if (!product.stockTracking) return null; - - if (product.stockStatus === 'out_of_stock') { + + if (product.stockStatus === "out_of_stock") { return ; - } else if (product.stockStatus === 'low_stock') { + } else if (product.stockStatus === "low_stock") { return ; } else { return ; } }; + const renderProductRow = (product: Product, isDisabled: boolean = false) => ( + + +
{product.name}
+
+ {getCategoryNameById(product.category)} +
+
+ + {getCategoryNameById(product.category)} + + + {product.unitType} + + + {product.stockTracking ? ( +
+ {getStockIcon(product)} + + {product.currentStock !== undefined ? product.currentStock : 0}{" "} + {product.unitType} + +
+ ) : ( + + Not Tracked + + )} +
+ + + onToggleEnabled(product._id as string, checked) + } + /> + + + {onProfitAnalysis && ( + + )} + {onClone && ( + + )} + + + +
+ ); + + const renderTableHeader = () => ( + + + Product + + Category + + Unit + Stock + + Enabled + + Actions + + + ); + return ( -
- - - - Product - Category - Unit - Stock - Enabled - Actions - - - - {loading ? ( - Array.from({ length: 1 }).map((_, index) => ( - - Loading... - Loading... - Loading... - Loading... - Loading... - Loading... - - )) - ) : sortedProducts.length > 0 ? ( - sortedProducts.map((product) => ( - - -
{product.name}
-
- {getCategoryNameById(product.category)} -
-
- {getCategoryNameById(product.category)} - {product.unitType} - - {product.stockTracking ? ( -
- {getStockIcon(product)} - - {product.currentStock !== undefined ? product.currentStock : 0} {product.unitType} - -
- ) : ( - Not Tracked - )} -
- - onToggleEnabled(product._id as string, checked)} - /> - - - {onProfitAnalysis && ( - - )} - {onClone && ( - - )} - - +
+ {/* Enabled Products Table */} +
+
+ {renderTableHeader()} + + {loading ? ( + Array.from({ length: 1 }).map((_, index) => ( + + Loading... + Loading... + Loading... + Loading... + Loading... + Loading... + + )) + ) : sortedEnabledProducts.length > 0 ? ( + sortedEnabledProducts.map((product) => renderProductRow(product)) + ) : ( + + + No enabled products found. - )) - ) : ( - - - No products found. - - - )} - -
+ )} + + +
+ + {/* Disabled Products Section */} + {!loading && disabledProducts.length > 0 && ( +
+ + {renderTableHeader()} + + {sortedDisabledProducts.map((product) => + renderProductRow(product, true), + )} + +
+
+ )}
); }; diff --git a/models/products.ts b/models/products.ts index df3434a..06a0734 100644 --- a/models/products.ts +++ b/models/products.ts @@ -2,17 +2,17 @@ export interface Product { _id?: string; name: string; description: string; - unitType: 'pcs' | 'gr' | 'kg' | 'ml' | 'oz' | 'lb'; + unitType: "pcs" | "gr" | "kg" | "ml" | "oz" | "lb"; category: string; enabled?: boolean; // Stock management fields stockTracking?: boolean; currentStock?: number; lowStockThreshold?: number; - stockStatus?: 'in_stock' | 'low_stock' | 'out_of_stock'; + stockStatus?: "in_stock" | "low_stock" | "out_of_stock"; pricing: Array<{ minQuantity: number; pricePerUnit: number; }>; image?: string | File | null | undefined; -} \ No newline at end of file +} diff --git a/public/git-info.json b/public/git-info.json index c06e7ec..ad40f7f 100644 --- a/public/git-info.json +++ b/public/git-info.json @@ -1,4 +1,4 @@ { - "commitHash": "66e9543", - "buildTime": "2025-12-31T07:04:51.067Z" + "commitHash": "74f3a2c", + "buildTime": "2026-01-05T21:55:11.573Z" } \ No newline at end of file