first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View 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>
);
}

View 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>
);
}