import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, useForm, Link } 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 { Search, 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'; // 待進貨採購單 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'; 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 Props { warehouses: { id: number; name: string; type: string }[]; pendingPurchaseOrders: PendingPO[]; vendors: Vendor[]; } export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders, vendors }: Props) { const [selectedPO, setSelectedPO] = useState(null); const [selectedVendor, setSelectedVendor] = useState(null); const [isSearching, setIsSearching] = useState(false); // Manual Product Search States const [productSearch, setProductSearch] = useState(''); const [foundProducts, setFoundProducts] = useState([]); const { data, setData, post, processing, errors } = useForm({ type: 'standard', // 'standard', 'miscellaneous', 'other' warehouse_id: '', purchase_order_id: '', vendor_id: '', received_date: new Date().toISOString().split('T')[0], remarks: '', items: [] as any[], }); // 搜尋商品 API(用於雜項入庫/其他類型) const searchProducts = async () => { if (!productSearch) return; setIsSearching(true); try { const response = await axios.get(route('goods-receipts.search-products'), { params: { query: productSearch }, }); setFoundProducts(response.data); } catch (error) { console.error('Failed to search products', error); } finally { setIsSearching(false); } }; // 選擇採購單 const handleSelectPO = (po: PendingPO) => { setSelectedPO(po); // 將採購單項目轉換為進貨單項目,預填剩餘可收貨量 const pendingItems = po.items.map((item) => ({ product_id: item.product_id, purchase_order_item_id: item.id, product_name: item.product_name, product_code: item.product_code, unit: item.unit, quantity_ordered: item.quantity, quantity_received_so_far: item.received_quantity, quantity_received: item.remaining, // 預填剩餘量 unit_price: item.unit_price, batch_number: '', batchMode: 'new', originCountry: 'TW', expiry_date: '', })); setData((prev) => ({ ...prev, purchase_order_id: po.id.toString(), vendor_id: po.vendor_id.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 handleAddProduct = (product: any) => { const newItem = { product_id: product.id, product_name: product.name, product_code: product.code, quantity_received: 0, unit_price: product.price || 0, batch_number: '', batchMode: 'new', originCountry: 'TW', expiry_date: '', }; setData('items', [...data.items, newItem]); setFoundProducts([]); setProductSearch(''); }; 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]; newItems[index] = { ...newItems[index], [field]: value }; 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 === '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'; // Only generate if we have a sequence (or default) // Note: fetch might not have returned yet, so seq might be default 001 until fetch updates nextSequences const datePart = dateStr.replace(/-/g, ''); const generatedBatch = `${item.product_code}-${country}-${datePart}-${seq}`; if (item.batch_number !== generatedBatch) { // Update WITHOUT triggering re-render loop // Need a way to update item silently or check condition carefully // Using setBatchNumber might trigger this effect again but value will be same. 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 = (e: React.FormEvent) => { e.preventDefault(); post(route('goods-receipts.store')); }; return (
{/* Header */}

新增進貨單

建立新的進貨單並入庫

{/* 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} 項
) ) : ( !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}
) )}
{/* Step 2: Details & Items */} {((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="選填..." />

商品明細

{data.type !== 'standard' && (
setProductSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && searchProducts()} className="h-9 w-64 pl-9" /> {foundProducts.length > 0 && (
{foundProducts.map(p => ( ))}
)}
)}
{/* Calculated Totals for usage in Table Footer or Summary */} {(() => { const subTotal = data.items.reduce((acc, item) => { 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 ( <>
商品資訊 總數量 待收貨 本次收貨 * 批號設定 * 效期 小計 {data.items.length === 0 ? ( 尚無明細,請搜尋商品加入。 ) : ( 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 */}
{item.product_name} {item.product_code}
{/* Total Quantity */} {Math.round(item.quantity_ordered)} {/* Remaining */} {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' : ''}`} /> {(errors as any)[errorKey] && (

{(errors as any)[errorKey]}

)}
{/* Batch Settings */}
updateItem(index, 'originCountry', e.target.value.toUpperCase().slice(0, 2))} placeholder="產地" maxLength={2} className="w-16 text-center px-1" />
{getBatchPreview(item.product_id, item.product_code, item.originCountry || 'TW', data.received_date)}
{/* Expiry Date */}
updateItem(index, 'expiry_date', e.target.value)} className={`pl-9 ${item.batchMode === 'existing' ? 'bg-gray-50' : ''}`} disabled={item.batchMode === 'existing'} />
{/* Subtotal */} ${itemTotal.toLocaleString()} {/* Actions */}
); }) )}
小計 ${subTotal.toLocaleString()}
稅額 (5%) ${taxAmount.toLocaleString()}
總計金額 ${grandTotal.toLocaleString()}
); })()}
)}
{/* Bottom Action Bar */}
); }