feat(remote): 指令中心新增卡貨貨道查詢/清除 (query_jam / clear_jam)

微波機出貨卡貨後,下位機會記錄該貨道異常;未清除前該貨道再出貨仍會失敗,
原本只能到機台端手動排除。此版把機台「系統設定 → 貨道卡貨排除」的能力
搬到後台指令中心,可遠端對個別機台查詢/清除。

對應下位機協定 4.5.32(CMD 0x32):
  0x00 查詢貨道卡貨狀態 → 回 卡貨貨道N + 皮帶自檢失敗貨道N
  0x01 清除卡貨故障(不帶貨道參數=一次清全部)

- RemoteController: command_type 白名單加 query_jam,clear_jam
- index.blade.php: 新增兩顆按鈕;getCommandName 對照;translateNote 將
  機台回傳的 JSON {jam:[2,5],belt:[]} 渲染為可讀文字
- lang(en/zh_TW/ja): 補 5 個標籤

機台端以 ACK message 回傳 JSON(App v40 JamSlotManager),走既有
ProcessCommandAck → note 的既有管道,故 ACK 處理與 migration 皆免改。
This commit is contained in:
twsystem1004 2026-07-17 08:29:34 +00:00
parent 7647b87f26
commit 84ec95d95f
5 changed files with 79 additions and 4 deletions

View File

@ -142,7 +142,7 @@ class RemoteController extends Controller
{
$validated = $request->validate([
'machine_id' => 'required|exists:machines,id',
'command_type' => 'required|string|in:reboot,reboot_force,reboot_card,checkout,lock,unlock,change,dispense,fanon,fanoff,fanauto',
'command_type' => 'required|string|in:reboot,reboot_force,reboot_card,checkout,lock,unlock,change,dispense,fanon,fanoff,fanauto,query_jam,clear_jam',
'amount' => 'nullable|integer|min:0',
'slot_no' => 'nullable|string',
'note' => 'nullable|string|max:255',

View File

@ -2748,5 +2748,10 @@
"video": "Video",
"visit_gift": "Visit Gift",
"vs Yesterday": "vs Yesterday",
"warning": "warning"
"warning": "warning",
"Query Jammed Slots": "Query Jammed Slots",
"Clear Jammed Slots": "Clear Jammed Slots",
"No jammed slots": "No jammed slots",
"Jammed slots:": "Jammed slots:",
"Belt self-test failed:": "Belt self-test failed:"
}

View File

@ -2709,5 +2709,10 @@
"video": "動画",
"visit_gift": "来店ギフト",
"vs Yesterday": "昨日比",
"warning": "警告"
"warning": "警告",
"Query Jammed Slots": "詰まりレーン照会",
"Clear Jammed Slots": "詰まりレーン解除",
"No jammed slots": "詰まりレーンなし",
"Jammed slots:": "詰まりレーン:",
"Belt self-test failed:": "ベルト自己診断failed"
}

View File

@ -2749,5 +2749,10 @@
"video": "影片",
"visit_gift": "來店禮",
"vs Yesterday": "較昨日",
"warning": "警告"
"warning": "警告",
"Query Jammed Slots": "查詢卡貨貨道",
"Clear Jammed Slots": "清除卡貨貨道",
"No jammed slots": "無卡貨貨道",
"Jammed slots:": "卡貨貨道:",
"Belt self-test failed:": "皮帶自檢失敗貨道:"
}

View File

@ -46,6 +46,11 @@
'Ambient Temperature Upper Limit' => __('Ambient Temperature Upper Limit'),
'Remote Change' => __('Remote Change'),
'Remote Dispense' => __('Remote Dispense'),
'Query Jammed Slots' => __('Query Jammed Slots'),
'Clear Jammed Slots' => __('Clear Jammed Slots'),
'No jammed slots' => __('No jammed slots'),
'Jammed slots:' => __('Jammed slots:'),
'Belt self-test failed:' => __('Belt self-test failed:'),
'Adjust Stock & Expiry' => __('Adjust Stock & Expiry'),
'Sync Ads' => __('Sync Ads'),
'Sync Products' => __('Sync Products'),
@ -456,6 +461,8 @@
'ambient_temp_limit': this.translations['Ambient Temperature Upper Limit'],
'change': this.translations['Remote Change'],
'dispense': this.translations['Remote Dispense'],
'query_jam': this.translations['Query Jammed Slots'],
'clear_jam': this.translations['Clear Jammed Slots'],
'reload_stock': this.translations['Adjust Stock & Expiry'],
'update_ads': this.translations['Sync Ads'],
'update_products': this.translations['Sync Products'],
@ -483,6 +490,21 @@
translateNote(note) {
if (!note) return '';
// query_jam 的 ACK message 是 JSON{"jam":[2,5],"belt":[]}(機台 JamSlotManager.toJson
// 這裡渲染成可讀文字;非此格式則走原本的字典/原樣顯示。
if (note.startsWith('{') && note.includes('"jam"')) {
try {
const d = JSON.parse(note);
const parts = [];
parts.push(d.jam && d.jam.length
? this.translations['Jammed slots:'] + ' ' + d.jam.join('、')
: this.translations['No jammed slots']);
if (d.belt && d.belt.length) {
parts.push(this.translations['Belt self-test failed:'] + ' ' + d.belt.join('、'));
}
return parts.join(' / ');
} catch (e) { /* 解析失敗就原樣顯示 */ }
}
const translations = {
'Superseded by new adjustment': this.translations['Superseded by new adjustment'],
'Superseded by new command': this.translations['Superseded by new command'],
@ -703,6 +725,44 @@
</div>
</button>
<!-- Query Jammed Slots (下位機 CMD 0x32 0x00) -->
<button @click="sendCommand('query_jam')"
class="p-6 rounded-3xl border border-slate-100 dark:border-slate-800 flex items-center gap-5 hover:border-cyan-500/50 dark:hover:border-cyan-400/60 hover:bg-cyan-500/5 dark:hover:bg-cyan-400/5 group transition-all bg-white/50 dark:bg-slate-900/40 shadow-sm">
<div
class="w-12 h-12 rounded-2xl bg-cyan-500/10 flex items-center justify-center text-cyan-500 dark:text-cyan-400 group-hover:scale-110 transition-transform duration-500 border border-cyan-500/20 dark:border-cyan-400/20">
<svg class="w-6 h-6" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div class="text-left">
<div
class="text-sm font-black text-slate-800 dark:text-white group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
{{ __('Query Jammed Slots') }}</div>
</div>
</button>
<!-- Clear Jammed Slots (下位機 CMD 0x32 0x01;一次清全部) -->
<button @click="sendCommand('clear_jam')"
class="p-6 rounded-3xl border border-slate-100 dark:border-slate-800 flex items-center gap-5 hover:border-amber-500/50 dark:hover:border-amber-400/60 hover:bg-amber-500/5 dark:hover:bg-amber-400/5 group transition-all bg-white/50 dark:bg-slate-900/40 shadow-sm">
<div
class="w-12 h-12 rounded-2xl bg-amber-500/10 flex items-center justify-center text-amber-500 dark:text-amber-400 group-hover:scale-110 transition-transform duration-500 border border-amber-500/20 dark:border-amber-400/20">
<svg class="w-6 h-6" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
<div class="text-left">
<div
class="text-sm font-black text-slate-800 dark:text-white group-hover:text-amber-600 dark:group-hover:text-amber-400 transition-colors">
{{ __('Clear Jammed Slots') }}</div>
</div>
</button>
<!-- Remote Settlement -->
<button @click="sendCommand('checkout')"
class="p-6 rounded-3xl border border-slate-100 dark:border-slate-800 flex items-center gap-5 hover:border-emerald-500/50 dark:hover:border-emerald-400/60 hover:bg-emerald-500/5 dark:hover:bg-emerald-400/5 group transition-all bg-white/50 dark:bg-slate-900/40 shadow-sm">