377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
|
|
|
|||
|
|
/**
|
|||
|
|
* 撥補單對話框元件
|
|||
|
|
* 重構後:加入驗證邏輯模組化
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from "react";
|
|||
|
|
import { getCurrentDateTime, generateOrderNumber } from "@/utils/format";
|
|||
|
|
import axios from "axios";
|
|||
|
|
import {
|
|||
|
|
Dialog,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogDescription,
|
|||
|
|
DialogFooter,
|
|||
|
|
DialogHeader,
|
|||
|
|
DialogTitle,
|
|||
|
|
} from "@/Components/ui/dialog";
|
|||
|
|
import { Label } from "@/Components/ui/label";
|
|||
|
|
import { Input } from "@/Components/ui/input";
|
|||
|
|
import { Button } from "@/Components/ui/button";
|
|||
|
|
import {
|
|||
|
|
Select,
|
|||
|
|
SelectContent,
|
|||
|
|
SelectItem,
|
|||
|
|
SelectTrigger,
|
|||
|
|
SelectValue,
|
|||
|
|
} from "@/Components/ui/select";
|
|||
|
|
import { Textarea } from "@/Components/ui/textarea";
|
|||
|
|
import { toast } from "sonner";
|
|||
|
|
import { Warehouse, TransferOrder, TransferOrderStatus } from "@/types/warehouse";
|
|||
|
|
import { validateTransferOrder, validateTransferQuantity } from "@/utils/validation";
|
|||
|
|
|
|||
|
|
export type { TransferOrder };
|
|||
|
|
|
|||
|
|
interface TransferOrderDialogProps {
|
|||
|
|
open: boolean;
|
|||
|
|
onOpenChange: (open: boolean) => void;
|
|||
|
|
order: TransferOrder | null;
|
|||
|
|
warehouses: Warehouse[];
|
|||
|
|
// inventories: WarehouseInventory[]; // Removed as we fetch from API
|
|||
|
|
onSave: (order: Omit<TransferOrder, "id" | "createdAt" | "orderNumber">) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface AvailableProduct {
|
|||
|
|
productId: string;
|
|||
|
|
productName: string;
|
|||
|
|
batchNumber: string;
|
|||
|
|
availableQty: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function TransferOrderDialog({
|
|||
|
|
open,
|
|||
|
|
onOpenChange,
|
|||
|
|
order,
|
|||
|
|
warehouses,
|
|||
|
|
// inventories,
|
|||
|
|
onSave,
|
|||
|
|
}: TransferOrderDialogProps) {
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
sourceWarehouseId: "",
|
|||
|
|
targetWarehouseId: "",
|
|||
|
|
productId: "",
|
|||
|
|
productName: "",
|
|||
|
|
batchNumber: "",
|
|||
|
|
quantity: 0,
|
|||
|
|
transferDate: getCurrentDateTime(),
|
|||
|
|
status: "待處理" as TransferOrderStatus,
|
|||
|
|
notes: "",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const [availableProducts, setAvailableProducts] = useState<AvailableProduct[]>([]);
|
|||
|
|
|
|||
|
|
// 當對話框開啟或訂單變更時,重置表單
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (order) {
|
|||
|
|
setFormData({
|
|||
|
|
sourceWarehouseId: order.sourceWarehouseId,
|
|||
|
|
targetWarehouseId: order.targetWarehouseId,
|
|||
|
|
productId: order.productId,
|
|||
|
|
productName: order.productName,
|
|||
|
|
batchNumber: order.batchNumber,
|
|||
|
|
quantity: order.quantity,
|
|||
|
|
transferDate: order.transferDate,
|
|||
|
|
status: order.status,
|
|||
|
|
notes: order.notes || "",
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
setFormData({
|
|||
|
|
sourceWarehouseId: "",
|
|||
|
|
targetWarehouseId: "",
|
|||
|
|
productId: "",
|
|||
|
|
productName: "",
|
|||
|
|
batchNumber: "",
|
|||
|
|
quantity: 0,
|
|||
|
|
transferDate: getCurrentDateTime(),
|
|||
|
|
status: "待處理",
|
|||
|
|
notes: "",
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, [order, open]);
|
|||
|
|
|
|||
|
|
// 當來源倉庫變更時,從 API 更新可用商品列表
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (formData.sourceWarehouseId) {
|
|||
|
|
axios.get(route('api.warehouses.inventories', formData.sourceWarehouseId))
|
|||
|
|
.then(response => {
|
|||
|
|
setAvailableProducts(response.data);
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error("Failed to fetch inventories:", error);
|
|||
|
|
toast.error("無法取得倉庫庫存資訊");
|
|||
|
|
setAvailableProducts([]);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
setAvailableProducts([]);
|
|||
|
|
}
|
|||
|
|
}, [formData.sourceWarehouseId]);
|
|||
|
|
|
|||
|
|
const handleSubmit = () => {
|
|||
|
|
// 基本驗證
|
|||
|
|
const validation = validateTransferOrder(formData);
|
|||
|
|
if (!validation.isValid) {
|
|||
|
|
toast.error(validation.error);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查可用數量
|
|||
|
|
const selectedProduct = availableProducts.find(
|
|||
|
|
(p) => p.productId === formData.productId && p.batchNumber === formData.batchNumber
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (selectedProduct) {
|
|||
|
|
const quantityValidation = validateTransferQuantity(
|
|||
|
|
formData.quantity,
|
|||
|
|
selectedProduct.availableQty
|
|||
|
|
);
|
|||
|
|
if (!quantityValidation.isValid) {
|
|||
|
|
toast.error(quantityValidation.error);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sourceWarehouse = warehouses.find((w) => w.id === formData.sourceWarehouseId);
|
|||
|
|
const targetWarehouse = warehouses.find((w) => w.id === formData.targetWarehouseId);
|
|||
|
|
|
|||
|
|
onSave({
|
|||
|
|
sourceWarehouseId: formData.sourceWarehouseId,
|
|||
|
|
sourceWarehouseName: sourceWarehouse?.name || "",
|
|||
|
|
targetWarehouseId: formData.targetWarehouseId,
|
|||
|
|
targetWarehouseName: targetWarehouse?.name || "",
|
|||
|
|
productId: formData.productId,
|
|||
|
|
productName: formData.productName,
|
|||
|
|
batchNumber: formData.batchNumber,
|
|||
|
|
quantity: formData.quantity,
|
|||
|
|
transferDate: formData.transferDate,
|
|||
|
|
status: formData.status,
|
|||
|
|
notes: formData.notes,
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleProductChange = (productKey: string) => {
|
|||
|
|
const [productId, batchNumber] = productKey.split("|||");
|
|||
|
|
const product = availableProducts.find(
|
|||
|
|
(p) => p.productId === productId && p.batchNumber === batchNumber
|
|||
|
|
);
|
|||
|
|
if (product) {
|
|||
|
|
setFormData({
|
|||
|
|
...formData,
|
|||
|
|
productId: product.productId,
|
|||
|
|
productName: product.productName,
|
|||
|
|
batchNumber: product.batchNumber,
|
|||
|
|
quantity: 0,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const selectedProduct = availableProducts.find(
|
|||
|
|
(p) => p.productId === formData.productId && p.batchNumber === formData.batchNumber
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|||
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
<DialogHeader>
|
|||
|
|
<DialogTitle>{order ? "編輯撥補單" : "新增撥補單"}</DialogTitle>
|
|||
|
|
<DialogDescription>填寫撥補單資訊</DialogDescription>
|
|||
|
|
</DialogHeader>
|
|||
|
|
|
|||
|
|
<div className="space-y-4 py-4">
|
|||
|
|
{/* 來源倉庫和目標倉庫 */}
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="sourceWarehouse">
|
|||
|
|
來源倉庫 <span className="text-red-500">*</span>
|
|||
|
|
</Label>
|
|||
|
|
<Select
|
|||
|
|
value={formData.sourceWarehouseId}
|
|||
|
|
onValueChange={(value) =>
|
|||
|
|
setFormData({
|
|||
|
|
...formData,
|
|||
|
|
sourceWarehouseId: value,
|
|||
|
|
productId: "",
|
|||
|
|
productName: "",
|
|||
|
|
batchNumber: "",
|
|||
|
|
quantity: 0,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
disabled={!!order}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger id="sourceWarehouse">
|
|||
|
|
<SelectValue placeholder="選擇來源倉庫" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{warehouses.map((warehouse) => (
|
|||
|
|
<SelectItem key={warehouse.id} value={warehouse.id}>
|
|||
|
|
{warehouse.name}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="targetWarehouse">
|
|||
|
|
目標倉庫 <span className="text-red-500">*</span>
|
|||
|
|
</Label>
|
|||
|
|
<Select
|
|||
|
|
value={formData.targetWarehouseId}
|
|||
|
|
onValueChange={(value) =>
|
|||
|
|
setFormData({ ...formData, targetWarehouseId: value })
|
|||
|
|
}
|
|||
|
|
disabled={!!order}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger id="targetWarehouse">
|
|||
|
|
<SelectValue placeholder="選擇目標倉庫" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{warehouses
|
|||
|
|
.filter((w) => w.id !== formData.sourceWarehouseId)
|
|||
|
|
.map((warehouse) => (
|
|||
|
|
<SelectItem key={warehouse.id} value={warehouse.id}>
|
|||
|
|
{warehouse.name}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 商品選擇 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="product">
|
|||
|
|
撥補商品 <span className="text-red-500">*</span>
|
|||
|
|
</Label>
|
|||
|
|
<Select
|
|||
|
|
value={
|
|||
|
|
formData.productId && formData.batchNumber
|
|||
|
|
? `${formData.productId}||| ${formData.batchNumber} `
|
|||
|
|
: ""
|
|||
|
|
}
|
|||
|
|
onValueChange={handleProductChange}
|
|||
|
|
disabled={!formData.sourceWarehouseId || !!order}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger id="product">
|
|||
|
|
<SelectValue placeholder="選擇商品與批號" />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{availableProducts.length === 0 ? (
|
|||
|
|
<div className="p-2 text-sm text-gray-500 text-center">
|
|||
|
|
{formData.sourceWarehouseId ? "該倉庫無可用庫存" : "請先選擇來源倉庫"}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
availableProducts.map((product) => (
|
|||
|
|
<SelectItem
|
|||
|
|
key={`${product.productId}||| ${product.batchNumber} `}
|
|||
|
|
value={`${product.productId}||| ${product.batchNumber} `}
|
|||
|
|
>
|
|||
|
|
{product.productName} - 批號: {product.batchNumber} (庫存:{" "}
|
|||
|
|
{product.availableQty})
|
|||
|
|
</SelectItem>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 數量和日期 */}
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="quantity">
|
|||
|
|
撥補數量 <span className="text-red-500">*</span>
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="quantity"
|
|||
|
|
type="number"
|
|||
|
|
min="0"
|
|||
|
|
max={selectedProduct?.availableQty || 0}
|
|||
|
|
value={formData.quantity}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setFormData({ ...formData, quantity: Number(e.target.value) })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
{selectedProduct && (
|
|||
|
|
<p className="text-sm text-gray-500">
|
|||
|
|
可用庫存: {selectedProduct.availableQty}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="transferDate">
|
|||
|
|
撥補日期 <span className="text-red-500">*</span>
|
|||
|
|
</Label>
|
|||
|
|
<Input
|
|||
|
|
id="transferDate"
|
|||
|
|
type="datetime-local"
|
|||
|
|
value={formData.transferDate}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setFormData({ ...formData, transferDate: e.target.value })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 狀態(僅編輯時顯示) */}
|
|||
|
|
{order && (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="status">狀態</Label>
|
|||
|
|
<Select
|
|||
|
|
value={formData.status}
|
|||
|
|
onValueChange={(value) =>
|
|||
|
|
setFormData({ ...formData, status: value as TransferOrderStatus })
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<SelectTrigger id="status">
|
|||
|
|
<SelectValue />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
<SelectItem value="待處理">待處理</SelectItem>
|
|||
|
|
<SelectItem value="處理中">處理中</SelectItem>
|
|||
|
|
<SelectItem value="已完成">已完成</SelectItem>
|
|||
|
|
<SelectItem value="已取消">已取消</SelectItem>
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 備註 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label htmlFor="notes">備註</Label>
|
|||
|
|
<Textarea
|
|||
|
|
id="notes"
|
|||
|
|
placeholder="請輸入備註(選填)"
|
|||
|
|
value={formData.notes}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
|||
|
|
rows={3}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<DialogFooter>
|
|||
|
|
<Button
|
|||
|
|
variant="outline"
|
|||
|
|
onClick={() => onOpenChange(false)}
|
|||
|
|
className="button-outlined-primary"
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</Button>
|
|||
|
|
<Button onClick={handleSubmit} className="button-filled-primary">
|
|||
|
|
{order ? "更新" : "新增"}
|
|||
|
|
</Button>
|
|||
|
|
</DialogFooter>
|
|||
|
|
</DialogContent>
|
|||
|
|
</Dialog>
|
|||
|
|
);
|
|||
|
|
}
|