Files
star-erp/resources/js/Pages/Dashboard.tsx

262 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Head, Link } from "@inertiajs/react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import {
Package,
AlertTriangle,
MinusCircle,
Clock,
ArrowRight,
LayoutDashboard,
} from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Badge } from "@/Components/ui/badge";
import { Button } from "@/Components/ui/button";
interface AbnormalItem {
id: number;
product_code: string;
product_name: string;
warehouse_name: string;
quantity: number;
safety_stock: number | null;
expiry_date: string | null;
statuses: string[];
}
interface Props {
stats: {
totalItems: number;
lowStockCount: number;
negativeCount: number;
expiringCount: number;
};
abnormalItems: AbnormalItem[];
}
// 狀態 Badge 映射
const statusConfig: Record<string, { label: string; className: string }> = {
negative: {
label: "負庫存",
className: "bg-red-100 text-red-800 border-red-200",
},
low_stock: {
label: "低庫存",
className: "bg-amber-100 text-amber-800 border-amber-200",
},
expiring: {
label: "即將過期",
className: "bg-yellow-100 text-yellow-800 border-yellow-200",
},
expired: {
label: "已過期",
className: "bg-red-100 text-red-800 border-red-200",
},
};
export default function Dashboard({ stats, abnormalItems }: Props) {
const cards = [
{
label: "庫存品項數",
value: stats.totalItems,
icon: <Package className="h-6 w-6" />,
color: "text-primary-main",
bgColor: "bg-primary-lightest",
borderColor: "border-primary-light",
href: "/inventory/stock-query",
},
{
label: "低庫存",
value: stats.lowStockCount,
icon: <AlertTriangle className="h-6 w-6" />,
color: "text-amber-600",
bgColor: "bg-amber-50",
borderColor: "border-amber-200",
href: "/inventory/stock-query?status=low_stock",
alert: stats.lowStockCount > 0,
},
{
label: "負庫存",
value: stats.negativeCount,
icon: <MinusCircle className="h-6 w-6" />,
color: "text-red-600",
bgColor: "bg-red-50",
borderColor: "border-red-200",
href: "/inventory/stock-query?status=negative",
alert: stats.negativeCount > 0,
},
{
label: "即將過期",
value: stats.expiringCount,
icon: <Clock className="h-6 w-6" />,
color: "text-yellow-600",
bgColor: "bg-yellow-50",
borderColor: "border-yellow-200",
href: "/inventory/stock-query?status=expiring",
alert: stats.expiringCount > 0,
},
];
return (
<AuthenticatedLayout
breadcrumbs={[
{
label: "儀表板",
href: "/",
isPage: true,
},
]}
>
<Head title="儀表板" />
<div className="container mx-auto p-6 max-w-7xl">
{/* 頁面標題 */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<LayoutDashboard className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
{/* 統計卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{cards.map((card) => (
<Link key={card.label} href={card.href}>
<div
className={`relative rounded-xl border ${card.borderColor} ${card.bgColor} p-5 transition-all hover:shadow-md hover:-translate-y-0.5 cursor-pointer`}
>
{card.alert && (
<span className="absolute top-3 right-3 h-2.5 w-2.5 rounded-full bg-red-500 animate-pulse" />
)}
<div className="flex items-center gap-3 mb-3">
<div className={card.color}>
{card.icon}
</div>
<span className="text-sm font-medium text-grey-1">
{card.label}
</span>
</div>
<div
className={`text-3xl font-bold ${card.color}`}
>
{card.value.toLocaleString()}
</div>
</div>
</Link>
))}
</div>
{/* 異常庫存清單 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
<h2 className="text-lg font-semibold text-grey-0 flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-amber-500" />
</h2>
<Link href="/inventory/stock-query?status=abnormal">
<Button
variant="outline"
size="sm"
className="button-outlined-primary gap-1"
>
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center">
#
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right">
</TableHead>
<TableHead className="text-center">
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{abnormalItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-8 text-gray-500"
>
🎉
</TableCell>
</TableRow>
) : (
abnormalItems.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell className="font-mono text-sm">
{item.product_code}
</TableCell>
<TableCell className="font-medium">
{item.product_name}
</TableCell>
<TableCell>
{item.warehouse_name}
</TableCell>
<TableCell
className={`text-right font-medium ${item.quantity < 0
? "text-red-600"
: ""
}`}
>
{item.quantity}
</TableCell>
<TableCell className="text-center">
<div className="flex flex-wrap items-center justify-center gap-1">
{item.statuses.map(
(status) => {
const config =
statusConfig[
status
];
if (!config)
return null;
return (
<Badge
key={status}
variant="outline"
className={
config.className
}
>
{config.label}
</Badge>
);
}
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</AuthenticatedLayout>
);
}