Files
star-erp/resources/js/Pages/Warehouse/Inventory.tsx
sky121113 f7238c2860
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 51s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
fix: 統一 UI 按鈕樣式並新增 button-outlined-error hover 效果
- 修正 5 處硬編碼顏色樣式改用預定義按鈕類別
- 新增 button-outlined-error 的 hover 狀態(bg-red-50)
- 修正倉庫模組刪除按鈕樣式統一性
- 角色管理權限 Badge 改用標準組件
- 新增 UI 統一性規範 skill
- 修復 1 處 lint 警告(移除未使用參數)

變更檔案:
- resources/css/app.css: 新增 button-outlined-error hover 樣式
- resources/js/Components/Warehouse/WarehouseDialog.tsx
- resources/js/Pages/Admin/Role/Index.tsx
- resources/js/Pages/Warehouse/EditInventory.tsx
- resources/js/Pages/Warehouse/Inventory.tsx
- resources/js/Pages/Warehouse/SafetyStockSettings.tsx
- .agent/skills/ui-consistency/SKILL.md (新增)
2026-01-14 11:31:36 +08:00

203 lines
8.5 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, useMemo } from "react";
import { ArrowLeft, PackagePlus, AlertTriangle, Shield, Boxes } from "lucide-react";
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router } from "@inertiajs/react";
import { Warehouse, WarehouseInventory, SafetyStockSetting, Product } from "@/types/warehouse";
import InventoryToolbar from "@/Components/Warehouse/Inventory/InventoryToolbar";
import InventoryTable from "@/Components/Warehouse/Inventory/InventoryTable";
import { calculateLowStockCount } from "@/utils/inventory";
import { toast } from "sonner";
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/Components/ui/alert-dialog";
import { Can } from "@/Components/Permission/Can";
// 庫存頁面 Props
interface Props {
warehouse: Warehouse;
inventories: WarehouseInventory[];
safetyStockSettings: SafetyStockSetting[];
availableProducts: Product[];
}
export default function WarehouseInventoryPage({
warehouse,
inventories,
safetyStockSettings,
availableProducts,
}: Props) {
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const [deleteId, setDeleteId] = useState<string | null>(null);
// 篩選庫存列表
const filteredInventories = useMemo(() => {
return inventories.filter((item) => {
// 搜尋條件:匹配商品名稱、編號或批號
const matchesSearch = !searchTerm ||
item.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(item.productCode && item.productCode.toLowerCase().includes(searchTerm.toLowerCase())) ||
item.batchNumber.toLowerCase().includes(searchTerm.toLowerCase());
// 類型篩選 (需要比對 availableProducts 找到類型)
let matchesType = true;
if (typeFilter !== "all") {
const product = availableProducts.find((p) => p.id === item.productId);
matchesType = product?.type === typeFilter;
}
return matchesSearch && matchesType;
});
}, [inventories, searchTerm, typeFilter, availableProducts]);
// 計算統計資訊
const lowStockItems = calculateLowStockCount(inventories, warehouse.id, safetyStockSettings);
// 導航至流動紀錄頁
const handleView = (inventoryId: string) => {
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId }));
};
const confirmDelete = (inventoryId: string) => {
setDeleteId(inventoryId);
};
const handleDelete = () => {
if (!deleteId) return;
// 暫存 ID 以免在對話框關閉的瞬間 state 被清空
const idToDelete = deleteId;
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), {
onSuccess: () => {
toast.success("庫存記錄已刪除");
setDeleteId(null);
},
onError: () => {
toast.error("刪除失敗");
// 保持對話框開啟以便重試,或根據需要關閉
}
});
};
return (
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name)}>
<Head title={`庫存管理 - ${warehouse.name}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* 頁面標題與導航 */}
<div className="mb-6">
<Link href="/warehouses">
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Boxes className="h-6 w-6 text-[#01ab83]" />
- {warehouse.name}
</h1>
<p className="text-gray-500 mt-1"></p>
</div>
</div>
</div>
{/* 操作按鈕 (位於標題下方) */}
<div className="flex items-center gap-3 mb-6">
{/* 安全庫存設定按鈕 */}
<Can permission="inventory.safety_stock">
<Link href={route('warehouses.safety-stock.index', warehouse.id)}>
<Button
variant="outline"
className="button-outlined-primary"
>
<Shield className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
{/* 庫存警告顯示 */}
<Button
variant="outline"
className={`button-outlined-primary cursor-default hover:bg-transparent ${lowStockItems > 0
? "border-orange-500 text-orange-600"
: "border-green-500 text-green-600"
}`}
>
<AlertTriangle className="mr-2 h-4 w-4" />
{lowStockItems}
</Button>
{/* 新增庫存按鈕 */}
<Can permission="inventory.adjust">
<Link href={route('warehouses.inventory.create', warehouse.id)}>
<Button
className="button-filled-primary"
>
<PackagePlus className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
</div>
{/* 篩選工具列 */}
<div className="mb-6 bg-white rounded-lg shadow-sm border p-4">
<InventoryToolbar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
typeFilter={typeFilter}
onTypeFilterChange={setTypeFilter}
/>
</div>
{/* 庫存表格 */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<InventoryTable
inventories={filteredInventories}
onView={handleView}
onDelete={confirmDelete}
/>
</div>
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete();
}}
className="button-filled-error"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</AuthenticatedLayout>
);
}