Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
168 lines
7.6 KiB
TypeScript
168 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { AlertCircle, Package, ArrowRight, ShoppingCart } from "lucide-react"
|
|
import { clientFetch } from "@/lib/api"
|
|
import Image from "next/image"
|
|
import Link from "next/link"
|
|
|
|
interface LowStockWidgetProps {
|
|
settings?: {
|
|
threshold?: number
|
|
itemCount?: number
|
|
}
|
|
}
|
|
|
|
interface LowStockProduct {
|
|
id: string
|
|
name: string
|
|
currentStock: number
|
|
unitType: string
|
|
image?: string
|
|
}
|
|
|
|
export default function LowStockWidget({ settings }: LowStockWidgetProps) {
|
|
const threshold = settings?.threshold || 5
|
|
const itemCount = settings?.itemCount || 5
|
|
const [products, setProducts] = useState<LowStockProduct[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const fetchLowStock = async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
// Implementation: We'll use the product-performance API and filter locally
|
|
// or a dedicated stock-report API if available.
|
|
// For now, let's use the product-performance endpoint which has stock info.
|
|
const response = await clientFetch('/analytics/product-performance')
|
|
|
|
const lowStockProducts = response
|
|
.filter((p: any) => p.currentStock <= threshold)
|
|
.sort((a: any, b: any) => a.currentStock - b.currentStock)
|
|
.slice(0, itemCount)
|
|
.map((p: any) => ({
|
|
id: p.productId,
|
|
name: p.name,
|
|
currentStock: p.currentStock,
|
|
unitType: p.unitType,
|
|
image: p.image
|
|
}))
|
|
|
|
setProducts(lowStockProducts)
|
|
} catch (err) {
|
|
console.error("Error fetching low stock data:", err)
|
|
setError("Failed to load inventory data")
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchLowStock()
|
|
}, [threshold, itemCount])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm">
|
|
<CardHeader className="pb-2">
|
|
<Skeleton className="h-5 w-32 mb-1" />
|
|
<Skeleton className="h-4 w-48" />
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="flex items-center gap-4">
|
|
<Skeleton className="h-12 w-12 rounded-lg" />
|
|
<div className="space-y-2 flex-1">
|
|
<Skeleton className="h-4 w-3/4" />
|
|
<Skeleton className="h-3 w-1/4" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm overflow-hidden flex flex-col h-full">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div className="space-y-1">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5 text-amber-500" />
|
|
Low Stock Alerts
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Inventory checks (Threshold: {threshold})
|
|
</CardDescription>
|
|
</div>
|
|
<Link href="/dashboard/stock">
|
|
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs">
|
|
Manage
|
|
<ArrowRight className="h-3 w-3" />
|
|
</Button>
|
|
</Link>
|
|
</CardHeader>
|
|
<CardContent className="pt-4 flex-grow">
|
|
{error ? (
|
|
<div className="flex flex-col items-center justify-center py-10 text-center">
|
|
<Package className="h-10 w-10 text-muted-foreground/20 mb-3" />
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
</div>
|
|
) : products.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center mb-4">
|
|
<Package className="h-6 w-6 text-green-500" />
|
|
</div>
|
|
<h3 className="font-medium">All systems go</h3>
|
|
<p className="text-sm text-muted-foreground mt-1 max-w-xs">
|
|
No products currently under your threshold of {threshold} units.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{products.map((product) => (
|
|
<div
|
|
key={product.id}
|
|
className="flex items-center gap-4 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
|
>
|
|
<div className="h-12 w-12 relative rounded-lg border bg-muted overflow-hidden flex-shrink-0">
|
|
{product.image ? (
|
|
<Image
|
|
src={`/api/products/${product.id}/image`}
|
|
alt={product.name}
|
|
fill
|
|
className="object-cover group-hover:scale-110 transition-transform"
|
|
/>
|
|
) : (
|
|
<div className="h-full w-full flex items-center justify-center">
|
|
<ShoppingCart className="h-5 w-5 text-muted-foreground/40" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-grow min-w-0">
|
|
<h4 className="font-semibold text-sm truncate">{product.name}</h4>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<span className="text-[10px] uppercase font-mono tracking-wider text-muted-foreground bg-muted-foreground/10 px-1.5 py-0.5 rounded">
|
|
ID: {product.id.slice(-6)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className={`text-sm font-bold ${product.currentStock === 0 ? 'text-destructive' : 'text-amber-500'}`}>
|
|
{product.currentStock} {product.unitType}
|
|
</div>
|
|
<div className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">Remaining</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|