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 { SearchableSelect } from "@/Components/ui/searchable-select"; 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, 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]); const canEdit = can('inventory_transfer.edit'); const isReadOnly = (order.status !== 'draft' || !canEdit); const isItemsReadOnly = isReadOnly || !!order.requisition; const isVending = order.to_warehouse_type === 'vending'; // Product Selection const [availableInventory, setAvailableInventory] = useState([]); const [loadingInventory, setLoadingInventory] = useState(false); useEffect(() => { if (!isItemsReadOnly && order.from_warehouse_id) { loadInventory(); } }, [isItemsReadOnly, order.from_warehouse_id]); 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 handleAddItem = () => { setItems([ ...items, { product_id: "", product_name: "", product_code: "", batch_number: "", expiry_date: null, unit: "", quantity: "", max_quantity: 0, position: "", notes: "" } ]); }; 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 { const payload: any = { remarks: remarks, transit_warehouse_id: transitWarehouseId || '', }; if (!order.requisition) { payload.items = items; } await router.put(route('inventory.transfer.update', [order.id]), payload, { onSuccess: () => { }, onError: () => toast.error("儲存失敗,請檢查輸入"), }); } finally { setIsSaving(false); } }; // 確認出貨 / 確認過帳(無在途倉) const handlePost = () => { const payload: any = { action: 'post', transit_warehouse_id: transitWarehouseId || '', remarks: remarks, }; if (!order.requisition) { payload.items = items; } router.put(route('inventory.transfer.update', [order.id]), payload, { 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); } }); }; // 狀態 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}」轉出。

{!isItemsReadOnly && (
)}
# 商品名稱 / 代號 批號 {stockColumnTitle()} 調撥數量 單位 {isVending && 貨道} 備註 {!isItemsReadOnly && } {items.length === 0 ? ( 尚未加入商品 ) : ( items.map((item: any, index: number) => ( {index + 1} {isItemsReadOnly || item.product_id ? (
{item.product_name} {item.product_code}
) : ( { const [pid, batch] = val.split('|'); const inv = availableInventory.find(i => String(i.product_id) === pid && (i.batch_number || '') === batch); if (inv) { const newItems = [...items]; newItems[index] = { ...newItems[index], 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, max_quantity: inv.quantity, quantity: newItems[index].quantity || 1 }; setItems(newItems); } }} options={availableInventory.map(inv => ({ label: `${inv.product_code} - ${inv.product_name} ${inv.batch_number ? `(批號: ${inv.batch_number})` : ''} - 庫存: ${inv.quantity}`, value: `${inv.product_id}|${inv.batch_number || ''}` }))} placeholder={loadingInventory ? "載入庫存中..." : "搜尋名稱或代號選擇庫存"} className="w-full min-w-[200px]" /> )}
{item.batch_number || '-'}
{item.expiry_date && (
效期: {item.expiry_date}
)}
{item.product_id ? `${item.max_quantity} ${item.unit || item.unit_name || ''}` : '-'} {isItemsReadOnly ? (
{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 && ( {isItemsReadOnly ? ( {item.position} ) : ( handleUpdateItem(index, 'position', e.target.value)} placeholder="貨道..." className="h-9 w-24 text-sm font-medium" /> )} )} {isItemsReadOnly ? ( {item.notes} ) : ( handleUpdateItem(index, 'notes', e.target.value)} placeholder="備註..." className="h-9 text-sm" /> )} {!isItemsReadOnly && ( )}
)) )}
); }