import React, { useState, useEffect } from "react"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, Link, router } from "@inertiajs/react"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Textarea } from "@/Components/ui/textarea"; import { Label } from "@/Components/ui/label"; import { StatusBadge } from "@/Components/shared/StatusBadge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/Components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/Components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/Components/ui/alert-dialog"; import { toast } from "sonner"; import { Can } from "@/Components/Permission/Can"; import { usePermission } from "@/hooks/usePermission"; import { Store, SendHorizontal, CheckCircle2, XCircle, Pencil, Loader2, ArrowLeft, } from "lucide-react"; import { formatDate } from "@/lib/date"; function getStatusBadge(status: string) { const statusMap: Record = { draft: { label: "草稿", variant: "neutral" }, pending: { label: "待審核", variant: "warning" }, approved: { label: "已核准", variant: "success" }, rejected: { label: "已駁回", variant: "destructive" }, completed: { label: "已完成", variant: "success" }, cancelled: { label: "已取消", variant: "neutral" }, }; const config = statusMap[status]; if (!config) return {status}; return ( {config.label} ); } interface RequisitionItem { id: number; product_id: number; product_name: string; product_code: string; unit_name: string; requested_qty: number; approved_qty: number | null; current_stock: number; supply_stock: number | null; supply_batches?: { inventory_id: number; batch_number: string | null; position: string | null; available_qty: number; expiry_date: string | null }[]; approved_batches?: { batch_number: string | null; qty: number }[]; remark: string | null; } interface Requisition { id: number; doc_no: string; status: string; store_warehouse_id: number; store_warehouse_name: string; supply_warehouse_id: number | null; supply_warehouse_name: string; remark: string | null; reject_reason: string | null; creator_name: string; approver_name: string; submitted_at: string | null; approved_at: string | null; transfer_order_id: number | null; created_at: string; items: RequisitionItem[]; } interface Props { requisition: Requisition; warehouses: { id: number; name: string }[]; activities: any[]; } export default function Show({ requisition, warehouses }: Props) { usePermission(); const [submitting, setSubmitting] = useState(false); const [approving, setApproving] = useState(false); const [rejecting, setRejecting] = useState(false); // 核准狀態 (支援批號分配) const [approvedItems, setApprovedItems] = useState<{ id: number; batches: { inventory_id: number | null; batch_number: string | null; qty: string }[]; }[]>( requisition.items.map((item) => { // 如果有批號,預設給空的陣列讓使用者自己填寫 if (item.supply_batches && item.supply_batches.length > 0) { return { id: item.id, batches: item.supply_batches.map(b => ({ inventory_id: b.inventory_id, batch_number: b.batch_number, qty: "" })) }; } // 無批號時,退回傳統單一輸入 return { id: item.id, batches: [{ inventory_id: null, batch_number: null, qty: Number(item.requested_qty).toString() }], }; }) ); // 當 requisition.items 變動時 (例如更換供貨倉庫導致 supply_batches 更新),同步更新核准狀態 useEffect(() => { setApprovedItems( requisition.items.map((item) => { if (item.supply_batches && item.supply_batches.length > 0) { return { id: item.id, batches: item.supply_batches.map(b => ({ inventory_id: b.inventory_id, batch_number: b.batch_number, qty: "" })) }; } return { id: item.id, batches: [{ inventory_id: null, batch_number: null, qty: Number(item.requested_qty).toString() }], }; }) ); }, [requisition.items]); // 駁回狀態 const [showRejectDialog, setShowRejectDialog] = useState(false); const [rejectReason, setRejectReason] = useState(""); // 提交確認 const [showSubmitDialog, setShowSubmitDialog] = useState(false); const handleSubmit = () => { setSubmitting(true); router.post(route("store-requisitions.submit", [requisition.id]), {}, { onFinish: () => { setSubmitting(false); setShowSubmitDialog(false); }, }); }; const handleApprove = () => { // 確認每個核准數量與庫存上限 for (const item of approvedItems) { const originalItem = requisition.items.find(i => i.id === item.id); if (!originalItem) continue; for (const batch of item.batches) { if (batch.qty !== "") { const qty = parseFloat(batch.qty); if (isNaN(qty) || qty < 0) { toast.error("核准數量不能為負數或無效數字"); return; } // 檢查是否超過批號最大可用庫存 if (batch.inventory_id && originalItem.supply_batches) { const originalBatch = originalItem.supply_batches.find(b => b.inventory_id === batch.inventory_id); if (originalBatch && qty > originalBatch.available_qty) { toast.error(`「${originalItem.product_name}」批號 ${originalBatch.batch_number || '無批號'} 數量不可大於庫存上限 (${originalBatch.available_qty})`); return; } } else if (batch.inventory_id === null) { // 無批號情境:檢查總可用庫存 if (originalItem.supply_stock !== null && qty > originalItem.supply_stock) { toast.error(`「${originalItem.product_name}」數量不可大於供貨倉庫存上限 (${originalItem.supply_stock})`); return; } } } } } setApproving(true); router.post( route("store-requisitions.approve", [requisition.id]), { items: approvedItems.map((item) => { // 計算總數,提供給沒有批號情境的相容性,或作為總數參考 const totalQty = item.batches.reduce((sum, b) => sum + (parseFloat(b.qty) || 0), 0); return { id: item.id, approved_qty: totalQty, batches: item.batches.filter(b => b.qty !== "" && parseFloat(b.qty) > 0).map(b => ({ inventory_id: b.inventory_id, batch_number: b.batch_number, qty: parseFloat(b.qty) })) }; }), }, { onFinish: () => { setApproving(false); }, } ); }; const handleReject = () => { if (!rejectReason.trim()) { toast.error("請填寫駁回原因"); return; } setRejecting(true); router.post( route("store-requisitions.reject", [requisition.id]), { reject_reason: rejectReason }, { onFinish: () => { setRejecting(false); setShowRejectDialog(false); }, } ); }; const updateApprovedBatchQty = (itemId: number, inventoryId: number | null, qty: string) => { setApprovedItems(prev => prev.map(item => { if (item.id === itemId) { return { ...item, batches: item.batches.map(b => b.inventory_id === inventoryId ? { ...b, qty } : b) }; } return item; })); }; const isEditable = ["draft", "rejected"].includes(requisition.status); const isPending = requisition.status === "pending"; const canApprove = usePermission().can("store_requisitions.approve"); const handleUpdateSupplyWarehouse = (warehouseId: string) => { setSubmitting(true); router.patch( route("store-requisitions.update-supply-warehouse", [requisition.id]), { supply_warehouse_id: warehouseId }, { onFinish: () => setSubmitting(false), onSuccess: () => toast.success("供貨倉庫已更新"), preserveScroll: true, } ); }; return (
{/* 返回按鈕 */}
{/* 頁面標題與操作 */}

門市叫貨單: {requisition.doc_no}

{getStatusBadge(requisition.status)}

申請倉庫: {requisition.store_warehouse_name} | 建立者: {requisition.creator_name} | 時間: {formatDate(requisition.created_at)} {requisition.transfer_order_id && ( <> | 關聯調撥單: {requisition.transfer_order_id} )}

{/* 操作按鈕 */}
{isEditable && ( <> {requisition.status === "draft" && ( )} )} {isPending && ( <> )}
{/* 基本資訊 */}

基本資訊

供貨倉庫
{isPending && canApprove ? ( w.id !== requisition.store_warehouse_id) .map((w) => ({ label: w.name, value: w.id.toString(), }))} placeholder="選擇供貨倉庫" className="h-9 w-full max-w-[200px]" disabled={submitting} /> ) : (

{requisition.supply_warehouse_name || "-"}

)}
{requisition.submitted_at && (
提交時間

{formatDate(requisition.submitted_at)}

)} {requisition.approved_at && ( <>
審核人

{requisition.approver_name}

審核時間

{formatDate(requisition.approved_at)}

)} {requisition.remark && (
備註

{requisition.remark}

)} {requisition.reject_reason && (
駁回原因

{requisition.reject_reason}

)} {requisition.transfer_order_id && (
關聯調撥單

查看調撥單 →

)}
{/* 商品明細 */}

商品明細

# 商品編號 商品名稱 申請倉現有 {requisition.supply_warehouse_id && ( 供貨倉可用 )} 需求數量 單位 {["approved", "completed"].includes(requisition.status) && ( 核准數量 )} {isPending && canApprove && ( 核准數量 )} 備註 {requisition.items.map((item, index) => { const approvedItem = approvedItems.find((ai) => ai.id === item.id); // 判斷是否為「多批號」情境 (審核時使用) const hasBatches = item.supply_batches && item.supply_batches.length > 0; // 判斷是否含有效已核准批號 (檢視時使用) const hasApprovedBatches = item.approved_batches && item.approved_batches.some(b => b.batch_number !== null); // 計算目前填寫的核准總量 const totalApprovedQty = approvedItem ? approvedItem.batches.reduce((sum, b) => sum + (parseFloat(b.qty) || 0), 0) : 0; const isOverTotalStock = item.supply_stock !== null && totalApprovedQty > item.supply_stock; const rowClassName = (isPending && canApprove && isOverTotalStock) ? "bg-red-50/50" : ""; return ( {index + 1} {item.product_code} {item.product_name} {hasBatches && isPending && canApprove && (
需分配批號出庫 ↑
)}
{Number(item.current_stock).toLocaleString()} {requisition.supply_warehouse_id && ( {item.supply_stock !== null ? Number(item.supply_stock).toLocaleString() : "-"} )} {Number(item.requested_qty).toLocaleString()} {item.unit_name} {["approved", "completed"].includes(requisition.status) && ( {item.approved_qty !== null ? Number(item.approved_qty).toLocaleString() : "-"} )} {isPending && canApprove && ( {!hasBatches ? ( updateApprovedBatchQty(item.id, null, e.target.value) } className={`h-8 text-right w-[100px] ${isOverTotalStock ? "border-red-400 text-red-600 focus-visible:ring-red-400" : ""}`} /> ) : (
總計: {totalApprovedQty > 0 ? totalApprovedQty : "-"}
)}
)} {item.remark || "-"}
{/* 展開批號/貨道輸入子行 */} {hasBatches && isPending && canApprove && item.supply_batches!.map((batch) => { const batchInput = approvedItem?.batches.find(b => b.inventory_id === batch.inventory_id); const inputQty = parseFloat(batchInput?.qty || "0"); const isBatchOverStock = inputQty > batch.available_qty; return ( {batch.batch_number ? ( <>批號: {batch.batch_number}
) : ( <>無批號
)} {batch.position && ( <>貨道: {batch.position}
)} (庫存: {Number(batch.available_qty)})
{batch.expiry_date && 效期: {batch.expiry_date}}
updateApprovedBatchQty(item.id, batch.inventory_id, e.target.value)} placeholder="輸入數量" className={`h-7 text-right w-[100px] text-xs ${isBatchOverStock ? "border-red-400 text-red-600 focus-visible:ring-red-400 bg-red-50" : ""}`} />
); })} {/* 展開已核准批號子行 (檢視用) */} {["approved", "completed"].includes(requisition.status) && hasApprovedBatches && item.approved_batches!.filter(b => b.batch_number).map((batch, bIndex) => ( 核准批號: {batch.batch_number} {Number(batch.qty).toLocaleString()} ))}
); })}
{/* 提交確認 */} 確認提交審核? 提交後將無法修改叫貨單內容,並會通知相關人員進行審核。 取消 {submitting && } 確認提交 {/* 駁回對話框 */} 駁回叫貨單 請說明駁回原因,申請人可根據原因修改後重新提交。