Add modular dashboard widgets and layout editor
Some checks failed
Build Frontend / build (push) Failing after 7s
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.
This commit is contained in:
167
components/dashboard/low-stock-widget.tsx
Normal file
167
components/dashboard/low-stock-widget.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user