[FEAT] 強化遠端指令中心的操作備註功能

1. 新增 remote_commands 資料表的 remark 欄位以儲存操作備註。
2. 更新 RemoteCommand 模型,將 note 與 remark 加入可批量賦值名單。
3. 更新 MachineService 的 dispatchDispense 函式,支援接收與記錄操作備註。
4. 更新 RemoteController 的 storeCommand 邏輯,將前端輸入的備註存入 remark 並支援遠端出貨指令。
5. 優化操作紀錄搜尋邏輯,支援搜尋備註欄位。
6. 重構指令中心操作紀錄介面,桌面版新增「操作備註」欄位,並優化手機版卡片顯示。
7. 調整表格欄位寬度與標頭樣式,解決「指令類型」寬度過大的問題。
This commit is contained in:
sky121113 2026-05-12 17:15:53 +08:00
parent ad921011f0
commit 426253b1c3
5 changed files with 84 additions and 16 deletions

View File

@ -53,7 +53,8 @@ class RemoteController extends Controller
->orWhere('serial_no', 'like', "%{$search}%"); ->orWhere('serial_no', 'like', "%{$search}%");
})->orWhereHas('user', function ($uq) use ($search) { })->orWhereHas('user', function ($uq) use ($search) {
$uq->where('name', 'like', "%{$search}%"); $uq->where('name', 'like', "%{$search}%");
}); })->orWhere('remark', 'like', "%{$search}%")
->orWhere('note', 'like', "%{$search}%");
}); });
} }
@ -145,7 +146,8 @@ class RemoteController extends Controller
$this->machineService->dispatchDispense( $this->machineService->dispatchDispense(
Machine::findOrFail($validated['machine_id']), Machine::findOrFail($validated['machine_id']),
$validated['slot_no'], $validated['slot_no'],
auth()->id() auth()->id(),
$validated['note'] ?? null
); );
} else { } else {
$payload = []; $payload = [];
@ -169,7 +171,7 @@ class RemoteController extends Controller
'command_type' => $validated['command_type'], 'command_type' => $validated['command_type'],
'payload' => $payload, 'payload' => $payload,
'status' => 'pending', 'status' => 'pending',
'note' => $validated['note'] ?? null, 'remark' => $validated['note'] ?? null,
]); ]);
// 推播 MQTT 指令 // 推播 MQTT 指令

View File

@ -16,6 +16,8 @@ class RemoteCommand extends Model
'payload', 'payload',
'status', 'status',
'ttl', 'ttl',
'note',
'remark',
'executed_at', 'executed_at',
]; ];

View File

@ -403,9 +403,9 @@ class MachineService
/** /**
* 遠端出貨指令下發 (指令模式,不再預扣庫存) * 遠端出貨指令下發 (指令模式,不再預扣庫存)
*/ */
public function dispatchDispense(Machine $machine, string $slotNo, ?int $userId = null): RemoteCommand public function dispatchDispense(Machine $machine, string $slotNo, ?int $userId = null, ?string $remark = null): RemoteCommand
{ {
return DB::transaction(function () use ($machine, $slotNo, $userId) { return DB::transaction(function () use ($machine, $slotNo, $userId, $remark) {
$slot = $machine->slots()->where('slot_no', $slotNo)->lockForUpdate()->firstOrFail(); $slot = $machine->slots()->where('slot_no', $slotNo)->lockForUpdate()->firstOrFail();
// 併行檢查:若有 pending 指令,超過 1 分鐘視為逾時可覆蓋 // 併行檢查:若有 pending 指令,超過 1 分鐘視為逾時可覆蓋
@ -440,6 +440,7 @@ class MachineService
'user_id' => $userId, 'user_id' => $userId,
'command_type' => 'dispense', 'command_type' => 'dispense',
'status' => 'pending', 'status' => 'pending',
'remark' => $remark,
'payload' => [ 'payload' => [
'slot_no' => (string)$slotNo, 'slot_no' => (string)$slotNo,
'current_stock' => $slot->stock, 'current_stock' => $slot->stock,

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('remote_commands', function (Blueprint $table) {
$table->string('remark')->nullable()->after('note')->comment('操作備註');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('remote_commands', function (Blueprint $table) {
$table->dropColumn('remark');
});
}
};

View File

@ -129,10 +129,13 @@
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 whitespace-nowrap"> 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 whitespace-nowrap">
{{ __('Picked up Time') }}</th> {{ __('Picked up Time') }}</th>
<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"> 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 whitespace-nowrap">
{{ __('Command Type') }}</th> {{ __('Command Type') }}</th>
<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"> 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 whitespace-nowrap">
{{ __('Operation Note') }}</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 whitespace-nowrap">
{{ __('Operator') }}</th> {{ __('Operator') }}</th>
<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"> 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">
@ -190,22 +193,39 @@
@endif @endif
</td> </td>
<td class="px-6 py-6"> <td class="px-6 py-6">
<div class="flex flex-col min-w-[200px]"> <div class="flex flex-col min-w-[140px]">
<span <span
class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight whitespace-nowrap"
x-text="getCommandName({{ Js::from($item->command_type) }})"></span> x-text="getCommandName({{ Js::from($item->command_type) }})"></span>
<div class="flex flex-col gap-0.5 mt-1"> <div class="flex flex-col gap-0.5 mt-1">
<span x-show="getPayloadDetails({{ Js::from($item) }})" <span x-show="getPayloadDetails({{ Js::from($item) }})"
class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
:class="{'line-through opacity-60': {{ Js::from($item->status) }} === 'failed'}" :class="{'line-through opacity-60': {{ Js::from($item->status) }} === 'failed'}"
x-text="getPayloadDetails({{ Js::from($item) }})"></span> x-text="getPayloadDetails({{ Js::from($item) }})"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic pl-1"
x-text="translateNote({{ Js::from($item->note) }})"></span>
@endif
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-6">
<div class="flex flex-col min-w-[200px]">
@if($item->remark)
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 break-words leading-tight">{{ $item->remark }}</span>
@endif
@if($item->note)
<div class="flex items-center gap-1 mt-1 opacity-60">
<svg class="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-[10px] text-slate-400 italic font-medium"
x-text="translateNote({{ Js::from($item->note) }})"></span>
</div>
@endif
@if(!$item->remark && !$item->note)
<span class="text-slate-300 dark:text-slate-700">-</span>
@endif
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap"> <td class="px-6 py-6 whitespace-nowrap">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
@ -301,12 +321,27 @@
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
:class="{'line-through opacity-60': {{ Js::from($item->status) }} === 'failed'}" :class="{'line-through opacity-60': {{ Js::from($item->status) }} === 'failed'}"
x-text="getPayloadDetails({{ Js::from($item) }})"></span> x-text="getPayloadDetails({{ Js::from($item) }})"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic" x-text="translateNote({{ Js::from($item->note) }})"></span>
@endif
</div> </div>
</div> </div>
</div> </div>
@if($item->remark || $item->note)
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Operation Note') }}</p>
<div class="flex flex-col gap-1.5 p-3 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800/50">
@if($item->remark)
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 leading-tight">{{ $item->remark }}</span>
@endif
@if($item->note)
<div class="flex items-center gap-1.5 opacity-60">
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-[10px] text-slate-400 italic font-medium" x-text="translateNote({{ Js::from($item->note) }})"></span>
</div>
@endif
</div>
</div>
@endif
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Creation Time') }}</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Creation Time') }}</p>