/** * 新增庫存頁面(手動入庫) */ 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"; interface Product { id: string; name: string; code: string; baseUnit: string; largeUnit?: string; conversionRate?: 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 handleAddItem = () => { const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", code: "", baseUnit: "個" }; 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', }; 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, }); } }; // 驗證表單 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 }; }) }, { 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}

)}
{/* 備註 */}