From 024155d054abc8f032f63fbe32df707e5c51e3de Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 12 Jan 2025 19:29:16 -0500 Subject: [PATCH] Add new dashboard frontend --- inventory/package-lock.json | 41 ++ inventory/package.json | 3 + .../dashboard/InventoryHealthSummary.tsx | 117 ++++ .../components/dashboard/KeyMetricsCharts.tsx | 135 ++++ .../src/components/dashboard/StockAlerts.tsx | 93 +++ .../components/dashboard/TrendingProducts.tsx | 117 ++-- .../dashboard/VendorPerformance.tsx | 77 +++ .../src/components/settings/Configuration.tsx | 630 ++++++++++++++++++ inventory/src/pages/Dashboard.tsx | 170 +---- 9 files changed, 1170 insertions(+), 213 deletions(-) create mode 100644 inventory/src/components/dashboard/InventoryHealthSummary.tsx create mode 100644 inventory/src/components/dashboard/KeyMetricsCharts.tsx create mode 100644 inventory/src/components/dashboard/StockAlerts.tsx create mode 100644 inventory/src/components/dashboard/VendorPerformance.tsx create mode 100644 inventory/src/components/settings/Configuration.tsx diff --git a/inventory/package-lock.json b/inventory/package-lock.json index 346037d..3bf5355 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -26,6 +26,8 @@ "@tanstack/react-query": "^5.63.0", "@tanstack/react-virtual": "^3.11.2", "@tanstack/virtual-core": "^3.11.2", + "chart.js": "^4.4.7", + "chartjs-adapter-date-fns": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -33,6 +35,7 @@ "lucide-react": "^0.469.0", "next-themes": "^0.4.4", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-router-dom": "^7.1.1", @@ -1117,6 +1120,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3210,6 +3219,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5770,6 +5801,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-day-picker": { "version": "8.10.1", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", diff --git a/inventory/package.json b/inventory/package.json index 4d19b2b..7f1eace 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -28,6 +28,8 @@ "@tanstack/react-query": "^5.63.0", "@tanstack/react-virtual": "^3.11.2", "@tanstack/virtual-core": "^3.11.2", + "chart.js": "^4.4.7", + "chartjs-adapter-date-fns": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -35,6 +37,7 @@ "lucide-react": "^0.469.0", "next-themes": "^0.4.4", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-router-dom": "^7.1.1", diff --git a/inventory/src/components/dashboard/InventoryHealthSummary.tsx b/inventory/src/components/dashboard/InventoryHealthSummary.tsx new file mode 100644 index 0000000..cb9c9e2 --- /dev/null +++ b/inventory/src/components/dashboard/InventoryHealthSummary.tsx @@ -0,0 +1,117 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Pie } from "react-chartjs-2" +import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js' +import { useQuery } from "@tanstack/react-query" + +ChartJS.register(ArcElement, Tooltip, Legend) + +interface InventoryHealthData { + critical: number + reorder: number + healthy: number + overstock: number +} + +export function InventoryHealthSummary() { + const { data, isLoading } = useQuery({ + queryKey: ['inventoryHealth'], + queryFn: async () => { + const response = await fetch('/api/inventory/health/summary') + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + } + }) + + const chartData = { + labels: ['Critical', 'Reorder', 'Healthy', 'Overstock'], + datasets: [ + { + data: [ + data?.critical || 0, + data?.reorder || 0, + data?.healthy || 0, + data?.overstock || 0 + ], + backgroundColor: [ + 'rgb(239, 68, 68)', // red-500 + 'rgb(234, 179, 8)', // yellow-500 + 'rgb(34, 197, 94)', // green-500 + 'rgb(59, 130, 246)', // blue-500 + ], + borderColor: [ + 'rgb(239, 68, 68, 0.2)', + 'rgb(234, 179, 8, 0.2)', + 'rgb(34, 197, 94, 0.2)', + 'rgb(59, 130, 246, 0.2)', + ], + borderWidth: 1, + }, + ], + } + + const options = { + responsive: true, + plugins: { + legend: { + position: 'right' as const, + }, + }, + } + + const total = data ? data.critical + data.reorder + data.healthy + data.overstock : 0 + + return ( + + + Inventory Health + + +
+
+ {!isLoading && } +
+
+
+ Critical + + {data?.critical || 0} + + + {total ? Math.round((data?.critical / total) * 100) : 0}% of total + +
+
+ Reorder + + {data?.reorder || 0} + + + {total ? Math.round((data?.reorder / total) * 100) : 0}% of total + +
+
+ Healthy + + {data?.healthy || 0} + + + {total ? Math.round((data?.healthy / total) * 100) : 0}% of total + +
+
+ Overstock + + {data?.overstock || 0} + + + {total ? Math.round((data?.overstock / total) * 100) : 0}% of total + +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/inventory/src/components/dashboard/KeyMetricsCharts.tsx b/inventory/src/components/dashboard/KeyMetricsCharts.tsx new file mode 100644 index 0000000..0d2fe35 --- /dev/null +++ b/inventory/src/components/dashboard/KeyMetricsCharts.tsx @@ -0,0 +1,135 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { useQuery } from "@tanstack/react-query" +import { Line } from "react-chartjs-2" +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale +} from 'chart.js' +import 'chartjs-adapter-date-fns' + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale +) + +interface TimeSeriesData { + date: string + revenue: number + cost: number + inventory_value: number +} + +export function KeyMetricsCharts() { + const { data, isLoading } = useQuery({ + queryKey: ['keyMetrics'], + queryFn: async () => { + const response = await fetch('/api/metrics/timeseries') + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + } + }) + + const revenueVsCostData = { + labels: data?.map(d => d.date), + datasets: [ + { + label: 'Revenue', + data: data?.map(d => d.revenue), + borderColor: 'rgb(34, 197, 94)', // green-500 + backgroundColor: 'rgba(34, 197, 94, 0.5)', + tension: 0.3, + }, + { + label: 'Cost', + data: data?.map(d => d.cost), + borderColor: 'rgb(239, 68, 68)', // red-500 + backgroundColor: 'rgba(239, 68, 68, 0.5)', + tension: 0.3, + } + ] + } + + const inventoryValueData = { + labels: data?.map(d => d.date), + datasets: [ + { + label: 'Inventory Value', + data: data?.map(d => d.inventory_value), + borderColor: 'rgb(59, 130, 246)', // blue-500 + backgroundColor: 'rgba(59, 130, 246, 0.5)', + tension: 0.3, + fill: true, + } + ] + } + + const options = { + responsive: true, + interaction: { + mode: 'index' as const, + intersect: false, + }, + scales: { + x: { + type: 'time' as const, + time: { + unit: 'month' as const, + }, + title: { + display: true, + text: 'Date' + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Amount ($)' + } + } + } + } + + return ( + + + Key Financial Metrics + + +
+
+ {!isLoading && ( + + )} +
+
+ {!isLoading && ( + + )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/inventory/src/components/dashboard/StockAlerts.tsx b/inventory/src/components/dashboard/StockAlerts.tsx new file mode 100644 index 0000000..dc1b4d4 --- /dev/null +++ b/inventory/src/components/dashboard/StockAlerts.tsx @@ -0,0 +1,93 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { useQuery } from "@tanstack/react-query" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Link } from "react-router-dom" + +interface StockAlert { + product_id: number + sku: string + title: string + stock_quantity: number + daily_sales_avg: number + days_of_inventory: number + reorder_point: number + stock_status: 'Critical' | 'Reorder' +} + +export function StockAlerts() { + const { data, isLoading } = useQuery({ + queryKey: ['stockAlerts'], + queryFn: async () => { + const response = await fetch('/api/inventory/alerts') + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + } + }) + + return ( + + + Low Stock Alerts + + + + + + + SKU + Title + Status + Stock + Daily Sales + Days Left + Reorder Point + + + + {!isLoading && data?.map((alert) => ( + + + + {alert.sku} + + + {alert.title} + + + {alert.stock_status} + + + {alert.stock_quantity} + + {alert.daily_sales_avg.toFixed(1)} + + + {alert.days_of_inventory.toFixed(1)} + + {alert.reorder_point} + + ))} + +
+
+
+ ) +} \ No newline at end of file diff --git a/inventory/src/components/dashboard/TrendingProducts.tsx b/inventory/src/components/dashboard/TrendingProducts.tsx index dd18ec6..42c70c3 100644 --- a/inventory/src/components/dashboard/TrendingProducts.tsx +++ b/inventory/src/components/dashboard/TrendingProducts.tsx @@ -1,71 +1,76 @@ -import { useQuery } from '@tanstack/react-query'; -import { Card, CardContent } from '@/components/ui/card'; -import { Progress } from '@/components/ui/progress'; -import config from '../../config'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { useQuery } from "@tanstack/react-query" +import { Link } from "react-router-dom" +import { ArrowUpIcon, ArrowDownIcon } from "lucide-react" interface TrendingProduct { - product_id: string; - title: string; - sku: string; - total_sales: number; - sales_growth: number; - stock_quantity: number; - image_url: string; + product_id: number + sku: string + title: string + daily_sales_avg: number + weekly_sales_avg: number + growth_rate: number // Percentage growth week over week + total_revenue: number } export function TrendingProducts() { - const { data, isLoading, error } = useQuery({ - queryKey: ['trending-products'], + const { data, isLoading } = useQuery({ + queryKey: ['trendingProducts'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/trending-products`); + const response = await fetch('/api/products/trending') if (!response.ok) { - throw new Error('Failed to fetch trending products'); + throw new Error('Network response was not ok') } - return response.json(); - }, - }); - - if (isLoading) { - return
Loading trending products...
; - } - - if (error) { - return
Error loading trending products
; - } + return response.json() + } + }) return ( -
- {data?.map((product) => ( - - -
- {product.image_url && ( - {product.title} - )} -
-

{product.title}

-

SKU: {product.sku}

-
- - - {product.sales_growth > 0 ? '+' : ''}{product.sales_growth}% growth - -
+ + + Trending Products + + +
+ {!isLoading && data?.map((product) => ( +
+
+ + {product.sku} + +

+ {product.title} +

-
-

${product.total_sales.toLocaleString()}

-

- {product.stock_quantity} in stock +

+
+ + {product.daily_sales_avg.toFixed(1)}/day + +
= 0 ? 'text-green-500' : 'text-red-500' + }`}> + {product.growth_rate >= 0 ? ( + + ) : ( + + )} + + {Math.abs(product.growth_rate).toFixed(1)}% + +
+
+

+ ${product.total_revenue.toLocaleString()}

- - - ))} -
- ); + ))} +
+
+
+ ) } \ No newline at end of file diff --git a/inventory/src/components/dashboard/VendorPerformance.tsx b/inventory/src/components/dashboard/VendorPerformance.tsx new file mode 100644 index 0000000..093898e --- /dev/null +++ b/inventory/src/components/dashboard/VendorPerformance.tsx @@ -0,0 +1,77 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { useQuery } from "@tanstack/react-query" +import { Progress } from "@/components/ui/progress" + +interface VendorMetrics { + vendor: string + avg_lead_time_days: number + on_time_delivery_rate: number + order_fill_rate: number + total_orders: number + total_late_orders: number +} + +export function VendorPerformance() { + const { data, isLoading } = useQuery({ + queryKey: ['vendorMetrics'], + queryFn: async () => { + const response = await fetch('/api/vendors/metrics') + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + } + }) + + // Sort vendors by on-time delivery rate + const sortedVendors = data?.sort((a, b) => + b.on_time_delivery_rate - a.on_time_delivery_rate + ).slice(0, 5) + + return ( + + + Top Vendor Performance + + +
+ {!isLoading && sortedVendors?.map((vendor) => ( +
+
+
+

+ {vendor.vendor} +

+

+ {vendor.total_orders} orders, avg {vendor.avg_lead_time_days.toFixed(1)} days +

+
+
+ {vendor.on_time_delivery_rate.toFixed(1)}% +
+
+
+
+ On-time Delivery + {vendor.on_time_delivery_rate.toFixed(1)}% +
+ +
+ Order Fill Rate + {vendor.order_fill_rate.toFixed(1)}% +
+ +
+
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/inventory/src/components/settings/Configuration.tsx b/inventory/src/components/settings/Configuration.tsx new file mode 100644 index 0000000..0d7a319 --- /dev/null +++ b/inventory/src/components/settings/Configuration.tsx @@ -0,0 +1,630 @@ +import { useState, useEffect } from 'react'; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { toast } from "sonner"; +import config from '../../config'; + +interface StockThreshold { + id: number; + category_id: number | null; + vendor: string | null; + critical_days: number; + reorder_days: number; + overstock_days: number; + low_stock_threshold: number; + min_reorder_quantity: number; + category_name?: string; + threshold_scope?: string; +} + +interface LeadTimeThreshold { + id: number; + category_id: number | null; + vendor: string | null; + target_days: number; + warning_days: number; + critical_days: number; +} + +interface SalesVelocityConfig { + id: number; + category_id: number | null; + vendor: string | null; + daily_window_days: number; + weekly_window_days: number; + monthly_window_days: number; +} + +interface ABCClassificationConfig { + id: number; + a_threshold: number; + b_threshold: number; + classification_period_days: number; +} + +interface SafetyStockConfig { + id: number; + category_id: number | null; + vendor: string | null; + coverage_days: number; + service_level: number; +} + +interface TurnoverConfig { + id: number; + category_id: number | null; + vendor: string | null; + calculation_period_days: number; + target_rate: number; +} + +export function Configuration() { + const [stockThresholds, setStockThresholds] = useState({ + id: 1, + category_id: null, + vendor: null, + critical_days: 7, + reorder_days: 14, + overstock_days: 90, + low_stock_threshold: 5, + min_reorder_quantity: 1 + }); + + const [leadTimeThresholds, setLeadTimeThresholds] = useState({ + id: 1, + category_id: null, + vendor: null, + target_days: 14, + warning_days: 21, + critical_days: 30 + }); + + const [salesVelocityConfig, setSalesVelocityConfig] = useState({ + id: 1, + category_id: null, + vendor: null, + daily_window_days: 30, + weekly_window_days: 7, + monthly_window_days: 90 + }); + + const [abcConfig, setAbcConfig] = useState({ + id: 1, + a_threshold: 20.0, + b_threshold: 50.0, + classification_period_days: 90 + }); + + const [safetyStockConfig, setSafetyStockConfig] = useState({ + id: 1, + category_id: null, + vendor: null, + coverage_days: 14, + service_level: 95.0 + }); + + const [turnoverConfig, setTurnoverConfig] = useState({ + id: 1, + category_id: null, + vendor: null, + calculation_period_days: 30, + target_rate: 1.0 + }); + + useEffect(() => { + const loadConfig = async () => { + try { + const response = await fetch(`${config.apiUrl}/config`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error('Failed to load configuration'); + } + const data = await response.json(); + setStockThresholds(data.stockThresholds); + setLeadTimeThresholds(data.leadTimeThresholds); + setSalesVelocityConfig(data.salesVelocityConfig); + setAbcConfig(data.abcConfig); + setSafetyStockConfig(data.safetyStockConfig); + setTurnoverConfig(data.turnoverConfig); + } catch (error) { + toast.error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + loadConfig(); + }, []); + + const handleUpdateStockThresholds = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/stock-thresholds/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(stockThresholds) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update stock thresholds'); + } + + toast.success('Stock thresholds updated successfully'); + } catch (error) { + toast.error(`Failed to update thresholds: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleUpdateLeadTimeThresholds = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/lead-time-thresholds/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(leadTimeThresholds) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update lead time thresholds'); + } + + toast.success('Lead time thresholds updated successfully'); + } catch (error) { + toast.error(`Failed to update thresholds: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleUpdateSalesVelocityConfig = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/sales-velocity/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(salesVelocityConfig) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update sales velocity configuration'); + } + + toast.success('Sales velocity configuration updated successfully'); + } catch (error) { + toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleUpdateABCConfig = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/abc-classification/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(abcConfig) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update ABC classification configuration'); + } + + toast.success('ABC classification configuration updated successfully'); + } catch (error) { + toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleUpdateSafetyStockConfig = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/safety-stock/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(safetyStockConfig) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update safety stock configuration'); + } + + toast.success('Safety stock configuration updated successfully'); + } catch (error) { + toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + const handleUpdateTurnoverConfig = async () => { + try { + const response = await fetch(`${config.apiUrl}/config/turnover/1`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(turnoverConfig) + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to update turnover configuration'); + } + + toast.success('Turnover configuration updated successfully'); + } catch (error) { + toast.error(`Failed to update configuration: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + return ( + + + Stock Management + Performance Metrics + Calculation Settings + + + + {/* Stock Thresholds Card */} + + + Stock Thresholds + Configure stock level thresholds for inventory management + + +
+
+
+ + setStockThresholds(prev => ({ + ...prev, + critical_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setStockThresholds(prev => ({ + ...prev, + reorder_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setStockThresholds(prev => ({ + ...prev, + overstock_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setStockThresholds(prev => ({ + ...prev, + low_stock_threshold: parseInt(e.target.value) || 0 + }))} + /> +
+
+ + setStockThresholds(prev => ({ + ...prev, + min_reorder_quantity: parseInt(e.target.value) || 1 + }))} + /> +
+
+ +
+
+
+ + {/* Safety Stock Configuration Card */} + + + Safety Stock + Configure safety stock parameters + + +
+
+
+ + setSafetyStockConfig(prev => ({ + ...prev, + coverage_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setSafetyStockConfig(prev => ({ + ...prev, + service_level: parseFloat(e.target.value) || 0 + }))} + /> +
+
+ +
+
+
+
+ + + {/* Lead Time Thresholds Card */} + + + Lead Time Thresholds + Configure lead time thresholds for vendor performance + + +
+
+
+ + setLeadTimeThresholds(prev => ({ + ...prev, + target_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setLeadTimeThresholds(prev => ({ + ...prev, + warning_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setLeadTimeThresholds(prev => ({ + ...prev, + critical_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ +
+
+
+ + {/* ABC Classification Card */} + + + ABC Classification + Configure ABC classification parameters + + +
+
+
+ + setAbcConfig(prev => ({ + ...prev, + a_threshold: parseFloat(e.target.value) || 0 + }))} + /> +
+
+ + setAbcConfig(prev => ({ + ...prev, + b_threshold: parseFloat(e.target.value) || 0 + }))} + /> +
+
+ + setAbcConfig(prev => ({ + ...prev, + classification_period_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ +
+
+
+ + {/* Turnover Configuration Card */} + + + Turnover Rate + Configure turnover rate calculations + + +
+
+
+ + setTurnoverConfig(prev => ({ + ...prev, + calculation_period_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setTurnoverConfig(prev => ({ + ...prev, + target_rate: parseFloat(e.target.value) || 0 + }))} + /> +
+
+ +
+
+
+
+ + + {/* Sales Velocity Configuration Card */} + + + Sales Velocity Windows + Configure time windows for sales velocity calculations + + +
+
+
+ + setSalesVelocityConfig(prev => ({ + ...prev, + daily_window_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setSalesVelocityConfig(prev => ({ + ...prev, + weekly_window_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ + setSalesVelocityConfig(prev => ({ + ...prev, + monthly_window_days: parseInt(e.target.value) || 1 + }))} + /> +
+
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/inventory/src/pages/Dashboard.tsx b/inventory/src/pages/Dashboard.tsx index 130bacb..84d4437 100644 --- a/inventory/src/pages/Dashboard.tsx +++ b/inventory/src/pages/Dashboard.tsx @@ -1,166 +1,22 @@ -import { useQuery } from '@tanstack/react-query'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Overview } from '@/components/dashboard/Overview'; -import { RecentSales } from '@/components/dashboard/RecentSales'; -import { InventoryStats } from '@/components/dashboard/InventoryStats'; -import { SalesByCategory } from '@/components/dashboard/SalesByCategory'; -import { TrendingProducts } from '@/components/dashboard/TrendingProducts'; -import config from '../config'; - -interface DashboardStats { - totalProducts: number; - lowStockProducts: number; - totalOrders: number; - averageOrderValue: number; - totalRevenue: number; - profitMargin: number; -} +import { InventoryHealthSummary } from "@/components/dashboard/InventoryHealthSummary" +import { StockAlerts } from "@/components/dashboard/StockAlerts" +import { TrendingProducts } from "@/components/dashboard/TrendingProducts" +import { VendorPerformance } from "@/components/dashboard/VendorPerformance" +import { KeyMetricsCharts } from "@/components/dashboard/KeyMetricsCharts" export function Dashboard() { - const { data: stats, isLoading: statsLoading } = useQuery({ - queryKey: ['dashboard-stats'], - queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/stats`); - if (!response.ok) { - throw new Error('Failed to fetch dashboard stats'); - } - const data = await response.json(); - return { - ...data, - averageOrderValue: parseFloat(data.averageOrderValue) || 0, - totalRevenue: parseFloat(data.totalRevenue) || 0, - profitMargin: parseFloat(data.profitMargin) || 0 - }; - }, - }); - - if (statsLoading) { - return
Loading dashboard...
; - } - return (

Dashboard

- - - Overview - Inventory - - -
- - - - Total Revenue - - - -
- ${(stats?.totalRevenue || 0).toLocaleString()} -
-
-
- - - - Total Orders - - - -
{stats?.totalOrders || 0}
-
-
- - - - Average Order Value - - - -
- ${(stats?.averageOrderValue || 0).toFixed(2)} -
-
-
- - - - Profit Margin - - - -
- {(stats?.profitMargin || 0).toFixed(1)}% -
-
-
-
-
- - - Sales Overview - - - - - - - - Recent Sales - - - - - -
-
- - - Sales by Category - - - - - - - - Trending Products - - - - - -
-
- -
- - - - Total Products - - - -
{stats?.totalProducts || 0}
-
-
- - - - Low Stock Products - - - -
{stats?.lowStockProducts || 0}
-
-
-
- -
-
+
+ + + + + +
- ); + ) } \ No newline at end of file