From f18fb169f3a5ceacbb0d2f8855172d7666116be1 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 13 Jan 2026 17:00:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B5=B1=E4=B8=80=E5=85=A8=E7=B3=BB?= =?UTF-8?q?=E7=B5=B1=E9=A0=81=E9=9D=A2=E6=A8=99=E9=A1=8C=E6=A8=A3=E5=BC=8F?= =?UTF-8?q?=E3=80=81=E5=84=AA=E5=8C=96=E5=81=B4=E9=82=8A=E6=AC=84=E8=88=87?= =?UTF-8?q?=E5=AF=A6=E4=BD=9C=E8=A7=92=E8=89=B2=E6=88=90=E5=93=A1=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Admin/RoleController.php | 46 ++++-- app/Http/Controllers/Admin/UserController.php | 4 +- bootstrap/app.php | 23 ++- ...160117_add_display_name_to_roles_table.php | 28 ++++ ...160741_add_inventory_delete_permission.php | 24 +++ ...13_162407_add_safety_stock_permissions.php | 24 +++ .../js/Components/Product/ProductTable.tsx | 70 ++++---- .../PurchaseOrder/PurchaseOrderActions.tsx | 35 ++-- .../js/Components/Vendor/VendorTable.tsx | 69 ++++---- .../Warehouse/Inventory/InventoryTable.tsx | 21 +-- .../Warehouse/SafetyStock/SafetyStockList.tsx | 35 ++-- resources/js/Layouts/AuthenticatedLayout.tsx | 62 +++++-- resources/js/Pages/Admin/Role/Create.tsx | 71 +++++--- resources/js/Pages/Admin/Role/Edit.tsx | 74 ++++++--- resources/js/Pages/Admin/Role/Index.tsx | 144 ++++++++++++----- resources/js/Pages/Admin/User/Create.tsx | 75 ++++----- resources/js/Pages/Admin/User/Edit.tsx | 60 ++++--- resources/js/Pages/Admin/User/Index.tsx | 47 +++--- resources/js/Pages/Dashboard.tsx | 7 +- resources/js/Pages/Error/403.tsx | 36 +++++ resources/js/Pages/Product/Index.tsx | 45 +++--- resources/js/Pages/PurchaseOrder/Create.tsx | 9 +- resources/js/Pages/PurchaseOrder/Index.tsx | 17 +- resources/js/Pages/PurchaseOrder/Show.tsx | 9 +- resources/js/Pages/Vendor/Index.tsx | 11 +- resources/js/Pages/Vendor/Show.tsx | 15 +- resources/js/Pages/Warehouse/AddInventory.tsx | 9 +- .../js/Pages/Warehouse/EditInventory.tsx | 19 +-- resources/js/Pages/Warehouse/Index.tsx | 33 ++-- resources/js/Pages/Warehouse/Inventory.tsx | 48 +++--- .../js/Pages/Warehouse/InventoryHistory.tsx | 19 +-- .../Pages/Warehouse/SafetyStockSettings.tsx | 70 ++++++-- routes/web.php | 151 ++++++++++++------ 33 files changed, 938 insertions(+), 472 deletions(-) create mode 100644 database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php create mode 100644 database/migrations/2026_01_13_160741_add_inventory_delete_permission.php create mode 100644 database/migrations/2026_01_13_162407_add_safety_stock_permissions.php create mode 100644 resources/js/Pages/Error/403.tsx diff --git a/app/Http/Controllers/Admin/RoleController.php b/app/Http/Controllers/Admin/RoleController.php index a986c9b..fd321e0 100644 --- a/app/Http/Controllers/Admin/RoleController.php +++ b/app/Http/Controllers/Admin/RoleController.php @@ -17,6 +17,7 @@ class RoleController extends Controller public function index() { $roles = Role::withCount('users', 'permissions') + ->with('users:id,name,username') ->orderBy('id') ->get(); @@ -44,11 +45,15 @@ class RoleController extends Controller { $validated = $request->validate([ 'name' => ['required', 'string', 'max:255', 'unique:roles,name'], + 'display_name' => ['required', 'string', 'max:255'], 'permissions' => ['array'], 'permissions.*' => ['exists:permissions,name'] ]); - $role = Role::create(['name' => $validated['name']]); + $role = Role::create([ + 'name' => $validated['name'], + 'display_name' => $validated['display_name'] + ]); if (!empty($validated['permissions'])) { $role->syncPermissions($validated['permissions']); @@ -92,11 +97,15 @@ class RoleController extends Controller $validated = $request->validate([ 'name' => ['required', 'string', 'max:255', Rule::unique('roles', 'name')->ignore($role->id)], + 'display_name' => ['required', 'string', 'max:255'], 'permissions' => ['array'], 'permissions.*' => ['exists:permissions,name'] ]); - $role->update(['name' => $validated['name']]); + $role->update([ + 'name' => $validated['name'], + 'display_name' => $validated['display_name'] + ]); if (isset($validated['permissions'])) { $role->syncPermissions($validated['permissions']); @@ -134,10 +143,15 @@ class RoleController extends Controller $grouped = []; foreach ($allPermissions as $permission) { - // 假設命名格式為 group.action (例如 products.create) $parts = explode('.', $permission->name); $group = $parts[0]; - + $action = $parts[1] ?? ''; + + // 特定權限遷移邏輯 + if ($permission->name === 'inventory.transfer') { + $group = 'warehouses'; // 調撥功能移至倉庫管理下 + } + if (!isset($grouped[$group])) { $grouped[$group] = []; } @@ -145,22 +159,34 @@ class RoleController extends Controller $grouped[$group][] = $permission; } - // 翻譯群組名稱 (可選,優化顯示) - $groupNames = [ + // 依照側邊欄順序定義 + $groupDefinitions = [ 'products' => '商品資料管理', - 'vendors' => '廠商資料管理', - 'purchase_orders' => '採購單管理', 'warehouses' => '倉庫管理', 'inventory' => '庫存管理', + 'vendors' => '廠商資料管理', + 'purchase_orders' => '採購單管理', 'users' => '使用者管理', - 'roles' => '角色權限管理', + 'roles' => '角色與權限', ]; $result = []; + foreach ($groupDefinitions as $key => $displayName) { + if (isset($grouped[$key])) { + $result[] = [ + 'key' => $key, + 'name' => $displayName, + 'permissions' => $grouped[$key] + ]; + unset($grouped[$key]); // 從待處理中移除 + } + } + + // 處理剩餘未定義在 groupDefinitions 中的群組 (安全機制) foreach ($grouped as $key => $permissions) { $result[] = [ 'key' => $key, - 'name' => $groupNames[$key] ?? ucfirst($key), + 'name' => ucfirst($key), 'permissions' => $permissions ]; } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 85f1d1f..6147253 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -31,7 +31,7 @@ class UserController extends Controller */ public function create() { - $roles = Role::pluck('name', 'id'); + $roles = Role::pluck('display_name', 'name'); return Inertia::render('Admin/User/Create', [ 'roles' => $roles @@ -71,7 +71,7 @@ class UserController extends Controller public function edit(string $id) { $user = User::with('roles')->findOrFail($id); - $roles = Role::get(['id', 'name']); + $roles = Role::get(['id', 'name', 'display_name']); return Inertia::render('Admin/User/Edit', [ 'user' => $user, diff --git a/bootstrap/app.php b/bootstrap/app.php index b5ffae3..8196c2a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,9 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Spatie\Permission\Exceptions\UnauthorizedException; +use Inertia\Inertia; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -14,7 +17,25 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); + + // 註冊 Spatie Permission 中間件別名 + $middleware->alias([ + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { - // + // 處理 Spatie Permission 的 UnauthorizedException + $exceptions->render(function (UnauthorizedException $e) { + return Inertia::render('Error/403')->toResponse(request())->setStatusCode(403); + }); + + // 處理一般的 403 HttpException + $exceptions->render(function (HttpException $e) { + if ($e->getStatusCode() === 403) { + return Inertia::render('Error/403')->toResponse(request())->setStatusCode(403); + } + }); })->create(); + diff --git a/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php b/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php new file mode 100644 index 0000000..b6f01ab --- /dev/null +++ b/database/migrations/2026_01_13_160117_add_display_name_to_roles_table.php @@ -0,0 +1,28 @@ +string('display_name')->nullable()->after('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('display_name'); + }); + } +}; diff --git a/database/migrations/2026_01_13_160741_add_inventory_delete_permission.php b/database/migrations/2026_01_13_160741_add_inventory_delete_permission.php new file mode 100644 index 0000000..b0a0069 --- /dev/null +++ b/database/migrations/2026_01_13_160741_add_inventory_delete_permission.php @@ -0,0 +1,24 @@ + 'inventory.delete', 'guard_name' => 'web']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + \Spatie\Permission\Models\Permission::where('name', 'inventory.delete')->delete(); + } +}; diff --git a/database/migrations/2026_01_13_162407_add_safety_stock_permissions.php b/database/migrations/2026_01_13_162407_add_safety_stock_permissions.php new file mode 100644 index 0000000..e79e0c9 --- /dev/null +++ b/database/migrations/2026_01_13_162407_add_safety_stock_permissions.php @@ -0,0 +1,24 @@ + 'inventory.safety_stock', 'guard_name' => 'web']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + \Spatie\Permission\Models\Permission::where('name', 'inventory.safety_stock')->delete(); + } +}; diff --git a/resources/js/Components/Product/ProductTable.tsx b/resources/js/Components/Product/ProductTable.tsx index cac8e54..35f68c6 100644 --- a/resources/js/Components/Product/ProductTable.tsx +++ b/resources/js/Components/Product/ProductTable.tsx @@ -20,8 +20,8 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/Components/ui/alert-dialog"; +import { Can } from "@/Components/Permission/Can"; import type { Product } from "@/Pages/Product/Index"; -// import BarcodeViewDialog from "@/Components/Product/BarcodeViewDialog"; interface ProductTableProps { products: Product[]; @@ -147,38 +147,42 @@ export default function ProductTable({ */} - - - - - - - - 確認刪除 - - 確定要刪除「{product.name}」嗎?此操作無法復原。 - - - - 取消 - onDelete(product.id)} - className="bg-red-600 hover:bg-red-700" - > - 刪除 - - - - + + + + + + + + + + + 確認刪除 + + 確定要刪除「{product.name}」嗎?此操作無法復原。 + + + + 取消 + onDelete(product.id)} + className="bg-red-600 hover:bg-red-700" + > + 刪除 + + + + + diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderActions.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderActions.tsx index 8029bb8..c831fd7 100644 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderActions.tsx +++ b/resources/js/Components/PurchaseOrder/PurchaseOrderActions.tsx @@ -3,6 +3,7 @@ import { Button } from "@/Components/ui/button"; import { Link, useForm } from "@inertiajs/react"; import type { PurchaseOrder } from "@/types/purchase-order"; import { toast } from "sonner"; +import { Can } from "@/Components/Permission/Can"; export function PurchaseOrderActions({ order, @@ -31,26 +32,30 @@ export function PurchaseOrderActions({ - + + + + + + - - + ); } diff --git a/resources/js/Components/Vendor/VendorTable.tsx b/resources/js/Components/Vendor/VendorTable.tsx index 1961f69..4adcf20 100644 --- a/resources/js/Components/Vendor/VendorTable.tsx +++ b/resources/js/Components/Vendor/VendorTable.tsx @@ -19,6 +19,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/Components/ui/alert-dialog"; +import { Can } from "@/Components/Permission/Can"; import type { Vendor } from "@/Pages/Vendor/Index"; interface VendorTableProps { @@ -122,38 +123,42 @@ export default function VendorTable({ > - - - - - - - - 確認刪除 - - 確定要刪除廠商「{vendor.name}」嗎?此操作無法復原。 - - - - 取消 - onDelete(vendor.id)} - className="bg-red-600 hover:bg-red-700" - > - 刪除 - - - - + + + + + + + + + + + 確認刪除 + + 確定要刪除廠商「{vendor.name}」嗎?此操作無法復原。 + + + + 取消 + onDelete(vendor.id)} + className="bg-red-600 hover:bg-red-700" + > + 刪除 + + + + + diff --git a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx index ba1358d..baacdde 100644 --- a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx +++ b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx @@ -27,6 +27,7 @@ import { Badge } from "@/Components/ui/badge"; import { WarehouseInventory } from "@/types/warehouse"; import { getSafetyStockStatus } from "@/utils/inventory"; import { formatDate } from "@/utils/format"; +import { Can } from "@/Components/Permission/Can"; interface InventoryTableProps { inventories: WarehouseInventory[]; @@ -280,15 +281,17 @@ export default function InventoryTable({ > - + + + diff --git a/resources/js/Components/Warehouse/SafetyStock/SafetyStockList.tsx b/resources/js/Components/Warehouse/SafetyStock/SafetyStockList.tsx index 6be192f..195d77f 100644 --- a/resources/js/Components/Warehouse/SafetyStock/SafetyStockList.tsx +++ b/resources/js/Components/Warehouse/SafetyStock/SafetyStockList.tsx @@ -15,6 +15,7 @@ import { Button } from "@/Components/ui/button"; import { Badge } from "@/Components/ui/badge"; import { SafetyStockSetting, WarehouseInventory } from "@/types/warehouse"; import { calculateProductTotalStock, getSafetyStockStatus } from "@/utils/inventory"; +import { Can } from "@/Components/Permission/Can"; interface SafetyStockListProps { settings: SafetyStockSetting[]; @@ -125,22 +126,24 @@ export default function SafetyStockList({
- - + + + +
diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx index f88b3ea..914f552 100644 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ b/resources/js/Layouts/AuthenticatedLayout.tsx @@ -11,7 +11,6 @@ import { Warehouse, Truck, Contact2, - FileText, LogOut, User, ChevronDown, @@ -20,7 +19,7 @@ import { Users } from "lucide-react"; import { toast, Toaster } from "sonner"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Link, usePage } from "@inertiajs/react"; import { cn } from "@/lib/utils"; import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav"; @@ -32,6 +31,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/Components/ui/dropdown-menu"; +import { usePermission } from "@/hooks/usePermission"; +import ApplicationLogo from "@/Components/ApplicationLogo"; interface MenuItem { id: string; @@ -39,6 +40,7 @@ interface MenuItem { icon?: React.ReactNode; route?: string; children?: MenuItem[]; + permission?: string | string[]; // 所需權限(單一或多個,滿足任一即可) } export default function AuthenticatedLayout({ @@ -51,6 +53,7 @@ export default function AuthenticatedLayout({ const { url, props } = usePage(); // @ts-ignore const user = props.auth?.user || { name: 'Guest', username: 'guest' }; + const { can, canAny } = usePermission(); const [isCollapsed, setIsCollapsed] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("sidebar-collapsed") === "true"; @@ -59,29 +62,34 @@ export default function AuthenticatedLayout({ }); const [isMobileOpen, setIsMobileOpen] = useState(false); - const menuItems: MenuItem[] = [ + // 完整的菜單定義(含權限配置) + const allMenuItems: MenuItem[] = [ { id: "dashboard", label: "儀表板", icon: , route: "/", + // 儀表板無需特定權限,所有登入使用者皆可存取 }, { id: "inventory-management", label: "商品與庫存管理", icon: , + permission: ["products.view", "warehouses.view"], // 滿足任一即可看到此群組 children: [ { id: "product-management", label: "商品資料管理", icon: , route: "/products", + permission: "products.view", }, { id: "warehouse-management", label: "倉庫管理", icon: , route: "/warehouses", + permission: "warehouses.view", }, ], }, @@ -89,12 +97,14 @@ export default function AuthenticatedLayout({ id: "vendor-management", label: "廠商管理", icon: , + permission: "vendors.view", children: [ { id: "vendor-list", label: "廠商資料管理", icon: , route: "/vendors", + permission: "vendors.view", }, ], }, @@ -102,12 +112,14 @@ export default function AuthenticatedLayout({ id: "purchase-management", label: "採購管理", icon: , + permission: "purchase_orders.view", children: [ { id: "purchase-order-list", - label: "管理採購單", - icon: , + label: "採購單管理", + icon: , route: "/purchase-orders", + permission: "purchase_orders.view", }, ], }, @@ -115,23 +127,53 @@ export default function AuthenticatedLayout({ id: "system-management", label: "系統管理", icon: , + permission: ["users.view", "roles.view"], children: [ { id: "user-management", label: "使用者管理", icon: , route: "/admin/users", + permission: "users.view", }, { id: "role-management", label: "角色與權限", icon: , route: "/admin/roles", + permission: "roles.view", }, ], }, ]; + // 檢查單一項目是否有權限 + const hasPermissionForItem = (item: MenuItem): boolean => { + if (!item.permission) return true; // 無指定權限則預設有權限 + if (Array.isArray(item.permission)) { + return canAny(item.permission); + } + return can(item.permission); + }; + + // 過濾菜單:移除無權限的項目,若父層所有子項目都無權限則隱藏父層 + const menuItems = useMemo(() => { + return allMenuItems + .map((item) => { + // 如果有子項目,先過濾子項目 + if (item.children && item.children.length > 0) { + const filteredChildren = item.children.filter(hasPermissionForItem); + // 若所有子項目都無權限,則隱藏整個群組 + if (filteredChildren.length === 0) return null; + return { ...item, children: filteredChildren }; + } + // 無子項目的單一選單,直接檢查權限 + if (!hasPermissionForItem(item)) return null; + return item; + }) + .filter((item): item is MenuItem => item !== null); + }, [can, canAny]); + // 初始化狀態:優先讀取 localStorage const [expandedItems, setExpandedItems] = useState(() => { try { @@ -296,7 +338,7 @@ export default function AuthenticatedLayout({ {isMobileOpen ? : } -
K
+ 小小冰室 ERP @@ -342,12 +384,14 @@ export default function AuthenticatedLayout({
{!isCollapsed && ( -
K
+ 小小冰室 ERP )} {isCollapsed && ( - K + + + )}
@@ -389,7 +433,7 @@ export default function AuthenticatedLayout({ )}>
-
K
+ 小小冰室 ERP - +
+ + {/* Header Area */} +
+ + + + +
+
+

+ + 建立新角色 +

+

+ 設定角色名稱並分配對應的操作權限 +

+
- +
+ + {/* Header Area */} +
+ + + + +
+
+

+ + 編輯角色 +

+

+ 修改角色資料與權限設定 +

+
- + + + + +
@@ -94,7 +105,7 @@ export default function RoleIndex({ roles }: Props) {
- {translateRoleName(role.name)} + {role.display_name}
@@ -106,10 +117,19 @@ export default function RoleIndex({ roles }: Props) { -
- +
+
{format(new Date(role.created_at), 'yyyy/MM/dd')} @@ -117,26 +137,30 @@ export default function RoleIndex({ roles }: Props) { {role.name !== 'super-admin' && (
- + + + + + + - - +
)}
@@ -146,6 +170,54 @@ export default function RoleIndex({ roles }: Props) {
+ + {/* 成員名單對話框 */} + !open && setSelectedRole(null)}> + + + + + {selectedRole?.display_name} - 成員名單 + + + 目前共有 {selectedRole?.users_count} 位使用者具備此角色權限 + + + +
+ {selectedRole?.users && selectedRole.users.length > 0 ? ( +
+ {selectedRole.users.map((user) => ( +
+
+
+ {user.name.charAt(0)} +
+
+

{user.name}

+

@{user.username}

+
+
+ + 查看帳號 + +
+ ))} +
+ ) : ( +
+ 暫無成員 +
+ )} +
+
+
); } diff --git a/resources/js/Pages/Admin/User/Create.tsx b/resources/js/Pages/Admin/User/Create.tsx index 882ab75..329bca6 100644 --- a/resources/js/Pages/Admin/User/Create.tsx +++ b/resources/js/Pages/Admin/User/Create.tsx @@ -8,7 +8,7 @@ import { Checkbox } from '@/Components/ui/checkbox'; import { FormEvent } from 'react'; interface Props { - roles: Record; // ID -> Name map from pluck + roles: Record; // Name (ID) -> DisplayName map from pluck } export default function UserCreate({ roles }: Props) { @@ -34,16 +34,6 @@ export default function UserCreate({ roles }: Props) { } }; - const translateRoleName = (name: string) => { - const map: Record = { - 'super-admin': '超級管理員', - 'admin': '管理員', - 'warehouse-manager': '倉庫主管', - 'purchaser': '採購人員', - 'viewer': '檢視者', - }; - return map[name] || name; - } return ( -
- - {/* Header */} -
-
-

- - 新增使用者 -

-

- 建立新帳號並設定初始密碼與角色 -

-
-
- - - +
+ + {/* Header Area */} +
+ + + + +
+
+

+ + 新增使用者 +

+

+ 建立新帳號並設定初始密碼與角色 +

+
- +
+ + {/* Header Area */} +
+ + + + +
+
+

+ + 編輯使用者 +

+

+ 修改使用者資料、重設密碼或變更角色 +

+
- - - + + + + +
@@ -151,25 +154,29 @@ export default function UserIndex({ users }: Props) {
- + + + + + + - - +
diff --git a/resources/js/Pages/Dashboard.tsx b/resources/js/Pages/Dashboard.tsx index b94e6c1..5d53501 100644 --- a/resources/js/Pages/Dashboard.tsx +++ b/resources/js/Pages/Dashboard.tsx @@ -79,8 +79,11 @@ export default function Dashboard({ stats }: Props) {
-

系統總覽

-

歡迎回來,這是您的小小冰室 ERP 營運數據概況。

+

+ + 系統總覽 +

+

歡迎回來,這是您的小小冰室 ERP 營運數據概況。

{/* 主要數據卡片 */} diff --git a/resources/js/Pages/Error/403.tsx b/resources/js/Pages/Error/403.tsx new file mode 100644 index 0000000..93ca739 --- /dev/null +++ b/resources/js/Pages/Error/403.tsx @@ -0,0 +1,36 @@ +import { Link } from "@inertiajs/react"; +import { ShieldAlert, Home } from "lucide-react"; + +export default function Error403() { + return ( +
+
+ {/* 圖示 */} +
+
+ +
+
+ + {/* 標題 */} +

+ 無此權限 +

+ + {/* 說明 */} +

+ 您沒有存取此頁面的權限,請洽系統管理員。 +

+ + {/* 返回按鈕 */} + + + 返回首頁 + +
+
+ ); +} diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx index cb8f0f7..10808ca 100644 --- a/resources/js/Pages/Product/Index.tsx +++ b/resources/js/Pages/Product/Index.tsx @@ -12,6 +12,7 @@ import { Head, router } from "@inertiajs/react"; import { debounce } from "lodash"; import Pagination from "@/Components/shared/Pagination"; import { getBreadcrumbs } from "@/utils/breadcrumb"; +import { Can } from "@/Components/Permission/Can"; export interface Category { id: number; @@ -217,26 +218,32 @@ export default function ProductManagement({ products, categories, units, filters className="w-full md:w-[180px]" /> - {/* Add Button */} + {/* Action Buttons */}
- - - + + + + + + + + +
diff --git a/resources/js/Pages/PurchaseOrder/Create.tsx b/resources/js/Pages/PurchaseOrder/Create.tsx index 0d22f0f..0cf103f 100644 --- a/resources/js/Pages/PurchaseOrder/Create.tsx +++ b/resources/js/Pages/PurchaseOrder/Create.tsx @@ -2,7 +2,7 @@ * 建立/編輯採購單頁面 */ -import { ArrowLeft, Plus, Info } from "lucide-react"; +import { ArrowLeft, Plus, Info, ShoppingCart } from "lucide-react"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Textarea } from "@/Components/ui/textarea"; @@ -173,8 +173,11 @@ export default function CreatePurchaseOrder({
-

{order ? "編輯採購單" : "建立採購單"}

-

+

+ + {order ? "編輯採購單" : "建立採購單"} +

+

{order ? `修改採購單 ${order.poNumber} 的詳細資訊` : "填寫新採購單的資訊以開始流程"}

diff --git a/resources/js/Pages/PurchaseOrder/Index.tsx b/resources/js/Pages/PurchaseOrder/Index.tsx index f291f89..b295e4a 100644 --- a/resources/js/Pages/PurchaseOrder/Index.tsx +++ b/resources/js/Pages/PurchaseOrder/Index.tsx @@ -14,6 +14,7 @@ import type { PurchaseOrder } from "@/types/purchase-order"; import { debounce } from "lodash"; import Pagination from "@/Components/shared/Pagination"; import { getBreadcrumbs } from "@/utils/breadcrumb"; +import { Can } from "@/Components/Permission/Can"; interface Props { orders: { @@ -101,13 +102,15 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop

- + + +
diff --git a/resources/js/Pages/PurchaseOrder/Show.tsx b/resources/js/Pages/PurchaseOrder/Show.tsx index 23eed22..adda90b 100644 --- a/resources/js/Pages/PurchaseOrder/Show.tsx +++ b/resources/js/Pages/PurchaseOrder/Show.tsx @@ -2,7 +2,7 @@ * 查看採購單詳情頁面 */ -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, ShoppingCart } from "lucide-react"; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, Link } from "@inertiajs/react"; @@ -37,8 +37,11 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
-

查看採購單

-

單號:{order.poNumber}

+

+ + 查看採購單 +

+

單號:{order.poNumber}

diff --git a/resources/js/Pages/Vendor/Index.tsx b/resources/js/Pages/Vendor/Index.tsx index 640d4a8..83fb241 100644 --- a/resources/js/Pages/Vendor/Index.tsx +++ b/resources/js/Pages/Vendor/Index.tsx @@ -8,6 +8,7 @@ import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router } from "@inertiajs/react"; import { debounce } from "lodash"; import { getBreadcrumbs } from "@/utils/breadcrumb"; +import { Can } from "@/Components/Permission/Can"; export interface Vendor { id: number; @@ -160,10 +161,12 @@ export default function VendorManagement({ vendors, filters }: PageProps) {
{/* Add Button */} - + + +
diff --git a/resources/js/Pages/Vendor/Show.tsx b/resources/js/Pages/Vendor/Show.tsx index 8362499..d604218 100644 --- a/resources/js/Pages/Vendor/Show.tsx +++ b/resources/js/Pages/Vendor/Show.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Head, Link, router } from "@inertiajs/react"; -import { Phone, Mail, Plus, ArrowLeft } from "lucide-react"; +import { Phone, Mail, Plus, ArrowLeft, Contact2 } from "lucide-react"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Label } from "@/Components/ui/label"; import { Button } from "@/Components/ui/button"; @@ -140,10 +140,15 @@ export default function VendorShow({ vendor, products }: ShowProps) { 返回廠商資料管理 -

廠商詳細資訊

-

- 查看並管理供應商的詳細資料與供貨商品 -

+
+
+

+ + 廠商詳細資訊 +

+

查看並管理供應商的詳細資料與供貨商品

+
+
{/* 基本資料 */} diff --git a/resources/js/Pages/Warehouse/AddInventory.tsx b/resources/js/Pages/Warehouse/AddInventory.tsx index 6dcd7da..78c78c4 100644 --- a/resources/js/Pages/Warehouse/AddInventory.tsx +++ b/resources/js/Pages/Warehouse/AddInventory.tsx @@ -3,7 +3,7 @@ */ import { useState } from "react"; -import { Plus, Trash2, Calendar, ArrowLeft, Save } from "lucide-react"; +import { Plus, Trash2, Calendar, ArrowLeft, Save, Boxes } from "lucide-react"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; @@ -183,8 +183,11 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
-

新增庫存(手動入庫)

-

+

+ + 新增庫存(手動入庫) +

+

{warehouse.name} 新增庫存記錄

diff --git a/resources/js/Pages/Warehouse/EditInventory.tsx b/resources/js/Pages/Warehouse/EditInventory.tsx index 090a886..60718f5 100644 --- a/resources/js/Pages/Warehouse/EditInventory.tsx +++ b/resources/js/Pages/Warehouse/EditInventory.tsx @@ -5,7 +5,7 @@ import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; -import { ArrowLeft, Save, Trash2 } from "lucide-react"; +import { ArrowLeft, Save, Trash2, Boxes } from "lucide-react"; import { Warehouse, WarehouseInventory } from "@/types/warehouse"; import { toast } from "sonner"; import { @@ -88,20 +88,13 @@ export default function EditInventory({ warehouse, inventory, transactions = [] -
- 商品與庫存管理 - / - 倉庫管理 - / - 庫存管理 - / - 編輯庫存品項 -
-
-

編輯庫存品項

-

+

+ + 編輯庫存品項 +

+

倉庫:{warehouse.name}

diff --git a/resources/js/Pages/Warehouse/Index.tsx b/resources/js/Pages/Warehouse/Index.tsx index d6af568..243e932 100644 --- a/resources/js/Pages/Warehouse/Index.tsx +++ b/resources/js/Pages/Warehouse/Index.tsx @@ -12,6 +12,7 @@ import { Warehouse } from "@/types/warehouse"; import Pagination from "@/Components/shared/Pagination"; import { toast } from "sonner"; import { getBreadcrumbs } from "@/utils/breadcrumb"; +import { Can } from "@/Components/Permission/Can"; interface PageProps { warehouses: { @@ -133,20 +134,24 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) { {/* 操作按鈕 */}
- - + + + + + +
diff --git a/resources/js/Pages/Warehouse/Inventory.tsx b/resources/js/Pages/Warehouse/Inventory.tsx index a5e9c42..cee9c5b 100644 --- a/resources/js/Pages/Warehouse/Inventory.tsx +++ b/resources/js/Pages/Warehouse/Inventory.tsx @@ -1,5 +1,5 @@ import { useState, useMemo } from "react"; -import { ArrowLeft, PackagePlus, AlertTriangle, Shield } from "lucide-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"; @@ -19,6 +19,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/Components/ui/alert-dialog"; +import { Can } from "@/Components/Permission/Can"; // 庫存頁面 Props interface Props { @@ -107,8 +108,11 @@ export default function WarehouseInventoryPage({
-

庫存管理 - {warehouse.name}

-

查看並管理此倉庫內的商品庫存數量與批號資訊

+

+ + 庫存管理 - {warehouse.name} +

+

查看並管理此倉庫內的商品庫存數量與批號資訊

@@ -116,15 +120,17 @@ export default function WarehouseInventoryPage({ {/* 操作按鈕 (位於標題下方) */}
{/* 安全庫存設定按鈕 */} - - - + + + + + {/* 庫存警告顯示 */} - + + + + +
{/* 篩選工具列 */} diff --git a/resources/js/Pages/Warehouse/InventoryHistory.tsx b/resources/js/Pages/Warehouse/InventoryHistory.tsx index 7be85af..4286383 100644 --- a/resources/js/Pages/Warehouse/InventoryHistory.tsx +++ b/resources/js/Pages/Warehouse/InventoryHistory.tsx @@ -1,7 +1,7 @@ import { Head, Link } from "@inertiajs/react"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Button } from "@/Components/ui/button"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, History } from "lucide-react"; import { Warehouse } from "@/types/warehouse"; import TransactionTable, { Transaction } from "@/Components/Warehouse/Inventory/TransactionTable"; import { getInventoryBreadcrumbs } from "@/utils/breadcrumb"; @@ -21,7 +21,7 @@ export default function InventoryHistory({ warehouse, inventory, transactions }: return ( -
+
{/* Header */}
@@ -34,18 +34,13 @@ export default function InventoryHistory({ warehouse, inventory, transactions }: -
- 倉庫管理 - / - 庫存管理 - / - 庫存異動紀錄 -
-
-

庫存異動紀錄

-

+

+ + 庫存異動紀錄 +

+

商品:{inventory.productName} {inventory.productCode && ({inventory.productCode})}

diff --git a/resources/js/Pages/Warehouse/SafetyStockSettings.tsx b/resources/js/Pages/Warehouse/SafetyStockSettings.tsx index d603a4d..80adf3c 100644 --- a/resources/js/Pages/Warehouse/SafetyStockSettings.tsx +++ b/resources/js/Pages/Warehouse/SafetyStockSettings.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect } from "react"; -import { ArrowLeft, Plus } from "lucide-react"; +import { ArrowLeft, Plus, Shield } from "lucide-react"; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, Link, router } from "@inertiajs/react"; @@ -14,6 +14,17 @@ import AddSafetyStockDialog from "@/Components/Warehouse/SafetyStock/AddSafetySt import EditSafetyStockDialog from "@/Components/Warehouse/SafetyStock/EditSafetyStockDialog"; import { toast } from "sonner"; import { getInventoryBreadcrumbs } from "@/utils/breadcrumb"; +import { Can } from "@/Components/Permission/Can"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/Components/ui/alert-dialog"; interface Props { warehouse: Warehouse; @@ -31,6 +42,7 @@ export default function SafetyStockPage({ const [settings, setSettings] = useState(initialSettings); const [showAddDialog, setShowAddDialog] = useState(false); const [editingSetting, setEditingSetting] = useState(null); + const [deleteId, setDeleteId] = useState(null); // 當 Props 更新時同步本地 State @@ -71,10 +83,16 @@ export default function SafetyStockPage({ }); }; - const handleDelete = (id: string) => { - router.delete(route('warehouses.safety-stock.destroy', [warehouse.id, id]), { + const handleDelete = () => { + if (!deleteId) return; + + router.delete(route('warehouses.safety-stock.destroy', [warehouse.id, deleteId]), { onSuccess: () => { + setDeleteId(null); toast.success("已刪除安全庫存設定"); + }, + onError: () => { + toast.error("刪除失敗"); } }); }; @@ -101,18 +119,23 @@ export default function SafetyStockPage({
-

安全庫存設定 - {warehouse.name}

-

+

+ + 安全庫存設定 - {warehouse.name} +

+

設定商品的安全庫存量,當庫存低於安全值時將發出警告

- + + +
@@ -121,7 +144,7 @@ export default function SafetyStockPage({ settings={settings} inventories={inventories} onEdit={setEditingSetting} - onDelete={handleDelete} + onDelete={setDeleteId} /> {/* 新增對話框 */} @@ -143,6 +166,27 @@ export default function SafetyStockPage({ onSave={handleEdit} /> )} + + {/* 刪除確認對話框 */} + !open && setDeleteId(null)}> + + + 確認刪除安全庫存設定 + + 您確定要刪除此項商品的安全庫存設定嗎?刪除後系統將不再針對此商品發出庫存不足警告。此動作無法復原。 + + + + 取消 + + 確認刪除 + + + +
); diff --git a/routes/web.php b/routes/web.php index 5b05b2e..8ac413d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,75 +22,126 @@ Route::post('/login', [LoginController::class, 'store']); Route::post('/logout', [LoginController::class, 'destroy'])->name('logout'); Route::middleware('auth')->group(function () { + // 儀表板 - 所有登入使用者皆可存取 Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); - // 類別管理 (用於商品對話框) - Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); - Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store'); - Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update'); - Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy'); + // 類別管理 (用於商品對話框) - 需要商品權限 + Route::middleware('permission:products.view')->group(function () { + Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); + Route::post('/categories', [CategoryController::class, 'store'])->middleware('permission:products.create')->name('categories.store'); + Route::put('/categories/{category}', [CategoryController::class, 'update'])->middleware('permission:products.edit')->name('categories.update'); + Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->middleware('permission:products.delete')->name('categories.destroy'); + }); - // 單位管理 - Route::post('/units', [UnitController::class, 'store'])->name('units.store'); - Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update'); - Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy'); + // 單位管理 - 需要商品權限 + Route::middleware('permission:products.create|products.edit')->group(function () { + Route::post('/units', [UnitController::class, 'store'])->name('units.store'); + Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update'); + Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy'); + }); // 商品管理 - Route::get('/products', [ProductController::class, 'index'])->name('products.index'); - Route::post('/products', [ProductController::class, 'store'])->name('products.store'); - Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update'); - Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy'); + Route::middleware('permission:products.view')->group(function () { + Route::get('/products', [ProductController::class, 'index'])->name('products.index'); + Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store'); + Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update'); + Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy'); + }); // 廠商管理 - Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); - Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); - Route::post('/vendors', [VendorController::class, 'store'])->name('vendors.store'); - Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->name('vendors.update'); - Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->name('vendors.destroy'); + Route::middleware('permission:vendors.view')->group(function () { + Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); + Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); + Route::post('/vendors', [VendorController::class, 'store'])->middleware('permission:vendors.create')->name('vendors.store'); + Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.update'); + Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->middleware('permission:vendors.delete')->name('vendors.destroy'); - // 供貨商品相關路由 - Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->name('vendors.products.store'); - Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->name('vendors.products.update'); - Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->name('vendors.products.destroy'); + // 供貨商品相關路由 + Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->middleware('permission:vendors.edit')->name('vendors.products.store'); + Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->middleware('permission:vendors.edit')->name('vendors.products.update'); + Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->middleware('permission:vendors.edit')->name('vendors.products.destroy'); + }); // 倉庫管理 - Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index'); - Route::post('/warehouses', [WarehouseController::class, 'store'])->name('warehouses.store'); - Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->name('warehouses.update'); - Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->name('warehouses.destroy'); + Route::middleware('permission:warehouses.view')->group(function () { + Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index'); + Route::post('/warehouses', [WarehouseController::class, 'store'])->middleware('permission:warehouses.create')->name('warehouses.store'); + Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->middleware('permission:warehouses.edit')->name('warehouses.update'); + Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->middleware('permission:warehouses.delete')->name('warehouses.destroy'); - // 倉庫庫存管理 - Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index'); - Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create'); - Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store'); - Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); - Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update'); - Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); - Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/history', [InventoryController::class, 'history'])->name('warehouses.inventory.history'); + // 倉庫庫存管理 - 需要庫存權限 + Route::middleware('permission:inventory.view')->group(function () { + Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index'); + Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/history', [InventoryController::class, 'history'])->name('warehouses.inventory.history'); + + Route::middleware('permission:inventory.adjust')->group(function () { + Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create'); + Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store'); + Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); + Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update'); + Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); + }); + }); - // 安全庫存設定 - Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index'); - Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store'); - Route::put('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update'); - Route::delete('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy'); + // 安全庫存設定 + Route::middleware('permission:inventory.view')->group(function () { + Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index'); + Route::middleware('permission:inventory.safety_stock')->group(function () { + Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store'); + Route::put('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update'); + Route::delete('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy'); + }); + }); + }); // 採購單管理 - Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); - Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); - Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); - Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); - Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->name('purchase-orders.edit'); - Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->name('purchase-orders.update'); - Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->name('purchase-orders.destroy'); + Route::middleware('permission:purchase_orders.view')->group(function () { + Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); + + Route::middleware('permission:purchase_orders.create')->group(function () { + Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); + Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); + }); + + Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); + + Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.edit'); + Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.update'); + Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy'); + }); // 撥補單 (在庫存調撥時使用) - Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store'); - Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories'])->name('api.warehouses.inventories'); + Route::middleware('permission:inventory.transfer')->group(function () { + Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store'); + }); + Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories']) + ->middleware('permission:inventory.view') + ->name('api.warehouses.inventories'); // 系統管理 Route::prefix('admin')->group(function () { - Route::resource('roles', RoleController::class); - Route::resource('users', UserController::class); + Route::middleware('permission:roles.view')->group(function () { + Route::get('/roles', [RoleController::class, 'index'])->name('roles.index'); + Route::middleware('permission:roles.create')->group(function () { + Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create'); + Route::post('/roles', [RoleController::class, 'store'])->name('roles.store'); + }); + Route::get('/roles/{role}/edit', [RoleController::class, 'edit'])->middleware('permission:roles.edit')->name('roles.edit'); + Route::put('/roles/{role}', [RoleController::class, 'update'])->middleware('permission:roles.edit')->name('roles.update'); + Route::delete('/roles/{role}', [RoleController::class, 'destroy'])->middleware('permission:roles.delete')->name('roles.destroy'); + }); + + Route::middleware('permission:users.view')->group(function () { + Route::get('/users', [UserController::class, 'index'])->name('users.index'); + Route::middleware('permission:users.create')->group(function () { + Route::get('/users/create', [UserController::class, 'create'])->name('users.create'); + Route::post('/users', [UserController::class, 'store'])->name('users.store'); + }); + Route::get('/users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:users.edit')->name('users.edit'); + Route::put('/users/{user}', [UserController::class, 'update'])->middleware('permission:users.edit')->name('users.update'); + Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware('permission:users.delete')->name('users.destroy'); + }); }); }); // End of auth middleware group +