first commit
This commit is contained in:
30
resources/js/Components/Inventory/InventoryStats.tsx
Normal file
30
resources/js/Components/Inventory/InventoryStats.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 庫存統計元件
|
||||
* 顯示庫存總覽統計資訊
|
||||
*/
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import StatsCard from "@/Components/shared/StatsCard";
|
||||
|
||||
interface InventoryStatsProps {
|
||||
totalItems: number;
|
||||
totalQuantity: number;
|
||||
lowStockItems: number;
|
||||
}
|
||||
|
||||
export default function InventoryStats({
|
||||
totalItems,
|
||||
totalQuantity,
|
||||
lowStockItems,
|
||||
}: InventoryStatsProps) {
|
||||
return (
|
||||
<div className="max-w-sm">
|
||||
<StatsCard
|
||||
label="庫存警告"
|
||||
value={`${lowStockItems} 項`}
|
||||
icon={AlertTriangle}
|
||||
valueClassName={lowStockItems > 0 ? "text-red-600" : "text-green-600"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
244
resources/js/Components/Inventory/InventoryTable.tsx
Normal file
244
resources/js/Components/Inventory/InventoryTable.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 庫存表格元件
|
||||
* 顯示庫存項目列表(依商品分類並支援折疊)
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle, Edit, ChevronDown, ChevronRight, CheckCircle, Package } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/Components/ui/collapsible";
|
||||
import { WarehouseInventory, SafetyStockSetting } from "@/types/warehouse";
|
||||
import { calculateProductTotalStock, 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) => {
|
||||
switch (status) {
|
||||
case "正常":
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-700 border-green-300">
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
正常
|
||||
</Badge>
|
||||
);
|
||||
case "接近":
|
||||
return (
|
||||
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
接近
|
||||
</Badge>
|
||||
);
|
||||
case "低於":
|
||||
return (
|
||||
<Badge className="bg-red-100 text-red-700 border-red-300">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
低於
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
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 && (
|
||||
<Badge variant="outline" className="text-gray-500">
|
||||
未設定
|
||||
</Badge>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
59
resources/js/Components/Inventory/InventoryToolbar.tsx
Normal file
59
resources/js/Components/Inventory/InventoryToolbar.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 庫存工具列元件
|
||||
* 包含搜尋和篩選功能
|
||||
*/
|
||||
|
||||
import { Filter } from "lucide-react";
|
||||
import SearchToolbar from "@/Components/shared/SearchToolbar";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
|
||||
interface InventoryToolbarProps {
|
||||
searchTerm: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
typeFilter: string;
|
||||
onTypeFilterChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function InventoryToolbar({
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
typeFilter,
|
||||
onTypeFilterChange,
|
||||
}: InventoryToolbarProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{/* 搜尋框 */}
|
||||
<SearchToolbar
|
||||
value={searchTerm}
|
||||
onChange={onSearchChange}
|
||||
placeholder="搜尋商品名稱或批號..."
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
{/* 類型篩選 */}
|
||||
<div className="w-full sm:w-56">
|
||||
<Select value={typeFilter} onValueChange={onTypeFilterChange}>
|
||||
<SelectTrigger className="border-gray-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4 text-gray-400" />
|
||||
<SelectValue placeholder="篩選類型" />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部類型</SelectItem>
|
||||
<SelectItem value="原物料">原物料</SelectItem>
|
||||
<SelectItem value="半成品">半成品</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user