[FEAT] 新增環境溫度上限監測與遠端風扇指令控制功能
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 排序與斜線不逸出規範。
This commit is contained in:
parent
6a251df81b
commit
953581db38
@ -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;
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
94
app/Jobs/Machine/ProcessAmbientTemp.php
Normal file
94
app/Jobs/Machine/ProcessAmbientTemp.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ProcessAmbientTemp implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serialNo;
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $serialNo, $payload)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
<?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('machines', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('machines', 'ambient_temperature')) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?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('machines', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('machines', 'ambient_temp_monitoring_enabled')) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
18
lang/en.json
18
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",
|
||||
|
||||
18
lang/ja.json
18
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": "使用履歴",
|
||||
|
||||
@ -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": "使用紀錄",
|
||||
|
||||
@ -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 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 硬體周邊設定 -->
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<div class="md:col-span-1">
|
||||
<h4 class="text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Hardware Peripheral Settings') }}</h4>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative inline-flex items-center">
|
||||
<input type="hidden" name="settings[ambient_temp_monitoring_enabled]" value="0">
|
||||
<input type="checkbox" name="settings[ambient_temp_monitoring_enabled]" value="1"
|
||||
x-model="machineSettings.ambient_temp_monitoring_enabled" class="peer sr-only">
|
||||
<div
|
||||
class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500">
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white transition-colors">{{
|
||||
__('Ambient Temperature Monitoring') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@ -82,6 +82,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
|
||||
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
|
||||
<td class="px-6 py-6 w-1/4">
|
||||
@ -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
|
||||
<div class="luxury-card p-5 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
|
||||
|
||||
|
||||
@ -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 @@
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
|
||||
<button type="button"
|
||||
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
|
||||
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}', '{{ $machine->ambient_temp_setting }}', '{{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}')"
|
||||
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all duration-200"
|
||||
title="{{ __('Edit Name') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
@ -622,7 +740,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button"
|
||||
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
|
||||
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}', '{{ $machine->ambient_temp_setting }}', '{{ $machine->ambient_temp_monitoring_enabled ? 1 : 0 }}')"
|
||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
@ -802,11 +920,16 @@
|
||||
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
||||
{{ __('Machine Login Logs') }}
|
||||
</button>
|
||||
<button @click="activeTab = 'submachine'"
|
||||
<button @click="activeTab = 'submachine'"
|
||||
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'submachine', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'submachine'}"
|
||||
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
||||
{{ __('Sub-machine Status Request') }}
|
||||
</button>
|
||||
<button @click="activeTab = 'ambient_temp'"
|
||||
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'ambient_temp', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'ambient_temp'}"
|
||||
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
||||
{{ __('Ambient Temperature Log') }}
|
||||
</button>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
@ -860,6 +983,21 @@
|
||||
<div id="temperature-chart" class="min-h-[200px] w-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- Ambient Temperature Trend Chart -->
|
||||
<div x-show="activeTab === 'ambient_temp'"
|
||||
class="mb-6 p-4 rounded-2xl bg-slate-50/50 dark:bg-white/[0.02] border border-slate-100 dark:border-slate-800/50" x-cloak>
|
||||
<div class="flex items-center justify-between mb-4 px-2">
|
||||
<h3 class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] flex items-center gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]"></span>
|
||||
{{ __('Ambient Temperature Trend') }}
|
||||
</h3>
|
||||
<span class="text-[10px] font-bold text-slate-400" x-show="logs.length > 0">
|
||||
<span x-text="currentMachineName"></span> @ <span x-text="startDate.split(' ')[0]"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="ambient-temperature-chart" class="min-h-[200px] w-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Container -->
|
||||
<div x-show="activeTab !== 'expiry'"
|
||||
class="luxury-card border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
|
||||
@ -1064,6 +1202,56 @@
|
||||
placeholder="{{ __('Enter machine name...') }}">
|
||||
</div>
|
||||
|
||||
<div class="space-y-4" x-show="editAmbientTempMonitoringEnabled" x-cloak>
|
||||
<label
|
||||
class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{
|
||||
__('Ambient Temperature Upper Limit') }} ({{ __('Upper limit; the fan will turn on when reached') }})</label>
|
||||
|
||||
{{-- Luxury +/- Counter UI --}}
|
||||
<div class="flex items-center h-14 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden w-full group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
|
||||
{{-- Minus Button --}}
|
||||
<button type="button" @click="editAmbientTempSetting = (editAmbientTempSetting ? Math.max(0, parseInt(editAmbientTempSetting) - 1) : 25)"
|
||||
class="shrink-0 w-14 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
|
||||
{{-- Input Field --}}
|
||||
<div class="flex-1 relative flex items-center">
|
||||
<input type="number" name="ambient_temp_setting" x-model="editAmbientTempSetting"
|
||||
class="w-full bg-transparent border-none text-center text-base font-extrabold text-slate-800 dark:text-white focus:ring-0 p-0 placeholder:text-slate-400 placeholder:font-bold placeholder:text-sm [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
placeholder="{{ __('Enter ambient temperature setting...') }}">
|
||||
|
||||
{{-- Clear Button --}}
|
||||
<button type="button" x-show="editAmbientTempSetting !== '' && editAmbientTempSetting !== null" @click="editAmbientTempSetting = ''"
|
||||
class="absolute right-3 p-1 rounded-full text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 active:scale-95 transition-all"
|
||||
style="display: none;">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
|
||||
{{-- Plus Button --}}
|
||||
<button type="button" @click="editAmbientTempSetting = (editAmbientTempSetting ? parseInt(editAmbientTempSetting) + 1 : 25)"
|
||||
class="shrink-0 w-14 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Help Text --}}
|
||||
<p class="text-[11px] font-bold text-slate-400 dark:text-slate-500 tracking-wide mt-1 pl-1">
|
||||
{{ __('Can be empty. If left blank, it means ambient temperature monitoring is disabled.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
<button type="button" @click="showEditModal = false"
|
||||
class="px-8 py-4 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black rounded-2xl border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 transition-all">
|
||||
|
||||
@ -38,6 +38,11 @@
|
||||
'Remote Settlement' => __('Remote Settlement'),
|
||||
'Lock Page Lock' => __('Lock Page Lock'),
|
||||
'Lock Page Unlock' => __('Lock Page Unlock'),
|
||||
'Fan On' => __('Fan On'),
|
||||
'Fan Off' => __('Fan Off'),
|
||||
'Fan Auto' => __('Fan Auto'),
|
||||
'Ambient Temperature' => __('Ambient Temperature'),
|
||||
'Ambient Temperature Upper Limit' => __('Ambient Temperature Upper Limit'),
|
||||
'Remote Change' => __('Remote Change'),
|
||||
'Remote Dispense' => __('Remote Dispense'),
|
||||
'Adjust Stock & Expiry' => __('Adjust Stock & Expiry'),
|
||||
@ -442,6 +447,10 @@
|
||||
'checkout': this.translations['Remote Settlement'],
|
||||
'lock': this.translations['Lock Page Lock'],
|
||||
'unlock': this.translations['Lock Page Unlock'],
|
||||
'fanon': this.translations['Fan On'],
|
||||
'fanoff': this.translations['Fan Off'],
|
||||
'fanauto': this.translations['Fan Auto'],
|
||||
'ambient_temp_limit': this.translations['Ambient Temperature Upper Limit'],
|
||||
'change': this.translations['Remote Change'],
|
||||
'dispense': this.translations['Remote Dispense'],
|
||||
'reload_stock': this.translations['Adjust Stock & Expiry'],
|
||||
@ -512,6 +521,11 @@
|
||||
return details;
|
||||
}
|
||||
|
||||
if (item.command_type === 'ambient_temp_limit' && item.payload) {
|
||||
const temp = item.payload.temperature;
|
||||
return `${this.translations['Ambient Temperature Upper Limit']}: ${temp !== null && temp !== undefined ? temp + '°C' : this.translations['Empty']}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@ -685,6 +699,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fan Controls -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 ml-1">
|
||||
<div class="w-1 h-3 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
||||
<span
|
||||
class="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">{{
|
||||
__('Fan Controls') }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Fan On -->
|
||||
<button @click="sendCommand('fanon')"
|
||||
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 animate-[spin_10s_linear_infinite]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 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">
|
||||
{{ __('Fan On') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Fan Off -->
|
||||
<button @click="sendCommand('fanoff')"
|
||||
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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</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">
|
||||
{{ __('Fan Off') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Fan Auto -->
|
||||
<button @click="sendCommand('fanauto')"
|
||||
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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</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">
|
||||
{{ __('Fan Auto') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Controls -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 ml-1">
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user