[STYLE] 優化機台型號與通知設定介面並調整溫度為整數告警

1. 調整 Discord 通知設定介面的間距與對齊,使其與基本設定中的機台設定保持一致。
2. 重構機台型號設定 Modal,將溫度上下限的輸入框改為「極簡奢華風」的加減按鈕元件。
3. 更新機台型號溫度相關的設定與顯示,強制限制與轉換為整數 (移除小數點)。
4. 將機台型號的溫度限制相關介面文字更新為「告警」相關字眼 (預設溫度告警上限、預設溫度告警下限等)。
5. 於中、英、日語系檔案中新增溫度告警相關之翻譯鍵值。
This commit is contained in:
sky121113 2026-05-21 13:25:28 +08:00
parent cde2718d2a
commit 2e3d6e5572
8 changed files with 444 additions and 231 deletions

View File

@ -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')) {

View File

@ -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([

View File

@ -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."
}

View File

@ -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.": "機器モデルが正常に追加されました。"
}

View File

@ -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.": "機台型號已成功新增。"
}

View File

@ -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

View File

@ -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>

View File

@ -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" />