diff --git a/.agent/skills/ui-consistency/SKILL.md b/.agent/skills/ui-consistency/SKILL.md
index e5ef3bd..e2f6f79 100644
--- a/.agent/skills/ui-consistency/SKILL.md
+++ b/.agent/skills/ui-consistency/SKILL.md
@@ -467,23 +467,27 @@ const handleSort = (field: string) => {
import Pagination from "@/Components/shared/Pagination";
import { SearchableSelect } from "@/Components/ui/searchable-select";
-// 在表格下方
+// 在表格下方(底部工具列)
-
-
每頁顯示
-
-
筆
+
+
+ 每頁顯示
+
+ 筆
+
+ {/* 總筆數顯示:統一放在每頁顯示右側,使用 text-gray-500 */}
+
共 {data.total} 筆紀錄
diff --git a/app/Modules/Core/Controllers/RoleController.php b/app/Modules/Core/Controllers/RoleController.php
index f54d214..1d53d32 100644
--- a/app/Modules/Core/Controllers/RoleController.php
+++ b/app/Modules/Core/Controllers/RoleController.php
@@ -123,7 +123,7 @@ class RoleController extends Controller
$role->syncPermissions($validated['permissions']);
}
- return redirect()->route('roles.index')->with('success', '角色更新成功');
+ return back()->with('success', '角色更新成功');
}
/**
@@ -160,8 +160,13 @@ class RoleController extends Controller
$action = $parts[1] ?? '';
// 特定權限遷移邏輯
- if ($permission->name === 'inventory.transfer') {
- $group = 'warehouses'; // 調撥功能移至倉庫管理下
+ if ($permission->name === 'inventory.view_cost') {
+ $group = 'inventory';
+ }
+
+ // 移除不再使用的權限選項
+ if (in_array($permission->name, ['inventory.count', 'inventory.transfer'])) {
+ continue;
}
if (!isset($grouped[$group])) {
@@ -175,7 +180,10 @@ class RoleController extends Controller
$groupDefinitions = [
'products' => '商品資料管理',
'warehouses' => '倉庫管理',
- 'inventory' => '庫存管理',
+ 'inventory' => '庫存資料管理',
+ 'inventory_count' => '庫存盤點管理',
+ 'inventory_adjust' => '庫存盤調管理',
+ 'inventory_transfer' => '庫存調撥管理',
'vendors' => '廠商資料管理',
'purchase_orders' => '採購單管理',
'goods_receipts' => '進貨單管理',
diff --git a/app/Modules/Inventory/Controllers/AdjustDocController.php b/app/Modules/Inventory/Controllers/AdjustDocController.php
index be8ddf4..2c5db51 100644
--- a/app/Modules/Inventory/Controllers/AdjustDocController.php
+++ b/app/Modules/Inventory/Controllers/AdjustDocController.php
@@ -39,7 +39,7 @@ class AdjustDocController extends Controller
$query->where('warehouse_id', $request->warehouse_id);
}
- $perPage = $request->input('per_page', 15);
+ $perPage = $request->input('per_page', 10);
$docs = $query->orderByDesc('created_at')
->paginate($perPage)
->withQueryString()
diff --git a/app/Modules/Inventory/Controllers/CountDocController.php b/app/Modules/Inventory/Controllers/CountDocController.php
index f00fc27..e2768dc 100644
--- a/app/Modules/Inventory/Controllers/CountDocController.php
+++ b/app/Modules/Inventory/Controllers/CountDocController.php
@@ -37,7 +37,7 @@ class CountDocController extends Controller
$perPage = $request->input('per_page', 10);
if (!in_array($perPage, [10, 20, 50, 100])) {
- $perPage = 15;
+ $perPage = 10;
}
$countQuery = function ($query) {
diff --git a/app/Modules/Inventory/Controllers/TransferOrderController.php b/app/Modules/Inventory/Controllers/TransferOrderController.php
index 32e0dc3..690efc1 100644
--- a/app/Modules/Inventory/Controllers/TransferOrderController.php
+++ b/app/Modules/Inventory/Controllers/TransferOrderController.php
@@ -32,8 +32,10 @@ class TransferOrderController extends Controller
});
}
+ $perPage = $request->input('per_page', 10);
$orders = $query->orderByDesc('created_at')
- ->paginate(15)
+ ->paginate($perPage)
+ ->withQueryString()
->through(function ($order) {
return [
'id' => (string) $order->id,
@@ -50,7 +52,7 @@ class TransferOrderController extends Controller
return Inertia::render('Inventory/Transfer/Index', [
'orders' => $orders,
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
- 'filters' => $request->only(['warehouse_id']),
+ 'filters' => $request->only(['warehouse_id', 'per_page']),
]);
}
diff --git a/app/Modules/Inventory/Controllers/WarehouseController.php b/app/Modules/Inventory/Controllers/WarehouseController.php
index a1391b5..658cf2a 100644
--- a/app/Modules/Inventory/Controllers/WarehouseController.php
+++ b/app/Modules/Inventory/Controllers/WarehouseController.php
@@ -24,6 +24,11 @@ class WarehouseController extends Controller
});
}
+ $perPage = $request->input('per_page', 10);
+ if (!in_array($perPage, [10, 20, 50, 100])) {
+ $perPage = 10;
+ }
+
$warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和
->withSum(['inventories as available_stock' => function ($query) {
// 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期) 且 倉庫類型不為瑕疵倉
@@ -44,7 +49,7 @@ class WarehouseController extends Controller
->whereRaw('(SELECT COALESCE(SUM(quantity), 0) FROM inventories WHERE warehouse_id = ss.warehouse_id AND product_id = ss.product_id) < ss.safety_stock');
}])
->orderBy('created_at', 'desc')
- ->paginate(10)
+ ->paginate($perPage)
->withQueryString();
// 移除原本對 is_sellable 的手動修正邏輯,現在由 type 自動過濾
@@ -67,7 +72,7 @@ class WarehouseController extends Controller
return Inertia::render('Warehouse/Index', [
'warehouses' => $warehouses,
'totals' => $totals,
- 'filters' => $request->only(['search']),
+ 'filters' => $request->only(['search', 'per_page']),
]);
}
diff --git a/app/Modules/Inventory/Routes/web.php b/app/Modules/Inventory/Routes/web.php
index ec5b579..27d7ec0 100644
--- a/app/Modules/Inventory/Routes/web.php
+++ b/app/Modules/Inventory/Routes/web.php
@@ -22,10 +22,10 @@ Route::middleware('auth')->group(function () {
});
// 單位管理 - 需要商品權限
- 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::middleware('permission:products.view')->group(function () {
+ Route::post('/units', [UnitController::class, 'store'])->middleware('permission:products.create')->name('units.store');
+ Route::put('/units/{unit}', [UnitController::class, 'update'])->middleware('permission:products.edit')->name('units.update');
+ Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->middleware('permission:products.delete')->name('units.destroy');
});
// 商品管理
@@ -75,37 +75,34 @@ Route::middleware('auth')->group(function () {
});
// 庫存盤點 (Stock Counting) - Global
- Route::middleware('permission:inventory.view')->group(function () {
- Route::get('/inventory/count-docs', [CountDocController::class, 'index'])->name('inventory.count.index');
- Route::get('/inventory/count-docs/{doc}', [CountDocController::class, 'show'])->name('inventory.count.show');
-
- Route::middleware('permission:inventory.adjust')->group(function () {
- Route::post('/inventory/count-docs', [CountDocController::class, 'store'])->name('inventory.count.store');
- Route::put('/inventory/count-docs/{doc}', [CountDocController::class, 'update'])->name('inventory.count.update');
- Route::delete('/inventory/count-docs/{doc}', [CountDocController::class, 'destroy'])->name('inventory.count.destroy');
- Route::put('/inventory/count-docs/{doc}/reopen', [CountDocController::class, 'reopen'])->name('inventory.count.reopen');
- });
- Route::get('/inventory/count-docs/{doc}/print', [CountDocController::class, 'print'])->name('inventory.count.print');
+ Route::middleware('permission:inventory_count.view')->group(function () {
+ Route::get('/inventory/count-docs', [CountDocController::class, 'index'])->name('inventory.count.index');
+ Route::get('/inventory/count-docs/{doc}', [CountDocController::class, 'show'])->name('inventory.count.show');
+ Route::get('/inventory/count-docs/{doc}/print', [CountDocController::class, 'print'])->name('inventory.count.print');
});
+ Route::post('/inventory/count-docs', [CountDocController::class, 'store'])->middleware('permission:inventory_count.create')->name('inventory.count.store');
+ Route::put('/inventory/count-docs/{doc}', [CountDocController::class, 'update'])->middleware('permission:inventory_count.edit')->name('inventory.count.update');
+ Route::delete('/inventory/count-docs/{doc}', [CountDocController::class, 'destroy'])->middleware('permission:inventory_count.delete')->name('inventory.count.destroy');
+ Route::put('/inventory/count-docs/{doc}/reopen', [CountDocController::class, 'reopen'])->middleware('permission:inventory_count.edit')->name('inventory.count.reopen');
// 庫存盤調 (Stock Adjustment) - Global
- Route::middleware('permission:inventory.adjust')->group(function () {
- Route::get('/inventory/adjust-docs', [AdjustDocController::class, 'index'])->name('inventory.adjust.index');
- Route::get('/inventory/adjust-docs/get-pending-counts', [AdjustDocController::class, 'getPendingCounts'])->name('inventory.adjust.pending-counts');
- Route::get('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'show'])->name('inventory.adjust.show');
- Route::post('/inventory/adjust-docs', [AdjustDocController::class, 'store'])->name('inventory.adjust.store');
- Route::put('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'update'])->name('inventory.adjust.update');
- Route::delete('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'destroy'])->name('inventory.adjust.destroy');
+ Route::middleware('permission:inventory_adjust.view')->group(function () {
+ Route::get('/inventory/adjust-docs', [AdjustDocController::class, 'index'])->name('inventory.adjust.index');
+ Route::get('/inventory/adjust-docs/get-pending-counts', [AdjustDocController::class, 'getPendingCounts'])->name('inventory.adjust.pending-counts');
+ Route::get('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'show'])->name('inventory.adjust.show');
});
+ Route::post('/inventory/adjust-docs', [AdjustDocController::class, 'store'])->middleware('permission:inventory_adjust.create')->name('inventory.adjust.store');
+ Route::put('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'update'])->middleware('permission:inventory_adjust.edit')->name('inventory.adjust.update');
+ Route::delete('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'destroy'])->middleware('permission:inventory_adjust.delete')->name('inventory.adjust.destroy');
// 撥補單/調撥單 (Transfer Order) - Global
- Route::middleware('permission:inventory.transfer')->group(function () {
+ Route::middleware('permission:inventory_transfer.view')->group(function () {
Route::get('/inventory/transfer-orders', [TransferOrderController::class, 'index'])->name('inventory.transfer.index');
Route::get('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'show'])->name('inventory.transfer.show');
- Route::post('/inventory/transfer-orders', [TransferOrderController::class, 'store'])->name('inventory.transfer.store');
- Route::put('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'update'])->name('inventory.transfer.update');
- Route::delete('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'destroy'])->name('inventory.transfer.destroy');
});
+ Route::post('/inventory/transfer-orders', [TransferOrderController::class, 'store'])->middleware('permission:inventory_transfer.create')->name('inventory.transfer.store');
+ Route::put('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'update'])->middleware('permission:inventory_transfer.edit')->name('inventory.transfer.update');
+ Route::delete('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'destroy'])->middleware('permission:inventory_transfer.delete')->name('inventory.transfer.destroy');
Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories'])
->middleware('permission:inventory.view')
->name('api.warehouses.inventories');
diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php
index f4b056f..732a078 100644
--- a/database/seeders/PermissionSeeder.php
+++ b/database/seeders/PermissionSeeder.php
@@ -35,11 +35,26 @@ class PermissionSeeder extends Seeder
// 庫存管理
'inventory.view',
'inventory.view_cost', // 查看成本與價值
- 'inventory.adjust',
- 'inventory.count', // 庫存盤點
- 'inventory.transfer', // 庫存調撥
'inventory.delete',
+ // 庫存盤點 (Stock Counting)
+ 'inventory_count.view',
+ 'inventory_count.create',
+ 'inventory_count.edit',
+ 'inventory_count.delete',
+
+ // 庫存調整 (Stock Adjustment)
+ 'inventory_adjust.view',
+ 'inventory_adjust.create',
+ 'inventory_adjust.edit',
+ 'inventory_adjust.delete',
+
+ // 庫存調撥 (Stock Transfer)
+ 'inventory_transfer.view',
+ 'inventory_transfer.create',
+ 'inventory_transfer.edit',
+ 'inventory_transfer.delete',
+
// 進貨單管理
'goods_receipts.view',
'goods_receipts.create',
@@ -118,7 +133,10 @@ class PermissionSeeder extends Seeder
'products.view', 'products.create', 'products.edit', 'products.delete',
'purchase_orders.view', 'purchase_orders.create', 'purchase_orders.edit',
'purchase_orders.delete', 'purchase_orders.publish',
- 'inventory.view', 'inventory.view_cost', 'inventory.adjust', 'inventory.transfer', 'inventory.delete',
+ 'inventory.view', 'inventory.view_cost', 'inventory.delete',
+ 'inventory_count.view', 'inventory_count.create', 'inventory_count.edit', 'inventory_count.delete',
+ 'inventory_adjust.view', 'inventory_adjust.create', 'inventory_adjust.edit', 'inventory_adjust.delete',
+ 'inventory_transfer.view', 'inventory_transfer.create', 'inventory_transfer.edit', 'inventory_transfer.delete',
'goods_receipts.view', 'goods_receipts.create', 'goods_receipts.edit', 'goods_receipts.delete',
'production_orders.view', 'production_orders.create', 'production_orders.edit', 'production_orders.delete',
'recipes.view', 'recipes.create', 'recipes.edit', 'recipes.delete',
@@ -134,7 +152,10 @@ class PermissionSeeder extends Seeder
// warehouse-manager 管理庫存與倉庫
$warehouseManager->givePermissionTo([
'products.view',
- 'inventory.view', 'inventory.adjust', 'inventory.count', 'inventory.transfer', 'inventory.delete',
+ 'inventory.view', 'inventory.delete',
+ 'inventory_count.view', 'inventory_count.create', 'inventory_count.edit', 'inventory_count.delete',
+ 'inventory_adjust.view', 'inventory_adjust.create', 'inventory_adjust.edit', 'inventory_adjust.delete',
+ 'inventory_transfer.view', 'inventory_transfer.create', 'inventory_transfer.edit', 'inventory_transfer.delete',
'goods_receipts.view', 'goods_receipts.create', 'goods_receipts.edit', 'goods_receipts.delete',
'production_orders.view', 'production_orders.create', 'production_orders.edit',
'warehouses.view', 'warehouses.create', 'warehouses.edit',
diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx
index b10dc70..65fdf7c 100644
--- a/resources/js/Layouts/AuthenticatedLayout.tsx
+++ b/resources/js/Layouts/AuthenticatedLayout.tsx
@@ -105,21 +105,21 @@ export default function AuthenticatedLayout({
label: "庫存盤點",
icon:
,
route: "/inventory/count-docs",
- permission: "inventory.view",
+ permission: "inventory_count.view",
},
{
id: "stock-adjustment",
label: "庫存盤調",
icon:
,
route: "/inventory/adjust-docs",
- permission: "inventory.adjust",
+ permission: "inventory_adjust.view",
},
{
id: "stock-transfer",
label: "庫存調撥",
icon:
,
route: "/inventory/transfer-orders",
- permission: "inventory.transfer",
+ permission: "inventory_transfer.view",
},
],
},
diff --git a/resources/js/Pages/Admin/Role/Edit.tsx b/resources/js/Pages/Admin/Role/Edit.tsx
index a5c8261..3dec114 100644
--- a/resources/js/Pages/Admin/Role/Edit.tsx
+++ b/resources/js/Pages/Admin/Role/Edit.tsx
@@ -70,16 +70,20 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
const translateAction = (permissionName: string) => {
const parts = permissionName.split('.');
if (parts.length < 2) return permissionName;
- const action = parts[1];
+ const action = parts[parts.length - 1];
const map: Record
= {
'view': '檢視',
'create': '新增',
'edit': '編輯',
'delete': '刪除',
- 'publish': '發布',
+ 'publish': '發佈',
'adjust': '調整',
'transfer': '調撥',
+ 'count': '盤點',
+ // 'inventory_count': '盤點', // Hide prefix
+ // 'inventory_adjust': '盤調', // Hide prefix
+ // 'inventory_transfer': '調撥', // Hide prefix
'safety_stock': '安全庫存設定',
'export': '匯出',
'complete': '完成',
@@ -88,7 +92,19 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
'activate': '啟用/停用',
};
- return map[action] || action;
+ const actionText = map[action] || action;
+
+ // 處理多段式權限 (例如 inventory_count.view)
+ if (parts.length >= 2) {
+ const middleKey = parts[parts.length - 2];
+
+ // 如果中間那段有翻譯且不等於動作本身,則顯示為 "標籤: 動作"
+ if (map[middleKey] && middleKey !== action) {
+ return `${map[middleKey]}: ${actionText}`;
+ }
+ }
+
+ return actionText;
};
return (
diff --git a/resources/js/Pages/Inventory/Adjust/Index.tsx b/resources/js/Pages/Inventory/Adjust/Index.tsx
index 4a69cf1..d7ba412 100644
--- a/resources/js/Pages/Inventory/Adjust/Index.tsx
+++ b/resources/js/Pages/Inventory/Adjust/Index.tsx
@@ -1,5 +1,6 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm, router, Link } from '@inertiajs/react';
+import { usePermission } from '@/hooks/usePermission';
import {
Table,
TableBody,
@@ -71,7 +72,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState(filters.search || '');
const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || '');
- const [perPage, setPerPage] = useState(filters.per_page || '15');
+ const [perPage, setPerPage] = useState(filters.per_page || '10');
const [deleteId, setDeleteId] = useState(null);
// For Count Doc Selection
@@ -115,9 +116,12 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
debouncedFilter({ search: searchQuery, warehouse_id: val, per_page: perPage });
};
- const handlePerPageChange = (val: string) => {
- setPerPage(val);
- debouncedFilter({ search: searchQuery, warehouse_id: warehouseId, per_page: val });
+ const handlePerPageChange = (value: string) => {
+ setPerPage(value);
+ router.get(route('inventory.adjust.index'),
+ { ...filters, search: searchQuery, warehouse_id: warehouseId, per_page: value, page: 1 },
+ { preserveState: false, replace: true, preserveScroll: true }
+ );
};
const confirmDelete = (id: string, e: React.MouseEvent) => {
@@ -134,6 +138,8 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
}
};
+ const { can } = usePermission();
+
const { data, setData, post, processing, reset } = useForm({
count_doc_id: null as string | null,
warehouse_id: '',
@@ -229,7 +235,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
{/* Action Buttons */}
-
+
- {isDraft && (
-
-
-
-
-
- 刪除
-
-
-
-
- 確定要刪除此盤調單嗎?
-
- 此動作將會永久移除本張草稿,且無法復原。
-
-
-
- 取消
- 確認刪除
-
-
-
+ {!isReadOnly && (
+ <>
+
+
+
+
+
+ 刪除
+
+
+
+
+ 確定要刪除此盤調單嗎?
+
+ 此動作將會永久移除本張草稿,且無法復原。
+
+
+
+ 取消
+ 確認刪除
+
+
+
+
-
-
- 儲存
-
+
+
+
+ 儲存
+
-
-
-
-
- 確認過帳
-
-
-
-
- 確定要過帳嗎?
-
- 過帳後庫存將立即根據調整數量進行增減,且無法再修改此盤調單。
-
-
-
- 取消
- 確認過帳
-
-
-
-
+
+
+
+
+ 確認過帳
+
+
+
+
+ 確定要過帳嗎?
+
+ 過帳後庫存將立即根據調整數量進行增減,且無法再修改此盤調單。
+
+
+
+ 取消
+ 確認過帳
+
+
+
+
+ >
)}
@@ -332,7 +336,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {