Files
star-erp/resources/js/Components/Warehouse/TransferOrderDialog.tsx
2025-12-30 15:03:19 +08:00

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