Files
star-erp/resources/js/Components/Inventory/InventoryTable.tsx
sky121113 4fa87925a2
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m8s
UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
2026-02-13 13:16:05 +08:00

240 lines
8.9 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 { useState } from "react";
import { Edit, ChevronDown, ChevronRight, Package } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Button } from "@/Components/ui/button";
import { StatusBadge } from "@/Components/shared/StatusBadge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/Components/ui/collapsible";
import { WarehouseInventory, SafetyStockSetting } from "@/types/warehouse";
import { getSafetyStockStatus } from "@/utils/inventory";
import { formatDate } from "@/utils/format";
export type InventoryItemWithId = WarehouseInventory & { inventoryId: string };
// 商品群組型別(包含有庫存和沒庫存的情況)
export interface ProductGroup {
productId: string;
productName: string;
items: InventoryItemWithId[]; // 可能是空陣列(沒有庫存)
safetySetting?: SafetyStockSetting;
}
interface InventoryTableProps {
productGroups: ProductGroup[];
onEdit: (inventoryId: string) => void;
}
export default function InventoryTable({
productGroups,
onEdit,
}: InventoryTableProps) {
// 每個商品的展開/折疊狀態
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
if (productGroups.length === 0) {
return (
<div className="text-center py-12 text-gray-400">
<p></p>
<p className="text-sm mt-1">調</p>
</div>
);
}
// 按商品名稱排序
const sortedProductGroups = [...productGroups].sort((a, b) =>
a.productName.localeCompare(b.productName, "zh-TW")
);
const toggleProduct = (productId: string) => {
setExpandedProducts((prev) => {
const newSet = new Set(prev);
if (newSet.has(productId)) {
newSet.delete(productId);
} else {
newSet.add(productId);
}
return newSet;
});
};
// 獲取狀態徽章
const getStatusBadge = (status: string) => {
if (status === '正常') {
return (
<StatusBadge variant="success">
</StatusBadge>
);
}
if (status === '接近') {
return (
<StatusBadge variant="warning">
</StatusBadge>
);
}
if (status === '低於') {
return (
<StatusBadge variant="destructive">
</StatusBadge>
);
}
return null;
};
return (
<div className="space-y-4 p-4">
{sortedProductGroups.map((group) => {
const totalQuantity = group.items.reduce(
(sum, item) => sum + item.quantity,
0
);
// 計算安全庫存狀態
const status = group.safetySetting
? getSafetyStockStatus(totalQuantity, group.safetySetting.safetyStock)
: null;
const isLowStock = status === "低於";
const isExpanded = expandedProducts.has(group.productId);
const hasInventory = group.items.length > 0;
return (
<Collapsible
key={group.productId}
open={isExpanded}
onOpenChange={() => toggleProduct(group.productId)}
>
<div className="border rounded-lg overflow-hidden">
{/* 商品標題 - 可點擊折疊 */}
<CollapsibleTrigger asChild>
<div
className={`px-4 py-3 border-b cursor-pointer hover:bg-gray-100 transition-colors ${isLowStock ? "bg-red-50" : "bg-gray-50"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* 折疊圖示 */}
{isExpanded ? (
<ChevronDown className="h-5 w-5 text-gray-600" />
) : (
<ChevronRight className="h-5 w-5 text-gray-600" />
)}
<h3 className="font-semibold text-gray-900">{group.productName}</h3>
<span className="text-sm text-gray-500">
{hasInventory ? `${group.items.length} 個批號` : '無庫存'}
</span>
</div>
<div className="flex items-center gap-4">
<div className="text-sm">
<span className="text-gray-600">
<span className={`font-medium ${isLowStock ? "text-red-600" : "text-gray-900"}`}>{totalQuantity} </span>
</span>
</div>
{group.safetySetting && (
<>
<div className="text-sm">
<span className="text-gray-600">
<span className="font-medium text-gray-900">{group.safetySetting.safetyStock} </span>
</span>
</div>
<div>
{status && getStatusBadge(status)}
</div>
</>
)}
{!group.safetySetting && (
<StatusBadge variant="neutral">
</StatusBadge>
)}
</div>
</div>
</div>
</CollapsibleTrigger>
{/* 商品表格 - 可折疊內容 */}
<CollapsibleContent>
{hasInventory ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[5%]">#</TableHead>
<TableHead className="w-[12%]"></TableHead>
<TableHead className="w-[12%]"></TableHead>
<TableHead className="w-[15%]"></TableHead>
<TableHead className="w-[14%]"></TableHead>
<TableHead className="w-[14%]"></TableHead>
<TableHead className="w-[14%]"></TableHead>
<TableHead className="w-[8%] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{group.items.map((item, index) => {
return (
<TableRow key={item.inventoryId}>
<TableCell className="text-grey-2">{index + 1}</TableCell>
<TableCell>{item.batchNumber || "-"}</TableCell>
<TableCell>
<span>{item.quantity}</span>
</TableCell>
<TableCell>{item.batchNumber || "-"}</TableCell>
<TableCell>
{item.expiryDate ? formatDate(item.expiryDate) : "-"}
</TableCell>
<TableCell>
{item.lastInboundDate ? formatDate(item.lastInboundDate) : "-"}
</TableCell>
<TableCell>
{item.lastOutboundDate ? formatDate(item.lastOutboundDate) : "-"}
</TableCell>
<TableCell className="text-right">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onEdit(item.inventoryId)}
className="hover:bg-primary/10 hover:text-primary"
>
<Edit className="h-4 w-4 mr-1" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<div className="px-4 py-8 text-center text-gray-400 bg-gray-50">
<Package className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
)}
</CollapsibleContent>
</div>
</Collapsible>
);
})}
</div>
);
}