style: 修正盤點與盤調畫面 Table Padding 並統一 UI 規範
This commit is contained in:
336
resources/js/Pages/Inventory/Transfer/Show.tsx
Normal file
336
resources/js/Pages/Inventory/Transfer/Show.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router, usePage } from "@inertiajs/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 {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from "@/Components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Plus, Save, Trash2, ArrowLeft, CheckCircle, Ban, History, Package } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
|
||||
export default function Show({ auth, order }) {
|
||||
const [items, setItems] = useState(order.items || []);
|
||||
const [remarks, setRemarks] = useState(order.remarks || "");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Product Selection
|
||||
const [isProductDialogOpen, setIsProductDialogOpen] = useState(false);
|
||||
const [availableInventory, setAvailableInventory] = useState([]);
|
||||
const [loadingInventory, setLoadingInventory] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isProductDialogOpen) {
|
||||
loadInventory();
|
||||
}
|
||||
}, [isProductDialogOpen]);
|
||||
|
||||
const loadInventory = async () => {
|
||||
setLoadingInventory(true);
|
||||
try {
|
||||
// Fetch inventory from SOURCE warehouse
|
||||
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 = (inventoryItem) => {
|
||||
// Check if already added
|
||||
const exists = items.find(i =>
|
||||
i.product_id === inventoryItem.product_id &&
|
||||
i.batch_number === inventoryItem.batch_number
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
toast.error("該商品與批號已在列表中");
|
||||
return;
|
||||
}
|
||||
|
||||
setItems([...items, {
|
||||
product_id: inventoryItem.product_id,
|
||||
product_name: inventoryItem.product_name,
|
||||
product_code: inventoryItem.product_code,
|
||||
batch_number: inventoryItem.batch_number,
|
||||
unit: inventoryItem.unit_name,
|
||||
quantity: 1, // Default 1
|
||||
max_quantity: inventoryItem.quantity, // Max available
|
||||
notes: "",
|
||||
}]);
|
||||
setIsProductDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleUpdateItem = (index, field, value) => {
|
||||
const newItems = [...items];
|
||||
newItems[index][field] = value;
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
const handleRemoveItem = (index) => {
|
||||
const newItems = items.filter((_, i) => i !== index);
|
||||
setItems(newItems);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await router.put(route('inventory.transfer.update', [order.id]), {
|
||||
items: items,
|
||||
remarks: remarks,
|
||||
}, {
|
||||
onSuccess: () => toast.success("儲存成功"),
|
||||
onError: () => toast.error("儲存失敗,請檢查輸入"),
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePost = () => {
|
||||
if (!confirm("確定要過帳嗎?過帳後庫存將立即轉移且無法修改。")) return;
|
||||
router.put(route('inventory.transfer.update', [order.id]), {
|
||||
action: 'post'
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!confirm("確定要刪除此草稿嗎?")) return;
|
||||
router.delete(route('inventory.transfer.destroy', [order.id]));
|
||||
};
|
||||
|
||||
const isReadOnly = order.status !== 'draft';
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout
|
||||
user={auth.user}
|
||||
header={
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-semibold text-xl text-gray-800 leading-tight flex items-center gap-2">
|
||||
<ArrowLeft className="h-5 w-5 cursor-pointer" onClick={() => router.visit(route('inventory.transfer.index'))} />
|
||||
調撥單詳情 ({order.doc_no})
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
刪除
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleSave} disabled={isSaving}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
儲存草稿
|
||||
</Button>
|
||||
<Button onClick={handlePost} disabled={items.length === 0}>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
確認過帳
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
breadcrumbs={[
|
||||
{ label: '首頁', href: '/' },
|
||||
{ label: '庫存調撥', href: route('inventory.transfer.index') },
|
||||
{ label: order.doc_no, href: route('inventory.transfer.show', [order.id]) },
|
||||
]}
|
||||
>
|
||||
<Head title={`調撥單 ${order.doc_no}`} />
|
||||
|
||||
<div className="py-12">
|
||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
{/* Header Info */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100 grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<Label className="text-gray-500">來源倉庫</Label>
|
||||
<div className="font-medium text-lg">{order.from_warehouse_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">目的倉庫</Label>
|
||||
<div className="font-medium text-lg">{order.to_warehouse_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">狀態</Label>
|
||||
<div className="mt-1">
|
||||
{order.status === 'draft' && <Badge variant="secondary">草稿</Badge>}
|
||||
{order.status === 'completed' && <Badge className="bg-green-600">已完成</Badge>}
|
||||
{order.status === 'voided' && <Badge variant="destructive">已作廢</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-500">備註</Label>
|
||||
{isReadOnly ? (
|
||||
<div className="mt-1 text-gray-700">{order.remarks || '-'}</div>
|
||||
) : (
|
||||
<Input
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="填寫備註..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">調撥明細</h3>
|
||||
{!isReadOnly && (
|
||||
<Dialog open={isProductDialogOpen} onOpenChange={setIsProductDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
加入商品
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>選擇來源庫存 ({order.from_warehouse_name})</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
{loadingInventory ? (
|
||||
<div className="text-center py-4">載入中...</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>商品代號</TableHead>
|
||||
<TableHead>品名</TableHead>
|
||||
<TableHead>批號</TableHead>
|
||||
<TableHead className="text-right">現有庫存</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{availableInventory.map((inv) => (
|
||||
<TableRow key={`${inv.product_id}-${inv.batch_number}`}>
|
||||
<TableCell>{inv.product_code}</TableCell>
|
||||
<TableCell>{inv.product_name}</TableCell>
|
||||
<TableCell>{inv.batch_number || '-'}</TableCell>
|
||||
<TableCell className="text-right">{inv.quantity} {inv.unit_name}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button size="sm" onClick={() => handleAddItem(inv)}>
|
||||
選取
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px]">#</TableHead>
|
||||
<TableHead>商品</TableHead>
|
||||
<TableHead>批號</TableHead>
|
||||
<TableHead className="w-[150px]">調撥數量</TableHead>
|
||||
<TableHead>單位</TableHead>
|
||||
<TableHead>備註</TableHead>
|
||||
{!isReadOnly && <TableHead className="w-[50px]"></TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center h-24 text-gray-500">
|
||||
尚未加入商品
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<div>{item.product_name}</div>
|
||||
<div className="text-xs text-gray-500">{item.product_code}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.batch_number || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{isReadOnly ? (
|
||||
item.quantity
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
value={item.quantity}
|
||||
onChange={(e) => handleUpdateItem(index, 'quantity', e.target.value)}
|
||||
/>
|
||||
{item.max_quantity && (
|
||||
<span className="text-xs text-gray-500">上限: {item.max_quantity}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{item.unit}</TableCell>
|
||||
<TableCell>
|
||||
{isReadOnly ? (
|
||||
item.notes
|
||||
) : (
|
||||
<Input
|
||||
value={item.notes}
|
||||
onChange={(e) => handleUpdateItem(index, 'notes', e.target.value)}
|
||||
placeholder="備註"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
{!isReadOnly && (
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleRemoveItem(index)}>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user