From 953581db38e5acb7203ed8f57371991a1f96023a Mon Sep 17 00:00:00 2001 From: sky121113 Date: Thu, 4 Jun 2026 11:45:48 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E6=BA=AB=E5=BA=A6=E4=B8=8A=E9=99=90=E7=9B=A3=E6=B8=AC=E8=88=87?= =?UTF-8?q?=E9=81=A0=E7=AB=AF=E9=A2=A8=E6=89=87=E6=8C=87=E4=BB=A4=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 資料庫移轉:新增機台環境溫度、環境溫度上限設定,以及是否啟用環境溫度監測等欄位。 2. 後端核心:更新 Machine Model 與 MachineController,實作機台環境溫度設定的更新與歷史日誌 Ajax API。 3. MQTT 佇列:於 ListenMqttQueue 指令與 ProcessAmbientTemp Job 實作機台上報環境溫度的接收與寫入,並加入 Redis 快取防止重複日誌洗版。 4. 遠端指令:擴充 RemoteController 並於指令中心實作 'fanon'、'fanoff'、'fanauto' 與 'ambient_temp_limit' (環境溫度上限) 指令下發。 5. 前端頁面:於機台列表加入「環境溫度上限」微調計數器 UI,並整合 ApexCharts 折線圖展示溫度趨勢;更新遠端控制中心按鈕與歷史篩選器。 6. 多語系:同步更新並完美對齊 'zh_TW.json'、'en.json' 與 'ja.json',遵循字母 ksort 排序與斜線不逸出規範。 --- app/Console/Commands/ListenMqttQueue.php | 4 + .../MachineSettingController.php | 3 + .../Controllers/Admin/MachineController.php | 69 ++++++ .../Controllers/Admin/RemoteController.php | 2 +- app/Jobs/Machine/ProcessAmbientTemp.php | 94 ++++++++ app/Models/Machine/Machine.php | 6 + config/api-docs.php | 24 +- ...perature_and_setting_to_machines_table.php | 33 +++ ...p_monitoring_enabled_to_machines_table.php | 30 +++ lang/en.json | 18 ++ lang/ja.json | 18 ++ lang/zh_TW.json | 18 ++ .../basic-settings/machines/index.blade.php | 26 ++- .../partials/tab-system-settings.blade.php | 2 + .../views/admin/machines/index.blade.php | 212 +++++++++++++++++- resources/views/admin/remote/index.blade.php | 74 ++++++ .../partials/tab-history-index.blade.php | 4 + routes/web.php | 1 + 18 files changed, 623 insertions(+), 15 deletions(-) create mode 100644 app/Jobs/Machine/ProcessAmbientTemp.php create mode 100644 database/migrations/2026_06_04_100930_add_ambient_temperature_and_setting_to_machines_table.php create mode 100644 database/migrations/2026_06_04_110221_add_ambient_temp_monitoring_enabled_to_machines_table.php diff --git a/app/Console/Commands/ListenMqttQueue.php b/app/Console/Commands/ListenMqttQueue.php index 1b541f5..d5b6ddd 100644 --- a/app/Console/Commands/ListenMqttQueue.php +++ b/app/Console/Commands/ListenMqttQueue.php @@ -95,6 +95,10 @@ class ListenMqttQueue extends Command $finalPayload = is_array($payload) ? $payload : ['raw_data' => $payload]; ProcessHeartbeat::dispatch($serialNo, $finalPayload); break; + case 'ambient_temp': + $finalPayload = is_array($payload) ? $payload : ['temperature' => $payload]; + \App\Jobs\Machine\ProcessAmbientTemp::dispatch($serialNo, $finalPayload); + break; case 'status': ProcessStatus::dispatch($serialNo, $payload); break; diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php index fa1dfc8..ad6a1b5 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -213,6 +213,7 @@ class MachineSettingController extends AdminController 'scan_pay_linepay_enabled' => 'boolean', 'shopping_cart_enabled' => 'boolean', 'cash_module_enabled' => 'boolean', + 'ambient_temp_monitoring_enabled' => 'boolean', 'machine_model_id' => 'required|exists:machine_models,id', 'payment_config_id' => 'nullable|exists:payment_configs,id', 'location' => 'nullable|string|max:255', @@ -333,6 +334,7 @@ class MachineSettingController extends AdminController 'welcome_gift_enabled', 'cash_module_enabled', 'member_system_enabled', + 'ambient_temp_monitoring_enabled', ]; $data = []; foreach ($allowedFields as $field) { @@ -361,6 +363,7 @@ class MachineSettingController extends AdminController 'welcome_gift_enabled', 'cash_module_enabled', 'member_system_enabled', + 'ambient_temp_monitoring_enabled', ]; if (!in_array($field, $allowedFields)) { diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index 4b6e017..235151f 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -43,10 +43,48 @@ class MachineController extends AdminController { $validated = $request->validate([ 'name' => 'required|string|max:255', + 'ambient_temp_setting' => 'nullable|integer', ]); $machine->update($validated); + if ($machine->wasChanged('ambient_temp_setting')) { + $newSetting = $machine->ambient_temp_setting; + + // 將先前未執行的同類型指令標記為 superseded (被取代) + \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id) + ->where('command_type', 'ambient_temp_limit') + ->where('status', 'pending') + ->update([ + 'status' => 'superseded', + 'remark' => __('Superseded by new command'), + 'executed_at' => now(), + ]); + + // 建立新指令 + $command = \App\Models\Machine\RemoteCommand::create([ + 'machine_id' => $machine->id, + 'user_id' => auth()->id() ?: 1, + 'command_type' => 'ambient_temp_limit', + 'payload' => [ + 'temperature' => $newSetting !== null ? (int)$newSetting : null + ], + 'status' => 'pending', + 'remark' => $newSetting !== null + ? __('Temperature reaches :temp degrees, the fan will turn on', ['temp' => $newSetting]) + : __('Disabled ambient temperature monitoring') + ]); + + // 發送 MQTT 指令 + $mqttService = app(\App\Services\Machine\MqttService::class); + $mqttService->pushCommand( + $machine->serial_no, + $command->command_type, + $command->payload, + (string)$command->id + ); + } + return redirect()->route('admin.machines.index') ->with('success', __('Machine updated successfully.')); } @@ -267,4 +305,35 @@ class MachineController extends AdminController 'data' => $chartData ]); } + + /** + * AJAX: 取得機台環境溫度歷史紀錄 (供圖表使用) + */ + public function ambientTemperatureAjax(Request $request, Machine $machine) + { + $startDate = $request->get('start_date'); + $endDate = $request->get('end_date'); + + $logs = $machine->logs() + ->where('type', 'ambient_temp') + ->where('message', 'Ambient temperature reported: :temp°C') + ->when($startDate, function ($query, $start) { + return $query->where('created_at', '>=', str_replace('T', ' ', $start)); + }) + ->when($endDate, function ($query, $end) { + return $query->where('created_at', '<=', str_replace('T', ' ', $end)); + }) + ->oldest() + ->get(); + + $chartData = $logs->map(fn($log) => [ + 'x' => $log->created_at->getTimestamp() * 1000, + 'y' => (int) ($log->context['temp'] ?? 0), + ])->values()->toArray(); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } } diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php index 13b832c..9fceafc 100644 --- a/app/Http/Controllers/Admin/RemoteController.php +++ b/app/Http/Controllers/Admin/RemoteController.php @@ -139,7 +139,7 @@ class RemoteController extends Controller { $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', - 'command_type' => 'required|string|in:reboot,reboot_card,checkout,lock,unlock,change,dispense', + 'command_type' => 'required|string|in:reboot,reboot_card,checkout,lock,unlock,change,dispense,fanon,fanoff,fanauto', 'amount' => 'nullable|integer|min:0', 'slot_no' => 'nullable|string', 'note' => 'nullable|string|max:255', diff --git a/app/Jobs/Machine/ProcessAmbientTemp.php b/app/Jobs/Machine/ProcessAmbientTemp.php new file mode 100644 index 0000000..83d9133 --- /dev/null +++ b/app/Jobs/Machine/ProcessAmbientTemp.php @@ -0,0 +1,94 @@ +serialNo = $serialNo; + $this->payload = (array) $payload; + } + + /** + * Execute the job. + */ + public function handle(): void + { + $machine = Machine::withoutGlobalScopes()->where('serial_no', $this->serialNo)->first(); + + if (!$machine) { + Log::warning("MQTT Ambient Temp: Machine not found", ['serial_no' => $this->serialNo]); + return; + } + + // 環境溫度欄位更新 + // 支持機台傳送 payload 為 {"temperature": 25} 或 {"ambient_temp": 25} 等結構 + $temp = null; + if (isset($this->payload['temperature'])) { + $temp = (int) $this->payload['temperature']; + } elseif (isset($this->payload['ambient_temp'])) { + $temp = (int) $this->payload['ambient_temp']; + } elseif (isset($this->payload['raw_data'])) { + $temp = (int) $this->payload['raw_data']; + } + + if ($temp === null) { + Log::warning("MQTT Ambient Temp: Missing temperature field in payload", [ + 'serial_no' => $this->serialNo, + 'payload' => $this->payload + ]); + return; + } + + Log::debug("ProcessAmbientTemp: Ambient temperature reported for {$this->serialNo}: {$temp}"); + + $updateData = [ + 'ambient_temperature' => $temp, + ]; + + // 讀取最後一次記錄日誌的溫度與時間,避免頻繁寫入相同的溫度日誌 + $tempLogCacheKey = "machine:{$this->serialNo}:last_ambient_temp_log"; + $lastLogEntry = Cache::get($tempLogCacheKey); + + // 優先從快取取值,若無則從資料庫取 + $oldTemp = isset($lastLogEntry['value']) ? (int)$lastLogEntry['value'] : $machine->ambient_temperature; + + if ($temp !== (int)$oldTemp) { + Log::debug("ProcessAmbientTemp: Ambient temperature changed from {$oldTemp} to {$temp}. Triggering log."); + \App\Jobs\Machine\ProcessStateLog::dispatch( + $machine->id, + $machine->company_id, + "Ambient temperature reported: :temp°C", + 'info', + ['temp' => $temp], + 'ambient_temp' // type 欄位 + ); + } + + // 更新快取 (存活時間為 7 天) + Cache::put($tempLogCacheKey, [ + 'value' => $temp, + 'at' => now()->toDateTimeString() + ], 604800); + + $machine->update($updateData); + } +} diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index 526b1ba..378fcd6 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -70,6 +70,9 @@ class Machine extends Model 'current_page', 'door_status', 'temperature', + 'ambient_temperature', + 'ambient_temp_setting', + 'ambient_temp_monitoring_enabled', 'firmware_version', 'api_token', 'last_heartbeat_at', @@ -309,6 +312,9 @@ class Machine extends Model protected $casts = [ 'last_heartbeat_at' => 'datetime', 'temperature' => 'integer', + 'ambient_temperature' => 'integer', + 'ambient_temp_setting' => 'integer', + 'ambient_temp_monitoring_enabled' => 'boolean', 'welcome_gift_enabled' => 'boolean', 'is_spring_slot_1_10' => 'boolean', 'is_spring_slot_11_20' => 'boolean', diff --git a/config/api-docs.php b/config/api-docs.php index 190c65a..cd1c8de 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -773,7 +773,7 @@ return [ 'qos' => 1, 'description' => '雲端主動下發的系統級別控制指令。包含重啟、設備鎖定解鎖、購物車結帳等無額外參數的指令。', 'payload_parameters' => [ - 'command' => ['type' => 'string', 'description' => '系統指令類型。支援:reboot (重啟機台), reboot_card (重啟刷卡機), checkout (購物車結帳), lock (設備鎖定), unlock (設備解鎖), update_ads (廣告同步), update_products (商品資料同步)'], + 'command' => ['type' => 'string', 'description' => '系統指令類型。支援:reboot (重啟機台), reboot_card (重啟刷卡機), checkout (購物車結帳), lock (設備鎖定), unlock (設備解鎖), update_ads (廣告同步), update_products (商品資料同步), fanon (開啟風扇), fanoff (關閉風扇), fanauto (風扇自動控制)'], 'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'], 'payload' => ['type' => 'object', 'description' => '空物件 (無額外參數)'], ], @@ -784,6 +784,28 @@ return [ ], ], + [ + 'name' => '指令下發:環境溫度上限 (Ambient Temperature Upper Limit)', + 'slug' => 'mqtt-command-ambient-temp-limit', + 'action' => 'SUB', + 'topic' => 'machine/{serial_no}/command', + 'qos' => 1, + 'description' => '雲端主動下發「環境溫度上限」指令。指示機台更新環境溫度開啟風扇的閾值設定。', + 'payload_parameters' => [ + 'command' => ['type' => 'string', 'description' => '固定為 "ambient_temp_limit"'], + 'command_id' => ['type' => 'string', 'description' => '指令唯一 ID'], + 'payload' => ['type' => 'object', 'description' => '溫度設定參數'], + 'payload.temperature' => ['type' => 'integer', 'description' => '環境溫度上限閾值(可為 null 表示停用監測)'], + ], + 'payload_example' => [ + 'command' => 'ambient_temp_limit', + 'command_id' => '1122334455', + 'payload' => [ + 'temperature' => 28 + ] + ], + ], + [ 'name' => '指令執行回報 (Machine to Cloud)', 'slug' => 'mqtt-command-ack', diff --git a/database/migrations/2026_06_04_100930_add_ambient_temperature_and_setting_to_machines_table.php b/database/migrations/2026_06_04_100930_add_ambient_temperature_and_setting_to_machines_table.php new file mode 100644 index 0000000..affd750 --- /dev/null +++ b/database/migrations/2026_06_04_100930_add_ambient_temperature_and_setting_to_machines_table.php @@ -0,0 +1,33 @@ +integer('ambient_temperature')->nullable()->after('temperature')->comment('環境溫度'); + } + if (!Schema::hasColumn('machines', 'ambient_temp_setting')) { + $table->integer('ambient_temp_setting')->nullable()->after('ambient_temperature')->comment('環境溫度設定值'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('machines', function (Blueprint $table) { + $table->dropColumn(['ambient_temperature', 'ambient_temp_setting']); + }); + } +}; diff --git a/database/migrations/2026_06_04_110221_add_ambient_temp_monitoring_enabled_to_machines_table.php b/database/migrations/2026_06_04_110221_add_ambient_temp_monitoring_enabled_to_machines_table.php new file mode 100644 index 0000000..a37ea67 --- /dev/null +++ b/database/migrations/2026_06_04_110221_add_ambient_temp_monitoring_enabled_to_machines_table.php @@ -0,0 +1,30 @@ +boolean('ambient_temp_monitoring_enabled')->default(false)->after('ambient_temp_setting')->comment('是否啟用環境溫度監測'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('machines', function (Blueprint $table) { + $table->dropColumn('ambient_temp_monitoring_enabled'); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 0e8263f..7fb4709 100644 --- a/lang/en.json +++ b/lang/en.json @@ -120,6 +120,13 @@ "All Warehouses": "All Warehouses", "All issues marked as resolved.": "All issues marked as resolved.", "All slots are fully stocked": "All slots are fully stocked", + "Ambient Temperature": "Ambient Temperature", + "Ambient Temperature Log": "Ambient Temperature Log", + "Ambient Temperature Monitoring": "Ambient Temperature Monitoring", + "Ambient Temperature Setting": "Ambient Temperature Setting", + "Ambient Temperature Trend": "Ambient Temperature Trend", + "Ambient Temperature Upper Limit": "Ambient Temperature Upper Limit", + "Ambient temperature reported: :temp°C": "Ambient temperature reported: :temp°C", "Amount": "Amount", "Amount / Payment": "Amount / Payment", "Amount Discount": "Amount Discount", @@ -236,6 +243,7 @@ "CASH": "CASH", "CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS", "Calculate Replenishment": "Calculate Replenishment", + "Can be empty. If left blank, it means ambient temperature monitoring is disabled.": "Can be empty. If left blank, it means ambient temperature monitoring is disabled.", "Cancel": "Cancel", "Cancel Gift": "Cancel Gift", "Cancel Order": "Cancel Order", @@ -511,6 +519,7 @@ "Disable Product Confirmation": "Disable Product Confirmation", "Disable Warehouse": "Disable Warehouse", "Disabled": "Disabled", + "Disabled ambient temperature monitoring": "Disabled ambient temperature monitoring", "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?", "Discord Notifications": "Discord Notifications", "Discord Webhook URL": "Discord Webhook URL", @@ -632,6 +641,7 @@ "English": "English", "Ensure your account is using a long, random password to stay secure.": "Ensure your account is using a long, random password to stay secure.", "Enter ad material name": "Enter ad material name", + "Enter ambient temperature setting...": "Enter ambient temperature setting...", "Enter full address": "Enter full address", "Enter login ID": "Enter login ID", "Enter machine location": "Enter machine location", @@ -690,6 +700,10 @@ "Failed to send test notification.": "Failed to send test notification.", "Failed to send test notification. Please verify the Webhook URL.": "Failed to send test notification. Please verify the Webhook URL.", "Failed to update machine images: ": "Failed to update machine images: ", + "Fan Auto": "Fan Auto", + "Fan Controls": "Fan Controls", + "Fan Off": "Fan Off", + "Fan On": "Fan On", "Feature Settings": "Feature Settings", "Feature Toggles": "Feature Toggles", "Field": "Field", @@ -822,6 +836,7 @@ "Half Points Amount": "Half Points Amount", "Hardware & Network": "Hardware & Network", "Hardware & Slots": "Hardware & Slots", + "Hardware Peripheral Settings": "Hardware Peripheral Settings", "HashIV": "HashIV", "HashKey": "HashKey", "Heartbeat": "Heartbeat", @@ -1955,6 +1970,7 @@ "Temperature Limit": "Temperature Limit", "Temperature Restored": "Temperature Restored", "Temperature Trend": "Temperature Trend", + "Temperature reaches :temp degrees, the fan will turn on": "Temperature reaches :temp°C, the fan will turn on", "Temperature reported: :temp°C": "Temperature reported: :temp°C", "Temperature updated to :temp°C": "Temperature updated to :temp°C", "Tenant": "Tenant", @@ -2084,6 +2100,7 @@ "Update Product": "Update Product", "Update Settings": "Update Settings", "Update Welcome Gift": "Update Welcome Gift", + "Update ambient temperature setting to :temp°C": "Update ambient temperature setting to :temp°C", "Update existing role and permissions.": "Update existing role and permissions.", "Update failed": "Update failed", "Update identification for your asset": "Update identification for your asset", @@ -2098,6 +2115,7 @@ "Upload file or provide URL": "Upload file or provide URL", "Uploading new images will replace all existing images.": "Uploading new images will replace all existing images.", "Uploading...": "Uploading...", + "Upper limit; the fan will turn on when reached": "Upper limit; the fan will turn on when reached", "Usage Completed": "Usage Completed", "Usage Limit": "Usage Limit", "Usage Logs": "Usage Logs", diff --git a/lang/ja.json b/lang/ja.json index c04b0d0..9d75944 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -120,6 +120,13 @@ "All Warehouses": "すべての倉庫", "All issues marked as resolved.": "すべての問題が解決済みとしてマークされました。", "All slots are fully stocked": "すべてのスロットが補充完了しました", + "Ambient Temperature": "周囲温度", + "Ambient Temperature Log": "周囲温度ログ", + "Ambient Temperature Monitoring": "環境温度監視", + "Ambient Temperature Setting": "周囲温度設定", + "Ambient Temperature Trend": "周囲温度トレンド", + "Ambient Temperature Upper Limit": "環境温度上限", + "Ambient temperature reported: :temp°C": "環境温度受信: :temp°C", "Amount": "金額", "Amount / Payment": "金額 / 支払い", "Amount Discount": "金額割引", @@ -236,6 +243,7 @@ "CASH": "現金", "CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "倉庫間の移動または機器からの返品を作成", "Calculate Replenishment": "補充量を計算", + "Can be empty. If left blank, it means ambient temperature monitoring is disabled.": "空欄にすることができます。空欄の場合、周囲温度の監視は無効になります。", "Cancel": "キャンセル", "Cancel Gift": "ギフトをキャンセル", "Cancel Order": "注文キャンセル", @@ -511,6 +519,7 @@ "Disable Product Confirmation": "商品無効化確認", "Disable Warehouse": "倉庫無効化", "Disabled": "無効済み", + "Disabled ambient temperature monitoring": "環境温度監視を停止しました", "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "顧客を無効にすると配下の全アカウントも無効になります。続行しますか?", "Discord Notifications": "Discord通知", "Discord Webhook URL": "Discord Webhook URL", @@ -632,6 +641,7 @@ "English": "英語", "Ensure your account is using a long, random password to stay secure.": "安全を保つため、長くランダムなパスワードを使用してください。", "Enter ad material name": "広告素材名を入力", + "Enter ambient temperature setting...": "周囲温度設定を入力してください...", "Enter full address": "完全な住所を入力", "Enter login ID": "ログインIDを入力してください", "Enter machine location": "機器の設置場所を入力してください", @@ -690,6 +700,10 @@ "Failed to send test notification.": "テスト通知の送信に失敗しました。", "Failed to send test notification. Please verify the Webhook URL.": "テスト通知の送信に失敗しました。Webhook URLを確認してください。", "Failed to update machine images: ": "機器画像の更新に失敗しました: ", + "Fan Auto": "ファン自動", + "Fan Controls": "ファン制御", + "Fan Off": "ファン停止", + "Fan On": "ファン起動", "Feature Settings": "機能設定", "Feature Toggles": "機能切り替え", "Field": "Field", @@ -822,6 +836,7 @@ "Half Points Amount": "ハーフポイント金額", "Hardware & Network": "ハードウェア・ネットワーク", "Hardware & Slots": "ハードウェア・スロット", + "Hardware Peripheral Settings": "ハードウェア周辺機器設定", "HashIV": "HashIV", "HashKey": "HashKey", "Heartbeat": "ハートビート", @@ -1955,6 +1970,7 @@ "Temperature Limit": "温度しきい値", "Temperature Restored": "温度正常復帰", "Temperature Trend": "温度傾向", + "Temperature reaches :temp degrees, the fan will turn on": "温度が :temp度 に達するとファンが作動します", "Temperature reported: :temp°C": "温度が報告されました: :temp°C", "Temperature updated to :temp°C": "温度が :temp°C に更新されました", "Tenant": "顧客 (テナント)", @@ -2084,6 +2100,7 @@ "Update Product": "商品更新", "Update Settings": "設定更新", "Update Welcome Gift": "ウェルカムギフトを更新", + "Update ambient temperature setting to :temp°C": "環境温度設定を :temp°C に更新しました", "Update existing role and permissions.": "既存のロールと権限設定を更新します。", "Update failed": "更新失敗", "Update identification for your asset": "資産の識別情報を更新", @@ -2098,6 +2115,7 @@ "Upload file or provide URL": "Upload file or provide URL", "Uploading new images will replace all existing images.": "新しい写真をアップロードすると、既存のすべての写真が置き換えられます。", "Uploading...": "アップロード中...", + "Upper limit; the fan will turn on when reached": "温度上限;この温度に達するとファンが作動します", "Usage Completed": "使用完了", "Usage Limit": "使用回数上限", "Usage Logs": "使用履歴", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 32e077e..c8dd558 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -120,6 +120,13 @@ "All Warehouses": "所有倉庫", "All issues marked as resolved.": "所有異常已標記為已排除。", "All slots are fully stocked": "所有貨道已滿貨", + "Ambient Temperature": "環境溫度", + "Ambient Temperature Log": "環境溫度回傳", + "Ambient Temperature Monitoring": "環境溫度監測", + "Ambient Temperature Setting": "環境溫度設定", + "Ambient Temperature Trend": "環境溫度趨勢", + "Ambient Temperature Upper Limit": "環境溫度上限", + "Ambient temperature reported: :temp°C": "環境溫度回傳::temp°C", "Amount": "金額", "Amount / Payment": "金額 / 支付", "Amount Discount": "固定金額", @@ -236,6 +243,7 @@ "CASH": "CASH", "CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "建立倉庫間調撥或機台退庫單", "Calculate Replenishment": "計算補貨量", + "Can be empty. If left blank, it means ambient temperature monitoring is disabled.": "可留空,留空表示不啟用監控功能", "Cancel": "取消", "Cancel Gift": "取消此來店禮", "Cancel Order": "取消訂單", @@ -511,6 +519,7 @@ "Disable Product Confirmation": "停用商品確認", "Disable Warehouse": "停用倉庫", "Disabled": "已停用", + "Disabled ambient temperature monitoring": "已停用環境溫度監測", "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?", "Discord Notifications": "Discord通知", "Discord Webhook URL": "Discord Webhook 網址", @@ -632,6 +641,7 @@ "English": "英文", "Ensure your account is using a long, random password to stay secure.": "確保您的帳號使用了足夠強度的隨機密碼以維持安全。", "Enter ad material name": "輸入廣告素材名稱", + "Enter ambient temperature setting...": "請輸入環境溫度設定...", "Enter full address": "輸入完整地址", "Enter login ID": "請輸入登入帳號", "Enter machine location": "請輸入機台地點", @@ -690,6 +700,10 @@ "Failed to send test notification.": "發送測試通知失敗。", "Failed to send test notification. Please verify the Webhook URL.": "發送測試通知失敗,請確認 Webhook 網址。", "Failed to update machine images: ": "更新機台圖片失敗:", + "Fan Auto": "風扇自動", + "Fan Controls": "風扇控制", + "Fan Off": "關閉風扇", + "Fan On": "開啟風扇", "Feature Settings": "功能設定", "Feature Toggles": "功能開關", "Field": "欄位", @@ -822,6 +836,7 @@ "Half Points Amount": "半點數金額", "Hardware & Network": "硬體與網路", "Hardware & Slots": "硬體與貨道", + "Hardware Peripheral Settings": "硬體周邊設定", "HashIV": "HashIV", "HashKey": "HashKey", "Heartbeat": "心跳狀態", @@ -1955,6 +1970,7 @@ "Temperature Limit": "溫度限制", "Temperature Restored": "溫度恢復正常", "Temperature Trend": "溫度趨勢", + "Temperature reaches :temp degrees, the fan will turn on": "溫度達 :temp度 將開啟風扇", "Temperature reported: :temp°C": "回報溫度 : :temp°C", "Temperature updated to :temp°C": "溫度已更新為::temp°C", "Tenant": "客戶", @@ -2084,6 +2100,7 @@ "Update Product": "更新商品", "Update Settings": "更新設定", "Update Welcome Gift": "更新來店禮", + "Update ambient temperature setting to :temp°C": "更新環境溫度設定為 :temp°C", "Update existing role and permissions.": "更新現有角色與權限設定。", "Update failed": "更新失敗", "Update identification for your asset": "更新您的資產識別名稱", @@ -2098,6 +2115,7 @@ "Upload file or provide URL": "上傳檔案或提供連結", "Uploading new images will replace all existing images.": "上傳新照片將會取代所有現有照片。", "Uploading...": "上傳中...", + "Upper limit; the fan will turn on when reached": "溫度上限,達到此溫度時風扇將會開啟", "Usage Completed": "使用完成", "Usage Limit": "使用次數上限", "Usage Logs": "使用紀錄", diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php index b3f3514..975f438 100644 --- a/resources/views/admin/basic-settings/machines/index.blade.php +++ b/resources/views/admin/basic-settings/machines/index.blade.php @@ -106,7 +106,8 @@ shopping_cart_enabled: machine.shopping_cart_enabled === true || machine.shopping_cart_enabled === 1 || machine.shopping_cart_enabled === '1', welcome_gift_enabled: machine.welcome_gift_enabled === true || machine.welcome_gift_enabled === 1 || machine.welcome_gift_enabled === '1', cash_module_enabled: machine.cash_module_enabled === true || machine.cash_module_enabled === 1 || machine.cash_module_enabled === '1', - member_system_enabled: machine.member_system_enabled === true || machine.member_system_enabled === 1 || machine.member_system_enabled === '1' + member_system_enabled: machine.member_system_enabled === true || machine.member_system_enabled === 1 || machine.member_system_enabled === '1', + ambient_temp_monitoring_enabled: machine.ambient_temp_monitoring_enabled === true || machine.ambient_temp_monitoring_enabled === 1 || machine.ambient_temp_monitoring_enabled === '1' }; this.showMachineSettingsModal = true; }, @@ -610,6 +611,29 @@ + + +
+
+

{{ __('Hardware Peripheral Settings') }}

+
+
+ +
+
welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift'); if ($machine->cash_module_enabled) $activeFeatures[] = __('Coin/Banknote Machine'); if ($machine->member_system_enabled) $activeFeatures[] = __('Member System'); + if ($machine->ambient_temp_monitoring_enabled) $activeFeatures[] = __('Ambient Temperature Monitoring'); @endphp @@ -139,6 +140,7 @@ if ($machine->welcome_gift_enabled) $activeFeatures[] = __('Welcome Gift'); if ($machine->cash_module_enabled) $activeFeatures[] = __('Coin/Banknote Machine'); if ($machine->member_system_enabled) $activeFeatures[] = __('Member System'); + if ($machine->ambient_temp_monitoring_enabled) $activeFeatures[] = __('Ambient Temperature Monitoring'); @endphp
diff --git a/resources/views/admin/machines/index.blade.php b/resources/views/admin/machines/index.blade.php index 56b0792..d0a0572 100644 --- a/resources/views/admin/machines/index.blade.php +++ b/resources/views/admin/machines/index.blade.php @@ -11,6 +11,8 @@ showResolveConfirm: false, editMachineId: '', editMachineName: '', + editAmbientTempSetting: '', + editAmbientTempMonitoringEnabled: false, activeTab: 'status', currentMachineId: '', isLoadingTable: false, @@ -55,6 +57,7 @@ totalLogs: 0, temperatureChart: null, + ambientTemperatureChart: null, init() { const now = new Date(); @@ -63,11 +66,24 @@ this.startDate = formatDate(now, '00:00'); this.endDate = formatDate(now, '23:59'); - this.$watch('activeTab', () => this.fetchLogs(1)); + this.$watch('activeTab', (val) => { + this.fetchLogs(1); + if (val === 'status') { + this.fetchTemperatureData(); + } else if (val === 'ambient_temp') { + this.fetchAmbientTemperatureData(); + } + }); // 監聽日期變動,同步更新圖表 (僅在當前為狀態分頁時) - this.$watch('startDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); }); - this.$watch('endDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); }); + this.$watch('startDate', () => { + if(this.activeTab === 'status') this.fetchTemperatureData(); + if(this.activeTab === 'ambient_temp') this.fetchAmbientTemperatureData(); + }); + this.$watch('endDate', () => { + if(this.activeTab === 'status') this.fetchTemperatureData(); + if(this.activeTab === 'ambient_temp') this.fetchAmbientTemperatureData(); + }); }, async openLogPanel(id, sn, name) { @@ -96,6 +112,106 @@ } catch (e) { console.error('fetchTemperatureData error:', e); } }, + async fetchAmbientTemperatureData() { + if (this.activeTab !== 'ambient_temp') return; + try { + let url = `/admin/machines/${this.currentMachineId}/ambient-temperature-ajax?`; + if (this.startDate) url += '&start_date=' + this.startDate; + if (this.endDate) url += '&end_date=' + this.endDate; + + const res = await fetch(url); + const data = await res.json(); + if (data.success) { + this.initAmbientTemperatureChart(data.data); + } + } catch (e) { console.error('fetchAmbientTemperatureData error:', e); } + }, + + initAmbientTemperatureChart(chartData) { + this.$nextTick(() => { + const chartEl = document.querySelector("#ambient-temperature-chart"); + if (!chartEl) return; + + const isDark = document.documentElement.classList.contains('dark'); + + const options = { + series: [{ + name: "{{ __('Ambient Temperature') }}", + data: chartData + }], + chart: { + id: 'ambient-temp-chart', + type: 'area', + height: 200, + toolbar: { show: false }, + zoom: { enabled: false }, + animations: { enabled: false }, + background: 'transparent', + accessibility: { enabled: false } + }, + colors: ['#3b82f6'], + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.45, + opacityTo: 0.05, + stops: [20, 100] + } + }, + dataLabels: { enabled: false }, + stroke: { + curve: 'smooth', + width: 3 + }, + grid: { + borderColor: isDark ? '#1e293b' : '#f1f5f9', + strokeDashArray: 4, + padding: { left: 10, right: 10 } + }, + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 }, + datetimeUTC: false, + format: 'HH:mm', + hideOverlappingLabels: true, + }, + axisBorder: { show: false }, + axisTicks: { show: false }, + tooltip: { enabled: false }, + tickAmount: 6 + }, + yaxis: { + labels: { + style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 }, + formatter: (val) => val + '°C' + } + }, + tooltip: { + theme: isDark ? 'dark' : 'light', + x: { + show: true, + format: 'yyyy/MM/dd HH:mm:ss' + }, + y: { + title: { + formatter: (seriesName) => seriesName + ': ' + } + } + } + }; + + if (this.ambientTemperatureChart) { + this.ambientTemperatureChart.updateOptions(options); + } else { + this.ambientTemperatureChart = new ApexCharts(chartEl, options); + this.ambientTemperatureChart.render(); + } + }); + }, + initTemperatureChart(chartData) { this.$nextTick(() => { const chartEl = document.querySelector("#temperature-chart"); @@ -181,9 +297,11 @@ }); }, - openEditModal(id, name) { + openEditModal(id, name, ambientTempSetting = '', ambientTempMonitoringEnabled = false) { this.editMachineId = id; this.editMachineName = name; + this.editAmbientTempSetting = ambientTempSetting; + this.editAmbientTempMonitoringEnabled = ambientTempMonitoringEnabled === true || ambientTempMonitoringEnabled === 1 || ambientTempMonitoringEnabled === '1' || ambientTempMonitoringEnabled === 'true'; this.showEditModal = true; }, @@ -493,7 +611,7 @@
- - + +
- +
- +
- +
{{ __('Loading Data') }}...

- +
@@ -859,6 +982,21 @@
+ + +
+
+

+ + {{ __('Ambient Temperature Trend') }} +

+ + @ + +
+
+
+ +
+ + + {{-- Luxury +/- Counter UI --}} +
+ {{-- Minus Button --}} + + +
+ + {{-- Input Field --}} +
+ + + {{-- Clear Button --}} + +
+ +
+ + {{-- Plus Button --}} + +
+ + {{-- Help Text --}} +

+ {{ __('Can be empty. If left blank, it means ambient temperature monitoring is disabled.') }} +

+
+ +
+
+
+ {{ + __('Fan Controls') }} +
+
+ + + + + + + + +
+
+
diff --git a/resources/views/admin/remote/partials/tab-history-index.blade.php b/resources/views/admin/remote/partials/tab-history-index.blade.php index 4021490..f474dff 100644 --- a/resources/views/admin/remote/partials/tab-history-index.blade.php +++ b/resources/views/admin/remote/partials/tab-history-index.blade.php @@ -89,6 +89,10 @@ 'checkout' => __('Remote Settlement'), 'lock' => __('Lock Page Lock'), 'unlock' => __('Lock Page Unlock'), + 'fanon' => __('Fan On'), + 'fanoff' => __('Fan Off'), + 'fanauto' => __('Fan Auto'), + 'ambient_temp_limit' => __('Ambient Temperature Upper Limit'), 'change' => __('Remote Change'), 'dispense' => __('Remote Dispense'), 'update_ads' => __('Sync Ads'), diff --git a/routes/web.php b/routes/web.php index 8ad5fa2..b7f68e9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -63,6 +63,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update'); Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax'); Route::get('/{machine}/temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'temperatureAjax'])->name('temperature-ajax'); + Route::get('/{machine}/ambient-temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'ambientTemperatureAjax'])->name('ambient-temperature-ajax'); Route::post('/{machine}/resolve-logs', [App\Http\Controllers\Admin\MachineController::class, 'resolveLogs'])->name('resolve-logs'); }); Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);