[PROMOTE] 晉升 dev 變更至 demo 環境
1. 於機台新增/詳情 Modal 及清單中新增鑰匙編號 (Key No) 展示與儲存。 2. 調整機台編輯畫面中的「鑰匙編號」至「客戶公司」旁邊以優化排版。 3. 補齊鑰匙編號輸入框的繁中、英文、日文多語系翻譯。 4. 新增基本設定機台管理 Feature Test 案例並通過驗證。
This commit is contained in:
commit
a39db64eac
@ -138,6 +138,7 @@ class MachineSettingController extends AdminController
|
|||||||
'company_id' => 'nullable|exists:companies,id',
|
'company_id' => 'nullable|exists:companies,id',
|
||||||
'machine_model_id' => 'required|exists:machine_models,id',
|
'machine_model_id' => 'required|exists:machine_models,id',
|
||||||
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||||
|
'key_no' => 'nullable|string|max:255',
|
||||||
'location' => 'nullable|string|max:255',
|
'location' => 'nullable|string|max:255',
|
||||||
'address' => 'nullable|string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'latitude' => 'nullable|numeric|between:-90,90',
|
'latitude' => 'nullable|numeric|between:-90,90',
|
||||||
|
|||||||
@ -663,6 +663,7 @@
|
|||||||
"Enter ad material name": "Enter ad material name",
|
"Enter ad material name": "Enter ad material name",
|
||||||
"Enter ambient temperature setting...": "Enter ambient temperature setting...",
|
"Enter ambient temperature setting...": "Enter ambient temperature setting...",
|
||||||
"Enter full address": "Enter full address",
|
"Enter full address": "Enter full address",
|
||||||
|
"Enter key number": "Enter key number",
|
||||||
"Enter login ID": "Enter login ID",
|
"Enter login ID": "Enter login ID",
|
||||||
"Enter machine location": "Enter machine location",
|
"Enter machine location": "Enter machine location",
|
||||||
"Enter machine name": "Enter machine name",
|
"Enter machine name": "Enter machine name",
|
||||||
|
|||||||
@ -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 login ID": "ログインIDを入力してください",
|
"Enter login ID": "ログインIDを入力してください",
|
||||||
"Enter machine location": "機器の設置場所を入力してください",
|
"Enter machine location": "機器の設置場所を入力してください",
|
||||||
"Enter machine name": "機器名を入力してください",
|
"Enter machine name": "機器名を入力してください",
|
||||||
|
|||||||
@ -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 login ID": "請輸入登入帳號",
|
"Enter login ID": "請輸入登入帳號",
|
||||||
"Enter machine location": "請輸入機台地點",
|
"Enter machine location": "請輸入機台地點",
|
||||||
"Enter machine name": "請輸入機台名稱",
|
"Enter machine name": "請輸入機台名稱",
|
||||||
|
|||||||
@ -161,6 +161,11 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@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 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">
|
<div class="md:col-span-2">
|
||||||
<label class="block text-xs font-bold text-slate-400 uppercase tracking-[0.15em] mb-2">{{ __('Address') }}</label>
|
<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>
|
<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">
|
<input type="text" name="card_reader_no" value="{{ $machine->card_reader_no }}" class="luxury-input w-full">
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -806,6 +806,15 @@
|
|||||||
placeholder="{{ __('Enter machine location') }}">
|
placeholder="{{ __('Enter machine location') }}">
|
||||||
</div>
|
</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]">
|
<div class="relative focus-within:z-[60]">
|
||||||
<label
|
<label
|
||||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
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"
|
<span class="text-sm font-black text-slate-700 dark:text-slate-300"
|
||||||
x-text="currentMachine?.card_reader_no || '--'"></span>
|
x-text="currentMachine?.card_reader_no || '--'"></span>
|
||||||
</div>
|
</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
|
<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">
|
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">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@ -55,6 +55,8 @@
|
|||||||
{{ __('Owner') }}</th>
|
{{ __('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">
|
<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>
|
{{ __('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">
|
<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>
|
{{ __('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">
|
<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">
|
<td class="px-6 py-6">
|
||||||
<x-status-badge :status="$status" size="sm" />
|
<x-status-badge :status="$status" size="sm" />
|
||||||
</td>
|
</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">
|
<td class="px-6 py-6">
|
||||||
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
|
<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>
|
{{ $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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<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"
|
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') }}">
|
title="{{ __('View Details') }}">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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-[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>
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{{-- Action Buttons --}}
|
{{-- Action Buttons --}}
|
||||||
@ -229,7 +240,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<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"
|
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') }}">
|
title="{{ __('Detail') }}">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
|||||||
83
tests/Feature/Admin/BasicSettings/MachineSettingTest.php
Normal file
83
tests/Feature/Admin/BasicSettings/MachineSettingTest.php
Normal 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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user