[FEAT] 刷卡機狀態顯示限基礎版啟用機台,並加機台日誌查詢索引

1. Machine 新增 show_card_terminal 取值器:僅 shopping_mode=basic 且 card_terminal_enabled 的機台才呈現刷卡機燈號與日誌分頁(購物車模式或未啟用刷卡機者隱藏)。
2. 機台列表頁:刷卡機狀態燈號與「刷卡機日誌」分頁改以 show_card_terminal 控制顯示;openLogPanel 多帶 cardTerminalEnabled 參數,分頁不可見時自動退回機台狀態分頁。
3. 列表狀態欄文案調整:欄位標頭由「Sub-machine Status」改為「Hardware Status」,下位機燈號標籤改顯示「Sub-machine」,與新增的刷卡機燈號並列更清楚。
4. 新增 migration:machine_logs 加複合索引 (machine_id, type, is_resolved, level),加速列表頁逐台的狀態未解決日誌查詢(正式環境資料量大,建議離峰執行)。
5. CardTerminalStatusTest 新增 show_card_terminal 顯示條件測試(基礎版啟用才顯示、未啟用或非基礎版皆不顯示)。
6. 新增三語系 key(Hardware Status、Sub-machine),zh_TW/en/ja 對齊並 ksort 排序。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-18 13:44:22 +08:00
parent df3c72e927
commit fd954c80a0
7 changed files with 93 additions and 11 deletions

View File

@ -512,6 +512,16 @@ class Machine extends Model
$this->attributes['card_terminal_enabled'] = (bool) $value;
}
/**
* 是否需要在後台呈現刷卡機狀態燈號與日誌分頁。
* 僅「基礎版(shopping_mode=basic)」且「刷卡機支付已啟用」的機台才有意義;
* 購物車模式或未啟用刷卡機的機台不顯示。
*/
public function getShowCardTerminalAttribute(): bool
{
return $this->shopping_mode === 'basic' && $this->card_terminal_enabled;
}
public function getScanPayEsunEnabledAttribute(): bool
{
return (bool) ($this->settings['scan_pay_esun_enabled'] ?? ($this->attributes['scan_pay_esun_enabled'] ?? false));

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 機台狀態燈號查詢加速。
* 連線(calculated_status)、下位機(hardware_status)、登入、刷卡機(card_terminal) 等狀態
* 皆以 WHERE machine_id=? AND type=? AND is_resolved=? AND level=? 過濾未解決日誌,
* 且在機台列表頁逐台執行。加複合索引避免隨日誌量成長而變慢。
*
* 註:正式環境 machine_logs 可能很大,加索引會短暫鎖表,建議離峰執行。
*/
public function up(): void
{
Schema::table('machine_logs', function (Blueprint $table) {
$table->index(['machine_id', 'type', 'is_resolved', 'level'], 'machine_logs_status_lookup_index');
});
}
public function down(): void
{
Schema::table('machine_logs', function (Blueprint $table) {
$table->dropIndex('machine_logs_status_lookup_index');
});
}
};

View File

@ -905,6 +905,7 @@
"Hardware & Network": "Hardware & Network",
"Hardware & Slots": "Hardware & Slots",
"Hardware Peripheral Settings": "Hardware Peripheral Settings",
"Hardware Status": "Hardware Status",
"HashIV": "HashIV",
"HashKey": "HashKey",
"Heartbeat": "Heartbeat",
@ -2042,6 +2043,7 @@
"Sub Account Roles": "Sub Account Roles",
"Sub Accounts": "Sub Accounts",
"Sub-actions": "Sub-actions",
"Sub-machine": "Sub-machine",
"Sub-machine Status": "Sub-machine Status",
"Sub-machine Status Request": "Sub-machine Status Request",
"Submachine Alert": "Submachine Alert",

View File

@ -905,6 +905,7 @@
"Hardware & Network": "ハードウェア・ネットワーク",
"Hardware & Slots": "ハードウェア・スロット",
"Hardware Peripheral Settings": "ハードウェア周辺機器設定",
"Hardware Status": "ハードウェア状態",
"HashIV": "HashIV",
"HashKey": "HashKey",
"Heartbeat": "ハートビート",
@ -2042,6 +2043,7 @@
"Sub Account Roles": "サブアカウントロール",
"Sub Accounts": "サブアカウント",
"Sub-actions": "サブアクション",
"Sub-machine": "サブマシン",
"Sub-machine Status": "Sub-machine Status",
"Sub-machine Status Request": "下位機ステータスリクエスト",
"Submachine Alert": "下位機アラート",

View File

@ -285,7 +285,7 @@
"Card Reader Seconds": "刷卡機秒數",
"Card Side Background": "左側卡片背景圖",
"Card System": "刷卡機系統",
"Card Terminal": "刷卡機",
"Card Terminal": "刷卡機",
"Card Terminal Logs": "刷卡機日誌",
"Card Terminal Status": "刷卡機狀態",
"Card Terminal System": "刷卡機系統",
@ -905,6 +905,7 @@
"Hardware & Network": "硬體與網路",
"Hardware & Slots": "硬體與貨道",
"Hardware Peripheral Settings": "硬體周邊設定",
"Hardware Status": "硬體狀態",
"HashIV": "HashIV",
"HashKey": "HashKey",
"Heartbeat": "心跳狀態",
@ -2042,6 +2043,7 @@
"Sub Account Roles": "子帳號角色",
"Sub Accounts": "子帳號",
"Sub-actions": "子項目",
"Sub-machine": "下位機",
"Sub-machine Status": "下位機狀態",
"Sub-machine Status Request": "下位機狀態回傳",
"Submachine Alert": "下位機告警",

View File

@ -43,6 +43,7 @@
currentMachineSn: '',
currentMachineName: '',
currentMachineAmbientTempEnabled: false,
currentMachineCardTerminalEnabled: false,
logs: [],
loading: false,
inventoryLoading: false,
@ -87,11 +88,16 @@
});
},
async openLogPanel(id, sn, name, ambientTempEnabled = false) {
async openLogPanel(id, sn, name, ambientTempEnabled = false, cardTerminalEnabled = false) {
this.currentMachineId = id;
this.currentMachineSn = sn;
this.currentMachineName = name;
this.currentMachineAmbientTempEnabled = ambientTempEnabled === true || ambientTempEnabled === 1 || ambientTempEnabled === '1' || ambientTempEnabled === 'true';
this.currentMachineCardTerminalEnabled = cardTerminalEnabled === true || cardTerminalEnabled === 1 || cardTerminalEnabled === '1' || cardTerminalEnabled === 'true';
// 若刷卡機分頁不可見且當前正停在該分頁,退回機台狀態分頁
if (!this.currentMachineCardTerminalEnabled && this.activeTab === 'card_terminal') {
this.activeTab = 'status';
}
this.slots = [];
this.showLogPanel = true;
this.activeTab = 'status';
@ -520,7 +526,7 @@
{{ __('Temperature') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Sub-machine Status') }}</th>
{{ __('Hardware Status') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('APP Version') }}</th>
@ -537,7 +543,7 @@
<tr
class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6 cursor-pointer group"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }})">
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}, {{ $machine->show_card_terminal ? 1 : 0 }})">
<div class="flex items-center gap-3">
<div
class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm">
@ -641,7 +647,6 @@
<td class="px-6 py-6 text-center">
@php
$hStatus = $machine->hardware_status;
$ctStatus = $machine->card_terminal_status;
@endphp
<div class="flex items-center justify-center gap-3">
{{-- 下位機 --}}
@ -654,13 +659,15 @@
$hStatus === 'error' ? 'text-rose-500' :
($hStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
}} uppercase tracking-widest">
{{ $hStatus === 'error' ? __('Error') : ($hStatus === 'warning' ? __('Warning') : __('Normal')) }}
{{ __('Sub-machine') }}
@if($machine->latest_hardware_log_time && $hStatus !== 'normal')
<span class="text-[9px] lowercase opacity-70 block -mt-0.5 ml-3 font-bold">{{ $machine->latest_hardware_log_time->diffForHumans() }}</span>
@endif
</span>
</div>
{{-- 刷卡機 (信用卡/電子票證/手機支付共用同一台) --}}
{{-- 刷卡機 (信用卡/電子票證/手機支付共用同一台):僅基礎版且啟用刷卡機才顯示 --}}
@if($machine->show_card_terminal)
@php $ctStatus = $machine->card_terminal_status; @endphp
<div class="flex items-center gap-1.5 tooltip" title="{{ __('Card Terminal Status') }}">
<span class="w-1.5 h-1.5 rounded-full {{
$ctStatus === 'error' ? 'bg-rose-500 animate-pulse' :
@ -676,6 +683,7 @@
@endif
</span>
</div>
@endif
</div>
</td>
<td
@ -701,7 +709,7 @@
</svg>
</button>
<button type="button"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }})"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}, {{ $machine->show_card_terminal ? 1 : 0 }})"
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all duration-200"
title="{{ __('View Logs') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
@ -748,7 +756,7 @@
@endif
</div>
<div class="min-w-0">
<h3 @click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }})"
<h3 @click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}, {{ $machine->show_card_terminal ? 1 : 0 }})"
class="text-base font-extrabold text-slate-800 dark:text-slate-100 hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors tracking-tight cursor-pointer flex items-center gap-2 min-w-0">
<span class="truncate">{{ $machine->name }}</span>
@if($machine->has_login_warning)
@ -836,7 +844,7 @@
{{ __('Edit') }}
</button>
<button type="button"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }})"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}', {{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}, {{ $machine->show_card_terminal ? 1 : 0 }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
@ -1029,8 +1037,10 @@
{{ __('Sub-machine Status Request') }}
</button>
<button @click="activeTab = 'card_terminal'"
x-show="currentMachineCardTerminalEnabled"
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'card_terminal', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'card_terminal'}"
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300"
x-cloak>
{{ __('Card Terminal Logs') }}
</button>
<button @click="activeTab = 'ambient_temp'"

View File

@ -148,6 +148,32 @@ class CardTerminalStatusTest extends TestCase
$this->assertStringContainsString('0003', $log->translated_message);
}
public function test_show_card_terminal_only_when_basic_and_enabled(): void
{
// 基礎版 + 刷卡機啟用 → 顯示
$m1 = Machine::factory()->create();
$m1->shopping_mode = 'basic';
$m1->card_terminal_enabled = true;
$m1->save();
$this->assertTrue($m1->fresh()->show_card_terminal);
// 基礎版 + 刷卡機未啟用 → 不顯示
$m2 = Machine::factory()->create();
$m2->shopping_mode = 'basic';
$m2->card_terminal_enabled = false;
$m2->save();
$this->assertFalse($m2->fresh()->show_card_terminal);
// 非基礎版(員工卡=晟崴 / 取物單=中國醫) → 無支付設備,不顯示
foreach (['employee_card', 'pickup_sheet'] as $mode) {
$m = Machine::factory()->create();
$m->shopping_mode = $mode;
$m->card_terminal_enabled = true; // 即便旗標誤開,非 basic 也不顯示
$m->save();
$this->assertFalse($m->fresh()->show_card_terminal, "shopping_mode={$mode} 不應顯示刷卡機");
}
}
public function test_preloaded_aggregate_path_avoids_n_plus_one(): void
{
$machine = Machine::factory()->create();