1. 實作 AnalysisController@machineReports 後端邏輯,支援租戶隔離與帳號機台授權過濾。 2. 建立 machine-reports.blade.php 視圖,加入極簡奢華風統計卡片、ApexCharts 趨勢圖與自訂多選支付工具篩選器,並停用圖表縮放優化滾輪滾動體驗。 3. 優化 product-reports.blade.php,新增機台下拉選單過濾,並啟用寬度優化與響應式自動換行,且同步停用圖表縮放。 4. 在 Machine model 全域 Scope 中排除單元測試阻擋,並更新 RoleSeeder 播種機台報表權限。 5. 新增 AnalysisReportsTest 測試,並更新 AnalysisPermissionTest 權限測試,且在 Sail 中驗證為 100% 通過。 6. 同步更新繁中、英文、日文翻譯 JSON 檔案,以 zh_TW 為基準完美對齊、字母排序且還原斜線。
275 lines
9.4 KiB
PHP
275 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Admin;
|
|
|
|
use App\Models\System\Company;
|
|
use App\Models\System\User;
|
|
use App\Models\Machine\Machine;
|
|
use App\Models\Product\Product;
|
|
use App\Models\Transaction\Order;
|
|
use App\Models\Transaction\OrderItem;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class AnalysisReportsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected $company;
|
|
protected $superAdmin;
|
|
protected $tenantUser;
|
|
protected $machineA;
|
|
protected $machineB;
|
|
protected $productX;
|
|
protected $productY;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutExceptionHandling();
|
|
|
|
// 1. Seed Roles & Permissions
|
|
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
|
|
|
|
// 2. Create Company
|
|
$this->company = Company::create([
|
|
'name' => 'Report Test Company',
|
|
'code' => 'REPTEST',
|
|
'status' => 1,
|
|
]);
|
|
|
|
// Register SQLite compatible DATE_FORMAT function
|
|
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. Create Super Admin
|
|
$this->superAdmin = User::create([
|
|
'name' => 'Super Admin',
|
|
'username' => 'super_reports_test',
|
|
'email' => 'superadmin@example.com',
|
|
'password' => bcrypt('password'),
|
|
'company_id' => null,
|
|
]);
|
|
$this->superAdmin->assignRole('super-admin');
|
|
|
|
// 4. Create Tenant User
|
|
$this->tenantUser = User::create([
|
|
'name' => 'Tenant User',
|
|
'username' => 'tenant_reports_test',
|
|
'email' => 'tenantuser@example.com',
|
|
'password' => bcrypt('password'),
|
|
'company_id' => $this->company->id,
|
|
]);
|
|
// Give them a role that has analysis permissions
|
|
$role = \App\Models\System\Role::create([
|
|
'name' => '測試角色',
|
|
'company_id' => $this->company->id,
|
|
'guard_name' => 'web',
|
|
'is_system' => false,
|
|
]);
|
|
$role->syncPermissions([
|
|
'menu.analysis',
|
|
'menu.analysis.machine-reports',
|
|
'menu.analysis.product-reports'
|
|
]);
|
|
$this->tenantUser->assignRole($role);
|
|
|
|
// 5. Create Machines
|
|
$this->machineA = Machine::create([
|
|
'company_id' => $this->company->id,
|
|
'name' => 'Machine Alpha',
|
|
'serial_no' => 'MC0000A',
|
|
'status' => 'online',
|
|
]);
|
|
|
|
$this->machineB = Machine::create([
|
|
'company_id' => $this->company->id,
|
|
'name' => 'Machine Beta',
|
|
'serial_no' => 'MC0000B',
|
|
'status' => 'online',
|
|
]);
|
|
|
|
// Authorize Tenant User to ONLY see Machine A
|
|
$this->machineA->users()->attach($this->tenantUser->id);
|
|
|
|
// 6. Create Products
|
|
$this->productX = Product::create([
|
|
'company_id' => $this->company->id,
|
|
'name' => 'Cola',
|
|
'barcode' => 'BAR0001',
|
|
'cost' => 10.00,
|
|
'price' => 15.00,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->productY = Product::create([
|
|
'company_id' => $this->company->id,
|
|
'name' => 'Soda',
|
|
'barcode' => 'BAR0002',
|
|
'cost' => 20.00,
|
|
'price' => 25.00,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// 7. Seed Orders
|
|
// Order 1: Machine A, Product X, payment type 1 (Credit Card), price 15, quantity 2. (Sales: 30, Cost: 20, Profit: 10)
|
|
$order1 = Order::create([
|
|
'company_id' => $this->company->id,
|
|
'order_no' => 'ORD0001',
|
|
'machine_id' => $this->machineA->id,
|
|
'payment_type' => 1,
|
|
'total_amount' => 30.00,
|
|
'pay_amount' => 30.00,
|
|
'payment_status' => Order::PAYMENT_STATUS_SUCCESS,
|
|
'payment_at' => now(),
|
|
]);
|
|
OrderItem::create([
|
|
'order_id' => $order1->id,
|
|
'product_id' => $this->productX->id,
|
|
'product_name' => $this->productX->name,
|
|
'barcode' => $this->productX->barcode,
|
|
'quantity' => 2,
|
|
'price' => 15.00,
|
|
'subtotal' => 30.00,
|
|
]);
|
|
|
|
// Order 2: Machine B, Product Y, payment type 2 (E-ticket), price 25, quantity 1. (Sales: 25, Cost: 20, Profit: 5)
|
|
$order2 = Order::create([
|
|
'company_id' => $this->company->id,
|
|
'order_no' => 'ORD0002',
|
|
'machine_id' => $this->machineB->id,
|
|
'payment_type' => 2,
|
|
'total_amount' => 25.00,
|
|
'pay_amount' => 25.00,
|
|
'payment_status' => Order::PAYMENT_STATUS_SUCCESS,
|
|
'payment_at' => now(),
|
|
]);
|
|
OrderItem::create([
|
|
'order_id' => $order2->id,
|
|
'product_id' => $this->productY->id,
|
|
'product_name' => $this->productY->name,
|
|
'barcode' => $this->productY->barcode,
|
|
'quantity' => 1,
|
|
'price' => 25.00,
|
|
'subtotal' => 25.00,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 測試超級管理員可以撈出所有機台的銷售統計
|
|
*/
|
|
public function test_super_admin_gets_all_machines_statistics()
|
|
{
|
|
$this->actingAs($this->superAdmin);
|
|
|
|
$response = $this->getJson(route('admin.analysis.machine-reports', [
|
|
'company_id' => $this->company->id
|
|
]));
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
$data = $response->json();
|
|
|
|
// 總和驗證
|
|
$this->assertEquals(3, $data['summary']['total_quantity']);
|
|
$this->assertEquals(55.0, $data['summary']['total_sales']);
|
|
$this->assertEquals(40.0, $data['summary']['total_cost']);
|
|
$this->assertEquals(15.0, $data['summary']['total_profit']);
|
|
|
|
// 機台明細驗證
|
|
$this->assertCount(2, $data['table_data']);
|
|
|
|
$machineAData = collect($data['table_data'])->firstWhere('machine_id', $this->machineA->id);
|
|
$this->assertNotNull($machineAData);
|
|
$this->assertEquals(2, $machineAData['total_quantity']);
|
|
$this->assertEquals(30.0, $machineAData['total_sales']);
|
|
|
|
$machineBData = collect($data['table_data'])->firstWhere('machine_id', $this->machineB->id);
|
|
$this->assertNotNull($machineBData);
|
|
$this->assertEquals(1, $machineBData['total_quantity']);
|
|
$this->assertEquals(25.0, $machineBData['total_sales']);
|
|
}
|
|
|
|
/**
|
|
* 測試一般租戶使用者只能撈出有被授權機台的數據 (Machine A),無法看到 Machine B
|
|
*/
|
|
public function test_tenant_user_only_sees_authorized_machine_statistics()
|
|
{
|
|
$this->actingAs($this->tenantUser);
|
|
|
|
$response = $this->getJson(route('admin.analysis.machine-reports'));
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('success', true);
|
|
|
|
$data = $response->json();
|
|
|
|
// 總和應只包含 Machine A
|
|
$this->assertEquals(2, $data['summary']['total_quantity']);
|
|
$this->assertEquals(30.0, $data['summary']['total_sales']);
|
|
$this->assertEquals(20.0, $data['summary']['total_cost']);
|
|
$this->assertEquals(10.0, $data['summary']['total_profit']);
|
|
|
|
// 機台明細應只有 1 筆 (Machine A)
|
|
$this->assertCount(1, $data['table_data']);
|
|
$this->assertEquals($this->machineA->id, $data['table_data'][0]['machine_id']);
|
|
}
|
|
|
|
/**
|
|
* 測試支付工具篩選器能正確過濾資料
|
|
*/
|
|
public function test_payment_types_filter_works()
|
|
{
|
|
$this->actingAs($this->superAdmin);
|
|
|
|
// 僅篩選支付類型 1 (信用卡)
|
|
$response = $this->getJson(route('admin.analysis.machine-reports', [
|
|
'company_id' => $this->company->id,
|
|
'payment_types' => '1'
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$data = $response->json();
|
|
|
|
// 應只有 Order 1 的資料 (Machine A)
|
|
$this->assertEquals(2, $data['summary']['total_quantity']);
|
|
$this->assertEquals(30.0, $data['summary']['total_sales']);
|
|
$this->assertCount(1, $data['table_data']);
|
|
$this->assertEquals($this->machineA->id, $data['table_data'][0]['machine_id']);
|
|
}
|
|
|
|
/**
|
|
* 測試商品報表中的機台過濾功能
|
|
*/
|
|
public function test_product_reports_machine_filter_works()
|
|
{
|
|
$this->actingAs($this->superAdmin);
|
|
|
|
// 篩選 Machine B
|
|
$response = $this->getJson(route('admin.analysis.product-reports', [
|
|
'company_id' => $this->company->id,
|
|
'machine_id' => $this->machineB->id
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$data = $response->json();
|
|
|
|
// Machine B 上面只有 Product Y (Soda)
|
|
$this->assertEquals(1, $data['summary']['total_quantity']);
|
|
$this->assertEquals(25.0, $data['summary']['total_sales']);
|
|
$this->assertCount(1, $data['table_data']);
|
|
$this->assertEquals($this->productY->id, $data['table_data'][0]['product_id']);
|
|
}
|
|
}
|