[STYLE] 優化機台型號與通知設定介面並調整溫度為整數告警
1. 調整 Discord 通知設定介面的間距與對齊,使其與基本設定中的機台設定保持一致。 2. 重構機台型號設定 Modal,將溫度上下限的輸入框改為「極簡奢華風」的加減按鈕元件。 3. 更新機台型號溫度相關的設定與顯示,強制限制與轉換為整數 (移除小數點)。 4. 將機台型號的溫度限制相關介面文字更新為「告警」相關字眼 (預設溫度告警上限、預設溫度告警下限等)。 5. 於中、英、日語系檔案中新增溫度告警相關之翻譯鍵值。
This commit is contained in:
parent
cde2718d2a
commit
2e3d6e5572
@ -37,7 +37,7 @@ class DiscordNotificationController extends AdminController
|
||||
// ── Tab 2: 機台個別設定 ──
|
||||
$machineQuery = \App\Models\Machine\Machine::query()->with(['company', 'machineModel']);
|
||||
if ($user->isSystemAdmin()) {
|
||||
if ($filterCompanyId = $request->input('filter_company_id')) {
|
||||
if ($filterCompanyId = trim($request->input('filter_company_id', ''))) {
|
||||
$machineQuery->where('company_id', $filterCompanyId);
|
||||
}
|
||||
if ($searchMachine = $request->input('search_machine')) {
|
||||
|
||||
@ -22,13 +22,13 @@ class MachineModelController extends AdminController
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'temp_upper_limit' => 'nullable|numeric|min:-50|max:100',
|
||||
'temp_lower_limit' => 'nullable|numeric|min:-50|max:100',
|
||||
'temp_upper_limit' => 'nullable|integer|min:-50|max:100',
|
||||
'temp_lower_limit' => 'nullable|integer|min:-50|max:100',
|
||||
]);
|
||||
|
||||
$settings = [
|
||||
'temp_upper_limit' => $request->filled('temp_upper_limit') ? (float)$request->temp_upper_limit : null,
|
||||
'temp_lower_limit' => $request->filled('temp_lower_limit') ? (float)$request->temp_lower_limit : null,
|
||||
'temp_upper_limit' => $request->filled('temp_upper_limit') ? (int)$request->temp_upper_limit : null,
|
||||
'temp_lower_limit' => $request->filled('temp_lower_limit') ? (int)$request->temp_lower_limit : null,
|
||||
];
|
||||
|
||||
MachineModel::create([
|
||||
@ -56,13 +56,13 @@ class MachineModelController extends AdminController
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'temp_upper_limit' => 'nullable|numeric|min:-50|max:100',
|
||||
'temp_lower_limit' => 'nullable|numeric|min:-50|max:100',
|
||||
'temp_upper_limit' => 'nullable|integer|min:-50|max:100',
|
||||
'temp_lower_limit' => 'nullable|integer|min:-50|max:100',
|
||||
]);
|
||||
|
||||
$settings = [
|
||||
'temp_upper_limit' => $request->filled('temp_upper_limit') ? (float)$request->temp_upper_limit : null,
|
||||
'temp_lower_limit' => $request->filled('temp_lower_limit') ? (float)$request->temp_lower_limit : null,
|
||||
'temp_upper_limit' => $request->filled('temp_upper_limit') ? (int)$request->temp_upper_limit : null,
|
||||
'temp_lower_limit' => $request->filled('temp_lower_limit') ? (int)$request->temp_lower_limit : null,
|
||||
];
|
||||
|
||||
$machine_model->update([
|
||||
|
||||
@ -2128,5 +2128,8 @@
|
||||
"Default Temp Upper Limit (°C)": "Default Temp Upper Limit (°C)",
|
||||
"Default Temp Lower Limit (°C)": "Default Temp Lower Limit (°C)",
|
||||
"Default Temp Limits": "Default Temp Limits",
|
||||
"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",
|
||||
"Machine model saved successfully.": "Machine model saved successfully."
|
||||
}
|
||||
@ -2131,5 +2131,8 @@
|
||||
"Default Temp Upper Limit (°C)": "デフォルト温度上限 (°C)",
|
||||
"Default Temp Lower Limit (°C)": "デフォルト温度下限 (°C)",
|
||||
"Default Temp Limits": "デフォルト温度制御範囲",
|
||||
"Default Temp Alert Upper Limit (°C)": "デフォルト温度アラート上限 (°C)",
|
||||
"Default Temp Alert Lower Limit (°C)": "デフォルト温度アラート下限 (°C)",
|
||||
"Default Temp Alert Limits": "デフォルト温度アラート範囲",
|
||||
"Machine model saved successfully.": "機器モデルが正常に追加されました。"
|
||||
}
|
||||
@ -2187,5 +2187,8 @@
|
||||
"Default Temp Upper Limit (°C)": "預設溫度上限 (°C)",
|
||||
"Default Temp Lower Limit (°C)": "預設溫度下限 (°C)",
|
||||
"Default Temp Limits": "預設溫控範圍",
|
||||
"Default Temp Alert Upper Limit (°C)": "預設溫度告警上限 (°C)",
|
||||
"Default Temp Alert Lower Limit (°C)": "預設溫度告警下限 (°C)",
|
||||
"Default Temp Alert Limits": "預設溫度告警範圍",
|
||||
"Machine model saved successfully.": "機台型號已成功新增。"
|
||||
}
|
||||
@ -1,126 +1,7 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4 pb-20" x-data="{
|
||||
activeTab: '{{ $activeTab }}',
|
||||
tabLoading: null,
|
||||
showCompanyModal: false,
|
||||
showMachineModal: false,
|
||||
companyId: null,
|
||||
companyName: '',
|
||||
companyForm: {
|
||||
discord_webhook_url: '',
|
||||
discord_notify_enabled: false,
|
||||
discord_notify_types: [],
|
||||
discord_notify_locale: 'zh_TW'
|
||||
},
|
||||
currentMachine: {
|
||||
id: null,
|
||||
name: '',
|
||||
serial_no: '',
|
||||
company_id: null,
|
||||
settings: {},
|
||||
machine_model: null
|
||||
},
|
||||
machineForm: {
|
||||
discord_webhook_url: '',
|
||||
temp_alert_enabled: 'inherit',
|
||||
temp_upper_limit: '',
|
||||
temp_lower_limit: ''
|
||||
},
|
||||
copyableMachines: @js($copyableMachines),
|
||||
isTesting: false,
|
||||
testStatus: null,
|
||||
testMessage: '',
|
||||
isLoadingTable: false,
|
||||
showCopyToast: false,
|
||||
copiedMachineName: '',
|
||||
|
||||
openCompanyEdit(company) {
|
||||
const settings = company.settings || {};
|
||||
this.companyId = company.id;
|
||||
this.companyName = company.name;
|
||||
this.companyForm.discord_webhook_url = settings.discord_webhook_url || '';
|
||||
this.companyForm.discord_notify_enabled = !!(settings.discord_notify_enabled);
|
||||
this.companyForm.discord_notify_types = settings.discord_notify_types || ['submachine', 'status'];
|
||||
this.companyForm.discord_notify_locale = settings.locale || 'zh_TW';
|
||||
|
||||
this.testStatus = null;
|
||||
this.testMessage = '';
|
||||
this.showCompanyModal = true;
|
||||
},
|
||||
|
||||
openMachineEdit(machine) {
|
||||
const settings = machine.settings || {};
|
||||
this.currentMachine = machine;
|
||||
this.machineForm.discord_webhook_url = settings.discord_webhook_url || '';
|
||||
this.machineForm.temp_alert_enabled = settings.temp_alert_enabled || 'inherit';
|
||||
this.machineForm.temp_upper_limit = settings.temp_upper_limit !== undefined && settings.temp_upper_limit !== null ? settings.temp_upper_limit : '';
|
||||
this.machineForm.temp_lower_limit = settings.temp_lower_limit !== undefined && settings.temp_lower_limit !== null ? settings.temp_lower_limit : '';
|
||||
|
||||
this.testStatus = null;
|
||||
this.testMessage = '';
|
||||
this.showMachineModal = true;
|
||||
},
|
||||
|
||||
async testWebhook(webhookUrl) {
|
||||
if (!webhookUrl) {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = '{{ __('Please enter Webhook URL first.') }}';
|
||||
return;
|
||||
}
|
||||
this.isTesting = true;
|
||||
this.testStatus = null;
|
||||
try {
|
||||
const response = await fetch('{{ route('admin.basic-settings.discord-notifications.test') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ discord_webhook_url: webhookUrl })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && data.success) {
|
||||
this.testStatus = 'success';
|
||||
this.testMessage = data.message;
|
||||
} else {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = data.message || '{{ __('Failed to send test notification.') }}';
|
||||
}
|
||||
} catch (error) {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = '{{ __('An error occurred during testing.') }}';
|
||||
} finally {
|
||||
this.isTesting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTabContent(url, containerId) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (!res.ok) throw new Error('Network error');
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newContent = doc.getElementById(containerId);
|
||||
const container = document.getElementById(containerId);
|
||||
if (newContent && container) {
|
||||
container.innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
if (window.HSStaticMethods?.autoInit) window.HSStaticMethods.autoInit();
|
||||
if (window.Alpine?.initTree) window.Alpine.initTree(container);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="space-y-2 pb-20" x-data="discordNotificationApp()">
|
||||
<!-- Header -->
|
||||
<x-page-header
|
||||
:title="__('Discord Notifications')"
|
||||
@ -128,15 +9,13 @@
|
||||
/>
|
||||
|
||||
<!-- Tab Selection -->
|
||||
<div class="mb-6 relative z-20">
|
||||
<x-tab-nav model="activeTab">
|
||||
<x-tab-nav-item value="company" :label="__('Company Settings')" />
|
||||
<x-tab-nav-item value="machine" :label="__('Machine Settings')" />
|
||||
</x-tab-nav>
|
||||
</div>
|
||||
<x-tab-nav model="activeTab">
|
||||
<x-tab-nav-item value="company" :label="__('Company Settings')" />
|
||||
<x-tab-nav-item value="machine" :label="__('Machine Settings')" />
|
||||
</x-tab-nav>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div id="ajax-discord-container" class="relative">
|
||||
<div id="ajax-discord-container" class="relative mt-6">
|
||||
<x-luxury-spinner show="isLoadingTable" />
|
||||
|
||||
<div :class="{ 'opacity-30 pointer-events-none': isLoadingTable }"
|
||||
@ -230,7 +109,7 @@
|
||||
default => $type
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
<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">
|
||||
{{ $label }}
|
||||
</span>
|
||||
@endforeach
|
||||
@ -298,7 +177,7 @@
|
||||
default => $type
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
<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">
|
||||
{{ $label }}
|
||||
</span>
|
||||
@endforeach
|
||||
@ -352,12 +231,11 @@
|
||||
<!-- 公司篩選下拉選單 (僅 System Admin 可見) -->
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full md:w-64 relative focus-within:z-[20]">
|
||||
<select name="filter_company_id" class="luxury-input py-2.5 px-4 block w-full text-sm">
|
||||
<option value="">{{ __('All Companies') }}</option>
|
||||
<x-searchable-select name="filter_company_id" :placeholder="__('All Companies')" :selected="request('filter_company_id')">
|
||||
@foreach($allCompanies as $c)
|
||||
<option value="{{ $c->id }}" {{ request('filter_company_id') == $c->id ? 'selected' : '' }}>{{ $c->name }}</option>
|
||||
<option value="{{ $c->id }}" data-title="{{ $c->name }}">{{ $c->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@ -466,17 +344,17 @@
|
||||
@elseif($mTempAlert === 'disabled')
|
||||
<x-status-badge status="disabled" size="sm" />
|
||||
@else
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] 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">
|
||||
<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>
|
||||
@endif
|
||||
</div>
|
||||
@if($mTempAlert !== 'disabled')
|
||||
<div class="mt-1 flex items-center gap-1.5">
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-mono font-black bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
{{ number_format($finalLower, 1) }}°C ~ {{ number_format($finalUpper, 1) }}°C
|
||||
<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-[9px] font-bold text-slate-400 dark:text-slate-500" title="{{ __('Source of temperature limit settings') }}">
|
||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500" title="{{ __('Source of temperature limit settings') }}">
|
||||
({{ $tempSource }})
|
||||
</span>
|
||||
</div>
|
||||
@ -549,7 +427,7 @@
|
||||
@elseif($mTempAlert === 'disabled')
|
||||
<x-status-badge status="disabled" size="sm" />
|
||||
@else
|
||||
<span class="px-2 py-0.5 rounded-full 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">
|
||||
<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
|
||||
@ -582,10 +460,10 @@
|
||||
<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">
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-mono font-black bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
{{ number_format($finalLower, 1) }}°C ~ {{ number_format($finalUpper, 1) }}°C
|
||||
<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-[9px] font-bold text-slate-400 dark:text-slate-500">
|
||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500">
|
||||
({{ $tempSource }})
|
||||
</span>
|
||||
</div>
|
||||
@ -696,13 +574,13 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Discord Webhook URL') }}</label>
|
||||
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Discord Webhook URL') }}</label>
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="relative flex-1">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-slate-400 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 dark:text-slate-500">
|
||||
<svg class="h-5 w-5 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
|
||||
</span>
|
||||
<input type="url" name="discord_webhook_url" x-model="companyForm.discord_webhook_url" placeholder="https://discord.com/api/webhooks/..." class="luxury-input py-3 pl-12 pr-6 block w-full text-sm">
|
||||
<input type="url" name="discord_webhook_url" x-model="companyForm.discord_webhook_url" class="luxury-input py-3 pl-12 pr-6 block w-full text-sm">
|
||||
</div>
|
||||
<button type="button" @click="testWebhook(companyForm.discord_webhook_url)" :disabled="isTesting" class="btn-luxury-ghost flex items-center justify-center gap-2 py-3 px-6 rounded-xl border border-slate-200 dark:border-slate-700/60 font-bold text-sm text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white active:scale-95 transition-all duration-300 shrink-0 disabled:opacity-50">
|
||||
<template x-if="isTesting">
|
||||
@ -727,12 +605,12 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Notification Language') }}</label>
|
||||
<select name="discord_notify_locale" x-model="companyForm.discord_notify_locale" class="luxury-input py-3 px-4 block w-full text-sm">
|
||||
<option value="zh_TW">繁體中文 (Traditional Chinese)</option>
|
||||
<option value="en">English</option>
|
||||
<option value="ja">日本語 (Japanese)</option>
|
||||
</select>
|
||||
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Notification Language') }}</label>
|
||||
<x-searchable-select id="company-locale-select" name="discord_notify_locale" x-model="companyForm.discord_notify_locale" :hasSearch="false">
|
||||
<option value="zh_TW" data-title="繁體中文 (Traditional Chinese)">繁體中文 (Traditional Chinese)</option>
|
||||
<option value="en" data-title="English">English</option>
|
||||
<option value="ja" data-title="日本語 (Japanese)">日本語 (Japanese)</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -835,38 +713,21 @@
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="px-10 py-6 space-y-6 max-h-[65vh] overflow-y-auto relative z-10">
|
||||
<div class="px-10 py-6 space-y-6 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 overflow-hidden">
|
||||
<div class="absolute -right-20 -bottom-20 w-40 h-40 bg-cyan-500/10 rounded-full blur-2xl"></div>
|
||||
<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]">
|
||||
|
||||
<div class="relative focus-within:z-[60] space-y-2">
|
||||
<label class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<label class="text-xs font-bold text-cyan-600 dark:text-cyan-400 tracking-wider mb-2 block flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376A8.965 8.965 0 0012 12.75c-.497 0-.982.04-1.455.12l-.178.03A8.965 8.965 0 006 17.25M17.25 12h-9.75m9.75 2.25h-9.75M17.25 16.5H7.5" /></svg>
|
||||
{{ __('Copy Settings From Another Machine') }}
|
||||
</label>
|
||||
<select @change="if ($event.target.value) {
|
||||
const source = copyableMachines.find(m => m.id == $event.target.value);
|
||||
if (source) {
|
||||
const s = source.settings || {};
|
||||
machineForm.discord_webhook_url = s.discord_webhook_url || '';
|
||||
machineForm.temp_alert_enabled = s.temp_alert_enabled || 'inherit';
|
||||
machineForm.temp_upper_limit = s.temp_upper_limit !== undefined && s.temp_upper_limit !== null ? s.temp_upper_limit : '';
|
||||
machineForm.temp_lower_limit = s.temp_lower_limit !== undefined && s.temp_lower_limit !== null ? s.temp_lower_limit : '';
|
||||
|
||||
copiedMachineName = source.name;
|
||||
showCopyToast = true;
|
||||
setTimeout(() => showCopyToast = false, 3000);
|
||||
}
|
||||
$event.target.value = '';
|
||||
}" class="luxury-input py-2.5 px-4 block w-full text-sm">
|
||||
<option value="">{{ __('Select a machine to copy settings from...') }}</option>
|
||||
<template x-for="m in copyableMachines.filter(m => m.company_id === currentMachine.company_id && m.id !== currentMachine.id)" :key="m.id">
|
||||
<option :value="m.id" x-text="m.name + ' (' + m.serial_no + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-1">
|
||||
|
||||
{{-- 標準動態選單包裝容器,支援 x-searchable-select 奢華外觀 --}}
|
||||
<div id="copy-select-wrapper" class="relative focus-within:z-[60] w-full"></div>
|
||||
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mt-1">
|
||||
{{ __('Only machines under the same company can be cloned for security.') }}
|
||||
</p>
|
||||
</div>
|
||||
@ -894,8 +755,8 @@
|
||||
<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="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Machine-Specific Webhook') }}</h4>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-0.5">{{ __('Leave empty to inherit company settings') }}</p>
|
||||
<h4 class="text-sm font-extrabold text-slate-800 dark:text-white">{{ __('Machine-Specific Webhook') }}</h4>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 mt-1">{{ __('Leave empty to inherit company settings') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -903,10 +764,10 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="relative flex-1">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-slate-400 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 dark:text-slate-500">
|
||||
<svg class="h-5 w-5 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg>
|
||||
</span>
|
||||
<input type="url" name="discord_webhook_url" x-model="machineForm.discord_webhook_url" placeholder="https://discord.com/api/webhooks/..." class="luxury-input py-3 pl-12 pr-6 block w-full text-sm">
|
||||
<input type="url" name="discord_webhook_url" x-model="machineForm.discord_webhook_url" class="luxury-input py-3 pl-12 pr-6 block w-full text-sm">
|
||||
</div>
|
||||
<button type="button" @click="testWebhook(machineForm.discord_webhook_url)" :disabled="isTesting" class="btn-luxury-ghost flex items-center justify-center gap-2 py-3 px-6 rounded-xl border border-slate-200 dark:border-slate-700/60 font-bold text-sm text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white active:scale-95 transition-all duration-300 shrink-0 disabled:opacity-50">
|
||||
<template x-if="isTesting">
|
||||
@ -939,20 +800,20 @@
|
||||
<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-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Temperature Alert Settings') }}</h4>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-0.5">{{ __('Define custom temperature limits or inherit from model') }}</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Enable State Selection -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Alert Switch') }}</label>
|
||||
<select name="temp_alert_enabled" x-model="machineForm.temp_alert_enabled" class="luxury-input py-3 px-4 block w-full text-sm">
|
||||
<option value="inherit">{{ __('Inherit Model / System default') }}</option>
|
||||
<option value="enabled">{{ __('Custom Enabled') }}</option>
|
||||
<option value="disabled">{{ __('Custom Disabled (Mute Alert)') }}</option>
|
||||
</select>
|
||||
<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>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
<!-- Custom Threshold Limits -->
|
||||
@ -960,16 +821,40 @@
|
||||
x-transition.duration.300ms
|
||||
class="grid grid-cols-2 gap-4 animate-luxury-in">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Custom Upper Limit (°C)') }}</label>
|
||||
<input type="number" step="0.1" name="temp_upper_limit" x-model="machineForm.temp_upper_limit"
|
||||
class="luxury-input py-2.5 px-4 block w-full text-sm"
|
||||
:placeholder="currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_upper_limit !== undefined ? currentMachine.machine_model.settings.temp_upper_limit : '40.0'">
|
||||
<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">
|
||||
<button type="button" @click="adjustTemp('temp_upper_limit', -1, currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_upper_limit !== undefined ? currentMachine.machine_model.settings.temp_upper_limit : '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" inputmode="decimal" name="temp_upper_limit" x-model="machineForm.temp_upper_limit"
|
||||
:placeholder="currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_upper_limit !== undefined ? Math.round(parseFloat(currentMachine.machine_model.settings.temp_upper_limit)) : '40'"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustTemp('temp_upper_limit', 1, currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_upper_limit !== undefined ? currentMachine.machine_model.settings.temp_upper_limit : '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Custom Lower Limit (°C)') }}</label>
|
||||
<input type="number" step="0.1" name="temp_lower_limit" x-model="machineForm.temp_lower_limit"
|
||||
class="luxury-input py-2.5 px-4 block w-full text-sm"
|
||||
:placeholder="currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_lower_limit !== undefined ? currentMachine.machine_model.settings.temp_lower_limit : '0.0'">
|
||||
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wider mb-2 block">{{ __('Custom Lower 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">
|
||||
<button type="button" @click="adjustTemp('temp_lower_limit', -1, currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_lower_limit !== undefined ? currentMachine.machine_model.settings.temp_lower_limit : '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" inputmode="decimal" name="temp_lower_limit" x-model="machineForm.temp_lower_limit"
|
||||
:placeholder="currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_lower_limit !== undefined ? Math.round(parseFloat(currentMachine.machine_model.settings.temp_lower_limit)) : '0'"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustTemp('temp_lower_limit', 1, currentMachine.machine_model && currentMachine.machine_model.settings && currentMachine.machine_model.settings.temp_lower_limit !== undefined ? currentMachine.machine_model.settings.temp_lower_limit : '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -991,3 +876,261 @@
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('discordNotificationApp', () => ({
|
||||
activeTab: '{{ $activeTab }}',
|
||||
tabLoading: null,
|
||||
showCompanyModal: false,
|
||||
showMachineModal: false,
|
||||
companyId: null,
|
||||
companyName: '',
|
||||
companyForm: {
|
||||
discord_webhook_url: '',
|
||||
discord_notify_enabled: false,
|
||||
discord_notify_types: [],
|
||||
discord_notify_locale: 'zh_TW'
|
||||
},
|
||||
currentMachine: {
|
||||
id: null,
|
||||
name: '',
|
||||
serial_no: '',
|
||||
company_id: null,
|
||||
settings: {},
|
||||
machine_model: null
|
||||
},
|
||||
machineForm: {
|
||||
discord_webhook_url: '',
|
||||
temp_alert_enabled: 'inherit',
|
||||
temp_upper_limit: '',
|
||||
temp_lower_limit: ''
|
||||
},
|
||||
copyableMachines: @js($copyableMachines),
|
||||
isTesting: false,
|
||||
testStatus: null,
|
||||
testMessage: '',
|
||||
isLoadingTable: false,
|
||||
showCopyToast: false,
|
||||
copiedMachineName: '',
|
||||
|
||||
openCompanyEdit(company) {
|
||||
const settings = company.settings || {};
|
||||
this.companyId = company.id;
|
||||
this.companyName = company.name;
|
||||
this.companyForm.discord_webhook_url = settings.discord_webhook_url || '';
|
||||
this.companyForm.discord_notify_enabled = !!(settings.discord_notify_enabled);
|
||||
this.companyForm.discord_notify_types = settings.discord_notify_types || ['submachine', 'status'];
|
||||
this.companyForm.discord_notify_locale = settings.locale || 'zh_TW';
|
||||
|
||||
this.testStatus = null;
|
||||
this.testMessage = '';
|
||||
this.showCompanyModal = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = window.HSSelect?.getInstance('#company-locale-select');
|
||||
if (el) el.setValue(this.companyForm.discord_notify_locale);
|
||||
});
|
||||
},
|
||||
|
||||
openMachineEdit(machine) {
|
||||
const settings = machine.settings || {};
|
||||
this.currentMachine = machine;
|
||||
this.machineForm.discord_webhook_url = settings.discord_webhook_url || '';
|
||||
this.machineForm.temp_alert_enabled = settings.temp_alert_enabled || 'inherit';
|
||||
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.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);
|
||||
this.rebuildCopySelect();
|
||||
});
|
||||
},
|
||||
|
||||
rebuildCopySelect() {
|
||||
this.$nextTick(() => {
|
||||
const wrapper = document.getElementById('copy-select-wrapper');
|
||||
if (!wrapper) return;
|
||||
|
||||
// 1. Destroy existing HSSelect instance if present
|
||||
const oldSelect = wrapper.querySelector('select');
|
||||
if (oldSelect) {
|
||||
const instance = window.HSSelect?.getInstance(oldSelect);
|
||||
if (instance) {
|
||||
instance.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clear wrapper DOM
|
||||
wrapper.innerHTML = '';
|
||||
|
||||
// 3. Filter other machines under the same company
|
||||
const list = this.copyableMachines.filter(m =>
|
||||
m.company_id === this.currentMachine.company_id &&
|
||||
m.id !== this.currentMachine.id
|
||||
);
|
||||
|
||||
// 4. Build standard select element with searchable select attributes
|
||||
const selectEl = document.createElement('select');
|
||||
selectEl.id = 'copy-machine-select';
|
||||
selectEl.name = 'copy_machine_id';
|
||||
selectEl.className = 'hidden';
|
||||
|
||||
const config = {
|
||||
"placeholder": "{{ __('Select a machine to copy settings from...') }}",
|
||||
"hasSearch": true,
|
||||
"searchPlaceholder": "{{ __('Search machines...') }}",
|
||||
"isHidePlaceholder": false,
|
||||
"searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
"searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
|
||||
"toggleClasses": "hs-select-toggle luxury-select-toggle w-full",
|
||||
"toggleTemplate": '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></div></button>',
|
||||
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in",
|
||||
"optionClasses": "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300",
|
||||
"optionTemplate": '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||
};
|
||||
|
||||
selectEl.setAttribute('data-hs-select', JSON.stringify(config));
|
||||
|
||||
// Append placeholder
|
||||
const placeholderOpt = document.createElement('option');
|
||||
placeholderOpt.value = ' ';
|
||||
placeholderOpt.textContent = "{{ __('Select a machine to copy settings from...') }}";
|
||||
placeholderOpt.setAttribute('data-title', "{{ __('Select a machine to copy settings from...') }}");
|
||||
selectEl.appendChild(placeholderOpt);
|
||||
|
||||
if (list.length === 0) {
|
||||
const emptyOpt = document.createElement('option');
|
||||
emptyOpt.value = ' ';
|
||||
emptyOpt.textContent = "{{ __('No machines found') }}";
|
||||
emptyOpt.setAttribute('data-title', "{{ __('No machines found') }}");
|
||||
emptyOpt.disabled = true;
|
||||
selectEl.appendChild(emptyOpt);
|
||||
} else {
|
||||
list.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = `${m.name} (${m.serial_no})`;
|
||||
opt.setAttribute('data-title', `${m.name} (${m.serial_no})`);
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.appendChild(selectEl);
|
||||
|
||||
// Bind change event to copy handler
|
||||
selectEl.addEventListener('change', (e) => {
|
||||
this.handleCopyMachine(e.target.value);
|
||||
});
|
||||
|
||||
// 5. Initialize select
|
||||
this.$nextTick(() => {
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit(['select']);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleCopyMachine(value) {
|
||||
if (!value) return;
|
||||
const source = this.copyableMachines.find(m => m.id == value);
|
||||
if (source) {
|
||||
const s = source.settings || {};
|
||||
this.machineForm.discord_webhook_url = s.discord_webhook_url || '';
|
||||
this.machineForm.temp_alert_enabled = s.temp_alert_enabled || 'inherit';
|
||||
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.$nextTick(() => {
|
||||
const el = window.HSSelect?.getInstance('#machine-alert-select');
|
||||
if (el) el.setValue(this.machineForm.temp_alert_enabled);
|
||||
});
|
||||
|
||||
this.copiedMachineName = source.name;
|
||||
this.showCopyToast = true;
|
||||
setTimeout(() => this.showCopyToast = false, 3000);
|
||||
}
|
||||
},
|
||||
|
||||
adjustTemp(field, delta, fallbackVal) {
|
||||
let current = this.machineForm[field];
|
||||
if (current === undefined || current === null || current === '') {
|
||||
current = parseInt(fallbackVal);
|
||||
} else {
|
||||
current = parseInt(current);
|
||||
}
|
||||
if (isNaN(current)) current = 0;
|
||||
let newValue = current + delta;
|
||||
if (newValue < -50) newValue = -50;
|
||||
if (newValue > 100) newValue = 100;
|
||||
this.machineForm[field] = newValue.toString();
|
||||
},
|
||||
|
||||
async testWebhook(webhookUrl) {
|
||||
if (!webhookUrl) {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = '{{ __('Please enter Webhook URL first.') }}';
|
||||
return;
|
||||
}
|
||||
this.isTesting = true;
|
||||
this.testStatus = null;
|
||||
try {
|
||||
const response = await fetch('{{ route('admin.basic-settings.discord-notifications.test') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ discord_webhook_url: webhookUrl })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && data.success) {
|
||||
this.testStatus = 'success';
|
||||
this.testMessage = data.message;
|
||||
} else {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = data.message || '{{ __('Failed to send test notification.') }}';
|
||||
}
|
||||
} catch (error) {
|
||||
this.testStatus = 'error';
|
||||
this.testMessage = '{{ __('An error occurred during testing.') }}';
|
||||
} finally {
|
||||
this.isTesting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTabContent(url, containerId) {
|
||||
if (!url || this.isLoadingTable) return;
|
||||
this.isLoadingTable = true;
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (!res.ok) throw new Error('Network error');
|
||||
const html = await res.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newContent = doc.getElementById(containerId);
|
||||
const container = document.getElementById(containerId);
|
||||
if (newContent && container) {
|
||||
container.innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
if (window.HSStaticMethods?.autoInit) window.HSStaticMethods.autoInit();
|
||||
if (window.Alpine?.initTree) window.Alpine.initTree(container);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
this.isLoadingTable = false;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@ -69,8 +69,22 @@
|
||||
currentMachine: null,
|
||||
showCreateModelModal: false,
|
||||
showEditModelModal: false,
|
||||
newModel: { name: '', temp_upper_limit: '', temp_lower_limit: '' },
|
||||
currentModel: { name: '', temp_upper_limit: '', temp_lower_limit: '' },
|
||||
modelActionUrl: '',
|
||||
adjustModelTemp(modelObj, field, delta, fallbackVal) {
|
||||
let current = modelObj[field];
|
||||
if (current === undefined || current === null || current === '') {
|
||||
current = parseInt(fallbackVal);
|
||||
} else {
|
||||
current = parseInt(current);
|
||||
}
|
||||
if (isNaN(current)) current = 0;
|
||||
let newValue = current + delta;
|
||||
if (newValue < -50) newValue = -50;
|
||||
if (newValue > 100) newValue = 100;
|
||||
modelObj[field] = newValue.toString();
|
||||
},
|
||||
selectedFileCount: 0,
|
||||
selectedFiles: [null, null, null],
|
||||
deletedPhotos: [false, false, false],
|
||||
@ -654,7 +668,7 @@
|
||||
</svg>
|
||||
<span class="font-black text-sm sm:text-base tracking-tight">{{ __('Add Machine') }}</span>
|
||||
</button>
|
||||
<button x-show="tab === 'models'" x-cloak @click="showCreateModelModal = true"
|
||||
<button x-show="tab === 'models'" x-cloak @click="newModel = { name: '', temp_upper_limit: '', temp_lower_limit: '' }; showCreateModelModal = true"
|
||||
class="btn-luxury-primary flex items-center gap-2 px-4 sm:px-6 py-2.5 sm:py-3 h-10 sm:h-auto rounded-xl shadow-lg shadow-cyan-500/20 transition-all duration-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
@ -883,29 +897,52 @@
|
||||
<label
|
||||
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{
|
||||
__('Model Name') }}</label>
|
||||
<input type="text" name="name" required class="luxury-input w-full"
|
||||
<input type="text" name="name" x-model="newModel.name" required class="luxury-input w-full"
|
||||
placeholder="{{ __('Enter model name') }}">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Default Temp Upper Limit (°C)') }}
|
||||
{{ __('Default Temp Alert Upper Limit (°C)') }}
|
||||
</label>
|
||||
<input type="number" step="0.1" name="temp_upper_limit"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g., 40.0') }}">
|
||||
<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">
|
||||
<button type="button" @click="adjustModelTemp(newModel, 'temp_upper_limit', -1, '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" name="temp_upper_limit" x-model="newModel.temp_upper_limit"
|
||||
placeholder="40"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustModelTemp(newModel, 'temp_upper_limit', 1, '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Default Temp Lower Limit (°C)') }}
|
||||
{{ __('Default Temp Alert Lower Limit (°C)') }}
|
||||
</label>
|
||||
<input type="number" step="0.1" name="temp_lower_limit"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g., 0.0') }}">
|
||||
<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">
|
||||
<button type="button" @click="adjustModelTemp(newModel, 'temp_lower_limit', -1, '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" name="temp_lower_limit" x-model="newModel.temp_lower_limit"
|
||||
placeholder="0"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustModelTemp(newModel, 'temp_lower_limit', 1, '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" @click="showCreateModelModal = false" class="btn-luxury-ghost">{{
|
||||
@ -959,19 +996,43 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Default Temp Upper Limit (°C)') }}
|
||||
{{ __('Default Temp Alert Upper Limit (°C)') }}
|
||||
</label>
|
||||
<input type="number" step="0.1" name="temp_upper_limit" x-model="currentModel.temp_upper_limit"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g., 40.0') }}">
|
||||
<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">
|
||||
<button type="button" @click="adjustModelTemp(currentModel, 'temp_upper_limit', -1, '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" name="temp_upper_limit" x-model="currentModel.temp_upper_limit"
|
||||
placeholder="40"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustModelTemp(currentModel, 'temp_upper_limit', 1, '40')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
|
||||
{{ __('Default Temp Lower Limit (°C)') }}
|
||||
{{ __('Default Temp Alert Lower Limit (°C)') }}
|
||||
</label>
|
||||
<input type="number" step="0.1" name="temp_lower_limit" x-model="currentModel.temp_lower_limit"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g., 0.0') }}">
|
||||
<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">
|
||||
<button type="button" @click="adjustModelTemp(currentModel, 'temp_lower_limit', -1, '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" 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 type="text" name="temp_lower_limit" x-model="currentModel.temp_lower_limit"
|
||||
placeholder="0"
|
||||
class="w-full bg-transparent border-none text-center text-sm font-bold text-slate-800 dark:text-white focus:ring-0 p-0">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<button type="button" @click="adjustModelTemp(currentModel, 'temp_lower_limit', 1, '0')"
|
||||
class="shrink-0 w-12 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-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Model Name') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Default Temp Limits') }}</th>
|
||||
{{ __('Default Temp Alert Limits') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Machine Count') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
@ -77,11 +77,11 @@
|
||||
@if(isset($model->settings['temp_upper_limit']) || isset($model->settings['temp_lower_limit']))
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="px-2 py-0.5 rounded bg-sky-50 dark:bg-sky-950/30 text-sky-600 dark:text-sky-400 text-xs font-bold font-mono">
|
||||
{{ $model->settings['temp_lower_limit'] ?? '-' }}°C
|
||||
{{ isset($model->settings['temp_lower_limit']) ? (int)round($model->settings['temp_lower_limit']) : '-' }}°C
|
||||
</span>
|
||||
<span class="text-slate-400 text-xs font-bold">~</span>
|
||||
<span class="px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-950/30 text-rose-600 dark:text-rose-400 text-xs font-bold font-mono">
|
||||
{{ $model->settings['temp_upper_limit'] ?? '-' }}°C
|
||||
{{ isset($model->settings['temp_upper_limit']) ? (int)round($model->settings['temp_upper_limit']) : '-' }}°C
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
@ -100,7 +100,7 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button @click="currentModel = { name: @js($model->name), temp_upper_limit: @js($model->settings['temp_upper_limit'] ?? ''), temp_lower_limit: @js($model->settings['temp_lower_limit'] ?? '') }; modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
<button @click="currentModel = { name: @js($model->name), temp_upper_limit: @js(isset($model->settings['temp_upper_limit']) ? (string)(int)round($model->settings['temp_upper_limit']) : ''), temp_lower_limit: @js(isset($model->settings['temp_lower_limit']) ? (string)(int)round($model->settings['temp_lower_limit']) : '') }; modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 dark:hover:text-cyan-400 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all"
|
||||
title="{{ __('Edit') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -154,7 +154,7 @@
|
||||
@if(isset($model->settings['temp_upper_limit']) || isset($model->settings['temp_lower_limit']))
|
||||
<span class="text-xs text-slate-300 dark:text-slate-700 font-bold">•</span>
|
||||
<span class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 font-mono">
|
||||
{{ $model->settings['temp_lower_limit'] ?? '-' }}°C ~ {{ $model->settings['temp_upper_limit'] ?? '-' }}°C
|
||||
{{ isset($model->settings['temp_lower_limit']) ? (int)round($model->settings['temp_lower_limit']) : '-' }}°C ~ {{ isset($model->settings['temp_upper_limit']) ? (int)round($model->settings['temp_upper_limit']) : '-' }}°C
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@ -166,7 +166,7 @@
|
||||
|
||||
{{-- Action Buttons --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="currentModel = { name: @js($model->name), temp_upper_limit: @js($model->settings['temp_upper_limit'] ?? ''), temp_lower_limit: @js($model->settings['temp_lower_limit'] ?? '') }; modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
<button @click="currentModel = { name: @js($model->name), temp_upper_limit: @js(isset($model->settings['temp_upper_limit']) ? (string)(int)round($model->settings['temp_upper_limit']) : ''), temp_lower_limit: @js(isset($model->settings['temp_lower_limit']) ? (string)(int)round($model->settings['temp_lower_limit']) : '') }; modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
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" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931Z" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user