Files
star-erp/resources/js/Pages/Inventory/Transfer/Show.tsx

347 lines
18 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from "react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, router, usePage, Link } 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}
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="container mx-auto p-6 max-w-7xl">
<div className="mb-6">
<Link href={route('inventory.transfer.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
調
</Button>
</Link>
<div className="flex justify-between items-center">
<h2 className="font-bold text-2xl text-gray-800 leading-tight flex items-center gap-2">
調 ({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>
</div>
<div className="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>
);
}