1. 修正 RemoteController 中的 MachineService 注入問題,解決遠端出貨指令失效的 500 錯誤。 2. 在 MachineService 實作貨道鎖定機制 (lockSlot/unlockSlot),防止重複扣庫存導致的數據不一致。 3. 採用樂觀扣除模式,發送指令時即更新預扣庫存,並在 ProcessCommandAck 實作失敗自動 Rollback。 4. 優化 UI 互動:在操作紀錄詳情顯示庫存變化 (如 6 -> 5),且針對執行失敗的指令加上刪除線樣式。 5. 完成指令詳情、操作紀錄與庫存歷史的語系對照表同步 (zh_TW, en, ja)。
302 lines
11 KiB
PHP
302 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Machine\Machine;
|
|
use App\Models\Machine\RemoteCommand;
|
|
use App\Services\Machine\MqttService;
|
|
use App\Services\Machine\MachineService;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class RemoteController extends Controller
|
|
{
|
|
protected $mqttService;
|
|
protected $machineService;
|
|
|
|
public function __construct(MqttService $mqttService, MachineService $machineService)
|
|
{
|
|
$this->mqttService = $mqttService;
|
|
$this->machineService = $machineService;
|
|
}
|
|
|
|
/**
|
|
* 遠端管理指揮中心
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$selectedMachine = null;
|
|
|
|
// --- 1. 機台列表處理 (New Command Tab) ---
|
|
$machineQuery = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc');
|
|
|
|
if ($request->filled('search') && $request->input('tab') === 'list') {
|
|
$search = $request->input('search');
|
|
$machineQuery->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('serial_no', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
$machines = $machineQuery->paginate($request->input('per_page', 10), ['*'], 'machine_page');
|
|
|
|
// --- 2. 歷史紀錄處理 (Operation Records Tab) ---
|
|
$historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock')
|
|
->with(['machine', 'user']);
|
|
|
|
if ($request->filled('search') && ($request->input('tab') === 'history' || !$request->has('tab'))) {
|
|
$search = $request->input('search');
|
|
$historyQuery->where(function ($q) use ($search) {
|
|
$q->whereHas('machine', function ($mq) use ($search) {
|
|
$mq->where('name', 'like', "%{$search}%")
|
|
->orWhere('serial_no', 'like', "%{$search}%");
|
|
})->orWhereHas('user', function ($uq) use ($search) {
|
|
$uq->where('name', 'like', "%{$search}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
// 時間區間過濾 (created_at)
|
|
if ($request->filled('start_date') || $request->filled('end_date')) {
|
|
try {
|
|
if ($request->filled('start_date')) {
|
|
$start = \Illuminate\Support\Carbon::parse($request->input('start_date'));
|
|
$historyQuery->where('created_at', '>=', $start);
|
|
}
|
|
if ($request->filled('end_date')) {
|
|
$end = \Illuminate\Support\Carbon::parse($request->input('end_date'));
|
|
$historyQuery->where('created_at', '<=', $end);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// 忽略解析錯誤
|
|
}
|
|
}
|
|
|
|
// 指令類型過濾
|
|
if ($request->filled('command_type')) {
|
|
$historyQuery->where('command_type', $request->input('command_type'));
|
|
}
|
|
|
|
// 狀態過濾
|
|
if ($request->filled('status')) {
|
|
$historyQuery->where('status', $request->input('status'));
|
|
}
|
|
|
|
$history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page');
|
|
|
|
// --- 3. 特定機台詳情處理 ---
|
|
if ($request->has('machine_id')) {
|
|
$selectedMachine = Machine::with([
|
|
'slots.product',
|
|
'commands' => function ($query) {
|
|
$query->where('command_type', '!=', 'reload_stock')
|
|
->latest()
|
|
->limit(5);
|
|
}
|
|
])->find($request->machine_id);
|
|
}
|
|
|
|
// --- 4. AJAX 回應處理 ---
|
|
if ($request->ajax()) {
|
|
if ($request->has('tab')) {
|
|
$tab = $request->input('tab');
|
|
$viewPath = $tab === 'list' ? 'admin.remote.partials.tab-machines-index' : 'admin.remote.partials.tab-history-index';
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'html' => view($viewPath, [
|
|
'machines' => $machines,
|
|
'history' => $history,
|
|
])->render()
|
|
]);
|
|
}
|
|
|
|
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')
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 儲存遠端指令
|
|
*/
|
|
public function storeCommand(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'machine_id' => 'required|exists:machines,id',
|
|
'command_type' => 'required|string|in:reboot,reboot_card,checkout,lock,unlock,change,dispense',
|
|
'amount' => 'nullable|integer|min:0',
|
|
'slot_no' => 'nullable|string',
|
|
'note' => 'nullable|string|max:255',
|
|
]);
|
|
|
|
if ($validated['command_type'] === 'dispense') {
|
|
$this->machineService->dispatchDispense(
|
|
Machine::findOrFail($validated['machine_id']),
|
|
$validated['slot_no'],
|
|
auth()->id()
|
|
);
|
|
} else {
|
|
$payload = [];
|
|
if ($validated['command_type'] === 'change') {
|
|
$payload['amount'] = $validated['amount'];
|
|
}
|
|
|
|
// 指令去重:將同機台、同類期的 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(),
|
|
]);
|
|
|
|
$command = RemoteCommand::create([
|
|
'machine_id' => $validated['machine_id'],
|
|
'user_id' => auth()->id(),
|
|
'command_type' => $validated['command_type'],
|
|
'payload' => $payload,
|
|
'status' => 'pending',
|
|
'note' => $validated['note'] ?? null,
|
|
]);
|
|
|
|
// 推播 MQTT 指令
|
|
$machine = Machine::find($validated['machine_id']);
|
|
if ($machine) {
|
|
$this->mqttService->pushCommand(
|
|
$machine->serial_no,
|
|
$command->command_type,
|
|
$command->payload,
|
|
(string) $command->id
|
|
);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* 機台庫存管理 (現有功能保留)
|
|
*/
|
|
public function stock(Request $request)
|
|
{
|
|
// 1. 機台查詢與分頁
|
|
$machineQuery = Machine::withCount([
|
|
'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());
|
|
}
|
|
]);
|
|
|
|
if ($request->filled('machine_search')) {
|
|
$ms = $request->input('machine_search');
|
|
$machineQuery->where(function ($q) use ($ms) {
|
|
$q->where('name', 'like', "%{$ms}%")
|
|
->orWhere('serial_no', 'like', "%{$ms}%");
|
|
});
|
|
}
|
|
|
|
$machines = $machineQuery->orderBy('last_heartbeat_at', 'desc')
|
|
->orderBy('id', 'desc')
|
|
->paginate($request->input('per_page', 10), ['*'], 'machine_page');
|
|
|
|
// 2. 歷史紀錄查詢與分頁
|
|
$historyQuery = RemoteCommand::with(['machine', 'user']);
|
|
$historyQuery->where('command_type', 'reload_stock');
|
|
|
|
if ($request->filled('search')) {
|
|
$search = $request->input('search');
|
|
$historyQuery->where(function ($q) use ($search) {
|
|
$q->whereHas('machine', function ($mq) use ($search) {
|
|
$mq->where('name', 'like', "%{$search}%")
|
|
->orWhere('serial_no', 'like', "%{$search}%");
|
|
})->orWhereHas('user', function ($uq) use ($search) {
|
|
$uq->where('name', 'like', "%{$search}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
// 時間區間過濾 (created_at)
|
|
if ($request->filled('start_date') || $request->filled('end_date')) {
|
|
try {
|
|
if ($request->filled('start_date')) {
|
|
$start = \Illuminate\Support\Carbon::parse($request->input('start_date'));
|
|
$historyQuery->where('created_at', '>=', $start);
|
|
}
|
|
if ($request->filled('end_date')) {
|
|
$end = \Illuminate\Support\Carbon::parse($request->input('end_date'));
|
|
$historyQuery->where('created_at', '<=', $end);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// 忽略解析錯誤
|
|
}
|
|
}
|
|
|
|
// 狀態過濾
|
|
if ($request->filled('status')) {
|
|
$historyQuery->where('status', $request->input('status'));
|
|
}
|
|
|
|
$history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page');
|
|
|
|
// 3. AJAX 回傳處理
|
|
if ($request->boolean('_ajax')) {
|
|
$tab = $request->input('tab', 'history');
|
|
if ($tab === 'machines') {
|
|
return response()->view('admin.remote.partials.tab-machines', [
|
|
'machines' => $machines,
|
|
]);
|
|
}
|
|
return response()->view('admin.remote.partials.tab-history', [
|
|
'history' => $history,
|
|
]);
|
|
}
|
|
|
|
$selectedMachine = null;
|
|
if ($request->has('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')
|
|
]);
|
|
}
|
|
}
|