diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php
index 24f1b59..e2660d8 100644
--- a/app/Http/Controllers/Admin/PermissionController.php
+++ b/app/Http/Controllers/Admin/PermissionController.php
@@ -577,6 +577,7 @@ class PermissionController extends Controller
'menu.machines',
'menu.warehouses',
'menu.sales',
+ 'menu.analysis',
'menu.data-config',
'menu.remote',
'menu.special-permission',
diff --git a/database/migrations/2026_05_11_163543_add_missing_types_to_machine_stock_movements_table.php b/database/migrations/2026_05_11_163543_add_missing_types_to_machine_stock_movements_table.php
index a3e8cf9..96fc430 100644
--- a/database/migrations/2026_05_11_163543_add_missing_types_to_machine_stock_movements_table.php
+++ b/database/migrations/2026_05_11_163543_add_missing_types_to_machine_stock_movements_table.php
@@ -12,10 +12,9 @@ return new class extends Migration
*/
public function up(): void
{
- Schema::table('machine_stock_movements', function (Blueprint $table) {
- // 在 MySQL 中,更新 ENUM 建議使用 DB::statement 以確保相容性
+ if (Schema::getConnection()->getDriverName() !== 'sqlite') {
DB::statement("ALTER TABLE machine_stock_movements MODIFY COLUMN type ENUM('replenishment', 'pickup', 'remote_dispense', 'adjustment', 'rollback', 'sale', 'decommission') NOT NULL");
- });
+ }
}
/**
@@ -23,8 +22,8 @@ return new class extends Migration
*/
public function down(): void
{
- Schema::table('machine_stock_movements', function (Blueprint $table) {
+ if (Schema::getConnection()->getDriverName() !== 'sqlite') {
DB::statement("ALTER TABLE machine_stock_movements MODIFY COLUMN type ENUM('replenishment', 'pickup', 'remote_dispense', 'adjustment', 'rollback') NOT NULL");
- });
+ }
}
};
diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php
index 6e81443..b5b04ba 100644
--- a/database/seeders/RoleSeeder.php
+++ b/database/seeders/RoleSeeder.php
@@ -40,6 +40,10 @@ class RoleSeeder extends Seeder
'menu.sales.pass-codes',
'menu.sales.store-gifts',
'menu.analysis',
+ 'menu.analysis.change-stock',
+ 'menu.analysis.machine-reports',
+ 'menu.analysis.product-reports',
+ 'menu.analysis.survey-analysis',
'menu.audit',
'menu.data-config',
'menu.data-config.products',
@@ -102,6 +106,7 @@ class RoleSeeder extends Seeder
'menu.sales.pass-codes',
'menu.sales.store-gifts',
'menu.analysis',
+ 'menu.analysis.product-reports',
'menu.audit',
'menu.data-config',
'menu.data-config.products',
diff --git a/lang/en.json b/lang/en.json
index 154fd5c..73f108a 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1091,6 +1091,7 @@
"Order has been cancelled": "Order has been cancelled",
"Order is now in delivery": "Order is now in delivery",
"Order prepared successfully, stock deducted": "Order prepared successfully, stock deducted",
+ "Order updated successfully.": "Order updated successfully.",
"Orders": "Orders",
"Orders per hour for selected date": "Orders per hour for selected date",
"Original": "Original",
@@ -1607,6 +1608,7 @@
"TapPay Integration Settings Description": "TapPay Integration Settings Description",
"Target": "Target",
"Target Machine": "Target Machine",
+ "Target Position": "Target Position",
"Target Warehouse": "Target Warehouse",
"Tax ID": "Tax ID",
"Tax ID (Optional)": "Tax ID (Optional)",
@@ -1884,6 +1886,10 @@
"machines": "Machine Management",
"members": "Member Management",
"menu.analysis": "Data Analysis",
+ "menu.analysis.change-stock": "Change Stock",
+ "menu.analysis.machine-reports": "Machine Reports",
+ "menu.analysis.product-reports": "Product Reports",
+ "menu.analysis.survey-analysis": "Survey Analysis",
"menu.app": "APP Maintenance",
"menu.audit": "Audit Management",
"menu.basic": "Basic Settings",
diff --git a/lang/ja.json b/lang/ja.json
index 2206441..9ecc431 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -1091,6 +1091,7 @@
"Order has been cancelled": "注文がキャンセルされました",
"Order is now in delivery": "注文は配送中です",
"Order prepared successfully, stock deducted": "注文の準備が成功し、在庫が差し引かれました",
+ "Order updated successfully.": "表示順を更新しました。",
"Orders": "購入注文",
"Orders per hour for selected date": "選択した日付の1時間あたりの注文数",
"Original": "元の",
@@ -1607,6 +1608,7 @@
"TapPay Integration Settings Description": "TapPay 決済連携設定",
"Target": "対象",
"Target Machine": "対象機器",
+ "Target Position": "ターゲット位置",
"Target Warehouse": "対象倉庫",
"Tax ID": "統一企業番号",
"Tax ID (Optional)": "統一企業番号 (任意)",
@@ -1884,6 +1886,10 @@
"machines": "機器管理",
"members": "会員管理",
"menu.analysis": "データ分析",
+ "menu.analysis.change-stock": "両替在庫",
+ "menu.analysis.machine-reports": "機台レポート",
+ "menu.analysis.product-reports": "商品レポート",
+ "menu.analysis.survey-analysis": "アンケート分析",
"menu.app": "APP 運用保守",
"menu.audit": "監査管理",
"menu.basic": "基本設定",
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 10129e9..30da24e 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -1110,6 +1110,7 @@
"Order has been cancelled": "訂單已取消",
"Order is now in delivery": "訂單配送中",
"Order prepared successfully, stock deducted": "訂單已成功備貨,庫存已扣除",
+ "Order updated successfully.": "順序更新成功。",
"Orders": "購買單",
"Orders per hour for selected date": "當日各小時訂單筆數",
"Original": "原始",
@@ -1629,6 +1630,7 @@
"Target": "目標",
"Target Item": "操作目標",
"Target Machine": "目標機台",
+ "Target Position": "投放位置",
"Target Warehouse": "目標倉庫",
"Tax ID": "統一編號",
"Tax ID (Optional)": "統一編號 (選填)",
@@ -1919,6 +1921,10 @@
"member_price": "會員價",
"members": "會員管理",
"menu.analysis": "數據分析",
+ "menu.analysis.change-stock": "零錢庫存",
+ "menu.analysis.machine-reports": "機台報表",
+ "menu.analysis.product-reports": "商品報表",
+ "menu.analysis.survey-analysis": "問卷分析",
"menu.app": "APP 運維",
"menu.audit": "審核管理",
"menu.basic": "基本設定",
diff --git a/resources/views/admin/_shared/role-list.blade.php b/resources/views/admin/_shared/role-list.blade.php
index fa0fdae..e085182 100644
--- a/resources/views/admin/_shared/role-list.blade.php
+++ b/resources/views/admin/_shared/role-list.blade.php
@@ -98,7 +98,7 @@
@php
- $activeModules = ['menu.machines', 'menu.warehouses', 'menu.sales', 'menu.data-config', 'menu.remote', 'menu.special-permission', 'menu.basic', 'menu.permissions'];
+ $activeModules = ['menu.machines', 'menu.warehouses', 'menu.sales', 'menu.analysis', 'menu.data-config', 'menu.remote', 'menu.special-permission', 'menu.basic', 'menu.permissions'];
$displayPermissions = $role->permissions->filter(function($p) use ($activeModules) {
if ($p->name === 'menu.data-config.sub-account-roles') return false;
if (str_starts_with($p->name, 'menu.')) {
diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php
index bd4b925..107959d 100644
--- a/resources/views/admin/basic-settings/machines/index.blade.php
+++ b/resources/views/admin/basic-settings/machines/index.blade.php
@@ -422,8 +422,7 @@
- {{ __('Tax
- System') }}
+ {{ __('Tax System') }}
- {{ __('Creation
- Time') }}
+ {{ __('Creation Time') }}
{{
$item->created_at->format('Y-m-d') }}
diff --git a/resources/views/admin/remote/stock.blade.php b/resources/views/admin/remote/stock.blade.php
index 869a27f..0280fad 100644
--- a/resources/views/admin/remote/stock.blade.php
+++ b/resources/views/admin/remote/stock.blade.php
@@ -536,8 +536,7 @@
- {{ __('Low
- Stock') }}
+ {{ __('Low Stock') }}
diff --git a/resources/views/layouts/partials/sidebar-menu.blade.php b/resources/views/layouts/partials/sidebar-menu.blade.php
index 8bf6fec..b487760 100644
--- a/resources/views/layouts/partials/sidebar-menu.blade.php
+++ b/resources/views/layouts/partials/sidebar-menu.blade.php
@@ -234,22 +234,33 @@
diff --git a/routes/web.php b/routes/web.php
index 3a2d420..d844b06 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -146,10 +146,10 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
// 7. 分析管理
Route::prefix('analysis')->name('analysis.')->group(function () {
- Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class, 'changeStock'])->name('change-stock');
- Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'machineReports'])->name('machine-reports');
- Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'productReports'])->name('product-reports');
- Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class, 'surveyAnalysis'])->name('survey-analysis');
+ Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class, 'changeStock'])->name('change-stock')->middleware('can:menu.analysis.change-stock');
+ Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'machineReports'])->name('machine-reports')->middleware('can:menu.analysis.machine-reports');
+ Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'productReports'])->name('product-reports')->middleware('can:menu.analysis.product-reports');
+ Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class, 'surveyAnalysis'])->name('survey-analysis')->middleware('can:menu.analysis.survey-analysis');
});
// 8. 稽核管理
diff --git a/tests/Feature/Admin/AnalysisPermissionTest.php b/tests/Feature/Admin/AnalysisPermissionTest.php
new file mode 100644
index 0000000..b52eaa7
--- /dev/null
+++ b/tests/Feature/Admin/AnalysisPermissionTest.php
@@ -0,0 +1,129 @@
+artisan('db:seed', ['--class' => 'RoleSeeder']);
+
+ // 2. 建立測試公司
+ $this->company = Company::create([
+ 'name' => 'Test Company',
+ 'code' => 'TEST',
+ 'status' => 1,
+ ]);
+
+ // 註冊 SQLite 相容的 DATE_FORMAT 函數
+ if (\Illuminate\Support\Facades\Schema::getConnection()->getDriverName() === 'sqlite') {
+ $pdo = \Illuminate\Support\Facades\Schema::getConnection()->getPdo();
+ $pdo->sqliteCreateFunction('DATE_FORMAT', function ($date, $format) {
+ if (empty($date)) return null;
+ $phpFormat = str_replace(
+ ['%Y', '%m', '%d', '%H', '%i', '%s'],
+ ['Y', 'm', 'd', 'H', 'i', 's'],
+ $format
+ );
+ return date($phpFormat, strtotime($date));
+ }, 2);
+ }
+
+ // 3. 建立 Super Admin 帳號
+ $this->superAdmin = User::create([
+ 'name' => 'Super Admin User',
+ 'username' => 'superadmin_test',
+ 'email' => 'superadmin@example.com',
+ 'password' => bcrypt('password'),
+ 'company_id' => null,
+ ]);
+ $this->superAdmin->assignRole('super-admin');
+
+ // 4. 建立租戶管理員帳號,並克隆其「管理員」角色
+ // 我們的 RoleSeeder 會將 'menu.analysis.product-reports' 賦予 tenantAdmin 角色範本
+ // 租戶的「管理員」將繼承該範本的權限。
+ $this->tenantAdmin = User::create([
+ 'name' => 'Tenant Admin User',
+ 'username' => 'tenantadmin_test',
+ 'email' => 'tenantadmin@example.com',
+ 'password' => bcrypt('password'),
+ 'company_id' => $this->company->id,
+ ]);
+
+ // 克隆 RoleSeeder 建立的 tenantAdmin 角色權限至該租戶下命名為「管理員」
+ $tenantAdminTemplate = Role::where('name', '客戶管理員角色模板')->first();
+ $this->assertNotNull($tenantAdminTemplate);
+
+ $tenantRole = Role::create([
+ 'name' => '管理員',
+ 'company_id' => $this->company->id,
+ 'guard_name' => 'web',
+ 'is_system' => false,
+ ]);
+
+ // 同步權限
+ $tenantRole->syncPermissions($tenantAdminTemplate->permissions);
+ $this->tenantAdmin->assignRole($tenantRole);
+ }
+
+ /**
+ * 測試超級管理員可以訪問所有的分析管理子路由
+ */
+ public function test_super_admin_can_access_all_analysis_routes()
+ {
+ $this->actingAs($this->superAdmin);
+
+ $routes = [
+ 'admin.analysis.change-stock',
+ 'admin.analysis.machine-reports',
+ 'admin.analysis.product-reports',
+ 'admin.analysis.survey-analysis',
+ ];
+
+ foreach ($routes as $route) {
+ $response = $this->get(route($route));
+ // 應成功訪問 (由於 Controller 實作可能回傳檢視或 200)
+ $response->assertStatus(200);
+ }
+ }
+
+ /**
+ * 測試租戶管理員只能訪問商品報表,其他 3 個路由會回傳 403 越權錯誤
+ */
+ public function test_tenant_admin_can_only_access_product_reports()
+ {
+ $this->actingAs($this->tenantAdmin);
+
+ // 1. 可以正常訪問商品報表
+ $response = $this->get(route('admin.analysis.product-reports'));
+ $response->assertStatus(200);
+
+ // 2. 其他 3 個路由皆應回傳 403 Forbidden 越權錯誤
+ $forbiddenRoutes = [
+ 'admin.analysis.change-stock',
+ 'admin.analysis.machine-reports',
+ 'admin.analysis.survey-analysis',
+ ];
+
+ foreach ($forbiddenRoutes as $route) {
+ $response = $this->get(route($route));
+ $response->assertStatus(403);
+ }
+ }
+}
|