Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
170 lines
7.6 KiB
TypeScript
170 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
|
import { Button } from "@/components/common/button"
|
|
import { Skeleton } from "@/components/common/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>
|
|
)
|
|
}
|
|
|
|
|