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

320 lines
19 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
/**
2026-01-22 15:39:35 +08:00
* (Warehouse )
*
2025-12-30 15:03:19 +08:00
*/
2026-01-22 15:39:35 +08:00
import { useState } from "react";
import { AlertTriangle, Edit, Trash2, Eye, ChevronDown, ChevronRight, CheckCircle, 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 { Badge } from "@/Components/ui/badge";
2026-01-22 15:39:35 +08:00
import {
Collapsible,
CollapsibleContent,
} from "@/Components/ui/collapsible";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/Components/ui/tooltip";
import { GroupedInventory } from "@/types/warehouse";
2025-12-30 15:03:19 +08:00
import { formatDate } from "@/utils/format";
import { Can } from "@/Components/Permission/Can";
2026-01-22 15:39:35 +08:00
import BatchAdjustmentModal from "./BatchAdjustmentModal";
2025-12-30 15:03:19 +08:00
interface InventoryTableProps {
2026-01-22 15:39:35 +08:00
inventories: GroupedInventory[];
2025-12-30 15:03:19 +08:00
onView: (id: string) => void;
onDelete: (id: string) => void;
2026-01-22 15:39:35 +08:00
onAdjust: (batchId: string, data: { operation: string; quantity: number; reason: string }) => void;
onViewProduct?: (productId: string) => void;
2025-12-30 15:03:19 +08:00
}
export default function InventoryTable({
inventories,
onView,
onDelete,
2026-01-22 15:39:35 +08:00
onAdjust,
onViewProduct,
2025-12-30 15:03:19 +08:00
}: InventoryTableProps) {
2026-01-22 15:39:35 +08:00
// 每個商品的展開/折疊狀態
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
// 調整彈窗狀態
const [adjustmentTarget, setAdjustmentTarget] = useState<{
id: string;
batchNumber: string;
currentQuantity: number;
productName: string;
} | null>(null);
2025-12-30 15:03:19 +08:00
if (inventories.length === 0) {
return (
<div className="text-center py-12 text-gray-400">
<p></p>
<p className="text-sm mt-1">調</p>
</div>
);
}
2026-01-22 15:39:35 +08:00
// 按商品名稱排序
const sortedInventories = [...inventories].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;
});
};
2025-12-30 15:03:19 +08:00
// 獲取狀態徽章
2026-01-22 15:39:35 +08:00
const getStatusBadge = (status: string) => {
2025-12-30 15:03:19 +08:00
switch (status) {
case "正常":
return (
2026-01-22 15:39:35 +08:00
<Badge className="bg-green-100 text-green-700 border-green-300">
2025-12-30 15:03:19 +08:00
<CheckCircle className="mr-1 h-3 w-3" />
</Badge>
);
2026-01-22 15:39:35 +08:00
case "低於":
2025-12-30 15:03:19 +08:00
return (
2026-01-22 15:39:35 +08:00
<Badge className="bg-red-100 text-red-700 border-red-300">
2025-12-30 15:03:19 +08:00
<AlertTriangle className="mr-1 h-3 w-3" />
</Badge>
);
default:
return null;
}
};
return (
2026-01-22 15:39:35 +08:00
<TooltipProvider>
<div className="space-y-4 p-4">
{sortedInventories.map((group) => {
const totalQuantity = group.totalQuantity;
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
// 使用後端提供的狀態
const status = group.status;
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
const isLowStock = status === "低於";
const isExpanded = expandedProducts.has(group.productId);
const hasInventory = group.batches.length > 0;
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
return (
<Collapsible
key={group.productId}
open={isExpanded}
onOpenChange={() => toggleProduct(group.productId)}
>
<div className="border rounded-lg overflow-hidden">
{/* 商品標題 - 可點擊折疊 */}
<div
onClick={() => toggleProduct(group.productId)}
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.batches.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} {group.baseUnit}</span>
2026-01-22 15:39:35 +08:00
</span>
</div>
<Can permission="inventory.view_cost">
<div className="text-sm">
<span className="text-gray-600">
<span className="font-medium text-gray-900">${group.totalValue?.toLocaleString()}</span>
</span>
</div>
</Can>
2026-01-22 15:39:35 +08:00
{group.safetyStock !== null ? (
<>
<div className="text-sm">
<span className="text-gray-600">
<span className="font-medium text-gray-900">{group.safetyStock} {group.baseUnit}</span>
2026-01-22 15:39:35 +08:00
</span>
</div>
<div>
{status && getStatusBadge(status)}
</div>
</>
) : (
<Badge variant="outline" className="text-gray-500">
</Badge>
)}
{onViewProduct && (
<Button
type="button"
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
onViewProduct(group.productId);
}}
className="button-outlined-primary"
>
<Eye className="h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
{/* 商品表格 - 可折疊內容 */}
<CollapsibleContent>
{hasInventory ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[5%]">#</TableHead>
<TableHead className="w-[12%]"></TableHead>
<TableHead className="w-[10%]"></TableHead>
<Can permission="inventory.view_cost">
<TableHead className="w-[10%]"></TableHead>
<TableHead className="w-[10%]"></TableHead>
</Can>
<TableHead className="w-[12%]"></TableHead>
<TableHead className="w-[12%]"></TableHead>
<TableHead className="w-[12%]"></TableHead>
2026-01-22 15:39:35 +08:00
<TableHead className="w-[8%] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{group.batches.map((batch, index) => {
return (
<TableRow key={batch.id}>
<TableCell className="text-grey-2">{index + 1}</TableCell>
<TableCell>{batch.batchNumber || "-"}</TableCell>
<TableCell>
<span>{batch.quantity} {batch.unit}</span>
2026-01-22 15:39:35 +08:00
</TableCell>
<Can permission="inventory.view_cost">
<TableCell>${batch.unit_cost?.toLocaleString()}</TableCell>
<TableCell>${batch.total_value?.toLocaleString()}</TableCell>
</Can>
2026-01-22 15:39:35 +08:00
<TableCell>
{batch.expiryDate ? formatDate(batch.expiryDate) : "-"}
</TableCell>
<TableCell>
{batch.lastInboundDate ? formatDate(batch.lastInboundDate) : "-"}
</TableCell>
<TableCell>
{batch.lastOutboundDate ? formatDate(batch.lastOutboundDate) : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onView(batch.id)}
className="button-outlined-primary"
>
<Eye className="h-4 w-4" />
</Button>
<Can permission="inventory.adjust">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setAdjustmentTarget({
id: batch.id,
batchNumber: batch.batchNumber,
currentQuantity: batch.quantity,
productName: group.productName
})}
className="button-outlined-primary"
>
<Edit className="h-4 w-4" />
</Button>
</Can>
<Can permission="inventory.delete">
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-block">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onDelete(batch.id)}
className={batch.quantity > 0 ? "opacity-50 cursor-not-allowed border-gray-200 text-gray-400 hover:bg-transparent" : "button-outlined-error"}
disabled={batch.quantity > 0}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{batch.quantity > 0 ? "庫存須為 0 才可刪除" : "刪除"}</p>
</TooltipContent>
</Tooltip>
</Can>
</div>
</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>
);
})}
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
<BatchAdjustmentModal
isOpen={!!adjustmentTarget}
onClose={() => setAdjustmentTarget(null)}
batch={adjustmentTarget || undefined}
onConfirm={(data) => {
if (adjustmentTarget) {
onAdjust(adjustmentTarget.id, data);
setAdjustmentTarget(null);
}
}}
/>
</div>
</TooltipProvider>
2025-12-30 15:03:19 +08:00
);
}