From 376f43fa3ad030e61cab04bafc920d8d81f7d42c Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 15 Apr 2026 10:54:58 +0800 Subject: [PATCH] =?UTF-8?q?[FIX]=20=E4=BF=AE=E6=AD=A3=20IoT=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=BB=8B=E9=9D=A2=E5=88=86=E9=A0=81=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E8=88=87=E5=AF=A6=E4=BD=9C=20B055=20=E9=81=A0?= =?UTF-8?q?=E7=AB=AF=E5=87=BA=E8=B2=A8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 視圖持久化優化:將 index/stock 視圖切換從 x-if 改為 x-show,解決 HSSelect 在切換分頁後失效的問題。 2. 變數存取安全:為所有 selectedMachine 屬性存取補上可選鏈 (?.) 保護,防止 x-show 模式下的 null 錯誤。 3. UI 體驗提升:放大「指令中心」與「庫存管理」歷史紀錄的時間字體至 15px 並加粗顯示。 4. API 功能實作:在 routes/api.php 與 MachineController 中實作 B055 遠端指令出貨控制端點。 5. 文件同步:更新 SKILL.md 技術規格文件,明確定義 B055 的請求與回應格式。 6. 樣式調整:修改 app.css 優化奢華風 UI 字體與間距細節。 --- .agents/skills/api-technical-specs/SKILL.md | 16 +- .../Controllers/Admin/RemoteController.php | 83 +- .../Api/V1/App/MachineController.php | 108 + lang/zh_TW.json | 9 +- resources/css/app.css | 18 +- resources/views/admin/remote/index.blade.php | 1801 ++++++++++------- resources/views/admin/remote/stock.blade.php | 162 +- routes/api.php | 4 + 8 files changed, 1404 insertions(+), 797 deletions(-) diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index 430daf4..6af4e55 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -341,16 +341,22 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 ### 3.11 B055: 遠端指令出貨控制 (Remote Dispense / Force Open) 用於遠端手動驅動機台出貨。通常用於補償使用者、測試機台或客服協助開門的情景。 -- **URL**: POST|PUT /api/v1/app/machine/dispense/B055 +- **URL**: GET|PUT /api/v1/app/machine/dispense/B055 - **Authentication**: Bearer Token (Header) - **運作模式**: - - **POST (查詢)**: 當 B010 收到 `status: 85` 時呼叫。雲端會回傳待執行的貨道編號與指令 ID。 - - **PUT (回報)**: 實體出貨完成後回報結果,以便雲端將該指令標記為「已執行」。 + - **GET (查詢)**:當 B010 收到 `status: 85` 時呼叫。雲端會回傳待執行的貨道編號與指令 ID。 + - **PUT (回報)**:實體出貨完成後回饋結果,以便雲端將該指令標記為「已執行」。 + +- **Response Body (GET - 查詢階段):** + +| 參數 | 類型 | 說明 | 範例 | +| :--- | :--- | :--- | :--- | +| data | Array | 指令物件陣列 | [{"res1": "ID", "res2": "SlotNo"}] | - **Request Body (PUT - 回報階段):** | 參數 | 類型 | 必填 | 說明 | 範例 | | :--- | :--- | :--- | :--- | :--- | -| id | String | 是 | 雲端下發的指令 ID | "20260414001" | -| type | String | 是 | 出貨類型代碼 (通常為 0) | "0" | +| id | String | 是 | 雲端下發的指令 ID | "99" | +| type | String | 是 | 出貨類型代碼 (0: 成功, 其他: 失敗) | "0" | | stock | String | 是 | 出貨後的貨道剩餘數量 | "9" | diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php index e2ed289..f10aee0 100644 --- a/app/Http/Controllers/Admin/RemoteController.php +++ b/app/Http/Controllers/Admin/RemoteController.php @@ -17,7 +17,49 @@ class RemoteController extends Controller { $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(); + + $historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock') + ->with(['machine', 'user']); + + 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('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)); if ($request->has('machine_id')) { $selectedMachine = Machine::with(['slots.product', 'commands' => function($query) { @@ -112,7 +154,44 @@ class RemoteController extends Controller } ])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get(); - $history = RemoteCommand::where('command_type', 'reload_stock')->with(['machine', 'user'])->latest()->limit(50)->get(); + $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)); $selectedMachine = null; if ($request->has('machine_id')) { diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index aacaa03..3d30a30 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -145,6 +145,114 @@ class MachineController extends Controller 'status' => $status ], 202); // 202 Accepted } + + /** + * B055: Get Remote Dispense Queue (POST/GET) + * 用於獲取雲端下發的遠端出貨指令詳情。 + */ + public function getDispenseQueue(Request $request) + { + $machine = $request->get('machine'); + + // 查找該機台狀態為「已發送 (sent)」且類型為「出貨 (dispense)」的最新指令 + $command = \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id) + ->where('command_type', 'dispense') + ->where('status', 'sent') + ->latest() + ->first(); + + if (!$command) { + return response()->json([ + 'success' => true, + 'code' => 200, + 'data' => [] + ]); + } + + // 映射 Java APP 預期格式: res1 = ID, res2 = SlotNo + return response()->json([ + 'success' => true, + 'code' => 200, + 'data' => [ + [ + 'res1' => (string) $command->id, + 'res2' => (string) ($command->payload['slot_no'] ?? ''), + ] + ] + ]); + } + + /** + * B055: Report Remote Dispense Result (PUT) + * 用於機台回報遠端出貨執行的最終結果。 + */ + public function reportDispenseResult(Request $request, \App\Services\Machine\MachineService $machineService) + { + $commandId = $request->input('id'); + $type = $request->input('type'); // 通常為 0 + $stockReported = $request->input('stock'); // 機台回報的剩餘庫存 + + $command = \App\Models\Machine\RemoteCommand::find($commandId); + + if (!$command) { + return response()->json([ + 'success' => false, + 'code' => 404, + 'message' => 'Command not found' + ], 404); + } + + // 更新指令狀態 (0 代表成功) + $status = ($type == '0') ? 'success' : 'failed'; + + $payloadUpdates = [ + 'reported_stock' => $stockReported, + 'report_type' => $type + ]; + + // 若成功,連動更新貨道庫存 + if ($status === 'success' && isset($command->payload['slot_no'])) { + $machine = $command->machine; + $slotNo = $command->payload['slot_no']; + + $slot = $machine->slots()->where('slot_no', $slotNo)->first(); + if ($slot) { + $oldStock = $slot->stock; + // 若 APP 回傳的庫存大於 0 則使用回傳值,否則執行扣減 + $newStock = (int)($stockReported ?? 0); + if ($newStock <= 0 && $oldStock > 0) { + $newStock = $oldStock - 1; + } + + $finalStock = max(0, $newStock); + $slot->update(['stock' => $finalStock]); + + // 紀錄庫存變化供前端顯示 + $payloadUpdates['old_stock'] = $oldStock; + $payloadUpdates['new_stock'] = $finalStock; + + // 記錄狀態變化 + ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + "Remote dispense successful for slot {$slotNo}", + 'info' + ); + } + } + + $command->update([ + 'status' => $status, + 'executed_at' => now(), + 'payload' => array_merge($command->payload, $payloadUpdates) + ]); + + return response()->json([ + 'success' => true, + 'code' => 200, + 'message' => 'Result reported' + ]); + } /** * B018: Record Machine Restock/Setup Report (Asynchronous) diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 9cb86f4..a0a0518 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -62,9 +62,11 @@ "All": "全部", "All Affiliations": "所有公司", "All Categories": "所有分類", + "All Command Types": "所有指令類型", "All Companies": "所有公司", "All Levels": "所有層級", "All Machines": "所有機台", + "All Status": "所有狀態", "All Stable": "狀態穩定", "All Times System Timezone": "所有時間為系統時區", "Amount": "金額", @@ -274,6 +276,7 @@ "Discord Notifications": "Discord通知", "Dispense Failed": "出貨失敗", "Dispense Success": "出貨成功", + "Displaying": "目前顯示", "Dispensing": "出貨", "Duration": "時長", "Duration (Seconds)": "播放秒數", @@ -393,7 +396,7 @@ "Initial Role": "初始角色", "Installation": "裝機", "Invoice Status": "發票開立狀態", - "Items": "個項目", + "Items": "筆", "items": "筆項目", "Japanese": "日文", "JKO_MERCHANT_ID": "街口支付 商店代號", @@ -631,7 +634,7 @@ "OEE.Hours": "小時", "OEE.Orders": "訂單", "OEE.Sales": "銷售", - "of": "總計", + "of": "/共", "Offline": "離線", "Offline Machines": "離線機台", "Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "一旦您的帳號被刪除,其所有資源和數據將被永久刪除。在刪除帳號之前,請下載您希望保留的任何數據或資訊。", @@ -853,6 +856,7 @@ "Select Category": "選擇類別", "Select Company": "選擇公司名稱", "Select Company (Default: System)": "選擇公司 (預設:系統)", + "Select Date Range": "選擇建立日期區間", "Select date to sync data": "選擇日期以同步數據", "Select Machine": "選擇機台", "Select Machine to view metrics": "請選擇機台以查看指標", @@ -908,6 +912,7 @@ "Stock & Expiry Overview": "庫存與效期一覽", "Stock Management": "庫存管理單", "Stock Quantity": "庫存數量", + "Stock Update": "同步庫存", "Stock:": "庫存:", "Store Gifts": "來店禮", "Store ID": "商店代號", diff --git a/resources/css/app.css b/resources/css/app.css index 76be933..88379b5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -373,11 +373,27 @@ color: #06b6d4 !important; } - .flatpickr-day.selected { + .flatpickr-day.selected, + .flatpickr-day.startRange, + .flatpickr-day.endRange { background: linear-gradient(135deg, #06b6d4, #3b82f6) !important; border-color: transparent !important; color: white !important; box-shadow: 0 8px 15px -3px rgba(6, 182, 212, 0.4) !important; + z-index: 10; + } + + .flatpickr-day.inRange { + background: #ecfeff !important; + border-color: transparent !important; + box-shadow: -5px 0 0 #ecfeff, 5px 0 0 #ecfeff !important; + color: #0891b2 !important; + } + + .dark .flatpickr-day.inRange { + background: rgba(6, 182, 212, 0.15) !important; + box-shadow: -5px 0 0 rgba(6, 182, 212, 0.15), 5px 0 0 rgba(6, 182, 212, 0.15) !important; + color: #22d3ee !important; } .flatpickr-day:not(.selected):hover { diff --git a/resources/views/admin/remote/index.blade.php b/resources/views/admin/remote/index.blade.php index d794ca7..d6fb835 100644 --- a/resources/views/admin/remote/index.blade.php +++ b/resources/views/admin/remote/index.blade.php @@ -2,360 +2,367 @@ @section('content') -
- +
+