/** * 編輯生產工單頁面 * 僅限草稿狀態可編輯 */ import { useState, useEffect } from "react"; import { Factory, Plus, Trash2, ArrowLeft, Save, Calendar } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router, useForm, Link } from "@inertiajs/react"; import toast, { Toaster } from 'react-hot-toast'; import { getBreadcrumbs } from "@/utils/breadcrumb"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; import { Textarea } from "@/Components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table"; interface Product { id: number; name: string; code: string; base_unit?: { id: number; name: string } | null; } interface Warehouse { id: number; name: string; } interface Unit { id: number; name: string; } interface InventoryOption { id: number; product_id: number; product_name: string; product_code: string; batch_number: string; box_number: string | null; quantity: number; arrival_date: string | null; expiry_date: string | null; unit_name: string | null; base_unit_id?: number; base_unit_name?: string; large_unit_id?: number; large_unit_name?: string; conversion_rate?: number; } interface BomItem { // Backend required inventory_id: string; quantity_used: string; unit_id: string; // UI State ui_warehouse_id: string; // Source Warehouse ui_product_id: string; ui_input_quantity: string; ui_selected_unit: 'base' | 'large'; // UI Helpers / Cache ui_product_name?: string; ui_batch_number?: string; ui_available_qty?: number; ui_expiry_date?: string; ui_conversion_rate?: number; ui_base_unit_name?: string; ui_large_unit_name?: string; ui_base_unit_id?: number; ui_large_unit_id?: number; } interface ProductionOrderItem { id: number; production_order_id: number; inventory_id: number; quantity_used: number; unit_id: number | null; inventory?: { product_id: number; product?: { name: string; code: string; base_unit?: { name: string }; }; batch_number: string; quantity: number; expiry_date?: string; warehouse_id?: number; }; unit?: { name: string; }; } interface ProductionOrder { id: number; code: string; product_id: number; warehouse_id: number | null; output_quantity: number; output_batch_number: string; output_box_count: string | null; production_date: string; expiry_date: string | null; remark: string | null; status: string; items: ProductionOrderItem[]; product?: Product; warehouse?: Warehouse; } interface Props { productionOrder: ProductionOrder; products: Product[]; warehouses: Warehouse[]; units: Unit[]; } export default function ProductionEdit({ productionOrder, products, warehouses }: Props) { // 日期格式轉換輔助函數 const formatDate = (dateValue: string | null | undefined): string => { if (!dateValue) return ''; // 處理可能的 ISO 格式或 YYYY-MM-DD 格式 const date = new Date(dateValue); if (isNaN(date.getTime())) return dateValue; return date.toISOString().split('T')[0]; }; const [selectedWarehouse, setSelectedWarehouse] = useState( productionOrder.warehouse_id ? String(productionOrder.warehouse_id) : "" ); // Output Warehouse // Cache map: warehouse_id -> inventories const [inventoryMap, setInventoryMap] = useState>({}); const [loadingWarehouses, setLoadingWareStates] = useState>({}); // Helper to fetch warehouse data const fetchWarehouseInventory = async (warehouseId: string) => { if (!warehouseId || inventoryMap[warehouseId] || loadingWarehouses[warehouseId]) return; setLoadingWareStates(prev => ({ ...prev, [warehouseId]: true })); try { const res = await fetch(route('api.production.warehouses.inventories', warehouseId)); const data = await res.json(); setInventoryMap(prev => ({ ...prev, [warehouseId]: data })); } catch (e) { console.error(e); } finally { setLoadingWareStates(prev => ({ ...prev, [warehouseId]: false })); } }; // 初始化 BOM items const initialBomItems: BomItem[] = productionOrder.items.map(item => ({ inventory_id: String(item.inventory_id), quantity_used: String(item.quantity_used), unit_id: item.unit_id ? String(item.unit_id) : "", // UI Initial State (復原) ui_warehouse_id: item.inventory?.warehouse_id ? String(item.inventory.warehouse_id) : "", ui_product_id: item.inventory ? String(item.inventory.product_id) : "", ui_input_quantity: String(item.quantity_used), // 假設已存的資料是基本單位 ui_selected_unit: 'base', // UI Helpers ui_product_name: item.inventory?.product?.name, ui_batch_number: item.inventory?.batch_number, ui_available_qty: item.inventory?.quantity, ui_expiry_date: item.inventory?.expiry_date, })); const [bomItems, setBomItems] = useState(initialBomItems); const { data, setData, processing, errors } = useForm({ product_id: String(productionOrder.product_id), warehouse_id: productionOrder.warehouse_id ? String(productionOrder.warehouse_id) : "", output_quantity: productionOrder.output_quantity ? String(productionOrder.output_quantity) : "", output_batch_number: productionOrder.output_batch_number || "", output_box_count: productionOrder.output_box_count || "", production_date: formatDate(productionOrder.production_date) || new Date().toISOString().split('T')[0], expiry_date: formatDate(productionOrder.expiry_date), remark: productionOrder.remark || "", items: [] as { inventory_id: number; quantity_used: number; unit_id: number | null }[], }); // 初始化載入既有 BOM 的來源倉庫資料 useEffect(() => { initialBomItems.forEach(item => { if (item.ui_warehouse_id) { fetchWarehouseInventory(item.ui_warehouse_id); } }); }, []); // 當 inventoryOptions (Map) 載入後,更新現有 BOM items 的詳細資訊 (如單位、轉換率) // 監聽 inventoryMap 變更 useEffect(() => { setBomItems(prevItems => prevItems.map(item => { if (item.ui_warehouse_id && inventoryMap[item.ui_warehouse_id] && item.inventory_id && !item.ui_conversion_rate) { const inv = inventoryMap[item.ui_warehouse_id].find(i => String(i.id) === item.inventory_id); if (inv) { return { ...item, ui_product_id: String(inv.product_id), ui_product_name: inv.product_name, ui_batch_number: inv.batch_number, ui_available_qty: inv.quantity, ui_expiry_date: inv.expiry_date || '', ui_base_unit_name: inv.base_unit_name || inv.unit_name || '', ui_large_unit_name: inv.large_unit_name || '', ui_base_unit_id: inv.base_unit_id, ui_large_unit_id: inv.large_unit_id, ui_conversion_rate: inv.conversion_rate || 1, }; } } return item; })); }, [inventoryMap]); // 同步 warehouse_id 到 form data useEffect(() => { setData('warehouse_id', selectedWarehouse); }, [selectedWarehouse]); // 新增 BOM 項目 const addBomItem = () => { setBomItems([...bomItems, { inventory_id: "", quantity_used: "", unit_id: "", ui_warehouse_id: "", ui_product_id: "", ui_input_quantity: "", ui_selected_unit: 'base', }]); }; // 移除 BOM 項目 const removeBomItem = (index: number) => { setBomItems(bomItems.filter((_, i) => i !== index)); }; // 更新 BOM 項目邏輯 const updateBomItem = (index: number, field: keyof BomItem, value: any) => { const updated = [...bomItems]; const item = { ...updated[index], [field]: value }; // 0. 當選擇來源倉庫變更時 if (field === 'ui_warehouse_id') { item.ui_product_id = ""; item.inventory_id = ""; item.quantity_used = ""; item.unit_id = ""; item.ui_input_quantity = ""; item.ui_selected_unit = "base"; delete item.ui_product_name; delete item.ui_batch_number; delete item.ui_available_qty; delete item.ui_expiry_date; delete item.ui_conversion_rate; delete item.ui_base_unit_name; delete item.ui_large_unit_name; delete item.ui_base_unit_id; delete item.ui_large_unit_id; if (value) { fetchWarehouseInventory(value); } } // 1. 當選擇商品變更時 -> 清空批號與相關資訊 if (field === 'ui_product_id') { item.inventory_id = ""; item.quantity_used = ""; item.unit_id = ""; item.ui_input_quantity = ""; item.ui_selected_unit = "base"; delete item.ui_product_name; delete item.ui_batch_number; delete item.ui_available_qty; delete item.ui_expiry_date; delete item.ui_conversion_rate; delete item.ui_base_unit_name; delete item.ui_large_unit_name; delete item.ui_base_unit_id; delete item.ui_large_unit_id; } // 2. 當選擇批號變更時 if (field === 'inventory_id' && value) { const currentOptions = inventoryMap[item.ui_warehouse_id] || []; const inv = currentOptions.find(i => String(i.id) === value); if (inv) { item.ui_product_id = String(inv.product_id); item.ui_product_name = inv.product_name; item.ui_batch_number = inv.batch_number; item.ui_available_qty = inv.quantity; item.ui_expiry_date = inv.expiry_date || ''; item.ui_base_unit_name = inv.base_unit_name || inv.unit_name || ''; item.ui_large_unit_name = inv.large_unit_name || ''; item.ui_base_unit_id = inv.base_unit_id; item.ui_large_unit_id = inv.large_unit_id; item.ui_conversion_rate = inv.conversion_rate || 1; item.ui_selected_unit = 'base'; item.unit_id = String(inv.base_unit_id || ''); } } // 3. 計算最終數量 if (field === 'ui_input_quantity' || field === 'ui_selected_unit' || field === 'inventory_id') { const inputQty = parseFloat(item.ui_input_quantity || '0'); const rate = item.ui_conversion_rate || 1; if (item.ui_selected_unit === 'large') { item.quantity_used = String(inputQty * rate); item.unit_id = String(item.ui_base_unit_id || ''); } else { item.quantity_used = String(inputQty); item.unit_id = String(item.ui_base_unit_id || ''); } } updated[index] = item; setBomItems(updated); }; // 同步 BOM items 到表單 data useEffect(() => { setData('items', bomItems.map(item => ({ inventory_id: Number(item.inventory_id), quantity_used: Number(item.quantity_used), unit_id: item.unit_id ? Number(item.unit_id) : null }))); }, [bomItems]); // 提交表單(完成模式) // 提交表單(完成模式) // 提交表單(完成模式) const submit = (status: 'draft' | 'completed') => { // 驗證(簡單前端驗證) if (status === 'completed') { const missingFields = []; if (!data.product_id) missingFields.push('成品商品'); if (!data.output_quantity) missingFields.push('生產數量'); if (!data.output_batch_number) missingFields.push('成品批號'); if (!data.production_date) missingFields.push('生產日期'); if (!selectedWarehouse) missingFields.push('入庫倉庫'); if (bomItems.length === 0) missingFields.push('原物料明細'); if (missingFields.length > 0) { toast.error(
請填寫必要欄位 缺漏:{missingFields.join('、')}
); return; } } const formattedItems = bomItems .filter(item => status === 'draft' || (item.inventory_id && item.quantity_used)) .map(item => ({ inventory_id: item.inventory_id ? parseInt(item.inventory_id) : null, quantity_used: item.quantity_used ? parseFloat(item.quantity_used) : 0, unit_id: item.unit_id ? parseInt(item.unit_id) : null, })); router.put(route('production-orders.update', productionOrder.id), { ...data, items: formattedItems, status: status, }, { onError: (errors) => { const errorCount = Object.keys(errors).length; toast.error(
更新失敗,請檢查表單 共有 {errorCount} 個欄位有誤,請修正後再試
); } }); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); submit('completed'); }; return (

編輯生產工單

編輯工單內容與排程

{/* 成品資訊 */}

成品資訊

setData('product_id', v)} options={products.map(p => ({ label: `${p.name} (${p.code})`, value: String(p.id), }))} placeholder="選擇成品" className="w-full h-9" /> {errors.product_id &&

{errors.product_id}

}
setData('output_quantity', e.target.value)} placeholder="例如: 50" className="h-9" /> {errors.output_quantity &&

{errors.output_quantity}

}
setData('output_batch_number', e.target.value)} placeholder="例如: AB-TW-20260122-01" className="h-9 font-mono" /> {errors.output_batch_number &&

{errors.output_batch_number}

}
setData('output_box_count', e.target.value)} placeholder="例如: 10" className="h-9" />
setData('production_date', e.target.value)} className="h-9 pl-9" />
{errors.production_date &&

{errors.production_date}

}
setData('expiry_date', e.target.value)} className="h-9 pl-9" />
({ label: w.name, value: String(w.id), }))} placeholder="選擇倉庫" className="w-full h-9" /> {errors.warehouse_id &&

{errors.warehouse_id}

}