[FEAT] 基本設定的機台管理新增鑰匙編號欄位與位置調整

1. 於 `MachineSettingController.php` 的 `store` 驗證規則中新增 `key_no` 欄位。
2. 於 `index.blade.php` 的新增機台 Modal 及詳情 Drawer 中,加入「鑰匙編號」欄位與展示。
3. 於機台列表 `tab-machines.blade.php` (桌面表格與行動版卡片) 新增「鑰匙編號」顯示,並同步更新 Detail Drawer 參數。
4. 於 `edit.blade.php` 將「鑰匙編號」欄位移至「基本資訊」區塊並與「客戶公司」並排顯示。
5. 補齊並自動對齊繁中、英文、日文翻譯。
6. 撰寫 `MachineSettingTest.php` 測試案例,驗證基本設定中新增與更新機台鑰匙編號之邏輯。
This commit is contained in:
sky121113 2026-06-05 11:50:00 +08:00
parent 11eca730f7
commit 0964981097
8 changed files with 120 additions and 6 deletions

View File

@ -138,6 +138,7 @@ class MachineSettingController extends AdminController
'company_id' => 'nullable|exists:companies,id',
'machine_model_id' => 'required|exists:machine_models,id',
'payment_config_id' => 'nullable|exists:payment_configs,id',
'key_no' => 'nullable|string|max:255',
'location' => 'nullable|string|max:255',
'address' => 'nullable|string|max:255',
'latitude' => 'nullable|numeric|between:-90,90',

View File

@ -663,6 +663,7 @@
"Enter ad material name": "Enter ad material name",
"Enter ambient temperature setting...": "Enter ambient temperature setting...",
"Enter full address": "Enter full address",
"Enter key number": "Enter key number",
"Enter login ID": "Enter login ID",
"Enter machine location": "Enter machine location",
"Enter machine name": "Enter machine name",

View File

@ -663,6 +663,7 @@
"Enter ad material name": "広告素材名を入力",
"Enter ambient temperature setting...": "周囲温度設定を入力してください...",
"Enter full address": "完全な住所を入力",
"Enter key number": "鍵番号を入力してください",
"Enter login ID": "ログインIDを入力してください",
"Enter machine location": "機器の設置場所を入力してください",
"Enter machine name": "機器名を入力してください",

View File

@ -663,6 +663,7 @@
"Enter ad material name": "輸入廣告素材名稱",
"Enter ambient temperature setting...": "請輸入環境溫度設定...",
"Enter full address": "輸入完整地址",
"Enter key number": "請輸入鑰匙編號",
"Enter login ID": "請輸入登入帳號",
"Enter machine location": "請輸入機台地點",
"Enter machine name": "請輸入機台名稱",

View File

@ -161,6 +161,11 @@
</div>
@endif
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Key No') }}</label>
<input type="text" name="key_no" value="{{ old('key_no', $machine->key_no) }}" class="luxury-input w-full" placeholder="{{ __('Enter key number') }}">
</div>
<div x-data="machineGeocoding()" class="col-span-full grid grid-cols-1 md:grid-cols-4 gap-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Address') }}</label>
@ -250,10 +255,6 @@
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Card Reader No') }}</label>
<input type="text" name="card_reader_no" value="{{ $machine->card_reader_no }}" class="luxury-input w-full">
</div>
<div>
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Key No') }}</label>
<input type="text" name="key_no" value="{{ $machine->key_no }}" class="luxury-input w-full">
</div>
</div>
<div>

View File

@ -806,6 +806,15 @@
placeholder="{{ __('Enter machine location') }}">
</div>
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Key No') }}
</label>
<input type="text" name="key_no" class="luxury-input w-full"
placeholder="{{ __('Enter key number') }}">
</div>
<div class="relative focus-within:z-[60]">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
@ -1385,6 +1394,12 @@
<span class="text-sm font-black text-slate-700 dark:text-slate-300"
x-text="currentMachine?.card_reader_no || '--'"></span>
</div>
<div
class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
<span class="text-sm font-bold text-slate-500">{{ __('Key No') }}</span>
<span class="text-sm font-black text-slate-700 dark:text-slate-300"
x-text="currentMachine?.key_no || '--'"></span>
</div>
<div
class="flex flex-col gap-3 p-3 mt-1 bg-slate-50 dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-700/50 relative">
<div class="flex items-center justify-between">

View File

@ -55,6 +55,8 @@
{{ __('Owner') }}</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">
{{ __('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">
{{ __('Key No') }}</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">
{{ __('Card Reader') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
@ -96,6 +98,11 @@
<td class="px-6 py-6">
<x-status-badge :status="$status" size="sm" />
</td>
<td class="px-6 py-6">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">
{{ $machine->key_no ?? '--' }}
</span>
</td>
<td class="px-6 py-6">
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
{{ $machine->card_reader_seconds ?? 0 }}s <span class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{ $machine->card_reader_no ?? '--' }}</span>
@ -126,7 +133,7 @@
</svg>
</a>
<button
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
class="p-2 rounded-lg 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"
title="{{ __('View Details') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -200,6 +207,10 @@
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Card Reader') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $machine->card_reader_seconds ?? 0 }}s / No.{{ $machine->card_reader_no ?? '--' }}</p>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Key No') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $machine->key_no ?? '--' }}</p>
</div>
</div>
{{-- Action Buttons --}}
@ -229,7 +240,7 @@
</svg>
</a>
<button
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls', 'key_no'])))"
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 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50"
title="{{ __('Detail') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">

View File

@ -0,0 +1,83 @@
<?php
namespace Tests\Feature\Admin\BasicSettings;
use Tests\TestCase;
use App\Models\System\User;
use App\Models\Machine\Machine;
use App\Models\Machine\MachineModel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Permission;
class MachineSettingTest extends TestCase
{
use RefreshDatabase;
protected $admin;
protected $machineModel;
protected function setUp(): void
{
parent::setUp();
// 建立擁有 menu.basic.machines 權限的系統管理員
$this->admin = User::factory()->create(['company_id' => null]);
$this->admin->givePermissionTo(Permission::create(['name' => 'menu.basic.machines']));
// 建立測試機台型號
$this->machineModel = MachineModel::create([
'name' => 'Test Model 01',
]);
}
public function test_system_admin_can_create_machine_with_key_no()
{
$payload = [
'name' => 'New Test Machine',
'serial_no' => 'TEST-SN-999',
'machine_model_id' => $this->machineModel->id,
'key_no' => 'KEY-999-ABC',
'location' => 'Taipei Main Station',
];
$response = $this->actingAs($this->admin)
->post(route('admin.basic-settings.machines.store'), $payload);
$response->assertRedirect(route('admin.basic-settings.machines.index'));
$this->assertDatabaseHas('machines', [
'name' => 'New Test Machine',
'serial_no' => 'TEST-SN-999',
'key_no' => 'KEY-999-ABC',
]);
}
public function test_system_admin_can_update_machine_key_no()
{
// 先建立一個預設機台
$machine = Machine::factory()->create([
'machine_model_id' => $this->machineModel->id,
'key_no' => 'OLD-KEY-111',
]);
$payload = [
'name' => 'Updated Machine Name',
'serial_no' => $machine->serial_no,
'machine_model_id' => $this->machineModel->id,
'card_reader_seconds' => 30,
'payment_buffer_seconds' => 5,
'invoice_status' => 0,
'key_no' => 'NEW-KEY-888',
];
$response = $this->actingAs($this->admin)
->put(route('admin.basic-settings.machines.update', $machine), $payload);
$response->assertRedirect(route('admin.basic-settings.machines.index'));
$this->assertDatabaseHas('machines', [
'id' => $machine->id,
'key_no' => 'NEW-KEY-888',
]);
}
}