diff --git a/app/Modules/Inventory/Models/InventoryAdjustDoc.php b/app/Modules/Inventory/Models/InventoryAdjustDoc.php index 4b307fc..87ebc5c 100644 --- a/app/Modules/Inventory/Models/InventoryAdjustDoc.php +++ b/app/Modules/Inventory/Models/InventoryAdjustDoc.php @@ -36,7 +36,7 @@ class InventoryAdjustDoc extends Model static::creating(function ($model) { if (empty($model->doc_no)) { $today = date('Ymd'); - $prefix = 'ADJ' . $today; + $prefix = 'ADJ-' . $today . '-'; $lastDoc = static::where('doc_no', 'like', $prefix . '%') ->orderBy('doc_no', 'desc') diff --git a/app/Modules/Inventory/Models/InventoryCountDoc.php b/app/Modules/Inventory/Models/InventoryCountDoc.php index 3ee6dec..c41973f 100644 --- a/app/Modules/Inventory/Models/InventoryCountDoc.php +++ b/app/Modules/Inventory/Models/InventoryCountDoc.php @@ -36,7 +36,7 @@ class InventoryCountDoc extends Model static::creating(function ($model) { if (empty($model->doc_no)) { $today = date('Ymd'); - $prefix = 'CNT' . $today; + $prefix = 'CNT-' . $today . '-'; // 查詢當天編號最大的單據 $lastDoc = static::where('doc_no', 'like', $prefix . '%') diff --git a/app/Modules/Inventory/Models/InventoryTransferOrder.php b/app/Modules/Inventory/Models/InventoryTransferOrder.php index d9cc54a..f6cb0c7 100644 --- a/app/Modules/Inventory/Models/InventoryTransferOrder.php +++ b/app/Modules/Inventory/Models/InventoryTransferOrder.php @@ -35,7 +35,7 @@ class InventoryTransferOrder extends Model static::creating(function ($model) { if (empty($model->doc_no)) { $today = date('Ymd'); - $prefix = 'TRF' . $today; + $prefix = 'TRF-' . $today . '-'; $lastDoc = static::where('doc_no', 'like', $prefix . '%') ->orderBy('doc_no', 'desc') diff --git a/app/Modules/Inventory/Services/GoodsReceiptService.php b/app/Modules/Inventory/Services/GoodsReceiptService.php index 139527d..fb2dd9c 100644 --- a/app/Modules/Inventory/Services/GoodsReceiptService.php +++ b/app/Modules/Inventory/Services/GoodsReceiptService.php @@ -90,8 +90,8 @@ class GoodsReceiptService private function generateCode(string $date) { - // Format: GR + YYYYMMDD + NNN - $prefix = 'GR' . date('Ymd', strtotime($date)); + // Format: GR-YYYYMMDD-NN + $prefix = 'GR-' . date('Ymd', strtotime($date)) . '-'; $last = GoodsReceipt::where('code', 'like', $prefix . '%') ->orderBy('id', 'desc') @@ -99,11 +99,11 @@ class GoodsReceiptService ->first(); if ($last) { - $seq = intval(substr($last->code, -3)) + 1; + $seq = intval(substr($last->code, -2)) + 1; } else { $seq = 1; } - return $prefix . str_pad($seq, 3, '0', STR_PAD_LEFT); + return $prefix . str_pad($seq, 2, '0', STR_PAD_LEFT); } } diff --git a/app/Modules/Procurement/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php index 68909f6..e82d538 100644 --- a/app/Modules/Procurement/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -187,20 +187,20 @@ class PurchaseOrderController extends Controller try { DB::beginTransaction(); - // 生成單號:POYYYYMMDD001 + // 生成單號:PO-YYYYMMDD-01 $today = now()->format('Ymd'); - $prefix = 'PO' . $today; + $prefix = 'PO-' . $today . '-'; $lastOrder = PurchaseOrder::where('code', 'like', $prefix . '%') ->lockForUpdate() // 鎖定以避免並發衝突 ->orderBy('code', 'desc') ->first(); if ($lastOrder) { - // 取得最後 3 碼序號並加 1 - $lastSequence = intval(substr($lastOrder->code, -3)); - $sequence = str_pad($lastSequence + 1, 3, '0', STR_PAD_LEFT); + // 取得最後 2 碼序號並加 1 + $lastSequence = intval(substr($lastOrder->code, -2)); + $sequence = str_pad($lastSequence + 1, 2, '0', STR_PAD_LEFT); } else { - $sequence = '001'; + $sequence = '01'; } $code = $prefix . $sequence; diff --git a/app/Modules/Production/Controllers/ProductionOrderController.php b/app/Modules/Production/Controllers/ProductionOrderController.php index 0ea7135..41af550 100644 --- a/app/Modules/Production/Controllers/ProductionOrderController.php +++ b/app/Modules/Production/Controllers/ProductionOrderController.php @@ -269,6 +269,33 @@ class ProductionOrderController extends Controller return response()->json($data); } + /** + * 取得商品在各倉庫的庫存分佈 + */ + public function getProductWarehouses($productId) + { + $inventories = \App\Modules\Inventory\Models\Inventory::with(['warehouse', 'product.baseUnit']) + ->where('product_id', $productId) + ->where('quantity', '>', 0) + ->get(); + + $data = $inventories->map(function ($inv) { + return [ + 'id' => $inv->id, // Inventory ID + 'warehouse_id' => $inv->warehouse_id, + 'warehouse_name' => $inv->warehouse->name ?? '未知倉庫', + 'batch_number' => $inv->batch_number, + 'quantity' => $inv->quantity, + 'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null, + 'unit_name' => $inv->product->baseUnit->name ?? '', + 'base_unit_id' => $inv->product->base_unit_id ?? null, + 'conversion_rate' => $inv->product->conversion_rate ?? 1, + ]; + }); + + return response()->json($data); + } + /** * 編輯生產單 */ diff --git a/app/Modules/Production/Routes/web.php b/app/Modules/Production/Routes/web.php index d7c47d5..b0ceb15 100644 --- a/app/Modules/Production/Routes/web.php +++ b/app/Modules/Production/Routes/web.php @@ -30,6 +30,10 @@ Route::middleware('auth')->group(function () { ->middleware('permission:production_orders.create') ->name('api.production.warehouses.inventories'); + Route::get('/api/production/products/{product}/inventories', [ProductionOrderController::class, 'getProductWarehouses']) + ->middleware('permission:production_orders.create') + ->name('api.production.products.inventories'); + Route::get('/api/production/recipes/latest-by-product/{productId}', [RecipeController::class, 'getLatestByProduct']) ->name('api.production.recipes.latest-by-product'); diff --git a/database/migrations/tenant/2026_02_04_111029_remove_purchase_orders_publish_permission.php b/database/migrations/tenant/2026_02_04_111029_remove_purchase_orders_publish_permission.php new file mode 100644 index 0000000..b405b56 --- /dev/null +++ b/database/migrations/tenant/2026_02_04_111029_remove_purchase_orders_publish_permission.php @@ -0,0 +1,37 @@ +delete(); + + // 重置權限快取 + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // 恢復權限(如果需要回滾) + \Spatie\Permission\Models\Permission::firstOrCreate(['name' => 'purchase_orders.publish']); + + // 重新分配給 admin (簡單恢復,可能無法完全還原所有角色配置) + $admin = \Spatie\Permission\Models\Role::where('name', 'admin')->first(); + if ($admin) { + $admin->givePermissionTo('purchase_orders.publish'); + } + + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 732a078..a05f462 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -30,7 +30,7 @@ class PermissionSeeder extends Seeder 'purchase_orders.create', 'purchase_orders.edit', 'purchase_orders.delete', - 'purchase_orders.publish', + // 庫存管理 'inventory.view', @@ -132,7 +132,7 @@ class PermissionSeeder extends Seeder $admin->givePermissionTo([ 'products.view', 'products.create', 'products.edit', 'products.delete', 'purchase_orders.view', 'purchase_orders.create', 'purchase_orders.edit', - 'purchase_orders.delete', 'purchase_orders.publish', + 'purchase_orders.delete', 'inventory.view', 'inventory.view_cost', 'inventory.delete', 'inventory_count.view', 'inventory_count.create', 'inventory_count.edit', 'inventory_count.delete', 'inventory_adjust.view', 'inventory_adjust.create', 'inventory_adjust.edit', 'inventory_adjust.delete', diff --git a/resources/js/Pages/Admin/Role/PermissionSelector.tsx b/resources/js/Pages/Admin/Role/PermissionSelector.tsx index c765b3a..b1e4b5e 100644 --- a/resources/js/Pages/Admin/Role/PermissionSelector.tsx +++ b/resources/js/Pages/Admin/Role/PermissionSelector.tsx @@ -36,7 +36,7 @@ export default function PermissionSelector({ groupedPermissions, selectedPermiss 'create': '新增', 'edit': '編輯', 'delete': '刪除', - 'publish': '發布', + 'adjust': '調整', 'transfer': '調撥', 'count': '盤點', diff --git a/resources/js/Pages/Inventory/Adjust/Show.tsx b/resources/js/Pages/Inventory/Adjust/Show.tsx index 6c26ce2..cabab2d 100644 --- a/resources/js/Pages/Inventory/Adjust/Show.tsx +++ b/resources/js/Pages/Inventory/Adjust/Show.tsx @@ -542,6 +542,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
updateItem(index, 'adjust_qty', e.target.value)} diff --git a/resources/js/Pages/Inventory/Count/Show.tsx b/resources/js/Pages/Inventory/Count/Show.tsx index f2c4204..e7c3aaf 100644 --- a/resources/js/Pages/Inventory/Count/Show.tsx +++ b/resources/js/Pages/Inventory/Count/Show.tsx @@ -265,14 +265,14 @@ export default function Show({ doc }: any) {
{item.batch_number || '-'} - {item.system_qty.toFixed(0)} + {Number(item.system_qty)} {isReadOnly ? ( {item.counted_qty} ) : ( updateItem(index, 'counted_qty', e.target.value)} onWheel={(e: any) => e.target.blur()} @@ -290,7 +290,7 @@ export default function Show({ doc }: any) { : 'text-red-600' }`}> {formItem.counted_qty !== '' && formItem.counted_qty !== null - ? diff.toFixed(0) + ? Number(diff.toFixed(2)) : '-'} diff --git a/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx b/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx index cf49f88..a9f8782 100644 --- a/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx +++ b/resources/js/Pages/Inventory/GoodsReceipt/Create.tsx @@ -314,7 +314,7 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, {/* Header */}
- diff --git a/resources/js/Pages/Production/Create.tsx b/resources/js/Pages/Production/Create.tsx index 60cc11e..580b1e2 100644 --- a/resources/js/Pages/Production/Create.tsx +++ b/resources/js/Pages/Production/Create.tsx @@ -39,6 +39,8 @@ interface InventoryOption { product_id: number; product_name: string; product_code: string; + warehouse_id: number; + warehouse_name: string; batch_number: string; box_number: string | null; quantity: number; @@ -84,9 +86,9 @@ interface Props { export default function ProductionCreate({ products, warehouses }: Props) { const [selectedWarehouse, setSelectedWarehouse] = useState(""); // 產出倉庫 - // 快取對照表:warehouse_id -> inventories - const [inventoryMap, setInventoryMap] = useState>({}); - const [loadingWarehouses, setLoadingWareStates] = useState>({}); + // 快取對照表:product_id -> inventories across warehouses + const [productInventoryMap, setProductInventoryMap] = useState>({}); + const [loadingProducts, setLoadingProducts] = useState>({}); const [bomItems, setBomItems] = useState([]); @@ -107,19 +109,21 @@ export default function ProductionCreate({ products, warehouses }: Props) { items: [] as { inventory_id: number; quantity_used: number; unit_id: number | null }[], }); - // 獲取倉庫資料的輔助函式 - const fetchWarehouseInventory = async (warehouseId: string) => { - if (!warehouseId || inventoryMap[warehouseId] || loadingWarehouses[warehouseId]) return; + // 獲取特定商品在各倉庫的庫存分佈 + const fetchProductInventories = async (productId: string) => { + if (!productId) return; + // 如果已經在載入中,則跳過,但如果已經有資料,還是可以考慮重新抓取以確保最新,這裡保持現狀或增加強制更新參數 + if (loadingProducts[productId]) return; - setLoadingWareStates(prev => ({ ...prev, [warehouseId]: true })); + setLoadingProducts(prev => ({ ...prev, [productId]: true })); try { - const res = await fetch(route('api.production.warehouses.inventories', warehouseId)); + const res = await fetch(route('api.production.products.inventories', productId)); const data = await res.json(); - setInventoryMap(prev => ({ ...prev, [warehouseId]: data })); + setProductInventoryMap(prev => ({ ...prev, [productId]: data })); } catch (e) { console.error(e); } finally { - setLoadingWareStates(prev => ({ ...prev, [warehouseId]: false })); + setLoadingProducts(prev => ({ ...prev, [productId]: false })); } }; @@ -151,33 +155,9 @@ export default function ProductionCreate({ products, warehouses }: Props) { 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. 當選擇商品變更時 -> 清空批號與相關資訊 + // 1. 當選擇商品變更時 -> 載入庫存分佈並重置後續欄位 if (field === 'ui_product_id') { + item.ui_warehouse_id = ""; item.inventory_id = ""; item.quantity_used = ""; item.unit_id = ""; @@ -193,24 +173,43 @@ export default function ProductionCreate({ products, warehouses }: Props) { delete item.ui_large_unit_name; delete item.ui_base_unit_id; delete item.ui_large_unit_id; + + if (value) { + const prod = products.find(p => String(p.id) === value); + if (prod) { + item.ui_product_name = prod.name; + item.ui_base_unit_name = prod.base_unit?.name || ''; + } + fetchProductInventories(value); + } } - // 2. 當選擇批號 (Inventory ID) 變更時 -> 載入該批號資訊 + // 2. 當選擇來源倉庫變更時 -> 重置批號與相關資訊 + if (field === 'ui_warehouse_id') { + item.inventory_id = ""; + item.quantity_used = ""; + item.unit_id = ""; + item.ui_input_quantity = ""; + item.ui_selected_unit = "base"; + // 清除某些 cache + delete item.ui_batch_number; + delete item.ui_available_qty; + delete item.ui_expiry_date; + } + + // 3. 當選擇批號 (Inventory ID) 變更時 -> 載入該批號資訊 if (field === 'inventory_id' && value) { - const currentOptions = inventoryMap[item.ui_warehouse_id] || []; + const currentOptions = productInventoryMap[item.ui_product_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_warehouse_id = String(inv.warehouse_id); 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_name = inv.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; // 預設單位 @@ -219,16 +218,13 @@ export default function ProductionCreate({ products, warehouses }: Props) { } } - // 3. 計算最終數量 (Base Quantity) - // 當 輸入數量 或 選擇單位 變更時 + // 4. 計算最終數量 (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); @@ -256,17 +252,21 @@ export default function ProductionCreate({ products, warehouses }: Props) { const yieldQty = parseFloat(recipe.yield_quantity || "0") || 1; // 自動帶入配方標準產量 setData('output_quantity', String(yieldQty)); - const ratio = 1; const newBomItems: BomItem[] = recipe.items.map((item: any) => { const baseQty = parseFloat(item.quantity || "0"); - const calculatedQty = (baseQty * ratio).toFixed(4); // 保持精度 + const calculatedQty = baseQty; // 保持精度 + + // 若有配方商品,預先載入庫存分佈 + if (item.product_id) { + fetchProductInventories(String(item.product_id)); + } return { inventory_id: "", quantity_used: String(calculatedQty), unit_id: String(item.unit_id), - ui_warehouse_id: selectedWarehouse || "", // 自動帶入目前選擇的倉庫 + ui_warehouse_id: "", ui_product_id: String(item.product_id), ui_product_name: item.product_name, ui_batch_number: "", @@ -280,11 +280,6 @@ export default function ProductionCreate({ products, warehouses }: Props) { }); setBomItems(newBomItems); - // 若有選倉庫,預先載入庫存資料以供選擇 - if (selectedWarehouse) { - fetchWarehouseInventory(selectedWarehouse); - } - toast.success(`已自動載入配方: ${recipe.name}`, { description: `標準產量: ${yieldQty} 份` }); @@ -607,8 +602,8 @@ export default function ProductionCreate({ products, warehouses }: Props) { - 來源倉庫 * 商品 * + 來源倉庫 * 批號 * 數量 * 單位 @@ -617,61 +612,72 @@ export default function ProductionCreate({ products, warehouses }: Props) { {bomItems.map((item, index) => { - // 取得此列已載入的 Inventory Options - const currentOptions = inventoryMap[item.ui_warehouse_id] || []; + // 1. 商品選項 + const productOptions = products.map(p => ({ + label: `${p.name} (${p.code})`, + value: String(p.id) + })); - // 過濾商品 - const uniqueProductOptions = Array.from(new Map( - currentOptions.map(inv => [inv.product_id, { label: inv.product_name, value: String(inv.product_id) }]) + // 2. 來源倉庫選項 (根據商品库庫存過濾) + const currentInventories = productInventoryMap[item.ui_product_id] || []; + const filteredWarehouseOptions = Array.from(new Map( + currentInventories.map((inv: InventoryOption) => [inv.warehouse_id, { label: inv.warehouse_name, value: String(inv.warehouse_id) }]) ).values()); - // 過濾批號 - const batchOptions = currentOptions - .filter(inv => String(inv.product_id) === item.ui_product_id) - .map(inv => ({ + // 如果篩選後沒有倉庫(即該商品無庫存),則顯示所有倉庫以供選取(或顯示無庫存提示) + const uniqueWarehouseOptions = filteredWarehouseOptions.length > 0 + ? filteredWarehouseOptions + : (item.ui_product_id && !loadingProducts[item.ui_product_id] ? [] : []); + + // 3. 批號選項 (根據商品與倉庫過濾) + const batchOptions = currentInventories + .filter((inv: InventoryOption) => String(inv.warehouse_id) === item.ui_warehouse_id) + .map((inv: InventoryOption) => ({ label: `${inv.batch_number} / ${inv.expiry_date || '無效期'} / ${inv.quantity}`, value: String(inv.id) })); - - return ( - {/* 0. 選擇來源倉庫 */} - - updateBomItem(index, 'ui_warehouse_id', v)} - options={warehouses.map(w => ({ label: w.name, value: String(w.id) }))} - placeholder="選擇倉庫" - className="w-full" - /> - - {/* 1. 選擇商品 */} updateBomItem(index, 'ui_product_id', v)} - options={uniqueProductOptions} + options={productOptions} placeholder="選擇商品" className="w-full" - disabled={!item.ui_warehouse_id} /> - {/* 2. 選擇批號 */} + {/* 2. 選擇來源倉庫 */} + + updateBomItem(index, 'ui_warehouse_id', v)} + options={uniqueWarehouseOptions as any} + placeholder={item.ui_product_id + ? (loadingProducts[item.ui_product_id] + ? "載入庫存中..." + : (uniqueWarehouseOptions.length === 0 ? "該商品目前無庫存" : "選擇倉庫")) + : "請先選商品"} + className="w-full" + disabled={!item.ui_product_id || (loadingProducts[item.ui_product_id])} + /> + + + {/* 3. 選擇批號 */} updateBomItem(index, 'inventory_id', v)} - options={batchOptions} - placeholder={item.ui_product_id ? "選擇批號" : "請先選商品"} + options={batchOptions as any} + placeholder={item.ui_warehouse_id ? "選擇批號" : "請先選倉庫"} className="w-full" - disabled={!item.ui_product_id} + disabled={!item.ui_warehouse_id} /> {item.inventory_id && (() => { - const selectedInv = currentOptions.find(i => String(i.id) === item.inventory_id); + const selectedInv = currentInventories.find((i: InventoryOption) => String(i.id) === item.inventory_id); if (selectedInv) return (
有效日期: {selectedInv.expiry_date || '無'} | 庫存: {selectedInv.quantity} diff --git a/resources/js/Pages/Production/Edit.tsx b/resources/js/Pages/Production/Edit.tsx index acf6c35..db14376 100644 --- a/resources/js/Pages/Production/Edit.tsx +++ b/resources/js/Pages/Production/Edit.tsx @@ -38,6 +38,8 @@ interface InventoryOption { product_id: number; product_name: string; product_code: string; + warehouse_id: number; + warehouse_name: string; batch_number: string; box_number: string | null; quantity: number; @@ -73,6 +75,7 @@ interface BomItem { ui_large_unit_name?: string; ui_base_unit_id?: number; ui_large_unit_id?: number; + ui_product_code?: string; } interface ProductionOrderItem { @@ -136,23 +139,24 @@ export default function ProductionEdit({ productionOrder, products, warehouses } productionOrder.warehouse_id ? String(productionOrder.warehouse_id) : "" ); // 產出倉庫 - // 快取對照表:warehouse_id -> inventories - const [inventoryMap, setInventoryMap] = useState>({}); - const [loadingWarehouses, setLoadingWareStates] = useState>({}); + // 快取對照表:product_id -> inventories + const [productInventoryMap, setProductInventoryMap] = useState>({}); + const [loadingProducts, setLoadingProducts] = useState>({}); - // 獲取倉庫資料的輔助函式 - const fetchWarehouseInventory = async (warehouseId: string) => { - if (!warehouseId || inventoryMap[warehouseId] || loadingWarehouses[warehouseId]) return; + // 獲取商品所有倉庫庫存的分佈 + const fetchProductInventories = async (productId: string) => { + if (!productId) return; + if (loadingProducts[productId]) return; - setLoadingWareStates(prev => ({ ...prev, [warehouseId]: true })); + setLoadingProducts(prev => ({ ...prev, [productId]: true })); try { - const res = await fetch(route('api.production.warehouses.inventories', warehouseId)); + const res = await fetch(route('api.production.products.inventories', productId)); const data = await res.json(); - setInventoryMap(prev => ({ ...prev, [warehouseId]: data })); + setProductInventoryMap(prev => ({ ...prev, [productId]: data })); } catch (e) { console.error(e); } finally { - setLoadingWareStates(prev => ({ ...prev, [warehouseId]: false })); + setLoadingProducts(prev => ({ ...prev, [productId]: false })); } }; @@ -188,25 +192,25 @@ export default function ProductionEdit({ productionOrder, products, warehouses } items: [] as { inventory_id: number; quantity_used: number; unit_id: number | null }[], }); - // 初始化載入既有 BOM 的來源倉庫資料 + // 初始化載入既有 BOM 的商品庫存資料 useEffect(() => { initialBomItems.forEach(item => { - if (item.ui_warehouse_id) { - fetchWarehouseInventory(item.ui_warehouse_id); + if (item.ui_product_id) { + fetchProductInventories(item.ui_product_id); } }); }, []); - // 當 inventoryOptions (Map) 載入後,更新現有 BOM items 的詳細資訊 (如單位、轉換率) - // 監聽 inventoryMap 變更 + // 當 inventoryOptions 載入後,更新現有 BOM items 的詳細資訊 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); + const currentOptions = productInventoryMap[item.ui_product_id] || []; + if (currentOptions.length > 0 && item.inventory_id && !item.ui_conversion_rate) { + const inv = currentOptions.find(i => String(i.id) === item.inventory_id); if (inv) { return { ...item, - ui_product_id: String(inv.product_id), + ui_warehouse_id: String(inv.warehouse_id), // 重要:還原倉庫 ID ui_product_name: inv.product_name, ui_batch_number: inv.batch_number, ui_available_qty: inv.quantity, @@ -221,7 +225,7 @@ export default function ProductionEdit({ productionOrder, products, warehouses } } return item; })); - }, [inventoryMap]); + }, [productInventoryMap]); // 同步 warehouse_id 到 form data useEffect(() => { @@ -251,53 +255,40 @@ export default function ProductionEdit({ productionOrder, products, warehouses } const updated = [...bomItems]; const item = { ...updated[index], [field]: value }; - // 0. 當選擇來源倉庫變更時 - if (field === 'ui_warehouse_id') { - item.ui_product_id = ""; + // 0. 當選擇商品變更時 (第一層) + if (field === 'ui_product_id') { + item.ui_warehouse_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); + const prod = products.find(p => String(p.id) === value); + if (prod) { + item.ui_product_name = prod.name; + item.ui_base_unit_name = prod.base_unit?.name || ''; + } + fetchProductInventories(value); } } - // 1. 當選擇商品變更時 -> 清空批號與相關資訊 - if (field === 'ui_product_id') { + // 1. 當選擇來源倉庫變更時 (第二層) + if (field === 'ui_warehouse_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. 當選擇批號變更時 + // 2. 當選擇批號 (Inventory) 變更時 (第三層) if (field === 'inventory_id' && value) { - const currentOptions = inventoryMap[item.ui_warehouse_id] || []; + const currentOptions = productInventoryMap[item.ui_product_id] || []; const inv = currentOptions.find(i => String(i.id) === value); if (inv) { - item.ui_product_id = String(inv.product_id); + item.ui_warehouse_id = String(inv.warehouse_id); item.ui_product_name = inv.product_name; item.ui_batch_number = inv.batch_number; item.ui_available_qty = inv.quantity; @@ -583,8 +574,8 @@ export default function ProductionEdit({ productionOrder, products, warehouses }
- 來源倉庫 * 商品 * + 來源倉庫 * 批號 * 數量 * 單位 @@ -593,19 +584,31 @@ export default function ProductionEdit({ productionOrder, products, warehouses } {bomItems.map((item, index) => { - // 取得此列已載入的 Inventory Options - const currentOptions = inventoryMap[item.ui_warehouse_id] || []; + // 1. 商品選項 + const productOptions = products.map(p => ({ + label: `${p.name} (${p.code})`, + value: String(p.id) + })); - const uniqueProductOptions = Array.from(new Map( - currentOptions.map(inv => [inv.product_id, { label: inv.product_name, value: String(inv.product_id) }]) + // 2. 來源倉庫選項 (根據商品库庫存過濾) + const currentInventories = productInventoryMap[item.ui_product_id] || []; + const filteredWarehouseOptions = Array.from(new Map( + currentInventories.map((inv: InventoryOption) => [inv.warehouse_id, { label: inv.warehouse_name, value: String(inv.warehouse_id) }]) ).values()); - // 在獲取前初始狀態的備案 - const displayProductOptions = uniqueProductOptions.length > 0 ? uniqueProductOptions : (item.ui_product_name ? [{ label: item.ui_product_name, value: item.ui_product_id }] : []); + const uniqueWarehouseOptions = filteredWarehouseOptions.length > 0 + ? filteredWarehouseOptions + : (item.ui_product_id && !loadingProducts[item.ui_product_id] ? [] : []); - const batchOptions = currentOptions - .filter(inv => String(inv.product_id) === item.ui_product_id) - .map(inv => ({ + // 備案 (初始載入時) + const displayWarehouseOptions = uniqueWarehouseOptions.length > 0 + ? uniqueWarehouseOptions + : (item.ui_warehouse_id ? [{ label: "載入中...", value: item.ui_warehouse_id }] : []); + + // 3. 批號選項 (根據商品與倉庫過濾) + const batchOptions = currentInventories + .filter((inv: InventoryOption) => String(inv.warehouse_id) === item.ui_warehouse_id) + .map((inv: InventoryOption) => ({ label: `${inv.batch_number} / ${inv.expiry_date || '無效期'} / ${inv.quantity}`, value: String(inv.id) })); @@ -614,44 +617,47 @@ export default function ProductionEdit({ productionOrder, products, warehouses } const displayBatchOptions = batchOptions.length > 0 ? batchOptions : (item.inventory_id && item.ui_batch_number ? [{ label: item.ui_batch_number, value: item.inventory_id }] : []); - return ( - {/* 0. 選擇來源倉庫 */} - - updateBomItem(index, 'ui_warehouse_id', v)} - options={warehouses.map(w => ({ label: w.name, value: String(w.id) }))} - placeholder="選擇倉庫" - className="w-full" - /> - - {/* 1. 選擇商品 */} updateBomItem(index, 'ui_product_id', v)} - options={displayProductOptions} + options={productOptions} placeholder="選擇商品" className="w-full" - disabled={!item.ui_warehouse_id} /> - {/* 2. 選擇批號 */} + {/* 2. 選擇來源倉庫 */} + + updateBomItem(index, 'ui_warehouse_id', v)} + options={displayWarehouseOptions as any} + placeholder={item.ui_product_id + ? (loadingProducts[item.ui_product_id] + ? "載入庫存中..." + : (uniqueWarehouseOptions.length === 0 ? "該商品目前無庫存" : "選擇倉庫")) + : "請先選商品"} + className="w-full" + disabled={!item.ui_product_id || (loadingProducts[item.ui_product_id])} + /> + + + {/* 3. 選擇批號 */} updateBomItem(index, 'inventory_id', v)} - options={displayBatchOptions} - placeholder={item.ui_product_id ? "選擇批號" : "請先選商品"} + options={displayBatchOptions as any} + placeholder={item.ui_warehouse_id ? "選擇批號" : "請先選倉庫"} className="w-full" - disabled={!item.ui_product_id} + disabled={!item.ui_warehouse_id} /> {item.inventory_id && (() => { - const selectedInv = currentOptions.find(i => String(i.id) === item.inventory_id); + const selectedInv = currentInventories.find((i: InventoryOption) => String(i.id) === item.inventory_id); if (selectedInv) return (
有效日期: {selectedInv.expiry_date || '無'} | 庫存: {selectedInv.quantity} diff --git a/resources/js/Pages/Production/Recipe/Create.tsx b/resources/js/Pages/Production/Recipe/Create.tsx index 7674b1e..8f3d22c 100644 --- a/resources/js/Pages/Production/Recipe/Create.tsx +++ b/resources/js/Pages/Production/Recipe/Create.tsx @@ -2,12 +2,11 @@ * 新增配方頁面 */ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { BookOpen, Plus, Trash2, ArrowLeft, Save } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; -import { Head, router, useForm, Link } from "@inertiajs/react"; -import { toast } from "sonner"; +import { Head, useForm, Link } from "@inertiajs/react"; import { getBreadcrumbs } from "@/utils/breadcrumb"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Input } from "@/Components/ui/input"; @@ -36,6 +35,7 @@ interface RecipeItem { // UI Helpers ui_product_name?: string; ui_product_code?: string; + ui_unit_name?: string; } interface Props { @@ -91,9 +91,11 @@ export default function RecipeCreate({ products, units }: Props) { if (product) { newItems[index].ui_product_name = product.name; newItems[index].ui_product_code = product.code; - // Default to base unit + // Default to base unit and fix it if (product.base_unit_id) { newItems[index].unit_id = String(product.base_unit_id); + const unit = units.find(u => u.id === product.base_unit_id); + newItems[index].ui_unit_name = unit?.name; } } } @@ -103,14 +105,7 @@ export default function RecipeCreate({ products, units }: Props) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - post(route('recipes.store'), { - onSuccess: () => { - toast.success("配方已建立"); - }, - onError: (errors) => { - toast.error("儲存失敗,請檢查欄位"); - } - }); + post(route('recipes.store')); }; return ( @@ -275,17 +270,10 @@ export default function RecipeCreate({ products, units }: Props) { placeholder="數量" /> - - updateItem(index, 'unit_id', v)} - options={units.map(u => ({ - label: u.name, - value: String(u.id) - }))} - placeholder="單位" - className="w-full" - /> + +
+ {item.ui_unit_name || (units.find(u => String(u.id) === item.unit_id)?.name) || '-'} +
u.id === product.base_unit_id); + newItems[index].ui_unit_name = unit?.name; } } } @@ -127,14 +130,7 @@ export default function RecipeEdit({ recipe, products, units }: Props) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - put(route('recipes.update', recipe.id), { - onSuccess: () => { - toast.success("配方已更新"); - }, - onError: (errors) => { - toast.error("儲存失敗,請檢查欄位"); - } - }); + put(route('recipes.update', recipe.id)); }; return ( @@ -299,17 +295,10 @@ export default function RecipeEdit({ recipe, products, units }: Props) { placeholder="數量" /> - - updateItem(index, 'unit_id', v)} - options={units.map(u => ({ - label: u.name, - value: String(u.id) - }))} - placeholder="單位" - className="w-full" - /> + +
+ {item.ui_unit_name || (units.find(u => String(u.id) === item.unit_id)?.name) || '-'} +