大更新
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 58s

This commit is contained in:
2026-01-08 16:32:10 +08:00
parent 7848976a06
commit 0b60dab208
25 changed files with 661 additions and 392 deletions

View File

@@ -33,7 +33,9 @@ import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
interface Product {
id: string;
name: string;
unit: string;
baseUnit: string;
largeUnit?: string;
conversionRate?: number;
}
interface Props {
@@ -58,13 +60,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
// 新增明細行
const handleAddItem = () => {
const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", unit: "kg" };
const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", baseUnit: "" };
const newItem: InboundItem = {
tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
productId: defaultProduct.id,
productName: defaultProduct.name,
quantity: 0,
unit: defaultProduct.unit,
unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱
baseUnit: defaultProduct.baseUnit,
largeUnit: defaultProduct.largeUnit,
conversionRate: defaultProduct.conversionRate,
selectedUnit: 'base',
};
setItems([...items, newItem]);
};
@@ -86,11 +92,16 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
// 處理商品變更
const handleProductChange = (tempId: string, productId: string) => {
const product = products.find((p) => p.id === productId);
if (product) {
handleUpdateItem(tempId, {
productId,
productName: product.name,
unit: product.unit,
unit: product.baseUnit,
baseUnit: product.baseUnit,
largeUnit: product.largeUnit,
conversionRate: product.conversionRate,
selectedUnit: 'base',
});
}
};
@@ -135,10 +146,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
inboundDate,
reason,
notes,
items: items.map(item => ({
productId: item.productId,
quantity: item.quantity
}))
items: items.map(item => {
// 如果選擇大單位,則換算為基本單位數量
const finalQuantity = item.selectedUnit === 'large' && item.conversionRate
? item.quantity * item.conversionRate
: item.quantity;
return {
productId: item.productId,
quantity: finalQuantity
};
})
}, {
onSuccess: () => {
toast.success("庫存記錄已儲存");
@@ -296,71 +314,106 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
<span className="text-red-500">*</span>
</TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
{/* <TableHead className="w-[180px]">效期</TableHead>
<TableHead className="w-[220px]">進貨編號</TableHead> */}
<TableHead className="w-[60px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item, index) => (
<TableRow key={item.tempId}>
{/* 商品 */}
<TableCell>
<Select
value={item.productId}
onValueChange={(value) =>
handleProductChange(item.tempId, value)
}
>
<SelectTrigger className="border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
{products.map((product) => (
<SelectItem key={product.id} value={product.id}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors[`item-${index}-product`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-product`]}
</p>
)}
</TableCell>
{items.map((item, index) => {
// 計算轉換數量
const convertedQuantity = item.selectedUnit === 'large' && item.conversionRate
? item.quantity * item.conversionRate
: item.quantity;
{/* 數量 */}
<TableCell>
<Input
type="number"
min="1"
value={item.quantity || ""}
onChange={(e) =>
handleUpdateItem(item.tempId, {
quantity: parseInt(e.target.value) || 0,
})
}
className="border-gray-300"
/>
{errors[`item-${index}-quantity`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-quantity`]}
</p>
)}
</TableCell>
return (
<TableRow key={item.tempId}>
{/* 商品 */}
<TableCell>
<Select
value={item.productId}
onValueChange={(value) =>
handleProductChange(item.tempId, value)
}
>
<SelectTrigger className="border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[9999]">
{products.map((product) => (
<SelectItem key={product.id} value={product.id}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors[`item-${index}-product`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-product`]}
</p>
)}
</TableCell>
{/* 單位 */}
<TableCell>
<Input
value={item.unit}
disabled
className="bg-gray-50 border-gray-200"
/>
</TableCell>
{/* 數量 */}
<TableCell>
<Input
type="number"
min="1"
value={item.quantity || ""}
onChange={(e) =>
handleUpdateItem(item.tempId, {
quantity: parseFloat(e.target.value) || 0,
})
}
className="border-gray-300"
/>
{errors[`item-${index}-quantity`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-quantity`]}
</p>
)}
</TableCell>
{/* 效期 */}
{/* <TableCell>
{/* 單位 */}
<TableCell>
{item.largeUnit ? (
<Select
value={item.selectedUnit}
onValueChange={(value) =>
handleUpdateItem(item.tempId, {
selectedUnit: value as 'base' | 'large',
unit: value === 'base' ? item.baseUnit : item.largeUnit
})
}
>
<SelectTrigger className="border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="base">{item.baseUnit}</SelectItem>
<SelectItem value="large">{item.largeUnit}</SelectItem>
</SelectContent>
</Select>
) : (
<Input
value={item.baseUnit || "個"}
disabled
className="bg-gray-50 border-gray-200"
/>
)}
</TableCell>
{/* 轉換數量 */}
<TableCell>
<div className="flex items-center text-gray-700 font-medium bg-gray-50 px-3 py-2 rounded-md border border-gray-200">
<span>{convertedQuantity}</span>
<span className="ml-1 text-gray-500 text-sm">{item.baseUnit || "個"}</span>
</div>
</TableCell>
{/* 效期 */}
{/* <TableCell>
<div className="relative">
<Input
type="date"
@@ -375,8 +428,8 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
</div>
</TableCell> */}
{/* 批號 */}
{/* <TableCell>
{/* 批號 */}
{/* <TableCell>
<Input
value={item.batchNumber}
onChange={(e) =>
@@ -392,20 +445,21 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
)}
</TableCell> */}
{/* 刪除按鈕 */}
<TableCell>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveItem(item.tempId)}
className="hover:bg-red-50 hover:text-red-600 h-8 w-8"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
{/* 刪除按鈕 */}
<TableCell>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveItem(item.tempId)}
className="hover:bg-red-50 hover:text-red-600 h-8 w-8"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>

View File

@@ -78,9 +78,17 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
};
const handleDeleteWarehouse = (id: string) => {
if (confirm("確定要停用此倉庫嗎?\n注意刪除倉庫將連帶刪除所有庫存與紀錄")) {
router.delete(route('warehouses.destroy', id));
}
router.delete(route('warehouses.destroy', id), {
onSuccess: () => {
toast.success('倉庫已刪除');
setEditingWarehouse(null);
},
onError: (errors: any) => {
// If backend returns error bag or flash error
// Flash error is handled by AuthenticatedLayout usually via usePage props.
// But we can also check errors bag here if needed.
}
});
};
const handleAddTransferOrder = () => {

View File

@@ -63,7 +63,7 @@ export default function WarehouseInventoryPage({
// 導航至流動紀錄頁
const handleView = (inventoryId: string) => {
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventory: inventoryId }));
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId }));
};
@@ -74,13 +74,17 @@ export default function WarehouseInventoryPage({
const handleDelete = () => {
if (!deleteId) return;
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventory: deleteId }), {
// 暫存 ID 以免在對話框關閉的瞬間 state 被清空
const idToDelete = deleteId;
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), {
onSuccess: () => {
toast.success("庫存記錄已刪除");
setDeleteId(null);
},
onError: () => {
toast.error("刪除失敗");
// 保持對話框開啟以便重試,或根據需要關閉
}
});
};
@@ -112,7 +116,7 @@ export default function WarehouseInventoryPage({
{/* 操作按鈕 (位於標題下方) */}
<div className="flex items-center gap-3 mb-6">
{/* 安全庫存設定按鈕 */}
<Link href={`/warehouses/${warehouse.id}/safety-stock-settings`}>
<Link href={route('warehouses.safety-stock.index', warehouse.id)}>
<Button
variant="outline"
className="button-outlined-primary"
@@ -135,7 +139,7 @@ export default function WarehouseInventoryPage({
</Button>
{/* 新增庫存按鈕 */}
<Link href={`/warehouses/${warehouse.id}/add-inventory`}>
<Link href={route('warehouses.inventory.create', warehouse.id)}>
<Button
className="button-filled-primary"
>
@@ -163,9 +167,6 @@ export default function WarehouseInventoryPage({
onDelete={confirmDelete}
/>
</div>
{/* 刪除確認對話框 */}
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
@@ -176,7 +177,12 @@ export default function WarehouseInventoryPage({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700 text-white">
<AlertDialogAction
onClick={(e) => {
handleDelete();
}}
className="bg-red-600 hover:bg-red-700 text-white"
>
</AlertDialogAction>
</AlertDialogFooter>