import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, useForm, Link, router } from '@inertiajs/react'; import { Button } from '@/Components/ui/button'; import { Input } from '@/Components/ui/input'; import { Label } from '@/Components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/Components/ui/select'; import { SearchableSelect } from '@/Components/ui/searchable-select'; import React, { useState, useEffect } from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/Components/ui/table'; import { StatusBadge } from "@/Components/shared/StatusBadge"; import { Plus, Trash2, Calendar as CalendarIcon, Save, ArrowLeft, Package } from 'lucide-react'; import axios from 'axios'; import { PurchaseOrderStatus } from '@/types/purchase-order'; import { STATUS_CONFIG } from '@/constants/purchase-order'; import { DuplicateWarningDialog } from './components/DuplicateWarningDialog'; // 待進貨採購單 Item 介面 interface PendingPOItem { id: number; product_id: number; product_name: string; product_code: string; unit: string; quantity: number; received_quantity: number; remaining: number; unit_price: number; batchMode?: 'existing' | 'new' | 'none'; originCountry?: string; // For new batch generation } // 待進貨採購單介面 interface PendingPO { id: number; code: string; status: PurchaseOrderStatus; vendor_id: number; vendor_name: string; warehouse_id: number | null; order_date: string; items: PendingPOItem[]; } // 廠商介面 interface Vendor { id: number; name: string; code: string; } // 編輯模式的進貨單資料 interface ReceiptData { id: number; code: string; type: string; warehouse_id: number; vendor_id: number | null; vendor: Vendor | null; purchase_order_id: number | null; purchase_order?: any; received_date: string; remarks: string; items: any[]; } interface Props { warehouses: { id: number; name: string; type: string }[]; pendingPurchaseOrders: PendingPO[]; vendors: Vendor[]; receipt?: ReceiptData; } export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, vendors, receipt }: Props) { const isEditMode = !!receipt; const [selectedPO, setSelectedPO] = useState(null); const [selectedVendor, setSelectedVendor] = useState(null); // 全商品清單(用於雜項入庫/其他類型的 SearchableSelect) const [allProducts, setAllProducts] = useState([]); const [isLoadingProducts, setIsLoadingProducts] = useState(false); // Duplicate Check States const [warningOpen, setWarningOpen] = useState(false); const [warnings, setWarnings] = useState([]); const [isCheckingDuplicate, setIsCheckingDuplicate] = useState(false); const { data, setData, processing, errors } = useForm({ type: receipt?.type || 'standard', warehouse_id: receipt?.warehouse_id?.toString() || '', purchase_order_id: receipt?.purchase_order_id?.toString() || '', vendor_id: receipt?.vendor_id?.toString() || '', received_date: receipt?.received_date || new Date().toISOString().split('T')[0], remarks: receipt?.remarks || '', items: (receipt?.items || []) as any[], }); // 編輯模式下初始化 vendor 與 PO 狀態 useEffect(() => { if (isEditMode) { if (receipt?.vendor) { setSelectedVendor(receipt.vendor); } if (receipt?.purchase_order) { setSelectedPO(receipt.purchase_order); } } }, []); // 判斷是否為非標準採購類型 const isNonStandard = data.type !== 'standard'; // 載入所有商品(用於雜項入庫/其他類型的 SearchableSelect) const fetchAllProducts = async () => { setIsLoadingProducts(true); try { const response = await axios.get(route('goods-receipts.search-products'), { params: { query: '*' }, }); setAllProducts(response.data); } catch (error) { console.error('Failed to load products', error); } finally { setIsLoadingProducts(false); } }; // 當選擇非標準類型且已選供應商時,載入所有商品 useEffect(() => { if (isNonStandard && selectedVendor) { fetchAllProducts(); } }, [isNonStandard, selectedVendor]); // 選擇採購單 const handleSelectPO = (po: any) => { setSelectedPO(po); // 將採購單項目轉換為進貨單項目,預填剩餘可收貨量 const pendingItems = po.items.map((item: any) => ({ product_id: item.productId, purchase_order_item_id: item.id, product_name: item.productName, product_code: item.product_code || '', unit: item.selectedUnit === 'large' ? item.large_unit_name : item.base_unit_name, selectedUnit: item.selectedUnit, base_unit_id: item.base_unit_id, base_unit_name: item.base_unit_name, large_unit_id: item.large_unit_id, large_unit_name: item.large_unit_name, conversion_rate: item.conversion_rate, quantity_ordered: item.quantity, quantity_received_so_far: item.received_quantity || 0, quantity_received: (item.quantity - (item.received_quantity || 0)).toString(), // 預填剩餘量 unit_price: item.unitPrice, batch_number: '', batchMode: 'new', originCountry: 'TW', expiry_date: '', })); setData((prev) => ({ ...prev, purchase_order_id: po.id.toString(), vendor_id: po.supplierId.toString(), warehouse_id: po.warehouse_id ? po.warehouse_id.toString() : prev.warehouse_id, items: pendingItems, })); }; // 選擇廠商(雜項入庫/其他) const handleSelectVendor = (vendorId: string) => { const vendor = vendors.find(v => v.id.toString() === vendorId); if (vendor) { setSelectedVendor(vendor); setData('vendor_id', vendor.id.toString()); } }; const handleAddEmptyItem = () => { const newItem = { product_id: '', product_name: '', product_code: '', quantity_received: 0, unit_price: 0, subtotal: 0, batch_number: '', batchMode: 'new', originCountry: 'TW', expiry_date: '', }; setData('items', [...data.items, newItem]); }; // 選擇商品後填入該列(用於空白列的 SearchableSelect) const handleSelectProduct = (index: number, productId: string) => { const product = allProducts.find(p => p.id.toString() === productId); if (product) { const newItems = [...data.items]; newItems[index] = { ...newItems[index], product_id: product.id, product_name: product.name, product_code: product.code, unit_price: product.price || 0, }; setData('items', newItems); } }; const removeItem = (index: number) => { const newItems = [...data.items]; newItems.splice(index, 1); setData('items', newItems); }; const updateItem = (index: number, field: string, value: any) => { const newItems = [...data.items]; const item = { ...newItems[index], [field]: value }; const qty = parseFloat(item.quantity_received) || 0; const price = parseFloat(item.unit_price) || 0; const subtotal = parseFloat(item.subtotal) || 0; if (field === 'quantity_received') { // 修改數量 -> 更新小計 (保持單價) item.subtotal = (qty * price).toString(); } else if (field === 'unit_price') { // 修改單價 -> 更新小計 (價格 * 數量) item.subtotal = (qty * price).toString(); } else if (field === 'subtotal') { // 修改小計 -> 更新單價 (小計 / 數量) if (qty > 0) { item.unit_price = (subtotal / qty).toString(); } } newItems[index] = item; setData('items', newItems); }; // Generate batch preview (Added) const getBatchPreview = (productId: number, productCode: string, country: string, dateStr: string) => { if (!productCode || !productId) return "--"; try { const datePart = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr; const [yyyy, mm, dd] = datePart.split('-'); const dateFormatted = `${yyyy}${mm}${dd}`; const seqKey = `${productId}-${country}-${datePart}`; // Handle sequence. Note: nextSequences values are numbers. const seq = nextSequences[seqKey]?.toString().padStart(2, '0') || "01"; return `${productCode}-${country}-${dateFormatted}-${seq}`; } catch (e) { return "--"; } }; // Batch management const [nextSequences, setNextSequences] = useState>({}); // Fetch batches and sequence for a product const fetchProductBatches = async (productId: number, country: string = 'TW', dateStr: string = '') => { if (!data.warehouse_id) return; // const cacheKey = `${productId}-${data.warehouse_id}`; // Unused try { const today = new Date().toISOString().split('T')[0]; const targetDate = dateStr || data.received_date || today; // Adjust API endpoint to match AddInventory logic // Assuming GoodsReceiptController or existing WarehouseController can handle this. // Using the same endpoint as AddInventory: /api/warehouses/{id}/inventory/batches/{productId} const response = await axios.get( `/api/warehouses/${data.warehouse_id}/inventory/batches/${productId}`, { params: { origin_country: country, arrivalDate: targetDate } } ); if (response.data) { // Remove unused batch cache update // Update next sequence for new batch generation if (response.data.nextSequence !== undefined) { const seqKey = `${productId}-${country}-${targetDate}`; setNextSequences(prev => ({ ...prev, [seqKey]: parseInt(response.data.nextSequence) })); } } } catch (error) { console.error("Failed to fetch batches", error); } }; // Trigger batch fetch when relevant fields change useEffect(() => { data.items.forEach(item => { if (item.product_id && data.warehouse_id) { const country = item.originCountry || 'TW'; const date = data.received_date; fetchProductBatches(item.product_id, country, date); } }); }, [data.items.length, data.warehouse_id, data.received_date, JSON.stringify(data.items.map(i => i.originCountry))]); useEffect(() => { data.items.forEach((item, index) => { if (item.batchMode === 'none') { if (item.batch_number !== 'NO-BATCH') { const newItems = [...data.items]; newItems[index].batch_number = 'NO-BATCH'; setData('items', newItems); } } else if (item.batchMode === 'new' && item.originCountry && data.received_date) { const country = item.originCountry; // Use date from form or today const dateStr = data.received_date || new Date().toISOString().split('T')[0]; const seqKey = `${item.product_id}-${country}-${dateStr}`; const seq = nextSequences[seqKey]?.toString().padStart(3, '0') || '001'; const datePart = dateStr.replace(/-/g, ''); const generatedBatch = `${item.product_code}-${country}-${datePart}-${seq}`; if (item.batch_number !== generatedBatch) { const newItems = [...data.items]; newItems[index].batch_number = generatedBatch; setData('items', newItems); } } }); }, [nextSequences, JSON.stringify(data.items.map(i => ({ m: i.batchMode, c: i.originCountry, s: i.product_code, p: i.product_id }))), data.received_date]); const submit = async (e: React.FormEvent, force: boolean = false) => { if (e) e.preventDefault(); // 格式化日期數據 const formattedItems = data.items.map(item => ({ ...item, expiry_date: item.expiry_date && item.expiry_date.includes('T') ? item.expiry_date.split('T')[0] : item.expiry_date })); const formattedDate = data.received_date.includes('T') ? data.received_date.split('T')[0] : data.received_date; // 建立一個臨時的提交資料對象 const submitData = { ...data, received_date: formattedDate, items: formattedItems }; // 編輯模式直接 PUT 更新 if (isEditMode) { // 使用 router.put 因為 useForm 的 put 不支援傳入自定義 data 物件而不影響 state // 或者先 setData 再 put,但 setData 是非同步的 // 這裡採用在提交前手動傳遞格式化後的資料給 router router.put(route('goods-receipts.update', receipt!.id), submitData, { onSuccess: () => setWarningOpen(false), }); return; } // 新增模式:先檢查重複 if (!force) { setIsCheckingDuplicate(true); try { const response = await axios.post(route('goods-receipts.check-duplicate'), submitData); if (response.data.has_warnings) { setWarnings(response.data.warnings); setWarningOpen(true); return; } } catch (error) { console.error("Duplicate check failed", error); } finally { setIsCheckingDuplicate(false); } } router.post(route('goods-receipts.store'), submitData, { onSuccess: () => setWarningOpen(false), }); }; return (
{/* Header */}

{isEditMode ? `編輯進貨單: ${receipt!.code}` : '新增進貨單'}

{isEditMode ? '修改進貨單內容' : '建立新的進貨單並入庫'}

{/* Step 0: Select Type */}
{[ { id: 'standard', label: '標準採購', desc: '從採購單帶入' }, { id: 'miscellaneous', label: '雜項入庫', desc: '非採購之入庫' }, { id: 'other', label: '其他', desc: '其他原因入庫' }, ].map((t) => ( ))}
{/* Step 1: Source Selection */}
{(data.type === 'standard' ? selectedPO : selectedVendor) ? '✓' : '1'}

{data.type === 'standard' ? '選擇來源採購單' : '選擇供應商'}

{data.type === 'standard' ? ( !selectedPO ? (
{pendingPurchaseOrders.length === 0 ? (
目前沒有待進貨的採購單
) : (
採購單號 供應商 狀態 待收項目 操作 {pendingPurchaseOrders.map((po) => ( {po.code} {po.vendor_name} {STATUS_CONFIG[po.status]?.label || po.status} {po.items.length} 項 ))}
)}
) : (
已選採購單 {selectedPO.code}
供應商 {selectedPO.vendor_name}
待收項目 {selectedPO.items.length} 項
{!isEditMode && ( )}
) ) : ( !selectedVendor ? (
({ label: `${v.name} (${v.code})`, value: v.id.toString() }))} placeholder="選擇供應商..." searchPlaceholder="搜尋供應商..." className="h-9 w-full max-w-md" />
{vendors.length === 0 && (
目前沒有可選擇的供應商
)}
) : (
已選供應商 {selectedVendor.name}
供應商代號 {selectedVendor.code}
{!isEditMode && ( )}
) )}
{/* Step 2: Details & Items */} {(isEditMode || (data.type === 'standard' && selectedPO) || (data.type !== 'standard' && selectedVendor)) && (
2

進貨資訊與明細

{errors.warehouse_id &&

{errors.warehouse_id}

}
setData('received_date', e.target.value)} className="pl-9 h-9 block w-full" />
{errors.received_date &&

{errors.received_date}

}
setData('remarks', e.target.value)} className="h-9" placeholder="選填..." />

商品明細

{isNonStandard && ( )}
{/* Calculated Totals for usage in Table Footer or Summary */} {(() => { const subTotal = data.items.reduce((acc, item) => { if (isNonStandard) { // 非標準類型:使用手動輸入的小計 return acc + (parseFloat(item.subtotal) || 0); } // 標準類型:自動計算 qty × price const qty = parseFloat(item.quantity_received) || 0; const price = parseFloat(item.unit_price) || 0; return acc + (qty * price); }, 0); const taxAmount = Math.round(subTotal * 0.05); const grandTotal = subTotal + taxAmount; return ( <>
商品資訊 {!isNonStandard && ( <> 總數量 待收貨 )} {isNonStandard ? '數量' : '本次收貨'} * {isNonStandard && ( 單價 )} 批號設定 * 效期 小計{isNonStandard && <> *} {data.items.length === 0 ? ( {isNonStandard ? '尚無明細,請點擊「新增品項」加入。' : '尚無明細,請搜尋商品加入。'} ) : ( data.items.map((item, index) => { const errorKey = `items.${index}.quantity_received` as keyof typeof errors; const itemTotal = (parseFloat(item.quantity_received || 0) * parseFloat(item.unit_price || 0)); return ( {/* Product Info */} {isNonStandard && !item.product_id ? ( handleSelectProduct(index, val)} options={allProducts.map(p => ({ label: `${p.name} (${p.code})`, value: p.id.toString() }))} placeholder={isLoadingProducts ? '載入中...' : '選擇商品...'} searchPlaceholder="搜尋商品名稱或代碼..." className="w-full" /> ) : (
{item.product_name} {item.product_code}
)}
{/* Total Quantity & Remaining - 僅標準採購顯示 */} {!isNonStandard && ( <> {Math.round(item.quantity_ordered)} {Math.round(item.quantity_ordered - item.quantity_received_so_far)} )} {/* Received Quantity */}
updateItem(index, 'quantity_received', e.target.value)} className={`w-full text-right ${errors && (errors as any)[errorKey] ? 'border-red-500' : ''}`} /> {item.selectedUnit === 'large' && item.conversion_rate > 1 && (
= {(parseFloat(item.quantity_received) * item.conversion_rate).toLocaleString()} {item.base_unit_name}
)}
{(errors as any)[errorKey] && (

{(errors as any)[errorKey]}

)}
{/* Unit Price - 僅非標準類型顯示 */} {isNonStandard && ( updateItem(index, 'unit_price', e.target.value)} className="w-full text-right" placeholder="0" /> )} {/* Batch Settings */}
{/* 統一批號選擇器 */} { if (value === 'new_batch') { const updatedItem = { ...item, batchMode: 'new', inventory_id: undefined, originCountry: 'TW', expiry_date: '', }; const newItems = [...data.items]; newItems[index] = updatedItem; setData('items', newItems); } else if (value === 'no_batch') { const updatedItem = { ...item, batchMode: 'none', batch_number: 'NO-BATCH', inventory_id: undefined, originCountry: 'TW', expiry_date: '', }; const newItems = [...data.items]; newItems[index] = updatedItem; setData('items', newItems); } else { // 選擇現有批號 (如果有快照的話,目前架構下先保留 basic 選項) updateItem(index, 'batchMode', 'existing'); updateItem(index, 'inventory_id', value); } }} options={[ { label: "📦 不使用批號 (自動累加)", value: "no_batch" }, { label: "+ 建立新批號", value: "new_batch" }, // 若有現有批號列表可在此擴充,目前進貨單主要處理 new/none ]} placeholder="選擇或建立批號" className="border-gray-200" /> {/* 建立新批號時的附加欄位 */} {item.batchMode === 'new' && (
updateItem(index, 'originCountry', e.target.value.toUpperCase().slice(0, 2))} placeholder="產地" maxLength={2} className="w-12 h-8 text-center px-1 text-xs" />
{getBatchPreview(item.product_id, item.product_code, item.originCountry || 'TW', data.received_date)}
)} {/* 不使用批號時的提示 */} {item.batchMode === 'none' && (

系統將自動累計至該商品的通用庫存紀錄

)}
{/* Expiry Date */}
updateItem(index, 'expiry_date', e.target.value)} className={`pl-9 ${item.batchMode === 'existing' ? 'bg-gray-50' : ''}`} disabled={item.batchMode === 'existing'} />
{/* Subtotal */} {isNonStandard ? ( updateItem(index, 'subtotal', e.target.value)} className="w-full text-right" placeholder="0" /> ) : ( ${itemTotal.toLocaleString()} )} {/* Actions */}
); }) )}
小計 ${subTotal.toLocaleString()}
稅額 (5%) ${taxAmount.toLocaleString()}
總計金額 ${grandTotal.toLocaleString()}
); })()}
)}
{/* Bottom Action Bar */}
setWarningOpen(false)} onConfirm={() => submit(null as any, true)} warnings={warnings} processing={processing} />
); }