2025-12-30 15:03:19 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 安全庫存設定頁面
|
|
|
|
|
|
* Last Updated: 2025-12-29
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
2026-01-13 17:00:58 +08:00
|
|
|
|
import { ArrowLeft, Plus, Shield } from "lucide-react";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
import { Button } from "@/Components/ui/button";
|
|
|
|
|
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
|
|
|
|
|
import { Head, Link, router } from "@inertiajs/react";
|
|
|
|
|
|
import { SafetyStockSetting, WarehouseInventory, Warehouse, Product } from "@/types/warehouse";
|
|
|
|
|
|
import SafetyStockList from "@/Components/Warehouse/SafetyStock/SafetyStockList";
|
|
|
|
|
|
import AddSafetyStockDialog from "@/Components/Warehouse/SafetyStock/AddSafetyStockDialog";
|
|
|
|
|
|
import EditSafetyStockDialog from "@/Components/Warehouse/SafetyStock/EditSafetyStockDialog";
|
|
|
|
|
|
import { toast } from "sonner";
|
2026-01-07 13:06:49 +08:00
|
|
|
|
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
2026-01-13 17:00:58 +08:00
|
|
|
|
import { Can } from "@/Components/Permission/Can";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AlertDialog,
|
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
|
} from "@/Components/ui/alert-dialog";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
warehouse: Warehouse;
|
|
|
|
|
|
safetyStockSettings: SafetyStockSetting[];
|
|
|
|
|
|
inventories: WarehouseInventory[];
|
|
|
|
|
|
availableProducts: Product[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function SafetyStockPage({
|
|
|
|
|
|
warehouse,
|
|
|
|
|
|
safetyStockSettings: initialSettings = [],
|
|
|
|
|
|
inventories = [],
|
|
|
|
|
|
availableProducts = [],
|
|
|
|
|
|
}: Props) {
|
|
|
|
|
|
const [settings, setSettings] = useState<SafetyStockSetting[]>(initialSettings);
|
|
|
|
|
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
|
|
|
|
const [editingSetting, setEditingSetting] = useState<SafetyStockSetting | null>(null);
|
2026-01-13 17:00:58 +08:00
|
|
|
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 當 Props 更新時同步本地 State
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setSettings(initialSettings);
|
|
|
|
|
|
}, [initialSettings]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleAdd = (newSettings: SafetyStockSetting[]) => {
|
|
|
|
|
|
router.post(route('warehouses.safety-stock.store', warehouse.id), {
|
|
|
|
|
|
settings: newSettings.map(s => ({
|
|
|
|
|
|
productId: s.productId,
|
|
|
|
|
|
quantity: s.safetyStock
|
|
|
|
|
|
})),
|
|
|
|
|
|
}, {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
setShowAddDialog(false);
|
|
|
|
|
|
toast.success(`成功新增 ${newSettings.length} 項安全庫存設定`);
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (errors) => {
|
|
|
|
|
|
const firstError = Object.values(errors)[0];
|
|
|
|
|
|
toast.error(typeof firstError === 'string' ? firstError : "新增失敗");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = (updatedSetting: SafetyStockSetting) => {
|
|
|
|
|
|
router.put(route('warehouses.safety-stock.update', [warehouse.id, updatedSetting.id]), {
|
|
|
|
|
|
safetyStock: updatedSetting.safetyStock,
|
|
|
|
|
|
}, {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
setEditingSetting(null);
|
|
|
|
|
|
toast.success(`成功更新 ${updatedSetting.productName} 的安全庫存`);
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (errors) => {
|
|
|
|
|
|
const firstError = Object.values(errors)[0];
|
|
|
|
|
|
toast.error(typeof firstError === 'string' ? firstError : "更新失敗");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-13 17:00:58 +08:00
|
|
|
|
const handleDelete = () => {
|
|
|
|
|
|
if (!deleteId) return;
|
|
|
|
|
|
|
|
|
|
|
|
router.delete(route('warehouses.safety-stock.destroy', [warehouse.id, deleteId]), {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
onSuccess: () => {
|
2026-01-13 17:00:58 +08:00
|
|
|
|
setDeleteId(null);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
toast.success("已刪除安全庫存設定");
|
2026-01-13 17:00:58 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("刪除失敗");
|
2025-12-30 15:03:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!warehouse) {
|
|
|
|
|
|
return <div className="p-8 text-center text-muted-foreground">正在載入倉庫資料...</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-07 13:06:49 +08:00
|
|
|
|
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name, "安全庫存設定")}>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Head title={`安全庫存設定 - ${warehouse.name}`} />
|
|
|
|
|
|
<div className="container mx-auto p-6 max-w-7xl">
|
|
|
|
|
|
{/* 頁面標題與導航 */}
|
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<Link href={route('warehouses.inventory.index', warehouse.id)}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
className="gap-2 button-outlined-primary mb-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
|
返回庫存管理
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
2026-01-13 17:00:58 +08:00
|
|
|
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
2026-01-16 14:36:24 +08:00
|
|
|
|
<Shield className="h-6 w-6 text-primary-main" />
|
2026-01-13 17:00:58 +08:00
|
|
|
|
安全庫存設定 - {warehouse.name}
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-gray-500 mt-1">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
設定商品的安全庫存量,當庫存低於安全值時將發出警告
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-01-13 17:00:58 +08:00
|
|
|
|
<Can permission="inventory.safety_stock">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => setShowAddDialog(true)}
|
|
|
|
|
|
className="button-filled-primary"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
|
新增安全庫存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Can>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 安全庫存列表 */}
|
|
|
|
|
|
<SafetyStockList
|
|
|
|
|
|
settings={settings}
|
|
|
|
|
|
inventories={inventories}
|
|
|
|
|
|
onEdit={setEditingSetting}
|
2026-01-13 17:00:58 +08:00
|
|
|
|
onDelete={setDeleteId}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 新增對話框 */}
|
|
|
|
|
|
<AddSafetyStockDialog
|
|
|
|
|
|
open={showAddDialog}
|
|
|
|
|
|
onOpenChange={setShowAddDialog}
|
|
|
|
|
|
warehouseId={warehouse.id}
|
|
|
|
|
|
existingSettings={settings}
|
|
|
|
|
|
availableProducts={availableProducts}
|
|
|
|
|
|
onAdd={handleAdd}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 編輯對話框 */}
|
|
|
|
|
|
{editingSetting && (
|
|
|
|
|
|
<EditSafetyStockDialog
|
|
|
|
|
|
open={!!editingSetting}
|
|
|
|
|
|
onOpenChange={(open) => !open && setEditingSetting(null)}
|
|
|
|
|
|
setting={editingSetting}
|
|
|
|
|
|
onSave={handleEdit}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-01-13 17:00:58 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 刪除確認對話框 */}
|
|
|
|
|
|
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>確認刪除安全庫存設定</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
您確定要刪除此項商品的安全庫存設定嗎?刪除後系統將不再針對此商品發出庫存不足警告。此動作無法復原。
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel className="button-outlined-primary">取消</AlertDialogCancel>
|
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
|
onClick={handleDelete}
|
2026-01-14 11:31:36 +08:00
|
|
|
|
className="button-filled-error"
|
2026-01-13 17:00:58 +08:00
|
|
|
|
>
|
|
|
|
|
|
確認刪除
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</AuthenticatedLayout>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|