diff --git a/components/analytics/CustomerInsightsChart.tsx b/components/analytics/CustomerInsightsChart.tsx new file mode 100644 index 0000000..a20b254 --- /dev/null +++ b/components/analytics/CustomerInsightsChart.tsx @@ -0,0 +1,268 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { clientFetch } from "@/lib/api"; +import { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Users, Crown, UserPlus, UserCheck, Star } from "lucide-react"; + +interface CustomerInsights { + totalCustomers: number; + segments: { + new: number; + returning: number; + loyal: number; + vip: number; + }; + topCustomers: Array<{ + _id: string; + orderCount: number; + totalSpent: number; + averageOrderValue: number; + firstOrder: string; + lastOrder: string; + }>; + averageOrdersPerCustomer: string; +} + +export default function CustomerInsightsChart() { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + useEffect(() => { + const fetchCustomerData = async () => { + try { + setIsLoading(true); + setError(null); + const response = await clientFetch('/analytics/customer-insights'); + setData(response); + } catch (err) { + console.error('Error fetching customer insights:', err); + setError('Failed to load customer data'); + toast({ + title: "Error", + description: "Failed to load customer insights data.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + fetchCustomerData(); + }, [toast]); + + const getSegmentColor = (segment: string) => { + switch (segment) { + case 'new': + return 'bg-blue-500'; + case 'returning': + return 'bg-green-500'; + case 'loyal': + return 'bg-purple-500'; + case 'vip': + return 'bg-yellow-500'; + default: + return 'bg-gray-500'; + } + }; + + const getSegmentIcon = (segment: string) => { + switch (segment) { + case 'new': + return ; + case 'returning': + return ; + case 'loyal': + return ; + case 'vip': + return ; + default: + return ; + } + }; + + const getSegmentLabel = (segment: string) => { + switch (segment) { + case 'new': + return 'New Customers'; + case 'returning': + return 'Returning Customers'; + case 'loyal': + return 'Loyal Customers'; + case 'vip': + return 'VIP Customers'; + default: + return 'Unknown'; + } + }; + + if (isLoading) { + return ( + + + + + Customer Insights + + + Customer segmentation and behavior analysis + + + +
+ +
+ + +
+
+
+
+ ); + } + + if (error || !data) { + return ( + + + + + Customer Insights + + + +
+ +

Failed to load customer data

+
+
+
+ ); + } + + const segments = Object.entries(data.segments); + const totalCustomers = data.totalCustomers; + + return ( +
+ {/* Customer Overview */} + + + + + Customer Overview + + + Total customers and average orders per customer + + + +
+
+
+ {data.totalCustomers} +
+
Total Customers
+
+
+
+ {data.averageOrdersPerCustomer} +
+
Avg Orders/Customer
+
+
+
+
+ + {/* Customer Segments */} + + + Customer Segments + + Breakdown of customers by purchase frequency + + + +
+ {segments.map(([segment, count]) => { + const percentage = totalCustomers > 0 ? (count / totalCustomers * 100).toFixed(1) : '0'; + const Icon = getSegmentIcon(segment); + + return ( +
+
+
+ {Icon} +
+
+
{getSegmentLabel(segment)}
+
+ {count} customers +
+
+
+
+
{percentage}%
+
of total
+
+
+ ); + })} +
+
+
+ + {/* Top Customers */} + + + + + Top Customers + + + Your highest-spending customers + + + + {data.topCustomers.length === 0 ? ( +
+ +

No customer data available

+
+ ) : ( +
+ {data.topCustomers.map((customer, index) => ( +
+
+
+ {index + 1} +
+
+
Customer #{customer._id.slice(-6)}
+
+ {customer.orderCount} orders +
+
+
+
+
+ ${customer.totalSpent.toFixed(2)} +
+
+ ${customer.averageOrderValue.toFixed(2)} avg +
+
+
+ ))} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/analytics/OrderAnalyticsChart.tsx b/components/analytics/OrderAnalyticsChart.tsx new file mode 100644 index 0000000..11bdcbb --- /dev/null +++ b/components/analytics/OrderAnalyticsChart.tsx @@ -0,0 +1,296 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { clientFetch } from "@/lib/api"; +import { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; +import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle } from "lucide-react"; + +interface OrderAnalytics { + statusDistribution: Array<{ + _id: string; + count: number; + }>; + dailyOrders: Array<{ + _id: { + year: number; + month: number; + day: number; + }; + orders: number; + revenue: number; + }>; + averageProcessingDays: number; +} + +interface OrderAnalyticsChartProps { + timeRange: string; +} + +export default function OrderAnalyticsChart({ timeRange }: OrderAnalyticsChartProps) { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + useEffect(() => { + const fetchOrderData = async () => { + try { + setIsLoading(true); + setError(null); + const response = await clientFetch(`/analytics/order-analytics?period=${timeRange}`); + setData(response); + } catch (err) { + console.error('Error fetching order analytics:', err); + setError('Failed to load order data'); + toast({ + title: "Error", + description: "Failed to load order analytics data.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + fetchOrderData(); + }, [timeRange, toast]); + + const getStatusColor = (status: string) => { + switch (status) { + case 'completed': + case 'acknowledged': + return 'bg-green-100 text-green-800'; + case 'paid': + case 'shipped': + return 'bg-blue-100 text-blue-800'; + case 'unpaid': + case 'confirming': + return 'bg-yellow-100 text-yellow-800'; + case 'cancelled': + return 'bg-red-100 text-red-800'; + case 'disputed': + return 'bg-orange-100 text-orange-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'completed': + case 'acknowledged': + return ; + case 'paid': + case 'shipped': + return ; + case 'unpaid': + case 'confirming': + return ; + case 'cancelled': + return ; + default: + return ; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'completed': + return 'Completed'; + case 'acknowledged': + return 'Acknowledged'; + case 'paid': + return 'Paid'; + case 'shipped': + return 'Shipped'; + case 'unpaid': + return 'Unpaid'; + case 'confirming': + return 'Confirming'; + case 'cancelled': + return 'Cancelled'; + case 'disputed': + return 'Disputed'; + default: + return status.charAt(0).toUpperCase() + status.slice(1); + } + }; + + if (isLoading) { + return ( + + + + + Order Analytics + + + Order status distribution and trends + + + +
+ + +
+
+
+ ); + } + + if (error || !data) { + return ( + + + + + Order Analytics + + + +
+ +

Failed to load order data

+
+
+
+ ); + } + + const totalOrders = data.statusDistribution.reduce((sum, item) => sum + item.count, 0); + const totalRevenue = data.dailyOrders.reduce((sum, item) => sum + item.revenue, 0); + + return ( +
+ {/* Order Status Distribution */} + + + Order Status Distribution + + Breakdown of orders by current status + + + +
+ {data.statusDistribution.map((status) => { + const percentage = totalOrders > 0 ? (status.count / totalOrders * 100).toFixed(1) : '0'; + const Icon = getStatusIcon(status._id); + + return ( +
+
+
+ {Icon} +
+
+
{getStatusLabel(status._id)}
+
+ {status.count} orders +
+
+
+
+
{percentage}%
+
of total
+
+
+ ); + })} +
+
+
+ + {/* Daily Order Trends */} + + + Daily Order Trends + + Orders and revenue over the selected time period + + + + {data.dailyOrders.length === 0 ? ( +
+ +

No order data available for this period

+
+ ) : ( +
+ {/* Summary Stats */} +
+
+
+ {data.dailyOrders.length} +
+
Days with Orders
+
+
+
+ {totalOrders} +
+
Total Orders
+
+
+
+ ${totalRevenue.toFixed(2)} +
+
Total Revenue
+
+
+ + {/* Chart */} +
+ {data.dailyOrders.map((item, index) => { + const maxOrders = Math.max(...data.dailyOrders.map(d => d.orders)); + const height = maxOrders > 0 ? (item.orders / maxOrders) * 100 : 0; + const date = new Date(item._id.year, item._id.month - 1, item._id.day); + const dateLabel = date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + + return ( +
+
+
+ {dateLabel} +
+
+ ); + })} +
+
+ )} + + + + {/* Processing Time */} + + + + + Order Processing Time + + + Average time to complete orders + + + +
+
+ {data.averageProcessingDays.toFixed(1)} +
+
Average Days to Complete
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/analytics/ProductPerformanceChart.tsx b/components/analytics/ProductPerformanceChart.tsx new file mode 100644 index 0000000..a5034fa --- /dev/null +++ b/components/analytics/ProductPerformanceChart.tsx @@ -0,0 +1,232 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { clientFetch } from "@/lib/api"; +import { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Package, TrendingUp } from "lucide-react"; + +interface ProductPerformance { + productId: string; + name: string; + image: string; + unitType: string; + currentStock: number; + stockStatus: string; + totalSold: number; + totalRevenue: number; + orderCount: number; + averagePrice: number; +} + +export default function ProductPerformanceChart() { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + useEffect(() => { + const fetchProductData = async () => { + try { + setIsLoading(true); + setError(null); + const response = await clientFetch('/analytics/product-performance'); + setData(response); + } catch (err) { + console.error('Error fetching product performance:', err); + setError('Failed to load product data'); + toast({ + title: "Error", + description: "Failed to load product performance data.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + fetchProductData(); + }, [toast]); + + const getStockStatusColor = (status: string) => { + switch (status) { + case 'in_stock': + return 'bg-green-100 text-green-800'; + case 'low_stock': + return 'bg-yellow-100 text-yellow-800'; + case 'out_of_stock': + return 'bg-red-100 text-red-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const getStockStatusText = (status: string) => { + switch (status) { + case 'in_stock': + return 'In Stock'; + case 'low_stock': + return 'Low Stock'; + case 'out_of_stock': + return 'Out of Stock'; + default: + return 'Unknown'; + } + }; + + if (isLoading) { + return ( + + + + + Product Performance + + + Top performing products by revenue and sales + + + +
+ + {[...Array(5)].map((_, i) => ( +
+ +
+ + +
+ + +
+ ))} +
+
+
+ ); + } + + if (error) { + return ( + + + + + Product Performance + + + +
+ +

Failed to load product data

+
+
+
+ ); + } + + if (data.length === 0) { + return ( + + + + + Product Performance + + + Top performing products by revenue and sales + + + +
+ +

No product performance data available

+

+ Start selling products to see performance metrics +

+
+
+
+ ); + } + + return ( + + + + + Product Performance + + + Top performing products by revenue and sales + + + + + + + Product + Stock + Sold + Revenue + Orders + Avg Price + + + + {data.map((product) => ( + + +
+
+
+
{product.name}
+
+ {product.unitType} +
+
+
+ + +
+ + {getStockStatusText(product.stockStatus)} + + + {product.currentStock} available + +
+
+ + {product.totalSold.toFixed(2)} + + + ${product.totalRevenue.toFixed(2)} + + + {product.orderCount} + + + ${product.averagePrice.toFixed(2)} + + + ))} + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/analytics/RevenueChart.tsx b/components/analytics/RevenueChart.tsx new file mode 100644 index 0000000..bdb04d2 --- /dev/null +++ b/components/analytics/RevenueChart.tsx @@ -0,0 +1,191 @@ +"use client" + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { clientFetch } from "@/lib/api"; +import { useToast } from "@/hooks/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; +import { TrendingUp, DollarSign } from "lucide-react"; + +interface RevenueData { + _id: { + year: number; + month: number; + day: number; + }; + revenue: number; + orders: number; +} + +interface RevenueChartProps { + timeRange: string; +} + +export default function RevenueChart({ timeRange }: RevenueChartProps) { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + useEffect(() => { + const fetchRevenueData = async () => { + try { + setIsLoading(true); + setError(null); + const response = await clientFetch(`/analytics/revenue-trends?period=${timeRange}`); + setData(response); + } catch (err) { + console.error('Error fetching revenue data:', err); + setError('Failed to load revenue data'); + toast({ + title: "Error", + description: "Failed to load revenue trends data.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + fetchRevenueData(); + }, [timeRange, toast]); + + const totalRevenue = data.reduce((sum, item) => sum + item.revenue, 0); + const totalOrders = data.reduce((sum, item) => sum + item.orders, 0); + const averageRevenue = data.length > 0 ? totalRevenue / data.length : 0; + + const formatDate = (item: RevenueData) => { + const date = new Date(item._id.year, item._id.month - 1, item._id.day); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + }; + + if (isLoading) { + return ( + + + + + Revenue Trends + + + Revenue performance over the selected time period + + + +
+ +
+ + + +
+
+
+
+ ); + } + + if (error) { + return ( + + + + + Revenue Trends + + + +
+ +

Failed to load revenue data

+
+
+
+ ); + } + + if (data.length === 0) { + return ( + + + + + Revenue Trends + + + Revenue performance over the selected time period + + + +
+ +

No revenue data available for this period

+
+
+
+ ); + } + + return ( + + + + + Revenue Trends + + + Revenue performance over the selected time period + + + + {/* Simple bar chart representation */} +
+
+ {data.map((item, index) => { + const maxRevenue = Math.max(...data.map(d => d.revenue)); + const height = maxRevenue > 0 ? (item.revenue / maxRevenue) * 100 : 0; + + return ( +
+
+
+ {formatDate(item)} +
+
+ ); + })} +
+ + {/* Summary stats */} +
+
+
+ ${totalRevenue.toFixed(2)} +
+
Total Revenue
+
+
+
+ {totalOrders} +
+
Total Orders
+
+
+
+ ${averageRevenue.toFixed(2)} +
+
Avg Daily Revenue
+
+
+
+ + + ); +} \ No newline at end of file diff --git a/config/sidebar.ts b/config/sidebar.ts index af775fb..1cbac8a 100644 --- a/config/sidebar.ts +++ b/config/sidebar.ts @@ -1,10 +1,11 @@ -import { Home, Package, Box, Truck, Settings, FolderTree, MessageCircle, BarChart3, Tag, Users } from "lucide-react" +import { Home, Package, Box, Truck, Settings, FolderTree, MessageCircle, BarChart3, Tag, Users, TrendingUp } from "lucide-react" export const sidebarConfig = [ { title: "Overview", items: [ { name: "Dashboard", href: "/dashboard", icon: Home }, + { name: "Analytics", href: "/dashboard/analytics", icon: TrendingUp }, ], }, {