Files
star-erp/resources/js/Components/Inventory/InventoryTable.tsx

240 lines
8.9 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
/**
*
*
*/
import { useState } from "react";
import { Edit, ChevronDown, ChevronRight, Package } from "lucide-react";
2025-12-30 15:03:19 +08:00
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Button } from "@/Components/ui/button";
import { StatusBadge } from "@/Components/shared/StatusBadge";
2025-12-30 15:03:19 +08:00
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/Components/ui/collapsible";
import { WarehouseInventory, SafetyStockSetting } from "@/types/warehouse";
import { getSafetyStockStatus } from "@/utils/inventory";
2025-12-30 15:03:19 +08:00
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>
);
2025-12-30 15:03:19 +08:00
}
return null;
2025-12-30 15:03:19 +08:00
};
return (
<div className="space-y-4 p-4">
{sortedProductGroups.map((group) => {
const totalQuantity = group.items.reduce(
(sum, item) => sum + item.quantity,
0
);
2025-12-30 15:03:19 +08:00
// 計算安全庫存狀態
const status = group.safetySetting
? getSafetyStockStatus(totalQuantity, group.safetySetting.safetyStock)
: null;
2025-12-30 15:03:19 +08:00
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"
}`}
2025-12-30 15:03:19 +08:00
>
<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">
2025-12-30 15:03:19 +08:00
</StatusBadge>
2025-12-30 15:03:19 +08:00
)}
</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>
);
}