import { useState, useEffect, useMemo } from "react"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router, Link, usePage } from "@inertiajs/react"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/Components/ui/table"; import { StatusBadge } from "@/Components/shared/StatusBadge"; import { Checkbox } from "@/Components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/Components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/Components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/Components/ui/select"; import { Plus, Save, Trash2, ArrowLeft, CheckCircle, Package, ArrowLeftRight, Printer, Search, Truck, PackageCheck } from "lucide-react"; import { toast } from "sonner"; import axios from "axios"; import { Can } from '@/Components/Permission/Can'; import { usePermission } from '@/hooks/usePermission'; import TransferImportDialog from '@/Components/Transfer/TransferImportDialog'; interface TransitWarehouse { id: string; name: string; license_plate: string | null; driver_name: string | null; } export default function Show({ order, transitWarehouses = [] }: { order: any; transitWarehouses?: TransitWarehouse[] }) { const { can } = usePermission(); const { url } = usePage(); // 解析 URL query 參數,判斷使用者從哪裡來 const backNav = useMemo(() => { const params = new URLSearchParams(url.split('?')[1] || ''); const from = params.get('from'); if (from === 'requisition') { const fromId = params.get('from_id'); const fromDoc = params.get('from_doc') || ''; return { href: route('store-requisitions.show', [fromId!]), label: `返回叫貨單: ${decodeURIComponent(fromDoc)}`, breadcrumbs: [ { label: '商品與庫存管理', href: '#' }, { label: '門市叫貨申請', href: route('store-requisitions.index') }, { label: `叫貨單: ${decodeURIComponent(fromDoc)}`, href: route('store-requisitions.show', [fromId!]) }, { label: `調撥單: ${order.doc_no}`, href: route('inventory.transfer.show', [order.id]), isPage: true }, ], }; } return { href: route('inventory.transfer.index'), label: '返回調撥單列表', breadcrumbs: [ { label: '商品與庫存管理', href: '#' }, { label: '庫存調撥', href: route('inventory.transfer.index') }, { label: `調撥單: ${order.doc_no}`, href: route('inventory.transfer.show', [order.id]), isPage: true }, ], }; }, [url, order]); const [items, setItems] = useState(order.items || []); const [remarks, setRemarks] = useState(order.remarks || ""); // 狀態初始化 const [transitWarehouseId, setTransitWarehouseId] = useState(order.transit_warehouse_id || null); const [isSaving, setIsSaving] = useState(false); const [deleteId, setDeleteId] = useState(null); const [isPostDialogOpen, setIsPostDialogOpen] = useState(false); const [isReceiveDialogOpen, setIsReceiveDialogOpen] = useState(false); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); // 判斷是否有在途倉流程 (包含前端暫選的) const hasTransit = !!transitWarehouseId; // 取得選中的在途倉資訊 const selectedTransitWarehouse = transitWarehouses.find(w => w.id === transitWarehouseId); // 當 order prop 變動時 (例如匯入後 router.reload),同步更新內部狀態 useEffect(() => { if (order) { setItems(order.items || []); setRemarks(order.remarks || ""); setTransitWarehouseId(order.transit_warehouse_id || null); } }, [order]); // Product Selection const [isProductDialogOpen, setIsProductDialogOpen] = useState(false); const [availableInventory, setAvailableInventory] = useState([]); const [loadingInventory, setLoadingInventory] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [selectedInventory, setSelectedInventory] = useState([]); // product_id-batch useEffect(() => { if (isProductDialogOpen) { loadInventory(); setSelectedInventory([]); setSearchQuery(''); } }, [isProductDialogOpen]); const loadInventory = async () => { setLoadingInventory(true); try { const response = await axios.get(route('api.warehouses.inventories', order.from_warehouse_id)); setAvailableInventory(response.data); } catch (error) { console.error("Failed to load inventory", error); toast.error("無法載入庫存資料"); } finally { setLoadingInventory(false); } }; const toggleSelect = (key: string) => { setSelectedInventory(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] ); }; const toggleSelectAll = () => { const filtered = availableInventory.filter(inv => inv.product_name.toLowerCase().includes(searchQuery.toLowerCase()) || inv.product_code.toLowerCase().includes(searchQuery.toLowerCase()) || (inv.product_barcode && inv.product_barcode.toLowerCase().includes(searchQuery.toLowerCase())) ); const filteredKeys = filtered.map(inv => `${inv.product_id}-${inv.batch_number}`); if (filteredKeys.length > 0 && filteredKeys.every(k => selectedInventory.includes(k))) { setSelectedInventory(prev => prev.filter(k => !filteredKeys.includes(k))); } else { setSelectedInventory(prev => Array.from(new Set([...prev, ...filteredKeys]))); } }; const handleAddSelected = () => { if (selectedInventory.length === 0) return; const newItems = [...items]; let addedCount = 0; availableInventory.forEach(inv => { const key = `${inv.product_id}-${inv.batch_number}`; if (selectedInventory.includes(key)) { newItems.push({ product_id: inv.product_id, product_name: inv.product_name, product_code: inv.product_code, batch_number: inv.batch_number, expiry_date: inv.expiry_date, unit: inv.unit_name, quantity: 1, max_quantity: inv.quantity, notes: "", }); addedCount++; } }); setItems(newItems); setIsProductDialogOpen(false); if (addedCount > 0) { toast.success(`已成功加入 ${addedCount} 個項目`); } }; const handleUpdateItem = (index: number, field: string, value: any) => { const newItems = [...items]; newItems[index][field] = value; setItems(newItems); }; const handleRemoveItem = (index: number) => { const newItems = items.filter((_: any, i: number) => i !== index); setItems(newItems); }; const handleSave = async () => { setIsSaving(true); try { await router.put(route('inventory.transfer.update', [order.id]), { items: items, remarks: remarks, transit_warehouse_id: transitWarehouseId || '', }, { onSuccess: () => { }, onError: () => toast.error("儲存失敗,請檢查輸入"), }); } finally { setIsSaving(false); } }; // 確認出貨 / 確認過帳(無在途倉) // 確認出貨 / 確認過帳(無在途倉) const handlePost = () => { router.put(route('inventory.transfer.update', [order.id]), { action: 'post', transit_warehouse_id: transitWarehouseId || '', items: items, remarks: remarks, }, { onSuccess: () => { setIsPostDialogOpen(false); }, onError: (errors) => { const message = Object.values(errors).join('\n') || "操作失敗,請檢查輸入或庫存狀態"; toast.error(message); setIsPostDialogOpen(false); } }); }; // 確認收貨 const handleReceive = () => { router.put(route('inventory.transfer.update', [order.id]), { action: 'receive' }, { onSuccess: () => { setIsReceiveDialogOpen(false); }, onError: (errors) => { const message = Object.values(errors).join('\n') || "收貨失敗"; toast.error(message); setIsReceiveDialogOpen(false); } }); }; const handleDelete = () => { router.delete(route('inventory.transfer.destroy', [order.id]), { onSuccess: () => { setDeleteId(null); } }); }; const canEdit = can('inventory_transfer.edit'); const isReadOnly = (order.status !== 'draft' || !canEdit); const isVending = order.to_warehouse_type === 'vending'; // 狀態 Badge 渲染 const renderStatusBadge = () => { const statusConfig: Record = { completed: { variant: 'success', label: '已完成' }, dispatched: { variant: 'warning', label: '配送中' }, draft: { variant: 'neutral', label: '草稿' }, voided: { variant: 'destructive', label: '已作廢' }, }; const config = statusConfig[order.status] || { variant: 'neutral', label: order.status }; return {config.label}; }; // 過帳時庫存欄標題 const stockColumnTitle = () => { if (order.status === 'completed' || order.status === 'dispatched') return '出貨時庫存'; return '可用庫存'; }; return (

調撥單: {order.doc_no}

{renderStatusBadge()}

來源: {order.from_warehouse_name} 目的: {order.to_warehouse_name} | 建立人: {order.created_by}

{/* 草稿狀態:儲存 + 出貨/過帳 + 刪除 */} {!isReadOnly && (
!open && setDeleteId(null)}> 確定要刪除此調撥單嗎? 此動作無法復原。如果單據已存在重要資料,請謹慎操作。 取消 確認刪除 {hasTransit ? '確定要出貨嗎?' : '確定要過帳嗎?'} {hasTransit ? ( <>庫存將從「{order.from_warehouse_name}」移至在途倉「{selectedTransitWarehouse?.name || order.transit_warehouse_name}」,等待目的倉庫確認收貨後才完成調撥。 ) : ( <>過帳後庫存將立即從「{order.from_warehouse_name}」轉移至「{order.to_warehouse_name}」,且無法再進行修改。 )} 取消 {hasTransit ? '確認出貨' : '確認過帳'}
)} {/* 已出貨狀態:確認收貨按鈕 */} {order.status === 'dispatched' && ( 確定要確認收貨嗎? 庫存將從在途倉「{order.transit_warehouse_name}」移至目的倉庫「{order.to_warehouse_name}」,完成此次調撥流程。 取消 確認收貨 )}
{/* 在途倉資訊卡片 */} {(hasTransit || transitWarehouses.length > 0) && (
{order.status === 'draft' && canEdit ? ( /* 草稿狀態:可選擇在途倉 */
{selectedTransitWarehouse && ( <>
{selectedTransitWarehouse.license_plate || '-'}
{selectedTransitWarehouse.driver_name || '-'}
)}
) : hasTransit ? ( /* 非草稿狀態:唯讀顯示在途倉資訊 */
{order.transit_warehouse_name}
{order.transit_warehouse_plate || '-'}
{order.transit_warehouse_driver || '-'}
{order.status === 'dispatched' && ( 配送中({order.dispatched_at}) )} {order.status === 'completed' && ( 已收貨({order.received_at}) )}
) : null} {/* 顯示時間軸(已出貨或已完成時) */} {(order.dispatched_at || order.received_at) && (
{order.dispatched_at && (
出貨:{order.dispatched_at} ({order.dispatched_by})
)} {order.received_at && (
收貨:{order.received_at} ({order.received_by})
)}
)}
)}
{isReadOnly ? (
{order.remarks || '無備註'}
) : ( setRemarks(e.target.value)} className="h-9 focus:ring-primary-main" placeholder="填寫調撥單備註..." /> )}

調撥明細

請選擇要調撥的商品並輸入數量。所有商品將從「{order.from_warehouse_name}」轉出。

{!isReadOnly && (
選擇來源庫存 ({order.from_warehouse_name})
setSearchQuery(e.target.value)} />
{loadingInventory ? (

庫存資料載入中...

) : (
0 && (() => { const filtered = availableInventory.filter(inv => inv.product_name.toLowerCase().includes(searchQuery.toLowerCase()) || inv.product_code.toLowerCase().includes(searchQuery.toLowerCase()) || (inv.product_barcode && inv.product_barcode.toLowerCase().includes(searchQuery.toLowerCase())) ); const filteredKeys = filtered.map(inv => `${inv.product_id}-${inv.batch_number}`); return filteredKeys.length > 0 && filteredKeys.every(k => selectedInventory.includes(k)); })()} onCheckedChange={() => toggleSelectAll()} /> 品名 / 代號 批號 效期 現有庫存 {(() => { const filtered = availableInventory.filter(inv => inv.product_name.toLowerCase().includes(searchQuery.toLowerCase()) || inv.product_code.toLowerCase().includes(searchQuery.toLowerCase()) || (inv.product_barcode && inv.product_barcode.toLowerCase().includes(searchQuery.toLowerCase())) ); if (filtered.length === 0) { return ( {searchQuery ? `找不到與 "${searchQuery}" 相關的商品` : '尚無庫存資料'} ); } return filtered.map((inv) => { const key = `${inv.product_id}-${inv.batch_number}`; const isSelected = selectedInventory.includes(key); return ( toggleSelect(key)} > e.stopPropagation()}> toggleSelect(key)} />
{inv.product_name} {inv.product_code}
{inv.batch_number || '-'} {inv.expiry_date || '-'} {inv.quantity} {inv.unit_name}
); }); })()}
)}
已選取 {selectedInventory.length} 項商品
{selectedInventory.length > 0 && ( )}
)}
# 商品名稱 / 代號 批號 {stockColumnTitle()} 調撥數量 單位 {isVending && 貨道} 備註 {!isReadOnly && } {items.length === 0 ? ( 尚未加入商品 ) : ( items.map((item: any, index: number) => ( {index + 1}
{item.product_name} {item.product_code}
{item.batch_number || '-'}
{item.expiry_date && (
效期: {item.expiry_date}
)}
{item.max_quantity} {item.unit || item.unit_name} {isReadOnly ? (
{item.quantity}
) : (
handleUpdateItem(index, 'quantity', e.target.value)} className="h-9 w-32 font-medium focus:ring-primary-main text-right" />
)}
{item.unit || item.unit_name} {isVending && ( {isReadOnly ? ( {item.position} ) : ( handleUpdateItem(index, 'position', e.target.value)} placeholder="貨道..." className="h-9 w-24 text-sm font-medium" /> )} )} {isReadOnly ? ( {item.notes} ) : ( handleUpdateItem(index, 'notes', e.target.value)} placeholder="備註..." className="h-9 text-sm" /> )} {!isReadOnly && ( )}
)) )}
); }