Files
star-erp/resources/js/Pages/Inventory/Transfer/Show.tsx
sky121113 4fa87925a2
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m8s
UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
2026-02-13 13:16:05 +08:00

831 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string | null>(order.transit_warehouse_id || null);
const [isSaving, setIsSaving] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(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<any[]>([]);
const [loadingInventory, setLoadingInventory] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedInventory, setSelectedInventory] = useState<string[]>([]); // 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<string, { variant: "success" | "warning" | "neutral" | "destructive" | "info", label: string }> = {
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 <StatusBadge variant={config.variant}>{config.label}</StatusBadge>;
};
// 過帳時庫存欄標題
const stockColumnTitle = () => {
if (order.status === 'completed' || order.status === 'dispatched') return '出貨時庫存';
return '可用庫存';
};
return (
<AuthenticatedLayout
breadcrumbs={backNav.breadcrumbs as any}
>
<Head title={`調撥單 ${order.doc_no}`} />
<div className="container mx-auto p-6 max-w-7xl animate-in fade-in duration-500 space-y-6">
<div>
<Link href={backNav.href}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
{backNav.label}
</Button>
</Link>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<ArrowLeftRight className="h-6 w-6 text-primary-main" />
調: {order.doc_no}
</h1>
{renderStatusBadge()}
</div>
<p className="text-sm text-gray-500 mt-1 font-medium">
: {order.from_warehouse_name} <ArrowLeftRight className="inline-block h-3 w-3 mx-1" /> : {order.to_warehouse_name} <span className="mx-2">|</span> : {order.created_by}
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
onClick={() => window.print()}
>
<Printer className="w-4 h-4 mr-2" />
</Button>
{/* 草稿狀態:儲存 + 出貨/過帳 + 刪除 */}
{!isReadOnly && (
<div className="flex items-center gap-2">
<Can permission="inventory_transfer.delete">
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" className="button-outlined-error" onClick={() => setDeleteId(order.id)}>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>調</AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="button-filled-error"></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Can>
<Can permission="inventory_transfer.edit">
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
onClick={handleSave}
disabled={isSaving}
>
<Save className="w-4 h-4 mr-2" />
稿
</Button>
<AlertDialog open={isPostDialogOpen} onOpenChange={setIsPostDialogOpen}>
<AlertDialogTrigger asChild>
<Button
size="sm"
className="button-filled-primary"
disabled={items.length === 0 || isSaving}
>
{hasTransit ? (
<><Truck className="w-4 h-4 mr-2" /></>
) : (
<><CheckCircle className="w-4 h-4 mr-2" /></>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{hasTransit ? '確定要出貨嗎?' : '確定要過帳嗎?'}
</AlertDialogTitle>
<AlertDialogDescription>
{hasTransit ? (
<>{order.from_warehouse_name}{selectedTransitWarehouse?.name || order.transit_warehouse_name}調</>
) : (
<>{order.from_warehouse_name}{order.to_warehouse_name}</>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handlePost} className="button-filled-primary">
{hasTransit ? '確認出貨' : '確認過帳'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Can>
</div>
)}
{/* 已出貨狀態:確認收貨按鈕 */}
{order.status === 'dispatched' && (
<Can permission="inventory_transfer.edit">
<AlertDialog open={isReceiveDialogOpen} onOpenChange={setIsReceiveDialogOpen}>
<AlertDialogTrigger asChild>
<Button
size="sm"
className="bg-green-600 hover:bg-green-700 text-white"
>
<PackageCheck className="w-4 h-4 mr-2" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{order.transit_warehouse_name}{order.to_warehouse_name}調
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleReceive} className="bg-green-600 hover:bg-green-700 text-white"></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Can>
)}
</div>
</div>
</div>
{/* 在途倉資訊卡片 */}
{(hasTransit || transitWarehouses.length > 0) && (
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center gap-2">
<Truck className="h-5 w-5 text-orange-500" />
<Label className="text-gray-700 font-semibold text-base"></Label>
</div>
{order.status === 'draft' && canEdit ? (
/* 草稿狀態:可選擇在途倉 */
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<Select
value={transitWarehouseId || ''}
onValueChange={(v) => setTransitWarehouseId(v === 'none' ? null : v)}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="不使用在途倉(直接過帳)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">使</SelectItem>
{transitWarehouses.map((w) => (
<SelectItem key={w.id} value={w.id}>
{w.name} {w.license_plate ? `(${w.license_plate})` : ''}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedTransitWarehouse && (
<>
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-medium text-gray-700 p-2 bg-gray-50 rounded border">
{selectedTransitWarehouse.license_plate || '-'}
</div>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-medium text-gray-700 p-2 bg-gray-50 rounded border">
{selectedTransitWarehouse.driver_name || '-'}
</div>
</div>
</>
)}
</div>
) : hasTransit ? (
/* 非草稿狀態:唯讀顯示在途倉資訊 */
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-semibold text-gray-700">{order.transit_warehouse_name}</div>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-medium text-gray-700">{order.transit_warehouse_plate || '-'}</div>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-medium text-gray-700">{order.transit_warehouse_driver || '-'}</div>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block"></Label>
<div className="text-sm font-medium">
{order.status === 'dispatched' && (
<span className="text-orange-600">{order.dispatched_at}</span>
)}
{order.status === 'completed' && (
<span className="text-green-600">{order.received_at}</span>
)}
</div>
</div>
</div>
) : null}
{/* 顯示時間軸(已出貨或已完成時) */}
{(order.dispatched_at || order.received_at) && (
<div className="border-t pt-3 mt-3 flex flex-wrap gap-6 text-sm text-gray-500">
{order.dispatched_at && (
<div className="flex items-center gap-1.5">
<Truck className="h-3.5 w-3.5 text-orange-400" />
<span>{order.dispatched_at}</span>
<span className="text-gray-400">({order.dispatched_by})</span>
</div>
)}
{order.received_at && (
<div className="flex items-center gap-1.5">
<PackageCheck className="h-3.5 w-3.5 text-green-500" />
<span>{order.received_at}</span>
<span className="text-gray-400">({order.received_by})</span>
</div>
)}
</div>
)}
</div>
)}
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center justify-between">
<Label className="text-gray-500 font-semibold"></Label>
</div>
{isReadOnly ? (
<div className="text-gray-700 p-2 bg-gray-50 rounded border text-sm min-h-[40px]">
{order.remarks || '無備註'}
</div>
) : (
<Input
value={remarks || ""}
onChange={(e) => setRemarks(e.target.value)}
className="h-9 focus:ring-primary-main"
placeholder="填寫調撥單備註..."
/>
)}
</div>
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-lg text-grey-900">調</h3>
<p className="text-sm text-grey-500">
調{order.from_warehouse_name}
</p>
</div>
{!isReadOnly && (
<div className="flex gap-2">
<Button variant="outline" className="button-outlined-primary" onClick={() => setIsImportDialogOpen(true)}>
<Package className="h-4 w-4 mr-2" />
Excel
</Button>
<TransferImportDialog
open={isImportDialogOpen}
onOpenChange={setIsImportDialogOpen}
orderId={order.id}
/>
<Dialog open={isProductDialogOpen} onOpenChange={setIsProductDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="button-outlined-primary">
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col p-6">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<DialogTitle className="text-xl"> ({order.from_warehouse_name})</DialogTitle>
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-grey-3" />
<Input
placeholder="搜尋品名、代號或條碼..."
className="pl-9 h-9 border-2 border-grey-3 focus:ring-primary-main"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</DialogHeader>
<div className="flex-1 overflow-auto pr-1">
{loadingInventory ? (
<div className="text-center py-12">
<Package className="h-10 w-10 animate-bounce mx-auto text-gray-300 mb-2" />
<p className="text-grey-2 text-sm">...</p>
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader className="bg-gray-50/80 sticky top-0 z-10 shadow-sm">
<TableRow>
<TableHead className="w-[50px] text-center">
<Checkbox
checked={availableInventory.length > 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()}
/>
</TableHead>
<TableHead className="font-medium text-grey-600"> / </TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="text-right font-medium text-grey-600 pr-6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(() => {
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 (
<TableRow>
<TableCell colSpan={5} className="text-center py-12 text-grey-3 italic font-medium">
{searchQuery ? `找不到與 "${searchQuery}" 相關的商品` : '尚無庫存資料'}
</TableCell>
</TableRow>
);
}
return filtered.map((inv) => {
const key = `${inv.product_id}-${inv.batch_number}`;
const isSelected = selectedInventory.includes(key);
return (
<TableRow
key={key}
className={`hover:bg-primary-lightest/20 cursor-pointer transition-colors ${isSelected ? 'bg-primary-lightest/40' : ''}`}
onClick={() => toggleSelect(key)}
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleSelect(key)}
/>
</TableCell>
<TableCell className="py-3">
<div className="flex flex-col">
<span className="font-semibold text-grey-0">{inv.product_name}</span>
<span className="text-xs text-grey-2 font-mono">{inv.product_code}</span>
</div>
</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.batch_number || '-'}</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.expiry_date || '-'}</TableCell>
<TableCell className="text-right font-bold text-primary-main pr-6">{inv.quantity} {inv.unit_name}</TableCell>
</TableRow>
);
});
})()}
</TableBody>
</Table>
</div>
)}
</div>
<div className="mt-6 flex items-center justify-between border-t pt-4">
<div className="flex items-center gap-3">
<div className="px-3 py-1 bg-primary-lightest/50 border border-primary-light/20 rounded-full text-sm font-medium text-primary-main animate-in zoom-in duration-200">
{selectedInventory.length}
</div>
{selectedInventory.length > 0 && (
<Button
variant="ghost"
size="sm"
className="text-grey-3 hover:text-red-500 hover:bg-red-50 text-xs px-2 h-7"
onClick={() => setSelectedInventory([])}
>
</Button>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="button-outlined-primary w-24"
onClick={() => setIsProductDialogOpen(false)}
>
</Button>
<Button
className="button-filled-primary min-w-32"
disabled={selectedInventory.length === 0}
onClick={handleAddSelected}
>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)}
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center font-medium text-grey-600">#</TableHead>
<TableHead className="font-medium text-grey-600"> / </TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="text-right w-32 font-medium text-grey-600">
{stockColumnTitle()}
</TableHead>
<TableHead className="text-right w-40 font-medium text-grey-600">調</TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
{isVending && <TableHead className="font-medium text-grey-600"></TableHead>}
<TableHead className="font-medium text-grey-600"></TableHead>
{!isReadOnly && <TableHead className="w-[50px]"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={isVending ? 9 : 8} className="text-center h-24 text-gray-500">
</TableCell>
</TableRow>
) : (
items.map((item: any, index: number) => (
<TableRow key={index}>
<TableCell className="text-center text-gray-500 font-medium">{index + 1}</TableCell>
<TableCell className="py-3">
<div className="flex flex-col">
<span className="font-semibold text-gray-900">{item.product_name}</span>
<span className="text-xs text-gray-500 font-mono">{item.product_code}</span>
</div>
</TableCell>
<TableCell className="text-sm font-mono">
<div>{item.batch_number || '-'}</div>
{item.expiry_date && (
<div className="text-xs text-gray-400 mt-1">
: {item.expiry_date}
</div>
)}
</TableCell>
<TableCell className="text-right font-semibold text-primary-main">
{item.max_quantity} {item.unit || item.unit_name}
</TableCell>
<TableCell className="px-1 py-3">
{isReadOnly ? (
<div className="text-right font-semibold mr-2">{item.quantity}</div>
) : (
<div className="flex flex-col gap-1 items-end pr-2">
<Input
type="number"
min="0.01"
step="any"
value={item.quantity ?? ""}
onChange={(e) => handleUpdateItem(index, 'quantity', e.target.value)}
className="h-9 w-32 font-medium focus:ring-primary-main text-right"
/>
</div>
)}
</TableCell>
<TableCell className="text-sm text-gray-500">{item.unit || item.unit_name}</TableCell>
{isVending && (
<TableCell className="px-1">
{isReadOnly ? (
<span className="text-sm font-medium">{item.position}</span>
) : (
<Input
value={item.position || ""}
onChange={(e) => handleUpdateItem(index, 'position', e.target.value)}
placeholder="貨道..."
className="h-9 w-24 text-sm font-medium"
/>
)}
</TableCell>
)}
<TableCell className="px-1">
{isReadOnly ? (
<span className="text-sm text-gray-600">{item.notes}</span>
) : (
<Input
value={item.notes || ""}
onChange={(e) => handleUpdateItem(index, 'notes', e.target.value)}
placeholder="備註..."
className="h-9 text-sm"
/>
)}
</TableCell>
{!isReadOnly && (
<TableCell className="text-center">
<Button variant="outline" size="icon" className="button-outlined-error h-8 w-8" onClick={() => handleRemoveItem(index)}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}