Files
star-erp/resources/js/Components/Warehouse/Inventory/InventoryAdjustmentDialog.tsx
sky121113 be5c121146
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 47s
feat: 優化商品管理規格顯示與修復重複通知問題
2026-02-02 17:24:49 +08:00

224 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react";
import { useForm } from "@inertiajs/react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { WarehouseInventory } from "@/types/warehouse";
import { toast } from "sonner";
import { Minus, Plus, Equal } from "lucide-react";
interface InventoryAdjustmentDialogProps {
warehouseId: string;
item: WarehouseInventory | null;
isOpen: boolean;
onClose: () => void;
}
type Operation = "add" | "subtract" | "set";
export default function InventoryAdjustmentDialog({
warehouseId,
item,
isOpen,
onClose,
}: InventoryAdjustmentDialogProps) {
const [operation, setOperation] = useState<Operation>("add");
const { data, setData, put, processing, reset, errors } = useForm({
quantity: 0,
reason: "盤點調整",
notes: "",
operation: "add" as Operation,
type: "adjustment", // 預設類型
});
// 重新開放時重置
useEffect(() => {
if (isOpen) {
reset();
setOperation("add");
}
}, [isOpen]);
useEffect(() => {
setData("operation", operation);
}, [operation]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!item) return;
put(route("warehouses.inventory.update", {
warehouse: warehouseId,
product: item.productId // 這裡後端接收 product (或可用 inventory ID 擴充)
}), {
onSuccess: () => {
onClose();
},
onError: () => {
toast.error("調整失敗,請檢查欄位資料");
}
});
};
if (!item) return null;
// 計算剩餘庫存預覽
const getResultQuantity = () => {
const inputQty = Number(data.quantity) || 0;
switch (operation) {
case "add": return item.quantity + inputQty;
case "subtract": return Math.max(0, item.quantity - inputQty);
case "set": return inputQty;
default: return item.quantity;
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>調</DialogTitle>
<DialogDescription>
調{item.productName} (: {item.batchNumber || "無"})
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6 py-4">
{/* 現有庫存 */}
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border">
<span className="text-gray-600"></span>
<span className="font-bold text-lg">{item.quantity}</span>
</div>
{/* 調整方式 */}
<div className="space-y-3">
<Label>調</Label>
<div className="grid grid-cols-3 gap-2">
<Button
type="button"
variant={operation === "add" ? "default" : "outline"}
className={`flex flex-col gap-1 h-auto py-2 ${operation === "add" ? "bg-primary text-white" : ""}`}
onClick={() => setOperation("add")}
>
<Plus className="h-4 w-4" />
<span className="text-xs"></span>
</Button>
<Button
type="button"
variant={operation === "subtract" ? "default" : "outline"}
className={`flex flex-col gap-1 h-auto py-2 ${operation === "subtract" ? "bg-primary text-white" : ""}`}
onClick={() => setOperation("subtract")}
>
<Minus className="h-4 w-4" />
<span className="text-xs"></span>
</Button>
<Button
type="button"
variant={operation === "set" ? "default" : "outline"}
className={`flex flex-col gap-1 h-auto py-2 ${operation === "set" ? "bg-primary text-white" : ""}`}
onClick={() => setOperation("set")}
>
<Equal className="h-4 w-4" />
<span className="text-xs"></span>
</Button>
</div>
</div>
{/* 調整數量 */}
<div className="space-y-2">
<Label htmlFor="quantity">調</Label>
<Input
id="quantity"
type="number"
step="0.01"
value={data.quantity === 0 ? "" : data.quantity}
onChange={e => setData("quantity", Number(e.target.value))}
placeholder="請輸入數量"
className={errors.quantity ? "border-red-500" : ""}
/>
{errors.quantity && <p className="text-xs text-red-500">{errors.quantity}</p>}
</div>
{/* 預計剩餘庫存 */}
<div className="flex items-center justify-between p-3 bg-primary/5 rounded-lg border border-primary/20">
<span className="text-gray-600">調</span>
<span className="font-bold text-lg text-primary">{getResultQuantity()}</span>
</div>
{/* 調整原因 */}
<div className="space-y-2">
<Label htmlFor="reason">調</Label>
<Select
value={data.reason}
onValueChange={val => setData("reason", val)}
>
<SelectTrigger>
<SelectValue placeholder="選擇原因" />
</SelectTrigger>
<SelectContent>
<SelectItem value="盤點調整">調</SelectItem>
<SelectItem value="損耗"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 備註 */}
<div className="space-y-2">
<Label htmlFor="notes"></Label>
<Textarea
id="notes"
value={data.notes}
onChange={e => setData("notes", e.target.value)}
placeholder="輸入調整細節..."
rows={2}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={processing}
>
</Button>
<Button
type="submit"
disabled={processing || data.quantity <= 0 && operation !== "set"}
>
調
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
// Helper: 假如沒有 route 函式 (Ziggy),則需要手動處理 URL
function route(name: string, params: any) {
if (name === "warehouses.inventory.update") {
return `/warehouses/${params.warehouse}/inventory/${params.product}`;
}
return "";
}