766 lines
27 KiB
Vue
766 lines
27 KiB
Vue
<script setup lang="ts">
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Alerts & Notifications · Settings')
|
|
|
|
const toast = useToast()
|
|
|
|
type AlertRecipient = 'handler' | 'manager' | 'customer' | 'custom'
|
|
|
|
interface Threshold {
|
|
id: string
|
|
daysBefore: number
|
|
enabled: boolean
|
|
}
|
|
|
|
interface PaymentTier {
|
|
id: string
|
|
daysOverdue: number
|
|
action: string
|
|
recipients: AlertRecipient[]
|
|
}
|
|
|
|
interface CustomAlertRule {
|
|
id: string
|
|
alertName: string
|
|
field: string
|
|
operator: string
|
|
value: number
|
|
recipients: AlertRecipient[]
|
|
enabled: boolean
|
|
}
|
|
|
|
interface AlertConfig {
|
|
emailSender: {
|
|
senderEmail: string
|
|
senderDisplayName: string
|
|
replyToEmail: string
|
|
}
|
|
renewals: {
|
|
enabled: boolean
|
|
thresholds: Threshold[]
|
|
}
|
|
cancellations: {
|
|
enabled: boolean
|
|
recipients: AlertRecipient[]
|
|
}
|
|
latePayments: {
|
|
enabled: boolean
|
|
tiers: PaymentTier[]
|
|
}
|
|
creditCardExpiry: {
|
|
enabled: boolean
|
|
autoDebitOnly: boolean
|
|
thresholds: Threshold[]
|
|
}
|
|
customRules: CustomAlertRule[]
|
|
}
|
|
|
|
const STORAGE_KEY = 'policy-ui.alerts'
|
|
|
|
function loadConfig(): AlertConfig {
|
|
if (import.meta.client) {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) {
|
|
try {
|
|
return JSON.parse(stored)
|
|
} catch {
|
|
return defaultConfig()
|
|
}
|
|
}
|
|
}
|
|
return defaultConfig()
|
|
}
|
|
|
|
function defaultConfig(): AlertConfig {
|
|
return {
|
|
emailSender: {
|
|
senderEmail: '',
|
|
senderDisplayName: '',
|
|
replyToEmail: ''
|
|
},
|
|
renewals: {
|
|
enabled: true,
|
|
thresholds: [
|
|
{ id: '1', daysBefore: 30, enabled: true },
|
|
{ id: '2', daysBefore: 7, enabled: true }
|
|
]
|
|
},
|
|
cancellations: {
|
|
enabled: true,
|
|
recipients: ['handler', 'manager']
|
|
},
|
|
latePayments: {
|
|
enabled: true,
|
|
tiers: [
|
|
{ id: '1', daysOverdue: 15, action: 'First reminder', recipients: ['handler'] },
|
|
{ id: '2', daysOverdue: 30, action: 'Second reminder', recipients: ['handler', 'manager'] },
|
|
{ id: '3', daysOverdue: 45, action: 'Escalate to collections', recipients: ['handler', 'manager', 'customer'] }
|
|
]
|
|
},
|
|
creditCardExpiry: {
|
|
enabled: true,
|
|
autoDebitOnly: true,
|
|
thresholds: [
|
|
{ id: '1', daysBefore: 30, enabled: true },
|
|
{ id: '2', daysBefore: 7, enabled: true }
|
|
]
|
|
},
|
|
customRules: []
|
|
}
|
|
}
|
|
|
|
const config = ref<AlertConfig>(loadConfig())
|
|
|
|
function saveConfig() {
|
|
if (import.meta.client) {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(config.value))
|
|
}
|
|
}
|
|
|
|
function addThreshold(type: 'renewals' | 'creditCardExpiry', days: number) {
|
|
const id = String(Date.now())
|
|
config.value[type].thresholds.push({ id, daysBefore: days, enabled: true })
|
|
saveConfig()
|
|
}
|
|
|
|
function removeThreshold(type: 'renewals' | 'creditCardExpiry', id: string) {
|
|
config.value[type].thresholds = config.value[type].thresholds.filter(t => t.id !== id)
|
|
saveConfig()
|
|
}
|
|
|
|
function addPaymentTier(days: number, action: string, recipients: AlertRecipient[]) {
|
|
const id = String(Date.now())
|
|
config.value.latePayments.tiers.push({ id, daysOverdue: days, action, recipients: [...recipients] })
|
|
saveConfig()
|
|
}
|
|
|
|
function removePaymentTier(id: string) {
|
|
config.value.latePayments.tiers = config.value.latePayments.tiers.filter(t => t.id !== id)
|
|
saveConfig()
|
|
}
|
|
|
|
function addCustomRule(rule: Omit<CustomAlertRule, 'id'>) {
|
|
const id = String(Date.now())
|
|
config.value.customRules.push({ ...rule, id })
|
|
saveConfig()
|
|
}
|
|
|
|
function updateCustomRule(id: string, updates: Partial<CustomAlertRule>) {
|
|
const idx = config.value.customRules.findIndex(r => r.id === id)
|
|
if (idx !== -1) {
|
|
config.value.customRules[idx] = { ...config.value.customRules[idx], ...updates }
|
|
saveConfig()
|
|
}
|
|
}
|
|
|
|
function removeCustomRule(id: string) {
|
|
config.value.customRules = config.value.customRules.filter(r => r.id !== id)
|
|
saveConfig()
|
|
}
|
|
|
|
const RECIPIENT_OPTIONS: { id: AlertRecipient; label: string }[] = [
|
|
{ id: 'handler', label: 'Handler' },
|
|
{ id: 'manager', label: 'Manager' },
|
|
{ id: 'customer', label: 'Customer' },
|
|
{ id: 'custom', label: 'Custom email' },
|
|
]
|
|
|
|
function toggleRecipient(list: AlertRecipient[], r: AlertRecipient) {
|
|
const idx = list.indexOf(r)
|
|
if (idx === -1) list.push(r)
|
|
else list.splice(idx, 1)
|
|
saveConfig()
|
|
}
|
|
|
|
const newRenewalDays = ref(7)
|
|
const newCcDays = ref(7)
|
|
|
|
function addRenewalThreshold() {
|
|
addThreshold('renewals', newRenewalDays.value)
|
|
newRenewalDays.value = 7
|
|
}
|
|
|
|
function addCcThreshold() {
|
|
addThreshold('creditCardExpiry', newCcDays.value)
|
|
newCcDays.value = 7
|
|
}
|
|
|
|
const newLpDays = ref(45)
|
|
function addNewPaymentTier() {
|
|
addPaymentTier(newLpDays.value, 'Notify assigned handler', ['handler'])
|
|
newLpDays.value = 45
|
|
}
|
|
|
|
const FIELD_OPTIONS = [
|
|
{ value: 'premium', label: 'Annual premium' },
|
|
{ value: 'policy_count', label: 'Policy count' },
|
|
{ value: 'days_to_renewal', label: 'Days to renewal' },
|
|
{ value: 'days_overdue', label: 'Days overdue' },
|
|
{ value: 'claim_count', label: 'Open claims' },
|
|
{ value: 'reserve_amount', label: 'Reserve amount' },
|
|
]
|
|
const OPERATOR_OPTIONS = [
|
|
{ value: 'gte', label: '≥' },
|
|
{ value: 'lte', label: '≤' },
|
|
{ value: 'eq', label: '=' },
|
|
{ value: 'gt', label: '>' },
|
|
{ value: 'lt', label: '<' },
|
|
]
|
|
|
|
const showNewRule = ref(false)
|
|
const newRule = ref({ alertName: '', field: 'premium', operator: 'gte' as const, value: 0 })
|
|
|
|
function submitNewRule() {
|
|
if (!newRule.value.alertName.trim()) return
|
|
addCustomRule({
|
|
alertName: newRule.value.alertName,
|
|
field: newRule.value.field,
|
|
operator: newRule.value.operator,
|
|
value: newRule.value.value,
|
|
recipients: ['handler'],
|
|
enabled: true,
|
|
})
|
|
newRule.value = { alertName: '', field: 'premium', operator: 'gte', value: 0 }
|
|
showNewRule.value = false
|
|
}
|
|
|
|
function tierDotClass(idx: number, total: number) {
|
|
if (total <= 1) return 'al-dot-green'
|
|
if (idx === total - 1) return 'al-dot-red'
|
|
if (idx >= total / 2) return 'al-dot-amber'
|
|
return 'al-dot-green'
|
|
}
|
|
|
|
function handleSave() {
|
|
saveConfig()
|
|
toast.add({ title: 'Alert configuration saved', color: 'green' })
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="al-page">
|
|
<!-- Header -->
|
|
<div class="al-header">
|
|
<div>
|
|
<NuxtLink to="/settings" class="al-back-link">
|
|
<UIcon name="i-heroicons-arrow-left" class="w-3.5 h-3.5" />
|
|
Settings
|
|
</NuxtLink>
|
|
<h1 class="al-title">Alerts & Notifications</h1>
|
|
<p class="al-subtitle">Configure automated email alerts for renewals, cancellations, late payments, and custom triggers.</p>
|
|
</div>
|
|
<button class="al-save-btn" @click="handleSave">
|
|
<UIcon name="i-heroicons-check" class="w-4 h-4" />
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ═══════════ 1. EMAIL SENDER ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-envelope" class="w-5 h-5" />
|
|
<div>
|
|
<p class="al-card-title">Email Sender Configuration</p>
|
|
<p class="al-card-desc">All automated alerts are sent from this email address.</p>
|
|
</div>
|
|
</div>
|
|
<div class="al-field-grid">
|
|
<div>
|
|
<label class="al-label">Sender email</label>
|
|
<input v-model="config.emailSender.senderEmail" class="al-input-med" type="email" placeholder="alertas@miagencia.com" />
|
|
</div>
|
|
<div>
|
|
<label class="al-label">Display name</label>
|
|
<input v-model="config.emailSender.senderDisplayName" class="al-input-med" type="text" placeholder="Segur-OS Alertas" />
|
|
</div>
|
|
<div>
|
|
<label class="al-label">Reply-to email</label>
|
|
<input v-model="config.emailSender.replyToEmail" class="al-input-med" type="email" placeholder="soporte@miagencia.com" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ 2. RENEWAL ALERTS ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-arrow-path" class="w-5 h-5" />
|
|
<div style="flex: 1;">
|
|
<p class="al-card-title">Renewal Alerts</p>
|
|
<p class="al-card-desc">Notify when policies approach their renewal date.</p>
|
|
</div>
|
|
<button
|
|
class="al-toggle"
|
|
:class="config.renewals.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="config.renewals.enabled = !config.renewals.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</div>
|
|
<template v-if="config.renewals.enabled">
|
|
<div class="al-table-wrap">
|
|
<table class="al-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Days before expiry</th>
|
|
<th style="width: 80px;">Active</th>
|
|
<th style="width: 50px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="t in config.renewals.thresholds" :key="t.id">
|
|
<td>
|
|
<div class="al-input-group">
|
|
<input v-model.number="t.daysBefore" class="al-input-sm" type="number" min="1" />
|
|
<span class="al-input-suffix">days</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<button
|
|
class="al-toggle"
|
|
:class="t.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="t.enabled = !t.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</td>
|
|
<td class="text-center">
|
|
<button class="al-remove-btn" @click="removeThreshold('renewals', t.id)">
|
|
<UIcon name="i-heroicons-x-mark" class="w-3.5 h-3.5" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="al-add-row">
|
|
<input v-model.number="newRenewalDays" class="al-input-sm" type="number" min="1" />
|
|
<span class="al-input-suffix">days</span>
|
|
<button class="al-add-btn" @click="addRenewalThreshold">
|
|
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
|
|
Add threshold
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══════════ 3. CANCELLATION ALERTS ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-x-circle" class="w-5 h-5" />
|
|
<div style="flex: 1;">
|
|
<p class="al-card-title">Cancellation Alerts</p>
|
|
<p class="al-card-desc">Alert when a policy enters the cancellation process.</p>
|
|
</div>
|
|
<button
|
|
class="al-toggle"
|
|
:class="config.cancellations.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="config.cancellations.enabled = !config.cancellations.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</div>
|
|
<template v-if="config.cancellations.enabled">
|
|
<p class="al-label" style="margin-bottom: 10px;">Notify these recipients:</p>
|
|
<div class="al-checkbox-list">
|
|
<label v-for="r in RECIPIENT_OPTIONS" :key="r.id" class="al-checkbox-item">
|
|
<input
|
|
type="checkbox"
|
|
:checked="config.cancellations.recipients.includes(r.id)"
|
|
@change="toggleRecipient(config.cancellations.recipients, r.id)"
|
|
/>
|
|
<span>{{ r.label }}</span>
|
|
</label>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══════════ 4. LATE PAYMENT ALERTS ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
|
|
<div style="flex: 1;">
|
|
<p class="al-card-title">Late Payment Alerts</p>
|
|
<p class="al-card-desc">Escalating notifications based on days overdue.</p>
|
|
</div>
|
|
<button
|
|
class="al-toggle"
|
|
:class="config.latePayments.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="config.latePayments.enabled = !config.latePayments.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</div>
|
|
<template v-if="config.latePayments.enabled">
|
|
<div class="al-escalation-list">
|
|
<div v-for="(tier, idx) in config.latePayments.tiers" :key="tier.id" class="al-escalation-row">
|
|
<span class="al-escalation-dot" :class="tierDotClass(idx, config.latePayments.tiers.length)" />
|
|
<div class="al-escalation-content">
|
|
<div class="al-escalation-fields">
|
|
<div class="al-field-inline">
|
|
<span class="al-label-sm">Days overdue</span>
|
|
<input v-model.number="tier.daysOverdue" class="al-input-sm" type="number" min="1" />
|
|
</div>
|
|
<div class="al-field-inline" style="min-width: 220px;">
|
|
<span class="al-label-sm">Action</span>
|
|
<input v-model="tier.action" class="al-input-med" type="text" />
|
|
</div>
|
|
<div class="al-field-inline">
|
|
<span class="al-label-sm">Notify</span>
|
|
<div class="al-chip-row">
|
|
<button
|
|
v-for="r in RECIPIENT_OPTIONS"
|
|
:key="r.id"
|
|
class="al-chip"
|
|
:class="tier.recipients.includes(r.id) ? 'al-chip-on' : 'al-chip-off'"
|
|
@click="toggleRecipient(tier.recipients, r.id)"
|
|
>{{ r.label }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="al-remove-btn" @click="removePaymentTier(tier.id)">
|
|
<UIcon name="i-heroicons-x-mark" class="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="al-add-row" style="margin-top: 12px;">
|
|
<input v-model.number="newLpDays" class="al-input-sm" type="number" min="1" />
|
|
<span class="al-input-suffix">days</span>
|
|
<button class="al-add-btn" @click="addNewPaymentTier">
|
|
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
|
|
Add tier
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══════════ 5. CREDIT CARD EXPIRY ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-credit-card" class="w-5 h-5" />
|
|
<div style="flex: 1;">
|
|
<p class="al-card-title">Credit Card Expiry Alerts</p>
|
|
<p class="al-card-desc">Notify customers before their payment card expires.</p>
|
|
</div>
|
|
<button
|
|
class="al-toggle"
|
|
:class="config.creditCardExpiry.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="config.creditCardExpiry.enabled = !config.creditCardExpiry.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</div>
|
|
<template v-if="config.creditCardExpiry.enabled">
|
|
<label class="al-checkbox-item" style="margin-bottom: 12px;">
|
|
<input
|
|
type="checkbox"
|
|
:checked="config.creditCardExpiry.autoDebitOnly"
|
|
@change="config.creditCardExpiry.autoDebitOnly = !config.creditCardExpiry.autoDebitOnly"
|
|
/>
|
|
<span>Only for customers with auto-debit enabled</span>
|
|
</label>
|
|
<div class="al-table-wrap">
|
|
<table class="al-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Days before expiry</th>
|
|
<th style="width: 80px;">Active</th>
|
|
<th style="width: 50px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="t in config.creditCardExpiry.thresholds" :key="t.id">
|
|
<td>
|
|
<div class="al-input-group">
|
|
<input v-model.number="t.daysBefore" class="al-input-sm" type="number" min="1" />
|
|
<span class="al-input-suffix">days</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<button
|
|
class="al-toggle"
|
|
:class="t.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="t.enabled = !t.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</td>
|
|
<td class="text-center">
|
|
<button class="al-remove-btn" @click="removeThreshold('creditCardExpiry', t.id)">
|
|
<UIcon name="i-heroicons-x-mark" class="w-3.5 h-3.5" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="al-add-row">
|
|
<input v-model.number="newCcDays" class="al-input-sm" type="number" min="1" />
|
|
<span class="al-input-suffix">days</span>
|
|
<button class="al-add-btn" @click="addCcThreshold">
|
|
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
|
|
Add threshold
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══════════ 6. CUSTOM ALERTS ═══════════ -->
|
|
<div class="al-card">
|
|
<div class="al-card-header">
|
|
<UIcon name="i-heroicons-adjustments-horizontal" class="w-5 h-5" />
|
|
<div>
|
|
<p class="al-card-title">Custom Parameter Alerts</p>
|
|
<p class="al-card-desc">Define custom rules that trigger alerts based on any field condition.</p>
|
|
</div>
|
|
</div>
|
|
<div class="al-table-wrap">
|
|
<table class="al-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Alert name</th>
|
|
<th>Field</th>
|
|
<th>Operator</th>
|
|
<th>Value</th>
|
|
<th>Recipients</th>
|
|
<th style="width: 70px;">Active</th>
|
|
<th style="width: 50px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="rule in config.customRules" :key="rule.id">
|
|
<td>
|
|
<input v-model="rule.alertName" class="al-input-med" type="text" />
|
|
</td>
|
|
<td>
|
|
<select v-model="rule.field" class="al-select">
|
|
<option v-for="f in FIELD_OPTIONS" :key="f.value" :value="f.value">{{ f.label }}</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select v-model="rule.operator" class="al-select" style="width: 56px;">
|
|
<option v-for="o in OPERATOR_OPTIONS" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<input v-model.number="rule.value" class="al-input-sm" type="number" />
|
|
</td>
|
|
<td>
|
|
<div class="al-chip-row">
|
|
<button
|
|
v-for="r in RECIPIENT_OPTIONS"
|
|
:key="r.id"
|
|
class="al-chip al-chip-sm"
|
|
:class="rule.recipients.includes(r.id) ? 'al-chip-on' : 'al-chip-off'"
|
|
@click="toggleRecipient(rule.recipients, r.id)"
|
|
>{{ r.label }}</button>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<button
|
|
class="al-toggle"
|
|
:class="rule.enabled ? 'al-toggle-on' : 'al-toggle-off'"
|
|
@click="rule.enabled = !rule.enabled"
|
|
>
|
|
<span class="al-toggle-dot" />
|
|
</button>
|
|
</td>
|
|
<td class="text-center">
|
|
<button class="al-remove-btn" @click="removeCustomRule(rule.id)">
|
|
<UIcon name="i-heroicons-trash" class="w-3.5 h-3.5" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<!-- Inline add row -->
|
|
<tr v-if="showNewRule" class="al-new-rule-row">
|
|
<td>
|
|
<input v-model="newRule.alertName" class="al-input-med" type="text" placeholder="Alert name…" />
|
|
</td>
|
|
<td>
|
|
<select v-model="newRule.field" class="al-select">
|
|
<option v-for="f in FIELD_OPTIONS" :key="f.value" :value="f.value">{{ f.label }}</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<select v-model="newRule.operator" class="al-select" style="width: 56px;">
|
|
<option v-for="o in OPERATOR_OPTIONS" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<input v-model.number="newRule.value" class="al-input-med" type="number" placeholder="0" />
|
|
</td>
|
|
<td colspan="2">
|
|
<div style="display: flex; gap: 6px;">
|
|
<button class="al-add-btn" @click="submitNewRule">Save</button>
|
|
<button class="al-remove-btn" @click="showNewRule = false">Cancel</button>
|
|
</div>
|
|
</td>
|
|
<td></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="!showNewRule" class="al-add-row">
|
|
<button class="al-add-btn" @click="showNewRule = true">
|
|
<UIcon name="i-heroicons-plus" class="w-3.5 h-3.5" />
|
|
Add custom rule
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* =====================================================================
|
|
ALERTS & NOTIFICATIONS SETTINGS — scoped, al- prefix
|
|
===================================================================== */
|
|
|
|
.al-page {
|
|
max-width: 64rem; margin: 0 auto;
|
|
display: flex; flex-direction: column; gap: 24px; padding-bottom: 48px;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.al-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
|
.al-back-link {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
font-size: 12px; font-weight: 500; color: #8a8a86;
|
|
text-decoration: none; margin-bottom: 8px; transition: color 150ms ease;
|
|
}
|
|
.al-back-link:hover { color: #01696f; }
|
|
.al-title { font-size: 22px; font-weight: 700; color: #1a1a1a; }
|
|
.al-subtitle { font-size: 13px; color: #8a8a86; margin-top: 4px; }
|
|
.al-save-btn {
|
|
display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px;
|
|
border-radius: 10px; font-size: 13px; font-weight: 600;
|
|
background: #01696f; color: white; border: none; cursor: pointer;
|
|
}
|
|
.al-save-btn:hover { opacity: 0.9; }
|
|
|
|
/* ── Card ── */
|
|
.al-card {
|
|
background: #fff; border: 1px solid rgba(0,0,0,0.06);
|
|
border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
|
}
|
|
.al-card-header { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; color: #01696f; }
|
|
.al-card-title { font-size: 16px; font-weight: 700; color: #1a1a1a; }
|
|
.al-card-desc { font-size: 12px; color: #8a8a86; margin-top: 2px; }
|
|
|
|
/* ── Field grid ── */
|
|
.al-field-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
|
|
@media (max-width: 640px) { .al-field-grid { grid-template-columns: 1fr; } }
|
|
|
|
/* ── Table ── */
|
|
.al-table-wrap { overflow-x: auto; }
|
|
.al-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.al-table thead th {
|
|
padding: 8px 12px; font-size: 10px; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
color: #8a8a86; border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
text-align: left; white-space: nowrap;
|
|
}
|
|
.al-table tbody td {
|
|
padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
vertical-align: middle;
|
|
}
|
|
.al-table tbody tr:last-child td { border-bottom: none; }
|
|
|
|
/* ── Inputs ── */
|
|
.al-input-sm {
|
|
width: 72px; padding: 5px 8px; border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 6px; font-size: 13px; text-align: center;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.al-input-sm:focus { outline: none; border-color: #01696f; }
|
|
.al-input-med {
|
|
width: 100%; padding: 5px 10px; border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 6px; font-size: 13px;
|
|
}
|
|
.al-input-med:focus { outline: none; border-color: #01696f; }
|
|
.al-input-group { display: flex; align-items: center; gap: 6px; }
|
|
.al-input-suffix { font-size: 12px; color: #8a8a86; }
|
|
.al-label { display: block; font-size: 13px; font-weight: 600; color: #1a1a1a; margin-bottom: 4px; }
|
|
.al-label-sm { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: #8a8a86; margin-bottom: 2px; }
|
|
|
|
/* ── Select ── */
|
|
.al-select {
|
|
padding: 5px 8px; border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 6px; font-size: 13px; background: #fff; cursor: pointer;
|
|
}
|
|
.al-select:focus { outline: none; border-color: #01696f; }
|
|
|
|
/* ── Toggle ── */
|
|
.al-toggle {
|
|
width: 36px; height: 20px; border-radius: 10px; border: none;
|
|
cursor: pointer; position: relative; transition: background 200ms ease;
|
|
padding: 0; flex-shrink: 0;
|
|
}
|
|
.al-toggle-on { background: #01696f; }
|
|
.al-toggle-off { background: rgba(0,0,0,0.15); }
|
|
.al-toggle-dot {
|
|
display: block; width: 16px; height: 16px; border-radius: 50%;
|
|
background: white; position: absolute; top: 2px;
|
|
transition: left 200ms ease;
|
|
}
|
|
.al-toggle-on .al-toggle-dot { left: 18px; }
|
|
.al-toggle-off .al-toggle-dot { left: 2px; }
|
|
|
|
/* ── Checkbox ── */
|
|
.al-checkbox-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.al-checkbox-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
font-size: 13px; color: #1a1a1a; cursor: pointer;
|
|
}
|
|
.al-checkbox-item input[type="checkbox"] { accent-color: #01696f; width: 15px; height: 15px; }
|
|
|
|
/* ── Escalation ── */
|
|
.al-escalation-list { display: flex; flex-direction: column; gap: 16px; }
|
|
.al-escalation-row { display: flex; align-items: flex-start; gap: 12px; }
|
|
.al-escalation-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
|
|
.al-dot-green { background: #059669; }
|
|
.al-dot-amber { background: #c27b1a; }
|
|
.al-dot-red { background: #c13838; }
|
|
.al-escalation-content { flex: 1; }
|
|
.al-escalation-fields { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
.al-field-inline { display: flex; flex-direction: column; flex: 1; min-width: 100px; }
|
|
|
|
/* ── Chips ── */
|
|
.al-chip-row { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
.al-chip {
|
|
padding: 3px 8px; border-radius: 6px; font-size: 11px; font-weight: 600;
|
|
border: 1px solid transparent; cursor: pointer; transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.al-chip-sm { padding: 2px 6px; font-size: 10px; }
|
|
.al-chip-on { background: rgba(1,105,111,0.08); color: #01696f; border-color: rgba(1,105,111,0.2); }
|
|
.al-chip-off { background: rgba(0,0,0,0.03); color: #8a8a86; border-color: rgba(0,0,0,0.06); }
|
|
.al-chip-off:hover { color: #1a1a1a; background: rgba(0,0,0,0.06); }
|
|
|
|
/* ── Add row ── */
|
|
.al-add-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
|
|
.al-add-btn {
|
|
display: inline-flex; align-items: center; gap: 4px; padding: 5px 12px;
|
|
border-radius: 8px; font-size: 12px; font-weight: 600;
|
|
background: rgba(1,105,111,0.06); color: #01696f;
|
|
border: 1px solid rgba(1,105,111,0.15); cursor: pointer;
|
|
}
|
|
.al-add-btn:hover { background: rgba(1,105,111,0.12); }
|
|
|
|
/* ── Remove button ── */
|
|
.al-remove-btn {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 28px; height: 28px; border-radius: 6px;
|
|
background: transparent; border: none; color: #8a8a86;
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.al-remove-btn:hover { background: rgba(193,56,56,0.06); color: #c13838; }
|
|
|
|
/* ── New rule row ── */
|
|
.al-new-rule-row td { background: rgba(1,105,111,0.02); }
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 640px) {
|
|
.al-escalation-fields { flex-direction: column; }
|
|
}
|
|
</style>
|