feat: 修正 BOM 單位顯示與完工入庫彈窗 UI 統一規範
This commit is contained in:
@@ -4,17 +4,17 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Factory, Plus, Trash2, ArrowLeft, Save, Calendar } from 'lucide-react';
|
||||
import { Trash2, Plus, ArrowLeft, Save, Factory } from "lucide-react";
|
||||
import { formatQuantity } from "@/lib/utils";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router, useForm } from "@inertiajs/react";
|
||||
import { router, useForm, Head, Link } from "@inertiajs/react";
|
||||
import { toast } from "sonner";
|
||||
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 {
|
||||
@@ -84,7 +84,7 @@ interface Props {
|
||||
units: Unit[];
|
||||
}
|
||||
|
||||
export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
export default function Create({ products, warehouses, units }: Props) {
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState<string>(""); // 產出倉庫
|
||||
// 快取對照表:product_id -> inventories across warehouses
|
||||
const [productInventoryMap, setProductInventoryMap] = useState<Record<string, InventoryOption[]>>({});
|
||||
@@ -100,11 +100,7 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
product_id: "",
|
||||
warehouse_id: "",
|
||||
output_quantity: "",
|
||||
output_batch_number: "",
|
||||
// 移除 Box Count UI
|
||||
// 移除相關邏輯
|
||||
production_date: new Date().toISOString().split('T')[0],
|
||||
expiry_date: "",
|
||||
// 移除成品批號、生產日期、有效日期,這些欄位改在完工時處理或自動記錄
|
||||
remark: "",
|
||||
items: [] as { inventory_id: number; quantity_used: number; unit_id: number | null }[],
|
||||
});
|
||||
@@ -184,13 +180,14 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 當選擇來源倉庫變更時 -> 重置批號與相關資訊
|
||||
// 2. 當選擇來源倉庫變更時 -> 重置批號與相關資訊 (保留數量)
|
||||
if (field === 'ui_warehouse_id') {
|
||||
item.inventory_id = "";
|
||||
item.quantity_used = "";
|
||||
item.unit_id = "";
|
||||
item.ui_input_quantity = "";
|
||||
item.ui_selected_unit = "base";
|
||||
// 不重置數量
|
||||
// item.quantity_used = "";
|
||||
// item.ui_input_quantity = "";
|
||||
// item.ui_selected_unit = "base";
|
||||
|
||||
// 清除某些 cache
|
||||
delete item.ui_batch_number;
|
||||
delete item.ui_available_qty;
|
||||
@@ -215,6 +212,11 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
// 預設單位
|
||||
item.ui_selected_unit = 'base';
|
||||
item.unit_id = String(inv.base_unit_id || '');
|
||||
|
||||
// 不重置數量,但如果原本沒數量可以從庫存帶入 (選填,通常配方已帶入則保留配方)
|
||||
if (!item.ui_input_quantity) {
|
||||
item.ui_input_quantity = formatQuantity(inv.quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,27 +300,6 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
useEffect(() => {
|
||||
if (!data.product_id) return;
|
||||
|
||||
// 1. 自動產生成品批號
|
||||
const product = products.find(p => String(p.id) === data.product_id);
|
||||
if (product) {
|
||||
const datePart = data.production_date;
|
||||
const dateFormatted = datePart.replace(/-/g, '');
|
||||
const originCountry = 'TW';
|
||||
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(() => {
|
||||
const suggested = `${product.code}-${originCountry}-${dateFormatted}-01`;
|
||||
setData('output_batch_number', suggested);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 自動載入配方列表
|
||||
const fetchRecipes = async () => {
|
||||
try {
|
||||
@@ -362,9 +343,7 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
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 (!selectedWarehouse) missingFields.push('預計入庫倉庫');
|
||||
if (bomItems.length === 0) missingFields.push('原物料明細');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
@@ -387,6 +366,7 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
// 使用 router.post 提交完整資料
|
||||
router.post(route('production-orders.store'), {
|
||||
...data,
|
||||
warehouse_id: selectedWarehouse ? parseInt(selectedWarehouse) : null,
|
||||
items: formattedItems,
|
||||
status: status,
|
||||
}, {
|
||||
@@ -430,25 +410,14 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
建立新的生產排程,選擇原物料並記錄產出
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => submit('draft')}
|
||||
disabled={processing}
|
||||
variant="outline"
|
||||
className="button-outlined-primary"
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
儲存草稿
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => submit('completed')}
|
||||
disabled={processing}
|
||||
className="button-filled-primary"
|
||||
>
|
||||
<Factory className="mr-2 h-4 w-4" />
|
||||
建立工單
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => submit('draft')}
|
||||
disabled={processing}
|
||||
className="gap-2 button-filled-primary"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
儲存工單 (草稿)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -499,56 +468,16 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={data.output_quantity}
|
||||
value={Number(data.output_quantity) === 0 ? '' : formatQuantity(data.output_quantity)}
|
||||
onChange={(e) => setData('output_quantity', e.target.value)}
|
||||
placeholder="例如: 50"
|
||||
className="h-9"
|
||||
className="h-9 font-mono"
|
||||
/>
|
||||
{errors.output_quantity && <p className="text-red-500 text-xs mt-1">{errors.output_quantity}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">成品批號 *</Label>
|
||||
<Input
|
||||
value={data.output_batch_number}
|
||||
onChange={(e) => setData('output_batch_number', e.target.value)}
|
||||
placeholder="選擇商品後自動產生"
|
||||
className="h-9 font-mono"
|
||||
/>
|
||||
{errors.output_batch_number && <p className="text-red-500 text-xs mt-1">{errors.output_batch_number}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">生產日期 *</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={data.production_date}
|
||||
onChange={(e) => setData('production_date', e.target.value)}
|
||||
className="h-9 pl-9"
|
||||
/>
|
||||
</div>
|
||||
{errors.production_date && <p className="text-red-500 text-xs mt-1">{errors.production_date}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">成品效期(選填)</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={data.expiry_date}
|
||||
onChange={(e) => setData('expiry_date', e.target.value)}
|
||||
className="h-9 pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium text-grey-2">入庫倉庫 *</Label>
|
||||
<Label className="text-xs font-medium text-grey-2">預計入庫倉庫 *</Label>
|
||||
<SearchableSelect
|
||||
value={selectedWarehouse}
|
||||
onValueChange={setSelectedWarehouse}
|
||||
@@ -602,12 +531,12 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50/50">
|
||||
<TableHead className="w-[20%]">商品 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[20%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[25%]">批號 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[15%]">數量 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[15%]">單位</TableHead>
|
||||
<TableHead className="w-[5%]"></TableHead>
|
||||
<TableHead className="w-[18%]">商品 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[15%]">來源倉庫 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[30%]">批號 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[12%]">數量 <span className="text-red-500">*</span></TableHead>
|
||||
<TableHead className="w-[10%]">單位</TableHead>
|
||||
<TableHead className="w-[10%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -618,7 +547,7 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
value: String(p.id)
|
||||
}));
|
||||
|
||||
// 2. 來源倉庫選項 (根據商品库庫存過濾)
|
||||
// 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) }])
|
||||
@@ -629,12 +558,13 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
? filteredWarehouseOptions
|
||||
: (item.ui_product_id && !loadingProducts[item.ui_product_id] ? [] : []);
|
||||
|
||||
// 3. 批號選項 (根據商品與倉庫過濾)
|
||||
// 3. 批號選項 (利用 sublabel 顯示詳細資訊,保持選中後簡潔)
|
||||
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)
|
||||
label: inv.batch_number,
|
||||
value: String(inv.id),
|
||||
sublabel: `(存:${inv.quantity} | 效:${inv.expiry_date || '無'})`
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -678,11 +608,16 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
/>
|
||||
{item.inventory_id && (() => {
|
||||
const selectedInv = currentInventories.find((i: InventoryOption) => String(i.id) === item.inventory_id);
|
||||
if (selectedInv) return (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
有效日期: {selectedInv.expiry_date || '無'} | 庫存: {selectedInv.quantity}
|
||||
</div>
|
||||
);
|
||||
if (selectedInv) {
|
||||
const isInsufficient = selectedInv.quantity < parseFloat(item.ui_input_quantity || '0');
|
||||
return (
|
||||
<div className={`text-xs mt-1 ${isInsufficient ? 'text-red-500 font-bold animate-pulse' : 'text-gray-500'}`}>
|
||||
有效日期: {selectedInv.expiry_date || '無'} |
|
||||
庫存: {formatQuantity(selectedInv.quantity)}
|
||||
{isInsufficient && ' (庫存不足!)'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</TableCell>
|
||||
@@ -692,7 +627,7 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
value={item.ui_input_quantity}
|
||||
value={formatQuantity(item.ui_input_quantity)}
|
||||
onChange={(e) => updateBomItem(index, 'ui_input_quantity', e.target.value)}
|
||||
placeholder="0"
|
||||
className="h-9 text-right"
|
||||
@@ -700,20 +635,22 @@ export default function ProductionCreate({ products, warehouses }: Props) {
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* 4. 選擇單位 */}
|
||||
<TableCell className="align-top pt-3">
|
||||
<span className="text-sm">{item.ui_base_unit_name}</span>
|
||||
{/* 4. 單位 */}
|
||||
<TableCell className="align-top">
|
||||
<div className="h-9 flex items-center px-1 text-sm text-gray-600 font-medium">
|
||||
{item.ui_base_unit_name || '-'}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
|
||||
|
||||
<TableCell className="align-top">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => removeBomItem(index)}
|
||||
className="text-red-500 hover:text-red-700 hover:bg-red-50 p-2"
|
||||
className="button-outlined-error"
|
||||
title="刪除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user