/** * 新增庫存頁面(手動入庫) */ import { useState, useEffect } from "react"; import { Plus, Trash2, Calendar, ArrowLeft, Save, Boxes } from "lucide-react"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; import { Textarea } from "@/Components/ui/textarea"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/Components/ui/table"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, Link, router } from "@inertiajs/react"; import { Warehouse, InboundItem, InboundReason } from "@/types/warehouse"; import { getCurrentDateTime } from "@/utils/format"; import { toast } from "sonner"; import { getInventoryBreadcrumbs } from "@/utils/breadcrumb"; import ScannerInput from "@/Components/Inventory/ScannerInput"; interface Product { id: string; name: string; code: string; barcode?: string; baseUnit: string; largeUnit?: string; conversionRate?: number; costPrice?: number; } interface Batch { inventoryId: string; batchNumber: string; originCountry: string; expiryDate: string | null; quantity: number; isDeleted?: boolean; } interface Props { warehouse: Warehouse; products: Product[]; } const INBOUND_REASONS: InboundReason[] = [ "期初建檔", "盤點調整", "實際入庫未走採購流程", "生產加工成品入庫", "其他", ]; export default function AddInventoryPage({ warehouse, products }: Props) { const [inboundDate, setInboundDate] = useState(getCurrentDateTime()); const [reason, setReason] = useState("期初建檔"); const [notes, setNotes] = useState(""); const [items, setItems] = useState([]); const [errors, setErrors] = useState>({}); const [batchesCache, setBatchesCache] = useState }>>({}); // 取得商品批號與流水號 const fetchProductBatches = async (productId: string, originCountry?: string, arrivalDate?: string) => { if (!productId) return; const country = originCountry || 'TW'; const date = arrivalDate || inboundDate.split('T')[0]; const cacheKey = `${country}_${date}`; // 如果該商品的批號列表尚未載入,強制載入 const existingCache = batchesCache[productId]; const hasBatches = existingCache && existingCache.batches.length >= 0 && existingCache.batches !== undefined; const hasThisSequence = existingCache?.nextSequences?.[cacheKey]; // 若 batches 尚未載入,或特定條件的 sequence 尚未載入,則呼叫 API if (!hasBatches || !hasThisSequence) { try { const response = await fetch(`/api/warehouses/${warehouse.id}/inventory/batches/${productId}?originCountry=${country}&arrivalDate=${date}`); const data = await response.json(); setBatchesCache(prev => { const existingProductCache = prev[productId] || { batches: [], nextSequences: {} }; return { ...prev, [productId]: { batches: data.batches, nextSequences: { ...existingProductCache.nextSequences, [cacheKey]: data.nextSequence } } }; }); } catch (error) { console.error("Failed to fetch batches", error); } } }; // 當 items 變動、日期變動時,確保資料同步 useEffect(() => { items.forEach(item => { if (item.productId) { // 無論 batchMode 為何,都要載入批號列表 // 若使用者切換到 new 模式,則額外傳入 originCountry 以取得正確流水號 const country = item.batchMode === 'new' ? item.originCountry : undefined; fetchProductBatches(item.productId, country, inboundDate.split('T')[0]); } }); }, [items, inboundDate]); // 處理掃碼輸入 const handleScan = async (code: string, mode: 'continuous' | 'single') => { const cleanCode = code.trim(); // 1. 搜尋商品 (優先比對 Code, Barcode, ID) let product = products.find(p => p.code === cleanCode || p.barcode === cleanCode || p.id === cleanCode); // 如果前端找不到,嘗試 API 搜尋 (Fallback) if (!product) { try { // 這裡假設有 API 可以搜尋商品,若沒有則會失敗 // 使用 Product/Index 的搜尋邏輯 (Inertia Props 比較難已 AJAX 取得) // 替代方案:直接請求 /products?search=CLEAN_CODE&per_page=1 // 加上 header 確認是 JSON 請求 const response = await fetch(`/products?search=${encodeURIComponent(cleanCode)}&per_page=1`, { headers: { 'X-Requested-With': 'XMLHttpRequest', // 強制 AJAX 識別 } }); if (response.ok) { const data = await response.json(); // Inertia 回傳的是 component props 結構,或 partial props // 根據 ProductController::index,回傳 props.products.data if (data.props && data.props.products && data.props.products.data && data.props.products.data.length > 0) { const foundProduct = data.props.products.data[0]; // 轉換格式以符合 AddInventory 的 Product 介面 product = { id: foundProduct.id, name: foundProduct.name, code: foundProduct.code, barcode: foundProduct.barcode, baseUnit: foundProduct.baseUnit?.name || '個', largeUnit: foundProduct.largeUnit?.name, conversionRate: foundProduct.conversionRate, costPrice: foundProduct.costPrice, }; } } } catch (err) { console.error("API Search failed", err); } } if (!product) { toast.error(`找不到商品: ${code}`); return; } // 2. 連續模式:尋找最近一筆相同商品並 +1 if (mode === 'continuous') { let foundIndex = -1; // 從後往前搜尋,找到最近加入的那一筆 for (let i = items.length - 1; i >= 0; i--) { if (items[i].productId === product.id) { foundIndex = i; break; } } if (foundIndex !== -1) { // 更新數量 const newItems = [...items]; const currentQty = newItems[foundIndex].quantity || 0; newItems[foundIndex] = { ...newItems[foundIndex], quantity: currentQty + 1 }; setItems(newItems); toast.success(`${product.name} 數量 +1 (總數: ${currentQty + 1})`); return; } } // 3. 單筆模式 或 連續模式但尚未加入過:新增一筆 const newItem: InboundItem = { tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, productId: product.id, productName: product.name, quantity: 1, unit: product.baseUnit, // 僅用於顯示當前選擇單位的名稱 baseUnit: product.baseUnit, largeUnit: product.largeUnit, conversionRate: product.conversionRate, selectedUnit: 'base', batchMode: 'existing', // 預設選擇現有批號 (需要使用者確認/輸入) originCountry: 'TW', unit_cost: product.costPrice || 0, }; setItems(prev => [...prev, newItem]); toast.success(`已加入 ${product.name}`); }; // 新增明細行 const handleAddItem = () => { const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", code: "", baseUnit: "個", costPrice: 0 }; const newItem: InboundItem = { tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, productId: defaultProduct.id, productName: defaultProduct.name, quantity: 0, unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱 baseUnit: defaultProduct.baseUnit, largeUnit: defaultProduct.largeUnit, conversionRate: defaultProduct.conversionRate, selectedUnit: 'base', batchMode: 'existing', // 預設選擇現有批號 originCountry: 'TW', unit_cost: defaultProduct.costPrice || 0, }; setItems([...items, newItem]); }; // 刪除明細行 const handleRemoveItem = (tempId: string) => { setItems(items.filter((item) => item.tempId !== tempId)); }; // 更新明細行 const handleUpdateItem = (tempId: string, updates: Partial) => { setItems( items.map((item) => item.tempId === tempId ? { ...item, ...updates } : item ) ); }; // 處理商品變更 const handleProductChange = (tempId: string, productId: string) => { const product = products.find((p) => p.id === productId); if (product) { handleUpdateItem(tempId, { productId, productName: product.name, unit: product.baseUnit, baseUnit: product.baseUnit, largeUnit: product.largeUnit, conversionRate: product.conversionRate, selectedUnit: 'base', batchMode: 'existing', inventoryId: undefined, // 清除已選擇的批號 expiryDate: undefined, unit_cost: product.costPrice || 0, }); } }; // 驗證表單 const validateForm = (): boolean => { const newErrors: Record = {}; if (!reason) { newErrors.reason = "請選擇入庫原因"; } if (reason === "其他" && !notes.trim()) { newErrors.notes = "原因為「其他」時,備註為必填"; } if (items.length === 0) { newErrors.items = "請至少新增一筆庫存明細"; } items.forEach((item, index) => { if (!item.productId) { newErrors[`item-${index}-product`] = "請選擇商品"; } if (item.quantity <= 0) { newErrors[`item-${index}-quantity`] = "數量必須大於 0"; } if (item.batchMode === 'existing' && !item.inventoryId) { newErrors[`item-${index}-batch`] = "請選擇批號"; } if (item.batchMode === 'new' && !item.originCountry) { newErrors[`item-${index}-country`] = "新批號必須輸入產地"; } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // 處理儲存 const handleSave = () => { if (!validateForm()) { toast.error("請檢查表單內容"); return; } router.post(`/warehouses/${warehouse.id}/inventory`, { inboundDate, reason, notes, items: items.map(item => { // 如果選擇大單位,則換算為基本單位數量 const finalQuantity = item.selectedUnit === 'large' && item.conversionRate ? item.quantity * item.conversionRate : item.quantity; return { productId: item.productId, quantity: finalQuantity, batchMode: item.batchMode, inventoryId: item.inventoryId, originCountry: item.originCountry, expiryDate: item.expiryDate, unit_cost: item.unit_cost }; }) }, { onSuccess: () => { toast.success("庫存記錄已儲存"); router.get(`/warehouses/${warehouse.id}/inventory`); }, onError: (err) => { toast.error("儲存失敗,請檢查輸入內容"); console.error(err); } }); }; // 生成批號預覽 const getBatchPreview = (productId: string | undefined, productCode: string | undefined, country: string, dateStr: string) => { if (!productCode || !productId) return "--"; try { // 直接字串處理,避免時區問題且確保與 fetchProductBatches 的 key 一致 const datePart = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr; const [yyyy, mm, dd] = datePart.split('-'); const dateFormatted = `${yyyy}${mm}${dd}`; const cacheKey = `${country}_${datePart}`; const seq = batchesCache[productId]?.nextSequences?.[cacheKey] || "XX"; return `${productCode}-${country}-${dateFormatted}-${seq}`; } catch (e) { return "--"; } }; return (
{/* 頁面標題與導航 - 已於先前任務優化 */}

新增庫存(手動入庫)

{warehouse.name} 新增庫存記錄

{/* 表單內容 */}
{/* 基本資訊區塊 */}

基本資訊

{/* 倉庫 */}
{/* 入庫日期 */}
setInboundDate(e.target.value)} className="border-gray-300 pl-9" />
{/* 入庫原因 */}
setReason(value as InboundReason)} options={INBOUND_REASONS.map((r) => ({ label: r, value: r }))} placeholder="選擇入庫原因" className="border-gray-300" /> {errors.reason && (

{errors.reason}

)}
{/* 備註 */}