[FEAT] 優化溫度告警狀態轉移邏輯與完善機台專屬通知設定 UI

1. 修正 `ProcessHeartbeat.php` 溫度告警判定邏輯,避免溫度在「高低溫異常狀態」直接轉換時,誤發含有異常溫度的「恢復正常」通知。
2. 實作機台層級自訂通知類型 (`notify_types_mode`) 功能,支援繼承全域設定或獨立指派通知類型(連線、溫度、硬體異常)。
3. 優化前端 UI 邏輯,當選擇自訂通知且包含「溫度告警」時,方才顯示溫度上下限輸入區塊。
4. 修正複製機台設定功能 (copy-machine) 的行為,確保能正確複製 `discord_notify_types` 並重設選擇器狀態。
5. 新增與更新中、英、日語系翻譯字串。
This commit is contained in:
sky121113 2026-05-21 14:34:55 +08:00
parent 2e3d6e5572
commit 205da14e94
6 changed files with 259 additions and 91 deletions

View File

@ -128,16 +128,37 @@ class DiscordNotificationController extends AdminController
$request->validate([
'discord_webhook_url' => 'nullable|url|max:500',
'temp_alert_enabled' => 'required|string|in:inherit,enabled,disabled',
'notify_types_mode' => 'required|string|in:inherit,custom',
'discord_notify_types' => 'nullable|array',
'discord_notify_types.*' => 'string|in:submachine,status,temperature',
'temp_upper_limit' => 'nullable|numeric|min:-50|max:100',
'temp_lower_limit' => 'nullable|numeric|min:-50|max:100',
]);
$settings = $machine->settings ?? [];
$settings['discord_webhook_url'] = $request->input('discord_webhook_url');
$settings['temp_alert_enabled'] = $request->input('temp_alert_enabled');
$settings['temp_upper_limit'] = $request->filled('temp_upper_limit') ? (float)$request->temp_upper_limit : null;
$settings['temp_lower_limit'] = $request->filled('temp_lower_limit') ? (float)$request->temp_lower_limit : null;
$notifyTypesMode = $request->input('notify_types_mode', 'inherit');
$notifyTypes = $request->input('discord_notify_types', []);
$hasTemperature = in_array('temperature', $notifyTypes, true);
if ($notifyTypesMode === 'custom') {
$settings['discord_notify_types'] = array_values($notifyTypes);
if ($hasTemperature) {
$settings['temp_alert_enabled'] = 'enabled';
$settings['temp_upper_limit'] = $request->filled('temp_upper_limit') ? (float)$request->temp_upper_limit : null;
$settings['temp_lower_limit'] = $request->filled('temp_lower_limit') ? (float)$request->temp_lower_limit : null;
} else {
$settings['temp_alert_enabled'] = 'disabled';
$settings['temp_upper_limit'] = null;
$settings['temp_lower_limit'] = null;
}
} else {
unset($settings['discord_notify_types']);
$settings['temp_alert_enabled'] = 'inherit';
$settings['temp_upper_limit'] = null;
$settings['temp_lower_limit'] = null;
}
$machine->settings = $settings;
$machine->save();

View File

@ -178,16 +178,8 @@ class ProcessHeartbeat implements ShouldQueue
$activeStatus = \Illuminate\Support\Facades\Cache::get($activeKey);
if ($temp > $upperLimit) {
// 若從低溫直接跳到高溫,先恢復低溫
// 若從低溫直接跳到高溫,清除低溫告警冷卻快取,但不發送恢復通知
if ($activeStatus === 'low') {
\App\Jobs\Machine\ProcessStateLog::dispatch(
$machine->id,
$machine->company_id,
"Temperature restored: :temp°C",
'info',
['temp' => $temp],
'temperature'
);
\Illuminate\Support\Facades\Cache::forget($cooldownKey);
}
@ -206,16 +198,8 @@ class ProcessHeartbeat implements ShouldQueue
\Illuminate\Support\Facades\Cache::put($activeKey, 'high', 86400);
}
} elseif ($temp < $lowerLimit) {
// 若從高溫直接跳到低溫,先恢復高溫
// 若從高溫直接跳到低溫,清除高溫告警冷卻快取,但不發送恢復通知
if ($activeStatus === 'high') {
\App\Jobs\Machine\ProcessStateLog::dispatch(
$machine->id,
$machine->company_id,
"Temperature restored: :temp°C",
'info',
['temp' => $temp],
'temperature'
);
\Illuminate\Support\Facades\Cache::forget($cooldownKey);
}

View File

@ -2131,5 +2131,11 @@
"Default Temp Alert Upper Limit (°C)": "Default Temp Alert Upper Limit (°C)",
"Default Temp Alert Lower Limit (°C)": "Default Temp Alert Lower Limit (°C)",
"Default Temp Alert Limits": "Default Temp Alert Limits",
"Alert Types Source": "Alert Types Source",
"Inherit Company Alert Types": "Inherit Company Alert Types",
"Custom Alert Types": "Custom Alert Types",
"Effective Alert Types": "Effective Alert Types",
"Machine Alert Settings": "Machine Alert Settings",
"Configure alert types and temperature thresholds": "Configure alert types and temperature thresholds",
"Machine model saved successfully.": "Machine model saved successfully."
}

View File

@ -2134,5 +2134,11 @@
"Default Temp Alert Upper Limit (°C)": "デフォルト温度アラート上限 (°C)",
"Default Temp Alert Lower Limit (°C)": "デフォルト温度アラート下限 (°C)",
"Default Temp Alert Limits": "デフォルト温度アラート範囲",
"Alert Types Source": "アラート種別ソース",
"Inherit Company Alert Types": "会社のアラート種別を継承",
"Custom Alert Types": "カスタムアラート種別",
"Effective Alert Types": "有効なアラート種別",
"Machine Alert Settings": "機台アラート設定",
"Configure alert types and temperature thresholds": "アラート種別と温度しきい値を設定",
"Machine model saved successfully.": "機器モデルが正常に追加されました。"
}

View File

@ -2190,5 +2190,11 @@
"Default Temp Alert Upper Limit (°C)": "預設溫度告警上限 (°C)",
"Default Temp Alert Lower Limit (°C)": "預設溫度告警下限 (°C)",
"Default Temp Alert Limits": "預設溫度告警範圍",
"Alert Types Source": "告警類型來源",
"Inherit Company Alert Types": "繼承公司告警類型",
"Custom Alert Types": "自訂告警類型",
"Effective Alert Types": "目前生效的告警類型",
"Machine Alert Settings": "機台告警設定",
"Configure alert types and temperature thresholds": "設定告警類型與溫度門檻",
"Machine model saved successfully.": "機台型號已成功新增。"
}

View File

@ -262,7 +262,8 @@
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Company') }}</th>
@endif
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Webhook Status') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Temp Alert State') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Configured Alert Types') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Temp Alert Range') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
@ -273,6 +274,11 @@
$mWebhook = $mSettings['discord_webhook_url'] ?? null;
$cSettings = $machine->company->settings ?? [];
$cWebhook = $cSettings['discord_webhook_url'] ?? null;
$cNotifyTypes = $cSettings['discord_notify_types'] ?? [];
$mNotifyTypes = array_key_exists('discord_notify_types', $mSettings) && is_array($mSettings['discord_notify_types']) ? $mSettings['discord_notify_types'] : null;
$effectiveNotifyTypes = $mNotifyTypes ?? $cNotifyTypes;
$isTypeInherited = $mNotifyTypes === null;
$hasTemperatureNotify = in_array('temperature', $effectiveNotifyTypes, true);
$mTempAlert = $mSettings['temp_alert_enabled'] ?? 'inherit';
@ -291,11 +297,11 @@
$finalLower = $sysLower;
$tempSource = __('System Default');
if ($mTempAlert === 'enabled' && ($mUpper !== null || $mLower !== null)) {
if ($mTempAlert === 'enabled' && $hasTemperatureNotify && ($mUpper !== null || $mLower !== null)) {
$finalUpper = $mUpper !== null ? $mUpper : $sysUpper;
$finalLower = $mLower !== null ? $mLower : $sysLower;
$tempSource = __('Custom');
} elseif ($modelUpper !== null || $modelLower !== null) {
} elseif ($hasTemperatureNotify && ($modelUpper !== null || $modelLower !== null)) {
$finalUpper = $modelUpper !== null ? $modelUpper : $sysUpper;
$finalLower = $modelLower !== null ? $modelLower : $sysLower;
$tempSource = __('Model Default');
@ -331,36 +337,48 @@
<span class="px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20 mt-1 inline-block">{{ __('Machine Specific') }}</span>
@elseif($cWebhook)
<span class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 block truncate" title="{{ $cWebhook }}">{{ Str::limit($cWebhook, 30) }}</span>
<span class="px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 mt-1 inline-block">{{ __('Inherit Company') }}</span>
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 mt-1 inline-block">{{ __('Inherit Company') }}</span>
@else
<span class="text-xs font-bold text-slate-300 dark:text-slate-600">{{ __('Not Configured') }}</span>
@endif
</td>
<td class="px-6 py-6">
<div class="flex flex-col">
<div class="flex items-center gap-2">
@if($mTempAlert === 'enabled')
<x-status-badge status="enabled" size="sm" />
@elseif($mTempAlert === 'disabled')
<x-status-badge status="disabled" size="sm" />
@else
<span class="px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200/50 dark:border-slate-700/50">
{{ __('Inherit') }}
</span>
<div class="space-y-1.5">
@if($isTypeInherited)
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 inline-block">{{ __('Inherit Company') }}</span>
@endif
<div class="flex flex-wrap gap-1.5">
@foreach($effectiveNotifyTypes as $type)
@php
$label = match($type) {
'submachine' => __('Device Exception'),
'status' => __('Connectivity Logs'),
'temperature' => __('Temperature Alert'),
default => $type
};
@endphp
<span class="px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest whitespace-nowrap bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">{{ $label }}</span>
@endforeach
@if(empty($effectiveNotifyTypes))
<span class="text-xs font-bold text-slate-300 dark:text-slate-600">{{ __('Not Configured') }}</span>
@endif
</div>
@if($mTempAlert !== 'disabled')
<div class="mt-1 flex items-center gap-1.5">
<span class="px-2.5 py-1 rounded-full text-xs font-mono font-black bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
{{ number_format($finalLower, 0) }}°C ~ {{ number_format($finalUpper, 0) }}°C
</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500" title="{{ __('Source of temperature limit settings') }}">
({{ $tempSource }})
</span>
</div>
@endif
</div>
</td>
<td class="px-6 py-6">
@if($mTempAlert !== 'disabled' && $hasTemperatureNotify)
<div class="flex items-center gap-1.5">
<span class="px-2.5 py-1 rounded-full text-xs font-mono font-black bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20 whitespace-nowrap">
{{ number_format($finalLower, 0) }}°C ~ {{ number_format($finalUpper, 0) }}°C
</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500" title="{{ __('Source of temperature limit settings') }}">
({{ $tempSource }})
</span>
</div>
@else
<span class="text-xs font-bold text-slate-300 dark:text-slate-600"></span>
@endif
</td>
<td class="px-6 py-6 text-right">
<button type="button" @click="openMachineEdit(@js($machine))"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit Settings') }}">
@ -369,7 +387,7 @@
</td>
</tr>
@empty
<x-empty-state mode="table" :colspan="6" :message="__('No records found')" />
<x-empty-state mode="table" :colspan="7" :message="__('No records found')" />
@endforelse
</tbody>
</table>
@ -383,6 +401,11 @@
$mWebhook = $mSettings['discord_webhook_url'] ?? null;
$cSettings = $machine->company->settings ?? [];
$cWebhook = $cSettings['discord_webhook_url'] ?? null;
$cNotifyTypes = $cSettings['discord_notify_types'] ?? [];
$mNotifyTypes = array_key_exists('discord_notify_types', $mSettings) && is_array($mSettings['discord_notify_types']) ? $mSettings['discord_notify_types'] : null;
$effectiveNotifyTypes = $mNotifyTypes ?? $cNotifyTypes;
$isTypeInherited = $mNotifyTypes === null;
$hasTemperatureNotify = in_array('temperature', $effectiveNotifyTypes, true);
$mTempAlert = $mSettings['temp_alert_enabled'] ?? 'inherit';
@ -401,11 +424,11 @@
$finalLower = $sysLower;
$tempSource = __('System Default');
if ($mTempAlert === 'enabled' && ($mUpper !== null || $mLower !== null)) {
if ($mTempAlert === 'enabled' && $hasTemperatureNotify && ($mUpper !== null || $mLower !== null)) {
$finalUpper = $mUpper !== null ? $mUpper : $sysUpper;
$finalLower = $mLower !== null ? $mLower : $sysLower;
$tempSource = __('Custom');
} elseif ($modelUpper !== null || $modelLower !== null) {
} elseif ($hasTemperatureNotify && ($modelUpper !== null || $modelLower !== null)) {
$finalUpper = $modelUpper !== null ? $modelUpper : $sysUpper;
$finalLower = $modelLower !== null ? $modelLower : $sysLower;
$tempSource = __('Model Default');
@ -422,15 +445,7 @@
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">{{ $machine->serial_no }}</p>
</div>
</div>
@if($mTempAlert === 'enabled')
<x-status-badge status="enabled" size="sm" />
@elseif($mTempAlert === 'disabled')
<x-status-badge status="disabled" size="sm" />
@else
<span class="px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700">
{{ __('Inherit') }}
</span>
@endif
{{-- Temperature status is implied by configured alert types; show only range below. --}}
</div>
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
@ -451,12 +466,34 @@
<span class="px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20 mt-1 inline-block">{{ __('Machine Specific') }}</span>
@elseif($cWebhook)
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 break-all leading-normal">{{ $cWebhook }}</p>
<span class="px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 mt-1 inline-block">{{ __('Inherit Company') }}</span>
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 mt-1 inline-block">{{ __('Inherit Company') }}</span>
@else
<p class="text-xs font-bold text-slate-300 dark:text-slate-600">{{ __('Not Configured') }}</p>
@endif
</div>
@if($mTempAlert !== 'disabled')
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Configured Alert Types') }}</p>
@if($isTypeInherited)
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500 border border-slate-200 dark:border-slate-700 inline-block mb-1">{{ __('Inherit Company') }}</span>
@endif
<div class="flex flex-wrap gap-1.5">
@foreach($effectiveNotifyTypes as $type)
@php
$label = match($type) {
'submachine' => __('Device Exception'),
'status' => __('Connectivity Logs'),
'temperature' => __('Temperature Alert'),
default => $type
};
@endphp
<span class="px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest whitespace-nowrap bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">{{ $label }}</span>
@endforeach
@if(empty($effectiveNotifyTypes))
<p class="text-xs font-bold text-slate-300 dark:text-slate-600">{{ __('Not Configured') }}</p>
@endif
</div>
</div>
@if($mTempAlert !== 'disabled' && $hasTemperatureNotify)
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Temp Alert Range') }}</p>
<div class="flex items-center gap-1.5 mt-1">
@ -624,19 +661,34 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label class="flex items-center gap-3 p-4 rounded-xl border border-slate-100 dark:border-slate-800/40 bg-slate-50/30 dark:bg-slate-900/10 hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-all duration-300 cursor-pointer">
<input type="checkbox" name="discord_notify_types[]" value="submachine" x-model="companyForm.discord_notify_types" class="luxury-checkbox">
<span class="text-xs font-extrabold text-slate-700 dark:text-slate-200">{{ __('Submachine Alert') }}</span>
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-slate-50/30 dark:bg-slate-900/20 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-sm font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Submachine Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="submachine" x-model="companyForm.discord_notify_types"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
<label class="flex items-center gap-3 p-4 rounded-xl border border-slate-100 dark:border-slate-800/40 bg-slate-50/30 dark:bg-slate-900/10 hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-all duration-300 cursor-pointer">
<input type="checkbox" name="discord_notify_types[]" value="status" x-model="companyForm.discord_notify_types" class="luxury-checkbox">
<span class="text-xs font-extrabold text-slate-700 dark:text-slate-200">{{ __('Machine Connection Alert') }}</span>
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-slate-50/30 dark:bg-slate-900/20 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-sm font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Machine Connection Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="status" x-model="companyForm.discord_notify_types"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
<label class="flex items-center gap-3 p-4 rounded-xl border border-slate-100 dark:border-slate-800/40 bg-slate-50/30 dark:bg-slate-900/10 hover:bg-slate-50 dark:hover:bg-slate-800/40 transition-all duration-300 cursor-pointer">
<input type="checkbox" name="discord_notify_types[]" value="temperature" x-model="companyForm.discord_notify_types" class="luxury-checkbox">
<span class="text-xs font-extrabold text-slate-700 dark:text-slate-200">{{ __('Temperature Alert') }}</span>
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-slate-50/30 dark:bg-slate-900/20 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-sm font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Temperature Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="temperature" x-model="companyForm.discord_notify_types"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
</div>
</div>
@ -713,7 +765,7 @@
@csrf
@method('PUT')
<div class="px-10 py-6 space-y-6 max-h-[65vh] overflow-y-auto overflow-x-hidden relative z-10">
<div class="px-10 py-6 space-y-4 max-h-[65vh] overflow-y-auto overflow-x-hidden relative z-10">
<!-- Pull-style Copy Settings (複製其他機台設定) -->
<div class="p-5 bg-gradient-to-br from-cyan-500/5 to-blue-500/5 rounded-2xl border border-cyan-500/15 relative z-[30]">
@ -793,33 +845,74 @@
</div>
<!-- Temperature Overrides -->
<div class="p-6 bg-slate-50 dark:bg-slate-800/25 rounded-2xl border border-slate-100 dark:border-slate-800/50 space-y-6">
<input type="hidden" name="temp_alert_enabled" x-model="machineForm.temp_alert_enabled">
<div class="p-6 bg-slate-50 dark:bg-slate-800/25 rounded-2xl border border-slate-100 dark:border-slate-800/50 space-y-5">
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/50">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-amber-500/10 text-amber-500 flex items-center justify-center border border-amber-500/20 shrink-0">
<svg class="w-4 h-4 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v18m0-18a9 9 0 110 18m0-18a9 9 0 000 18" /></svg>
</div>
<div>
<h4 class="text-sm font-extrabold text-slate-800 dark:text-white">{{ __('Temperature Alert Settings') }}</h4>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mt-1">{{ __('Define custom temperature limits or inherit from model') }}</p>
<h4 class="text-sm font-extrabold text-slate-800 dark:text-white">{{ __('Machine Alert Settings') }}</h4>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mt-1">{{ __('Configure alert types and temperature thresholds') }}</p>
</div>
</div>
</div>
<!-- Alert Enable State Selection -->
<div class="space-y-2">
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Alert Switch') }}</label>
<x-searchable-select id="machine-alert-select" name="temp_alert_enabled" x-model="machineForm.temp_alert_enabled" :hasSearch="false">
<option value="inherit" data-title="{{ __('Inherit Model / System default') }}">{{ __('Inherit Model / System default') }}</option>
<option value="enabled" data-title="{{ __('Custom Enabled') }}">{{ __('Custom Enabled') }}</option>
<option value="disabled" data-title="{{ __('Custom Disabled (Mute Alert)') }}">{{ __('Custom Disabled (Mute Alert)') }}</option>
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Alert Types Source') }}</label>
<x-searchable-select id="machine-notify-mode-select" name="notify_types_mode" x-model="machineForm.notify_types_mode" :hasSearch="false" @change="syncTempAlertState()">
<option value="inherit" data-title="{{ __('Inherit Company Alert Types') }}">{{ __('Inherit Company Alert Types') }}</option>
<option value="custom" data-title="{{ __('Custom Alert Types') }}">{{ __('Custom Alert Types') }}</option>
</x-searchable-select>
</div>
<div x-show="machineForm.notify_types_mode === 'custom'" x-transition.duration.300ms class="space-y-3 animate-luxury-in">
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Alert Types') }}</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-white/70 dark:bg-slate-900/40 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-base font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Submachine Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="submachine" x-model="machineForm.discord_notify_types" @change="syncTempAlertState()"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-white/70 dark:bg-slate-900/40 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-base font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Machine Connection Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="status" x-model="machineForm.discord_notify_types" @change="syncTempAlertState()"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
<label class="group relative flex items-center justify-between p-4 rounded-2xl border border-slate-100/80 dark:border-slate-800/80 bg-white/70 dark:bg-slate-900/40 hover:border-cyan-500/40 cursor-pointer transition-all duration-300">
<div class="flex flex-col flex-1 min-w-0 mr-3">
<span class="text-base font-black text-slate-500 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ __('Temperature Alert') }}</span>
</div>
<div class="flex-shrink-0 pr-1">
<input type="checkbox" name="discord_notify_types[]" value="temperature" x-model="machineForm.discord_notify_types" @change="syncTempAlertState()"
class="w-4 h-4 rounded border-2 border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500/20 transition-all cursor-pointer accent-cyan-500">
</div>
</label>
</div>
</div>
<div x-show="machineForm.notify_types_mode === 'inherit'" x-transition.duration.300ms class="space-y-2 animate-luxury-in">
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-1 block">{{ __('Effective Alert Types') }}</label>
<div class="flex flex-wrap gap-2">
<template x-for="type in effectiveNotifyTypes()" :key="type">
<span class="px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20" x-text="notifyTypeLabel(type)"></span>
</template>
</div>
</div>
<!-- Custom Threshold Limits -->
<div x-show="machineForm.temp_alert_enabled === 'enabled'"
<div x-show="machineForm.notify_types_mode === 'custom' && isTemperatureChecked()"
x-transition.duration.300ms
class="grid grid-cols-2 gap-4 animate-luxury-in">
class="grid grid-cols-1 sm:grid-cols-2 gap-4 animate-luxury-in">
<div class="space-y-2">
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Custom Upper Limit (°C)') }}</label>
<div class="flex items-center h-12 rounded-xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all w-full">
@ -903,6 +996,8 @@
},
machineForm: {
discord_webhook_url: '',
notify_types_mode: 'inherit',
discord_notify_types: [],
temp_alert_enabled: 'inherit',
temp_upper_limit: '',
temp_lower_limit: ''
@ -936,23 +1031,63 @@
openMachineEdit(machine) {
const settings = machine.settings || {};
const machineNotifyTypes = Array.isArray(settings.discord_notify_types) ? settings.discord_notify_types : null;
this.currentMachine = machine;
this.machineForm.discord_webhook_url = settings.discord_webhook_url || '';
this.machineForm.temp_alert_enabled = settings.temp_alert_enabled || 'inherit';
this.machineForm.notify_types_mode = machineNotifyTypes === null ? 'inherit' : 'custom';
this.machineForm.discord_notify_types = machineNotifyTypes === null ? [] : [...machineNotifyTypes];
this.machineForm.temp_alert_enabled = settings.temp_alert_enabled || (this.isTemperatureChecked() ? 'enabled' : 'disabled');
this.machineForm.temp_upper_limit = settings.temp_upper_limit !== undefined && settings.temp_upper_limit !== null ? Math.round(parseFloat(settings.temp_upper_limit)).toString() : '';
this.machineForm.temp_lower_limit = settings.temp_lower_limit !== undefined && settings.temp_lower_limit !== null ? Math.round(parseFloat(settings.temp_lower_limit)).toString() : '';
this.syncTempAlertState();
this.testStatus = null;
this.testMessage = '';
this.showMachineModal = true;
this.$nextTick(() => {
const alertSel = window.HSSelect?.getInstance('#machine-alert-select');
if (alertSel) alertSel.setValue(this.machineForm.temp_alert_enabled);
const modeSel = window.HSSelect?.getInstance('#machine-notify-mode-select');
if (modeSel) modeSel.setValue(this.machineForm.notify_types_mode);
this.rebuildCopySelect();
});
},
notifyTypeLabel(type) {
if (type === 'submachine') return '{{ __('Submachine Alert') }}';
if (type === 'status') return '{{ __('Machine Connection Alert') }}';
if (type === 'temperature') return '{{ __('Temperature Alert') }}';
return type;
},
effectiveNotifyTypes() {
if (this.machineForm.notify_types_mode === 'custom') {
return this.machineForm.discord_notify_types;
}
return this.currentMachine?.company?.settings?.discord_notify_types || [];
},
isTemperatureChecked() {
return this.effectiveNotifyTypes().includes('temperature');
},
syncTempAlertState() {
if (this.machineForm.notify_types_mode === 'inherit') {
this.machineForm.temp_alert_enabled = 'inherit';
this.machineForm.temp_upper_limit = '';
this.machineForm.temp_lower_limit = '';
return;
}
if (this.machineForm.discord_notify_types.includes('temperature')) {
this.machineForm.temp_alert_enabled = 'enabled';
return;
}
this.machineForm.temp_alert_enabled = 'disabled';
this.machineForm.temp_upper_limit = '';
this.machineForm.temp_lower_limit = '';
},
rebuildCopySelect() {
this.$nextTick(() => {
const wrapper = document.getElementById('copy-select-wrapper');
@ -1039,18 +1174,28 @@
},
handleCopyMachine(value) {
if (!value) return;
if (!value || !String(value).trim()) return;
const source = this.copyableMachines.find(m => m.id == value);
if (source) {
const s = source.settings || {};
const sourceNotifyTypes = Array.isArray(s.discord_notify_types) ? s.discord_notify_types : null;
this.machineForm.discord_webhook_url = s.discord_webhook_url || '';
this.machineForm.temp_alert_enabled = s.temp_alert_enabled || 'inherit';
this.machineForm.notify_types_mode = sourceNotifyTypes === null ? 'inherit' : 'custom';
this.machineForm.discord_notify_types = sourceNotifyTypes === null ? [] : [...sourceNotifyTypes];
this.machineForm.temp_alert_enabled = s.temp_alert_enabled || (this.isTemperatureChecked() ? 'enabled' : 'disabled');
this.machineForm.temp_upper_limit = s.temp_upper_limit !== undefined && s.temp_upper_limit !== null ? Math.round(parseFloat(s.temp_upper_limit)).toString() : '';
this.machineForm.temp_lower_limit = s.temp_lower_limit !== undefined && s.temp_lower_limit !== null ? Math.round(parseFloat(s.temp_lower_limit)).toString() : '';
this.syncTempAlertState();
this.$nextTick(() => {
const el = window.HSSelect?.getInstance('#machine-alert-select');
if (el) el.setValue(this.machineForm.temp_alert_enabled);
const modeSel = window.HSSelect?.getInstance('#machine-notify-mode-select');
if (modeSel) modeSel.setValue(this.machineForm.notify_types_mode);
const copyEl = document.getElementById('copy-machine-select');
if (copyEl) {
copyEl.value = ' ';
const copyInstance = window.HSSelect?.getInstance('#copy-machine-select');
if (copyInstance) copyInstance.setValue(' ');
}
});
this.copiedMachineName = source.name;