first commit
This commit is contained in:
Binary file not shown.
100
resources/js/Components/SafetyStock/EditSafetyStockDialog.tsx
Normal file
100
resources/js/Components/SafetyStock/EditSafetyStockDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 編輯安全庫存對話框
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogDescription,
|
||||
} from "@/Components/ui/dialog";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { SafetyStockSetting } from "@/types/warehouse";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
|
||||
interface EditSafetyStockDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
setting: SafetyStockSetting;
|
||||
onSave: (setting: SafetyStockSetting) => void;
|
||||
}
|
||||
|
||||
export default function EditSafetyStockDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
setting,
|
||||
onSave,
|
||||
}: EditSafetyStockDialogProps) {
|
||||
const [safetyStock, setSafetyStock] = useState(setting.safetyStock);
|
||||
|
||||
useEffect(() => {
|
||||
setSafetyStock(setting.safetyStock);
|
||||
}, [setting]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (safetyStock <= 0) {
|
||||
toast.error("安全庫存量必須大於 0");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSetting: SafetyStockSetting = {
|
||||
...setting,
|
||||
safetyStock,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
onSave(updatedSetting);
|
||||
toast.success("安全庫存設定已更新");
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>編輯安全庫存</DialogTitle>
|
||||
<DialogDescription>請輸入新的安全庫存量。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>商品名稱</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{setting.productName}</span>
|
||||
<Badge variant="outline">{setting.productType}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="safetyStock">
|
||||
安全庫存量 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="safetyStock"
|
||||
type="number"
|
||||
min="1"
|
||||
value={safetyStock}
|
||||
onChange={(e) => setSafetyStock(parseInt(e.target.value) || 0)}
|
||||
placeholder="請輸入安全庫存量"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} className="button-filled-primary">
|
||||
儲存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
152
resources/js/Components/SafetyStock/SafetyStockList.tsx
Normal file
152
resources/js/Components/SafetyStock/SafetyStockList.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 安全庫存列表組件
|
||||
*/
|
||||
|
||||
import { Edit, Trash2, AlertCircle, CheckCircle, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { SafetyStockSetting, WarehouseInventory, SafetyStockStatus } from "@/types/warehouse";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
|
||||
interface SafetyStockListProps {
|
||||
settings: SafetyStockSetting[];
|
||||
inventories: WarehouseInventory[];
|
||||
onEdit: (setting: SafetyStockSetting) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
// 計算安全庫存狀態
|
||||
function getSafetyStockStatus(
|
||||
currentStock: number,
|
||||
safetyStock: number
|
||||
): SafetyStockStatus {
|
||||
const ratio = currentStock / safetyStock;
|
||||
if (ratio >= 1.2) return "正常";
|
||||
if (ratio >= 1.0) return "接近";
|
||||
return "低於";
|
||||
}
|
||||
|
||||
// 獲取狀態徽章
|
||||
function getStatusBadge(status: SafetyStockStatus) {
|
||||
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">
|
||||
<AlertCircle className="mr-1 h-3 w-3" />
|
||||
低於
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function SafetyStockList({
|
||||
settings,
|
||||
inventories,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: SafetyStockListProps) {
|
||||
if (settings.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-12 text-center text-gray-400">
|
||||
<p>尚未設定任何安全庫存</p>
|
||||
<p className="text-sm mt-1">點擊「新增安全庫存」開始設定</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 計算每個商品的目前總庫存
|
||||
const getCurrentStock = (productId: string): number => {
|
||||
return inventories
|
||||
.filter((inv) => inv.productId === productId)
|
||||
.reduce((sum, inv) => sum + inv.quantity, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[5%]">#</TableHead>
|
||||
<TableHead className="w-[25%]">商品名稱</TableHead>
|
||||
<TableHead className="w-[12%]">商品類型</TableHead>
|
||||
<TableHead className="w-[12%]">目前庫存</TableHead>
|
||||
<TableHead className="w-[12%]">安全庫存</TableHead>
|
||||
<TableHead className="w-[15%]">狀態</TableHead>
|
||||
<TableHead className="w-[12%] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{settings.map((setting, index) => {
|
||||
const currentStock = getCurrentStock(setting.productId);
|
||||
const status = getSafetyStockStatus(currentStock, setting.safetyStock);
|
||||
const isLowStock = status === "低於";
|
||||
|
||||
return (
|
||||
<TableRow key={setting.id} className={isLowStock ? "bg-red-50" : ""}>
|
||||
<TableCell className="text-grey-2">{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">{setting.productName}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{setting.productType}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={isLowStock ? "text-red-600 font-medium" : ""}>
|
||||
{currentStock}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{setting.safetyStock}</TableCell>
|
||||
<TableCell>{getStatusBadge(status)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(setting)}
|
||||
className="hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
編輯
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(setting.id)}
|
||||
className="hover:bg-red-50 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
移除
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user