/** * 建立生產工單頁面 * 動態 BOM 表單:選擇倉庫 → 選擇原物料 → 選擇批號 → 輸入用量 */ import { useState, useEffect } from "react"; import { Factory, Plus, Trash2, ArrowLeft, Save, Calendar, AlertCircle } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router, useForm } 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 { Link } from "@inertiajs/react"; 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 { // 後端必填 inventory_id: string; // 所選庫存記錄 ID(特定批號) quantity_used: string; // 轉換後的最終數量(基本單位) unit_id: string; // 單位 ID(通常為基本單位 ID) // UI 狀態 ui_warehouse_id: string; // 來源倉庫 ui_product_id: string; // 批號列表篩選 ui_input_quantity: string; // 使用者輸入數量 ui_selected_unit: 'base' | 'large'; // 使用者選擇單位 // UI 輔助 / 快取 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 Props { products: Product[]; warehouses: Warehouse[]; units: Unit[]; } export default function ProductionCreate({ products, warehouses }: Props) { const [selectedWarehouse, setSelectedWarehouse] = useState(""); // 產出倉庫 // 快取對照表:warehouse_id -> inventories const [inventoryMap, setInventoryMap] = useState>({}); const [loadingWarehouses, setLoadingWareStates] = useState>({}); const [bomItems, setBomItems] = useState([]); const { data, setData, processing, errors } = useForm({ product_id: "", warehouse_id: "", output_quantity: "", output_batch_number: "", output_box_count: "", production_date: new Date().toISOString().split('T')[0], expiry_date: "", remark: "", items: [] as { inventory_id: number; quantity_used: number; unit_id: number | null }[], }); // 獲取倉庫資料的輔助函式 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 })); } }; // 同步 warehouse_id 到 form data (Output) 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"; // 清除 cache 資訊 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. 當選擇批號 (Inventory ID) 變更時 -> 載入該批號資訊 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. 計算最終數量 (Base Quantity) // 當 輸入數量 或 選擇單位 變更時 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); // 注意:後端需要的是 Base Unit ID? 這裡我們都送 Base Unit ID,因為 quantity_used 是 Base Unit // 但為了保留 User 的選擇,我們可能可以在 remark 註記? 目前先從簡 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]); // 自動產生成品批號(當選擇商品或日期變動時) useEffect(() => { if (!data.product_id) return; const product = products.find(p => String(p.id) === data.product_id); if (!product) return; const datePart = data.production_date; // YYYY-MM-DD const dateFormatted = datePart.replace(/-/g, ''); const originCountry = 'TW'; // 呼叫 API 取得下一組流水號 // 複用庫存批號 API,但這裡可能沒有選 warehouse,所以用第一個預設 const warehouseId = selectedWarehouse || (warehouses.length > 0 ? String(warehouses[0].id) : '1'); fetch(`/api/warehouses/${warehouseId}/inventory/batches/${product.id}?originCountry=${originCountry}&arrivalDate=${datePart}`) .then(res => res.json()) .then(result => { const seq = result.nextSequence || '01'; const suggested = `${product.code}-${originCountry}-${dateFormatted}-${seq}`; setData('output_batch_number', suggested); }) .catch(() => { // Fallback:若 API 失敗,使用預設 01 const suggested = `${product.code}-${originCountry}-${dateFormatted}-01`; setData('output_batch_number', suggested); }); }, [data.product_id, data.production_date]); // 提交表單 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; } } // 轉換 BOM items 格式 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.post 提交完整資料 router.post(route('production-orders.store'), { ...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="選擇商品後自動產生" 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}

}