| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- <script>
- import {
- ShieldCheck,
- Clock,
- CheckCircle,
- AlertTriangle,
- XCircle,
- Plus,
- X
- } from 'lucide-svelte';
- import { onMount } from 'svelte';
- let isLoading = $state(true);
- onMount(() => {
- setTimeout(() => {
- isLoading = false;
- }, 600);
- });
- // ── Department configs ────────────────────────────────────────────────────
- let departments = $state([
- {
- id: 'sac',
- name: 'SAC',
- firstResponseH: 1,
- firstResponseM: 30,
- resolutionH: 24,
- alertPct: 80,
- liveStatus: 'breach',
- liveDetail: '14h estourado',
- lastUpdated: 'há 8 minutos'
- },
- {
- id: 'vendas',
- name: 'Vendas',
- firstResponseH: 0,
- firstResponseM: 45,
- resolutionH: 8,
- alertPct: 80,
- liveStatus: 'warning',
- liveDetail: 'próximo de estourar (18% restante)',
- lastUpdated: 'há 3 minutos'
- },
- {
- id: 'suporte',
- name: 'Suporte',
- firstResponseH: 2,
- firstResponseM: 0,
- resolutionH: 48,
- alertPct: 80,
- liveStatus: 'ok',
- liveDetail: 'dentro do SLA',
- lastUpdated: 'há 1 minuto'
- }
- ]);
- let savedDeptIds = $state([]);
- function saveDepartment(deptId, deptName) {
- savedDeptIds = [...savedDeptIds, deptId];
- setTimeout(() => {
- savedDeptIds = savedDeptIds.filter((id) => id !== deptId);
- }, 3000);
- }
- // ── Add department modal ──────────────────────────────────────────────────
- let showAddModal = $state(false);
- let newDept = $state({ name: '', firstResponseH: 1, firstResponseM: 0, resolutionH: 24, alertPct: 80 });
- function addDepartment() {
- if (!newDept.name.trim()) return;
- departments = [
- ...departments,
- {
- id: newDept.name.toLowerCase().replace(/\s+/g, '-'),
- name: newDept.name,
- firstResponseH: newDept.firstResponseH,
- firstResponseM: newDept.firstResponseM,
- resolutionH: newDept.resolutionH,
- alertPct: newDept.alertPct,
- liveStatus: 'ok',
- liveDetail: 'dentro do SLA',
- lastUpdated: 'agora'
- }
- ];
- newDept = { name: '', firstResponseH: 1, firstResponseM: 0, resolutionH: 24, alertPct: 80 };
- showAddModal = false;
- }
- // ── Live status helpers ───────────────────────────────────────────────────
- function statusBadge(status) {
- const map = {
- ok: {
- label: 'OK',
- cls: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-400 dark:border-emerald-400/20'
- },
- warning: {
- label: 'ALERTA',
- cls: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-400/10 dark:text-amber-400 dark:border-amber-400/20'
- },
- breach: {
- label: 'CRÍTICO',
- cls: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-400/10 dark:text-red-400 dark:border-red-400/20'
- }
- };
- return map[status] ?? map['ok'];
- }
- function statusIndicatorCls(status) {
- if (status === 'ok') return 'bg-emerald-500';
- if (status === 'warning') return 'bg-amber-500';
- return 'bg-red-500';
- }
- function statusCardBorder(status) {
- if (status === 'ok') return 'border-emerald-200 dark:border-emerald-400/20';
- if (status === 'warning') return 'border-amber-200 dark:border-amber-400/20';
- return 'border-red-200 dark:border-red-400/20';
- }
- function statusIcon(status) {
- if (status === 'ok') return CheckCircle;
- if (status === 'warning') return AlertTriangle;
- return XCircle;
- }
- function statusIconCls(status) {
- if (status === 'ok') return 'text-emerald-600 dark:text-emerald-400';
- if (status === 'warning') return 'text-amber-600 dark:text-amber-400';
- return 'text-red-600 dark:text-red-400';
- }
- const inputCls =
- 'rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 text-sm text-slate-900 transition-colors focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 w-full';
- </script>
- <svelte:head>
- <title>Configuração de SLA - Nettown Analytics</title>
- </svelte:head>
- <!-- Add Department Modal -->
- {#if showAddModal}
- <div
- class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 p-4"
- onclick={(e) => e.target === e.currentTarget && (showAddModal = false)}
- onkeydown={(e) => e.key === 'Escape' && (showAddModal = false)}
- role="dialog"
- aria-modal="true"
- tabindex="-1"
- >
- <div
- class="w-full max-w-md rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-[#1e293b]"
- >
- <div
- class="flex items-center justify-between border-b border-slate-200 p-5 dark:border-slate-700"
- >
- <h2 class="text-base font-bold text-slate-900 dark:text-white">Adicionar Departamento</h2>
- <button
- onclick={() => (showAddModal = false)}
- class="rounded-lg p-1.5 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
- >
- <X size={18} />
- </button>
- </div>
- <div class="space-y-4 p-5">
- <div>
- <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Nome do departamento</label>
- <input type="text" bind:value={newDept.name} placeholder="Ex: Financeiro" class={inputCls} />
- </div>
- <div class="grid grid-cols-2 gap-4">
- <div>
- <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Primeira resposta (h)</label>
- <input type="number" min="0" max="72" bind:value={newDept.firstResponseH} class={inputCls} />
- </div>
- <div>
- <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Primeira resposta (min)</label>
- <input type="number" min="0" max="59" bind:value={newDept.firstResponseM} class={inputCls} />
- </div>
- </div>
- <div>
- <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Tempo máx. de resolução (h)</label>
- <input type="number" min="1" max="720" bind:value={newDept.resolutionH} class={inputCls} />
- </div>
- <div>
- <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Alerta de estouro em (% do tempo)</label>
- <input type="number" min="1" max="99" bind:value={newDept.alertPct} class={inputCls} />
- </div>
- </div>
- <div class="flex justify-end gap-3 border-t border-slate-200 p-5 dark:border-slate-700">
- <button
- onclick={() => (showAddModal = false)}
- class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700"
- >
- Cancelar
- </button>
- <button
- onclick={addDepartment}
- disabled={!newDept.name.trim()}
- class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-700 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-600"
- >
- Adicionar
- </button>
- </div>
- </div>
- </div>
- {/if}
- <div class="mx-auto max-w-[1600px] space-y-6">
- <!-- Page header -->
- <div
- class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
- >
- <div class="flex items-center gap-3">
- <div
- class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-500/20 text-indigo-500"
- >
- <ShieldCheck size={20} strokeWidth={2.5} />
- </div>
- <h1 class="text-xl font-bold text-slate-900 dark:text-white">Configuração de SLA</h1>
- </div>
- <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
- Defina os tempos de atendimento por departamento. O sistema alertará automaticamente quando os
- limites estiverem próximos de estourar.
- </p>
- </div>
- <!-- Department cards -->
- {#if isLoading}
- <div class="space-y-4">
- {#each [1, 2, 3] as _}
- <div
- class="h-56 animate-pulse rounded-xl border border-slate-200 bg-slate-100 dark:border-slate-800 dark:bg-slate-800"
- ></div>
- {/each}
- </div>
- {:else}
- <div class="space-y-4">
- {#each departments as dept, i}
- {@const badge = statusBadge(dept.liveStatus)}
- {@const StatusIcon = statusIcon(dept.liveStatus)}
- <div
- class="overflow-hidden rounded-xl border bg-white shadow-sm transition-colors duration-200 dark:bg-[#1e293b] {statusCardBorder(dept.liveStatus)}"
- >
- <!-- Card header -->
- <div
- class="flex items-center justify-between border-b border-slate-200 bg-slate-50 px-5 py-4 dark:border-slate-800 dark:bg-slate-900/30"
- >
- <div class="flex items-center gap-3">
- <div class="h-3 w-3 shrink-0 rounded-full {statusIndicatorCls(dept.liveStatus)}"></div>
- <h2 class="text-base font-bold text-slate-900 dark:text-white">{dept.name}</h2>
- <span class="rounded-md border px-2 py-0.5 text-xs font-bold {badge.cls}"
- >{badge.label}</span
- >
- </div>
- <div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
- <StatusIcon size={14} class={statusIconCls(dept.liveStatus)} />
- <span>{dept.liveDetail}</span>
- </div>
- </div>
- <!-- Form fields -->
- <div class="grid grid-cols-1 gap-6 p-5 sm:grid-cols-2 xl:grid-cols-4">
- <!-- Primeira resposta horas -->
- <div>
- <label
- class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >
- Primeira resposta — horas
- </label>
- <div class="flex items-center gap-2">
- <input
- type="number"
- min="0"
- max="72"
- bind:value={departments[i].firstResponseH}
- class={inputCls}
- />
- <span class="shrink-0 text-xs text-slate-500 dark:text-slate-400">h</span>
- </div>
- </div>
- <!-- Primeira resposta minutos -->
- <div>
- <label
- class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >
- Primeira resposta — minutos
- </label>
- <div class="flex items-center gap-2">
- <input
- type="number"
- min="0"
- max="59"
- bind:value={departments[i].firstResponseM}
- class={inputCls}
- />
- <span class="shrink-0 text-xs text-slate-500 dark:text-slate-400">min</span>
- </div>
- </div>
- <!-- Tempo máx. de resolução -->
- <div>
- <label
- class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >
- Tempo máx. de resolução
- </label>
- <div class="flex items-center gap-2">
- <input
- type="number"
- min="1"
- max="720"
- bind:value={departments[i].resolutionH}
- class={inputCls}
- />
- <span class="shrink-0 text-xs text-slate-500 dark:text-slate-400">horas</span>
- </div>
- </div>
- <!-- Alerta de estouro -->
- <div>
- <label
- class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >
- Alertar quando atingir
- </label>
- <div class="flex items-center gap-2">
- <input
- type="number"
- min="1"
- max="99"
- bind:value={departments[i].alertPct}
- class={inputCls}
- />
- <span class="shrink-0 text-xs text-slate-500 dark:text-slate-400">% do tempo</span>
- </div>
- </div>
- </div>
- <!-- Summary + save -->
- <div
- class="flex flex-wrap items-center justify-between gap-4 border-t border-slate-100 px-5 py-4 dark:border-slate-800"
- >
- <div class="flex flex-wrap gap-4 text-xs text-slate-500 dark:text-slate-400">
- <span>
- Primeira resposta:
- <strong class="text-slate-900 dark:text-slate-200">
- {dept.firstResponseH > 0 ? `${dept.firstResponseH}h ` : ''}{dept.firstResponseM > 0
- ? `${dept.firstResponseM}min`
- : dept.firstResponseH === 0
- ? '0min'
- : ''}
- </strong>
- </span>
- <span>
- Resolução: <strong class="text-slate-900 dark:text-slate-200">{dept.resolutionH}h</strong>
- </span>
- <span>
- Alerta em: <strong class="text-slate-900 dark:text-slate-200">{dept.alertPct}%</strong>
- </span>
- </div>
- <div class="flex items-center gap-3">
- {#if savedDeptIds.includes(dept.id)}
- <span class="flex items-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400">
- <CheckCircle size={16} />
- SLA do {dept.name} atualizado
- </span>
- {/if}
- <button
- onclick={() => saveDepartment(dept.id, dept.name)}
- class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-all hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600"
- >
- Salvar
- </button>
- </div>
- </div>
- </div>
- {/each}
- </div>
- {/if}
- <!-- Add Department button -->
- {#if !isLoading}
- <div class="flex justify-center">
- <button
- onclick={() => (showAddModal = true)}
- class="flex items-center gap-2 rounded-lg border border-dashed border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-600 shadow-sm transition-all hover:border-indigo-400 hover:text-indigo-600 dark:border-slate-700 dark:bg-[#1e293b] dark:text-slate-400 dark:hover:border-indigo-500 dark:hover:text-indigo-400"
- >
- <Plus size={16} strokeWidth={2.5} />
- + Adicionar Departamento
- </button>
- </div>
- {/if}
- <!-- Live SLA Status panel -->
- {#if !isLoading}
- <div
- class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
- >
- <div
- class="flex items-center gap-3 border-b border-slate-200 bg-slate-50 px-5 py-4 dark:border-slate-800 dark:bg-slate-900/30"
- >
- <div
- class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400"
- >
- <Clock size={18} strokeWidth={2} />
- </div>
- <h2 class="text-base font-bold text-slate-900 dark:text-white">
- Status de SLA em Tempo Real
- </h2>
- </div>
- <div class="overflow-x-auto">
- <table class="w-full text-sm">
- <thead>
- <tr
- class="border-b border-slate-100 bg-slate-50/50 dark:border-slate-800 dark:bg-slate-900/20"
- >
- <th
- class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Departamento</th
- >
- <th
- class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Status atual</th
- >
- <th
- class="px-5 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Situação</th
- >
- <th
- class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Primeira resposta máx.</th
- >
- <th
- class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Resolução máx.</th
- >
- <th
- class="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
- >Atualizado</th
- >
- </tr>
- </thead>
- <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
- {#each departments as dept}
- {@const badge = statusBadge(dept.liveStatus)}
- {@const StatusIcon = statusIcon(dept.liveStatus)}
- <tr class="transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/40">
- <td class="px-5 py-4">
- <div class="flex items-center gap-2">
- <div
- class="h-2.5 w-2.5 shrink-0 rounded-full {statusIndicatorCls(dept.liveStatus)}"
- ></div>
- <span class="font-semibold text-slate-900 dark:text-white">{dept.name}</span>
- </div>
- </td>
- <td class="px-5 py-4">
- <div class="flex items-center gap-2 text-sm">
- <StatusIcon size={15} class={statusIconCls(dept.liveStatus)} />
- <span class="text-slate-700 dark:text-slate-300">{dept.liveDetail}</span>
- </div>
- </td>
- <td class="px-5 py-4 text-center">
- <span class="rounded-md border px-2.5 py-1 text-xs font-bold {badge.cls}"
- >{badge.label}</span
- >
- </td>
- <td class="px-5 py-4 text-slate-700 dark:text-slate-300">
- {dept.firstResponseH > 0 ? `${dept.firstResponseH}h` : ''}{dept.firstResponseM > 0
- ? ` ${dept.firstResponseM}min`
- : ''}
- {dept.firstResponseH === 0 && dept.firstResponseM === 0 ? '—' : ''}
- </td>
- <td class="px-5 py-4 text-slate-700 dark:text-slate-300">
- {dept.resolutionH}h
- </td>
- <td class="px-5 py-4 text-right text-xs text-slate-400 dark:text-slate-500">
- {dept.lastUpdated}
- </td>
- </tr>
- {/each}
- </tbody>
- </table>
- </div>
- </div>
- {/if}
- </div>
|