diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index aec6116..175f16e 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -50,6 +50,8 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - `4`: 補貨頁 / `5`: 教學頁 / `6`: 購買中 / `7`: 鎖定頁 - `60`: 出貨成功 / `61`: 貨道測試 / `62`: 付款選擇 - `63`: 等待付款 / `64`: 出貨 / `65`: 收據簽單 +- `66`: 通行碼 / `67`: 取貨碼 / `68`: 訊息顯示 +- `69`: 取消購買 / `610`: 購買結束 / `611`: 來店禮 - `612`: 出貨失敗 **雲端指令代碼 (status):** @@ -62,6 +64,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 - `72`: sellCode reload B023 (即期品) - `75`: exp reload B026 (效期) - `79`: read B050 (參數讀取) +- `81`: sync timer status (B710) - `85`: reload B0552 (出貨腳本) --- diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index 8df0cd9..7aa02e5 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -131,7 +131,9 @@ class MachineController extends AdminController 'batch_no' => 'nullable|string|max:50', ]); - $this->machineService->updateSlot($machine, $validated); + $this->machineService->updateSlot($machine, $validated, auth()->id()); + + session()->flash('success', __('Slot updated successfully.')); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php index bbec9c6..21b3c21 100644 --- a/app/Http/Controllers/Admin/RemoteController.php +++ b/app/Http/Controllers/Admin/RemoteController.php @@ -15,18 +15,32 @@ class RemoteController extends Controller */ public function index(Request $request) { - $machines = Machine::withCount(['slots'])->orderBy('name')->get(); + $machines = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get(); $selectedMachine = null; + $history = RemoteCommand::where('command_type', '!=', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get(); if ($request->has('machine_id')) { $selectedMachine = Machine::with(['slots.product', 'commands' => function($query) { - $query->latest()->limit(10); + $query->where('command_type', '!=', 'reload_stock') + ->latest() + ->limit(10); }])->find($request->machine_id); } + if ($request->ajax()) { + return response()->json([ + 'success' => true, + 'machine' => $selectedMachine, + 'commands' => $selectedMachine ? $selectedMachine->commands : [] + ]); + } + return view('admin.remote.index', [ 'machines' => $machines, 'selectedMachine' => $selectedMachine, + 'history' => $history, + 'title' => __('Remote Command Center'), + 'subtitle' => __('Execute maintenance and operational commands remotely') ]); } @@ -50,15 +64,35 @@ class RemoteController extends Controller $payload['slot_no'] = $validated['slot_no']; } + // 指令去重:將同機台、同類型的 pending 指令標記為「已取代」 + RemoteCommand::where('machine_id', $validated['machine_id']) + ->where('command_type', $validated['command_type']) + ->where('status', 'pending') + ->update([ + 'status' => 'superseded', + 'note' => __('Superseded by new command'), + 'executed_at' => now(), + ]); + RemoteCommand::create([ 'machine_id' => $validated['machine_id'], + 'user_id' => auth()->id(), 'command_type' => $validated['command_type'], 'payload' => $payload, 'status' => 'pending', 'note' => $validated['note'] ?? null, ]); - return redirect()->back()->with('success', __('Command has been queued successfully.')); + session()->flash('success', __('Command has been queued successfully.')); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => __('Command has been queued successfully.') + ]); + } + + return redirect()->back(); } /** @@ -70,17 +104,31 @@ class RemoteController extends Controller 'slots as slots_count', 'slots as low_stock_count' => function ($query) { $query->where('stock', '<=', 5); + }, + 'slots as expiring_soon_count' => function ($query) { + $query->whereNotNull('expiry_date') + ->where('expiry_date', '<=', now()->addDays(7)) + ->where('expiry_date', '>=', now()->startOfDay()); } - ])->orderBy('name')->get(); + ])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get(); + $history = RemoteCommand::where('command_type', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get(); + $selectedMachine = null; if ($request->has('machine_id')) { - $selectedMachine = Machine::with('slots.product')->find($request->machine_id); + $selectedMachine = Machine::with(['slots.product', 'commands' => function($query) { + $query->where('command_type', 'reload_stock') + ->latest() + ->limit(50); + }])->find($request->machine_id); } return view('admin.remote.stock', [ 'machines' => $machines, 'selectedMachine' => $selectedMachine, + 'history' => $history, + 'title' => __('Stock & Expiry Management'), + 'subtitle' => __('Real-time monitoring and adjustment of cargo lane inventory and expiration dates') ]); } } diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 51cbbd8..6c2dd12 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -104,6 +104,12 @@ class MachineController extends Controller { $machine = $request->get('machine'); $slots = $machine->slots()->with('product')->get(); + + // 自動轉 Success: 若機台來撈 B017,代表之前的 reload_stock 指令已成功被機台響應 + \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id) + ->where('command_type', 'reload_stock') + ->where('status', 'sent') + ->update(['status' => 'success', 'executed_at' => now()]); return response()->json([ 'success' => true, diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index da15586..26df42c 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -106,6 +106,11 @@ class Machine extends Model return $this->hasMany(MachineSlot::class); } + public function commands() + { + return $this->hasMany(RemoteCommand::class); + } + public function machineModel() { return $this->belongsTo(MachineModel::class); @@ -133,21 +138,20 @@ class Machine extends Model '3' => 'Admin Page', '4' => 'Replenishment Page', '5' => 'Tutorial Page', - '60' => 'Purchasing', - '61' => 'Locked Page', - '62' => 'Dispense Failed', - '301' => 'Slot Test', - '302' => 'Slot Test', - '401' => 'Payment Selection', - '402' => 'Waiting for Payment', - '403' => 'Dispensing', - '404' => 'Receipt Printing', - '601' => 'Pass Code', - '602' => 'Pickup Code', - '603' => 'Message Display', - '604' => 'Cancel Purchase', - '605' => 'Purchase Finished', - '611' => 'Welcome Gift Status', + '6' => 'Purchasing', + '7' => 'Locked Page', + '60' => 'Dispense Success', + '61' => 'Slot Test', + '62' => 'Payment Selection', + '63' => 'Waiting for Payment', + '64' => 'Dispensing', + '65' => 'Receipt Printing', + '66' => 'Pass Code', + '67' => 'Pickup Code', + '68' => 'Message Display', + '69' => 'Cancel Purchase', + '610' => 'Purchase Finished', + '611' => 'Welcome Gift', '612' => 'Dispense Failed', ]; diff --git a/app/Models/Machine/RemoteCommand.php b/app/Models/Machine/RemoteCommand.php index f70e66c..27ba7af 100644 --- a/app/Models/Machine/RemoteCommand.php +++ b/app/Models/Machine/RemoteCommand.php @@ -11,6 +11,7 @@ class RemoteCommand extends Model protected $fillable = [ 'machine_id', + 'user_id', 'command_type', 'payload', 'status', @@ -28,6 +29,11 @@ class RemoteCommand extends Model return $this->belongsTo(Machine::class); } + public function user() + { + return $this->belongsTo(\App\Models\System\User::class); + } + /** * Scope for pending commands */ diff --git a/app/Services/Machine/MachineService.php b/app/Services/Machine/MachineService.php index f2e9898..367836e 100644 --- a/app/Services/Machine/MachineService.php +++ b/app/Services/Machine/MachineService.php @@ -4,6 +4,7 @@ namespace App\Services\Machine; use App\Models\Machine\Machine; use App\Models\Machine\MachineLog; +use App\Models\Machine\RemoteCommand; use Illuminate\Support\Facades\DB; use Carbon\Carbon; @@ -96,17 +97,25 @@ class MachineService * * @param Machine $machine * @param array $data + * @param int|null $userId * @return void */ - public function updateSlot(Machine $machine, array $data): void + public function updateSlot(Machine $machine, array $data, ?int $userId = null): void { - DB::transaction(function () use ($machine, $data) { + DB::transaction(function () use ($machine, $data, $userId) { $slotNo = $data['slot_no']; $stock = $data['stock'] ?? null; $expiryDate = $data['expiry_date'] ?? null; $batchNo = $data['batch_no'] ?? null; $slot = $machine->slots()->where('slot_no', $slotNo)->firstOrFail(); + // 紀錄舊數據以供 Payload 使用 + $oldData = [ + 'stock' => $slot->stock, + 'expiry_date' => $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null, + 'batch_no' => $slot->batch_no, + ]; + $updateData = [ 'expiry_date' => $expiryDate, 'batch_no' => $batchNo, @@ -114,6 +123,33 @@ class MachineService if ($stock !== null) $updateData['stock'] = (int)$stock; $slot->update($updateData); + + // 指令去重:將該機台所有尚未領取的舊庫存同步指令標記為「已取代」 + RemoteCommand::where('machine_id', $machine->id) + ->where('command_type', 'reload_stock') + ->where('status', 'pending') + ->update([ + 'status' => 'superseded', + 'note' => __('Superseded by new adjustment'), + 'executed_at' => now(), + ]); + + // 建立遠端指令紀錄 (Unified Command Concept) + RemoteCommand::create([ + 'machine_id' => $machine->id, + 'user_id' => $userId, + 'command_type' => 'reload_stock', + 'status' => 'pending', + 'payload' => [ + 'slot_no' => $slotNo, + 'old' => $oldData, + 'new' => [ + 'stock' => $stock !== null ? (int)$stock : $oldData['stock'], + 'expiry_date' => $expiryDate ?: null, + 'batch_no' => $batchNo ?: null, + ] + ] + ]); }); } diff --git a/database/migrations/2026_04_02_100848_add_user_id_to_remote_commands_table.php b/database/migrations/2026_04_02_100848_add_user_id_to_remote_commands_table.php new file mode 100644 index 0000000..056be0e --- /dev/null +++ b/database/migrations/2026_04_02_100848_add_user_id_to_remote_commands_table.php @@ -0,0 +1,28 @@ +foreignId('user_id')->nullable()->after('machine_id')->constrained()->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('remote_commands', function (Blueprint $table) { + $table->dropConstrainedForeignId('user_id'); + }); + } +}; diff --git a/database/migrations/2026_04_02_110119_update_remote_commands_status_enum.php b/database/migrations/2026_04_02_110119_update_remote_commands_status_enum.php new file mode 100644 index 0000000..77d972d --- /dev/null +++ b/database/migrations/2026_04_02_110119_update_remote_commands_status_enum.php @@ -0,0 +1,28 @@ + __('Please select a slot'), + 'Search cargo lane' => __('Search cargo lane'), + 'Stock:' => __('Stock:'), + 'Loading...' => __('Loading...'), + 'No active cargo lanes found' => __('No active cargo lanes found'), + 'Empty' => __('Empty'), + 'Machine Reboot' => __('Machine Reboot'), + 'Card Reader Reboot' => __('Card Reader Reboot'), + 'Remote Reboot' => __('Remote Reboot'), + 'Lock Page Lock' => __('Lock Page Lock'), + 'Lock Page Unlock' => __('Lock Page Unlock'), + 'Remote Change' => __('Remote Change'), + 'Remote Dispense' => __('Remote Dispense'), + 'Adjust Stock & Expiry' => __('Adjust Stock & Expiry'), + 'Pending' => __('Pending'), + 'Sent' => __('Sent'), + 'Success' => __('Success'), + 'Failed' => __('Failed'), + 'Superseded' => __('Superseded'), + 'System' => __('System'), + 'Superseded by new adjustment' => __('Superseded by new adjustment'), + 'Superseded by new command' => __('Superseded by new command'), + 'Slot' => __('Slot'), + 'Stock' => __('Stock'), + 'Expiry' => __('Expiry'), + 'Batch' => __('Batch'), + 'Amount' => __('Amount'), + 'Command error:' => __('Command error:'), + 'Command has been queued successfully.' => __('Command has been queued successfully.'), + 'Just now' => __('Just now'), + 'mins ago' => __('mins ago'), + 'hours ago' => __('hours ago'), + ]), + // Form States - lockStatus: false, // false = unlocked, true = locked + lockStatus: false, changeAmount: 100, selectedSlot: '', note: '', @@ -25,6 +69,95 @@ window.remoteControlApp = function(initialMachineId) { await this.selectMachine(machine); } } + + // Watch for machine data changes to rebuild slot select + this.$watch('selectedMachine.slots', () => { + this.$nextTick(() => this.updateSlotSelect()); + }); + }, + + updateSlotSelect() { + const wrapper = document.getElementById('slot-select-wrapper'); + if (!wrapper) return; + + // Clear previous and reset + const oldSelect = wrapper.querySelector('select'); + if (oldSelect) { + try { + const instance = window.HSSelect.getInstance(oldSelect); + if (instance) instance.destroy(); + } catch (e) {} + } + wrapper.innerHTML = ''; + + // If loading, show a skeleton or simple text + if (this.loading) { + wrapper.innerHTML = `
${this.translations['Loading...']}
`; + return; + } + + if (!this.selectedMachine || !this.selectedMachine.slots || this.selectedMachine.slots.length === 0) { + wrapper.innerHTML = ` +
+ ${this.translations['No active cargo lanes found']} +
+ `; + return; + } + + const selectEl = document.createElement('select'); + selectEl.className = 'hidden'; + selectEl.id = 'dynamic-slot-select-' + Date.now(); + + const config = { + "placeholder": this.translations['Please select a slot'] + "...", + "hasSearch": true, + "searchPlaceholder": this.translations['Search cargo lane'] + "...", + "isHidePlaceholder": false, + "searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500", + "searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10", + "toggleClasses": "hs-select-toggle luxury-select-toggle", + "dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[100] animate-luxury-in", + "optionClasses": "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300", + "optionTemplate": '
' + }; + selectEl.setAttribute('data-hs-select', JSON.stringify(config)); + + const placeholderOpt = document.createElement('option'); + placeholderOpt.value = ''; + placeholderOpt.textContent = this.translations['Please select a slot'] + "..."; + placeholderOpt.dataset.title = this.translations['Please select a slot'] + "..."; + selectEl.appendChild(placeholderOpt); + + const sortedSlots = [...this.selectedMachine.slots].sort((a, b) => { + const aNo = parseInt(a.slot_no); + const bNo = parseInt(b.slot_no); + return isNaN(aNo) || isNaN(bNo) ? a.slot_no.localeCompare(b.slot_no) : aNo - bNo; + }); + + sortedSlots.forEach(slot => { + const opt = document.createElement('option'); + opt.value = slot.slot_no; + const productName = slot.product ? slot.product.name : this.translations['Empty']; + const label = `[${slot.slot_no}] ${productName} (${this.translations['Stock:']} ${slot.stock})`; + opt.textContent = label; + opt.dataset.title = label; + if (slot.slot_no === this.selectedSlot) opt.selected = true; + selectEl.appendChild(opt); + }); + + wrapper.appendChild(selectEl); + selectEl.addEventListener('change', (e) => { this.selectedSlot = e.target.value; }); + + if (window.HSStaticMethods && window.HSStaticMethods.autoInit) { + window.HSStaticMethods.autoInit(['select']); + } + }, + + confirmModal: { + show: false, + type: '', + params: {} }, async selectMachine(machine) { @@ -39,18 +172,19 @@ window.remoteControlApp = function(initialMachineId) { window.history.pushState({}, '', url); try { - // Fetch recent commands and full machine info (with slots) const res = await fetch(`/admin/remote?machine_id=${machine.id}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); - // Note: For now we'll just use the already loaded $selectedMachine if available - // But in a real app, an AJAX fetch for commands is better. - // Since we are doing a full page load or partial, I'll assume we have commands passed. - this.commands = @js($selectedMachine ? $selectedMachine->commands : []); + const data = await res.json(); + this.commands = data.commands || []; + if (data.machine) { + this.selectedMachine = data.machine; + } } catch (e) { console.error('Fetch error:', e); } finally { this.loading = false; + this.$nextTick(() => this.updateSlotSelect()); } }, @@ -62,12 +196,28 @@ window.remoteControlApp = function(initialMachineId) { window.history.pushState({}, '', url); }, - async sendCommand(type, params = {}) { - if (!confirm(`{{ __('Are you sure you want to send this command?') }}`)) return; + backToHistory() { + this.viewMode = 'history'; + this.selectedMachine = null; + const url = new URL(window.location); + url.searchParams.delete('machine_id'); + window.history.pushState({}, '', url); + }, + sendCommand(type, params = {}) { + this.note = ''; // Reset note for new command + this.confirmModal.type = type; + this.confirmModal.params = params; + this.confirmModal.show = true; + }, + + async executeCommand() { + const type = this.confirmModal.type; + const params = this.confirmModal.params; + this.confirmModal.show = false; this.submitting = true; + try { - const csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); const formData = new FormData(); formData.append('machine_id', this.selectedMachine.id); formData.append('command_type', type); @@ -76,114 +226,380 @@ window.remoteControlApp = function(initialMachineId) { if (params.amount) formData.append('amount', params.amount); if (params.slot_no) formData.append('slot_no', params.slot_no); - const res = await fetch('{{ route('admin.remote.store-command') }}', { + const res = await fetch(this.appConfig.storeUrl, { method: 'POST', - headers: { 'X-CSRF-TOKEN': csrf }, + headers: { + 'X-CSRF-TOKEN': this.appConfig.csrfToken, + 'Accept': 'application/json' + }, body: formData }); if (res.ok) { - // Success feedback - const toast = window.HSStaticMethods.autoInit ? null : null; // Use Preline toast if available - alert('{{ __('Command queued successfully.') }}'); - location.reload(); // Simple refresh to see new command in list + window.location.href = this.appConfig.indexUrl; } } catch (e) { + window.dispatchEvent(new CustomEvent('toast', { + detail: { + message: this.translations['Command error:'] + ' ' + e.message, + type: 'error' + } + })); console.error('Command error:', e); } finally { this.submitting = false; } }, + formatTime(dateStr) { + if (!dateStr) return '--'; + const date = new Date(dateStr); + const now = new Date(); + const diff = Math.floor((now - date) / 1000); // seconds + + if (diff < 60) return this.translations['Just now']; + if (diff < 3600) return Math.floor(diff / 60) + ' ' + this.translations['mins ago']; + if (diff < 7200) return '1 ' + this.translations['hours ago']; + + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + }, + getCommandBadgeClass(status) { switch(status) { case 'pending': return 'bg-amber-100 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400 border-amber-200 dark:border-amber-500/20'; case 'sent': return 'bg-cyan-100 text-cyan-600 dark:bg-cyan-500/10 dark:text-cyan-400 border-cyan-200 dark:border-cyan-500/20'; case 'success': return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20'; case 'failed': return 'bg-rose-100 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400 border-rose-200 dark:border-rose-500/20'; + case 'superseded': return 'bg-slate-100 text-slate-500 dark:bg-slate-500/10 dark:text-slate-400 border-slate-200 dark:border-slate-500/20 opacity-80'; default: return 'bg-slate-100 text-slate-600 border-slate-200'; } }, getCommandName(type) { const names = { - 'reboot': {{ Js::from(__('System Reboot')) }}, - 'reboot_card': {{ Js::from(__('Card Reader Reboot')) }}, - 'checkout': {{ Js::from(__('Remote Settlement')) }}, - 'lock': {{ Js::from(__('Page Lock')) }}, - 'unlock': {{ Js::from(__('Page Unlock')) }}, - 'change': {{ Js::from(__('Remote Change')) }}, - 'dispense': {{ Js::from(__('Remote Dispense')) }}, - 'reload_stock': {{ Js::from(__('Stock Sync')) }} + 'reboot': this.translations['Machine Reboot'], + 'reboot_card': this.translations['Card Reader Reboot'], + 'checkout': this.translations['Remote Reboot'], + 'lock': this.translations['Lock Page Lock'], + 'unlock': this.translations['Lock Page Unlock'], + 'change': this.translations['Remote Change'], + 'dispense': this.translations['Remote Dispense'], + 'reload_stock': this.translations['Adjust Stock & Expiry'] }; return names[type] || type; + }, + + getCommandStatus(status) { + const statuses = { + 'pending': this.translations['Pending'], + 'sent': this.translations['Sent'], + 'success': this.translations['Success'], + 'failed': this.translations['Failed'], + 'superseded': this.translations['Superseded'] + }; + return statuses[status] || status; + }, + + getOperatorName(user) { + return user ? user.name : this.translations['System']; + }, + + translateNote(note) { + if (!note) return ''; + const translations = { + 'Superseded by new adjustment': this.translations['Superseded by new adjustment'] + }; + return translations[note] || note; + }, + + getPayloadDetails(item) { + if (item.command_type === 'reload_stock' && item.payload) { + const p = item.payload; + let details = `${this.translations['Slot']} ${p.slot_no}: `; + + if (p.old.stock !== p.new.stock) { + details += `${this.translations['Stock']} ${p.old.stock} → ${p.new.stock}`; + } + + if (p.old.expiry_date !== p.new.expiry_date) { + if (p.old.stock !== p.new.stock) details += ', '; + details += `${this.translations['Expiry']} ${p.old.expiry_date || 'N/A'} → ${p.new.expiry_date || 'N/A'}`; + } + + if (p.old.batch_no !== p.new.batch_no) { + if (p.old.stock !== p.new.stock || p.old.expiry_date !== p.new.expiry_date) details += ', '; + details += `${this.translations['Batch']} ${p.old.batch_no || 'N/A'} → ${p.new.batch_no || 'N/A'}`; + } + + return details; + } + + if (item.command_type === 'change' && item.payload) { + return `${this.translations['Amount']}: ${item.payload.amount}`; + } + + if (item.command_type === 'dispense' && item.payload) { + return `${this.translations['Slot']}: ${item.payload.slot_no}`; + } + + return ''; } }; }; -
- -