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