2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
import { Head, Link, useForm } from "@inertiajs/react";
|
|
|
|
|
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
|
|
|
|
|
import { Button } from "@/Components/ui/button";
|
|
|
|
|
|
import { Input } from "@/Components/ui/input";
|
|
|
|
|
|
import { Label } from "@/Components/ui/label";
|
|
|
|
|
|
import { ArrowLeft, Save, Trash2 } from "lucide-react";
|
|
|
|
|
|
import { Warehouse, WarehouseInventory } from "@/types/warehouse";
|
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AlertDialog,
|
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
|
} from "@/Components/ui/alert-dialog";
|
|
|
|
|
|
import TransactionTable, { Transaction } from "@/Components/Warehouse/Inventory/TransactionTable";
|
2026-01-07 13:06:49 +08:00
|
|
|
|
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
warehouse: Warehouse;
|
|
|
|
|
|
inventory: WarehouseInventory;
|
|
|
|
|
|
transactions: Transaction[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function EditInventory({ warehouse, inventory, transactions = [] }: Props) {
|
|
|
|
|
|
const { data, setData, put, delete: destroy, processing, errors } = useForm({
|
|
|
|
|
|
quantity: inventory.quantity,
|
|
|
|
|
|
batchNumber: inventory.batchNumber || "",
|
|
|
|
|
|
expiryDate: inventory.expiryDate || "",
|
|
|
|
|
|
lastInboundDate: inventory.lastInboundDate || "",
|
|
|
|
|
|
lastOutboundDate: inventory.lastOutboundDate || "",
|
|
|
|
|
|
// 為了記錄異動原因,還是需要傳這兩個欄位,雖然 UI 上原本的 EditPage 沒有原因輸入框
|
|
|
|
|
|
// 但為了符合我們後端的交易紀錄邏輯,我們可能需要預設一個,或者偷加一個欄位?
|
|
|
|
|
|
// 原 source code 沒有原因欄位。
|
|
|
|
|
|
// 我們可以預設 reason 為 "手動編輯更新"
|
|
|
|
|
|
reason: "編輯頁面手動更新",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
|
if (data.quantity < 0) {
|
|
|
|
|
|
toast.error("庫存數量不可為負數");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
put(route("warehouses.inventory.update", { warehouse: warehouse.id, inventory: inventory.id }), {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
toast.success("庫存資料已更新");
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("更新失敗,請檢查欄位");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = () => {
|
|
|
|
|
|
destroy(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventory: inventory.id }), {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
toast.success("庫存品項已刪除");
|
|
|
|
|
|
setShowDeleteDialog(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("刪除失敗");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-07 13:06:49 +08:00
|
|
|
|
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("warehouses", "修正庫存")}>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Head title={`編輯庫存 - ${inventory.productName} `} />
|
|
|
|
|
|
<div className="container mx-auto p-6 max-w-4xl">
|
|
|
|
|
|
{/* 頁面標題與麵包屑 */}
|
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<Link href={`/warehouses/${warehouse.id}/inventory`}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
className="gap-2 button-outlined-primary mb-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
|
返回庫存管理
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Link >
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
|
|
|
|
|
|
<span>商品與庫存管理</span>
|
|
|
|
|
|
<span>/</span>
|
|
|
|
|
|
<span>倉庫管理</span>
|
|
|
|
|
|
<span>/</span>
|
|
|
|
|
|
<span>庫存管理</span>
|
|
|
|
|
|
<span>/</span>
|
|
|
|
|
|
<span className="text-gray-900">編輯庫存品項</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="mb-2">編輯庫存品項</h1>
|
|
|
|
|
|
<p className="text-gray-600">
|
|
|
|
|
|
倉庫:<span className="font-medium text-gray-900">{warehouse.name}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => setShowDeleteDialog(true)}
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
className="group border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
|
刪除品項
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button onClick={handleSave} className="button-filled-primary" disabled={processing}>
|
|
|
|
|
|
<Save className="mr-2 h-4 w-4" />
|
|
|
|
|
|
儲存變更
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div >
|
|
|
|
|
|
|
|
|
|
|
|
{/* 表單內容 */}
|
|
|
|
|
|
< div className="bg-white rounded-lg shadow-sm border p-6 mb-6" >
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{/* 商品基本資訊 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<h3 className="font-medium border-b pb-2 text-lg">商品基本資訊</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="productName">
|
|
|
|
|
|
商品名稱 <span className="text-red-500">*</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="productName"
|
|
|
|
|
|
value={inventory.productName}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="bg-gray-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
|
商品名稱無法修改
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="batchNumber">批號</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="batchNumber"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={data.batchNumber}
|
|
|
|
|
|
onChange={(e) => setData("batchNumber", e.target.value)}
|
|
|
|
|
|
placeholder="例:FL20251101"
|
|
|
|
|
|
className="button-outlined-primary"
|
|
|
|
|
|
// 目前後端可能尚未支援儲存,但依需求顯示
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 庫存數量 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<h3 className="font-medium border-b pb-2 text-lg">庫存數量</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="quantity">
|
|
|
|
|
|
庫存數量 <span className="text-red-500">*</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="quantity"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={data.quantity}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setData("quantity", parseFloat(e.target.value) || 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="0"
|
|
|
|
|
|
className={`button-outlined-primary ${errors.quantity ? "border-red-500" : ""}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{errors.quantity && <p className="text-xs text-red-500">{errors.quantity}</p>}
|
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
|
批號層級的庫存數量,安全庫存請至「安全庫存設定」頁面進行商品層級設定
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 日期資訊 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<h3 className="font-medium border-b pb-2 text-lg">日期資訊</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="expiryDate">保存期限</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="expiryDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={data.expiryDate}
|
|
|
|
|
|
onChange={(e) => setData("expiryDate", e.target.value)}
|
|
|
|
|
|
className="button-outlined-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="lastInboundDate">最新入庫時間</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="lastInboundDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={data.lastInboundDate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setData("lastInboundDate", e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
className="button-outlined-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="lastOutboundDate">最新出庫時間</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="lastOutboundDate"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={data.lastOutboundDate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setData("lastOutboundDate", e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
className="button-outlined-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div >
|
|
|
|
|
|
|
|
|
|
|
|
{/* 庫存異動紀錄 */}
|
|
|
|
|
|
< div className="bg-white rounded-lg shadow-sm border p-6" >
|
|
|
|
|
|
<h3 className="font-medium text-lg border-b pb-4 mb-4">庫存異動紀錄</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<TransactionTable transactions={transactions} />
|
|
|
|
|
|
</div >
|
|
|
|
|
|
|
|
|
|
|
|
{/* 刪除確認對話框 */}
|
|
|
|
|
|
< AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog} >
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>確認刪除庫存品項</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
您確定要刪除「{inventory.productName}」的此筆庫存嗎?此操作無法復原。
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel className="button-outlined-primary">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</AlertDialogCancel>
|
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
|
className="bg-red-600 text-white hover:bg-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
確認刪除
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog >
|
|
|
|
|
|
</div >
|
|
|
|
|
|
</AuthenticatedLayout >
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|