|
|
@@ -0,0 +1,750 @@
|
|
|
+<script>
|
|
|
+ import {
|
|
|
+ Users,
|
|
|
+ UserPlus,
|
|
|
+ Search,
|
|
|
+ CheckCircle,
|
|
|
+ XCircle,
|
|
|
+ Clock,
|
|
|
+ Edit,
|
|
|
+ Power,
|
|
|
+ X,
|
|
|
+ ShieldCheck,
|
|
|
+ MessageSquare
|
|
|
+ } from 'lucide-svelte';
|
|
|
+ import { onMount } from 'svelte';
|
|
|
+
|
|
|
+ let isLoading = $state(true);
|
|
|
+ onMount(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ isLoading = false;
|
|
|
+ }, 700);
|
|
|
+ });
|
|
|
+
|
|
|
+ // ── Mock agents ───────────────────────────────────────────────────────────
|
|
|
+ let agents = $state([
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ name: 'Maria Santos',
|
|
|
+ initials: 'MS',
|
|
|
+ department: 'SAC',
|
|
|
+ channels: ['whatsapp'],
|
|
|
+ status: 'Ativo',
|
|
|
+ availableForEscalation: true,
|
|
|
+ todayAttendances: 24,
|
|
|
+ avgResponseTime: '3m 12s',
|
|
|
+ responseTimeTrend: 'down',
|
|
|
+ slaPct: 94
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 2,
|
|
|
+ name: 'Lívia Ferreira',
|
|
|
+ initials: 'LF',
|
|
|
+ department: 'Vendas',
|
|
|
+ channels: ['whatsapp', 'instagram'],
|
|
|
+ status: 'Em Atendimento',
|
|
|
+ availableForEscalation: false,
|
|
|
+ todayAttendances: 18,
|
|
|
+ avgResponseTime: '5m 44s',
|
|
|
+ responseTimeTrend: 'up',
|
|
|
+ slaPct: 88
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 3,
|
|
|
+ name: 'Julia Costa',
|
|
|
+ initials: 'JC',
|
|
|
+ department: 'Suporte',
|
|
|
+ channels: ['whatsapp'],
|
|
|
+ status: 'Ativo',
|
|
|
+ availableForEscalation: true,
|
|
|
+ todayAttendances: 31,
|
|
|
+ avgResponseTime: '2m 08s',
|
|
|
+ responseTimeTrend: 'down',
|
|
|
+ slaPct: 97
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 4,
|
|
|
+ name: 'Fernanda Lima',
|
|
|
+ initials: 'FL',
|
|
|
+ department: 'SAC',
|
|
|
+ channels: ['instagram'],
|
|
|
+ status: 'Inativo',
|
|
|
+ availableForEscalation: false,
|
|
|
+ todayAttendances: 0,
|
|
|
+ avgResponseTime: '—',
|
|
|
+ responseTimeTrend: 'stable',
|
|
|
+ slaPct: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 5,
|
|
|
+ name: 'Roberto Souza',
|
|
|
+ initials: 'RS',
|
|
|
+ department: 'Vendas',
|
|
|
+ channels: ['whatsapp', 'instagram'],
|
|
|
+ status: 'Em Atendimento',
|
|
|
+ availableForEscalation: true,
|
|
|
+ todayAttendances: 12,
|
|
|
+ avgResponseTime: '8m 30s',
|
|
|
+ responseTimeTrend: 'up',
|
|
|
+ slaPct: 72
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 6,
|
|
|
+ name: 'Amanda Rocha',
|
|
|
+ initials: 'AR',
|
|
|
+ department: 'Suporte',
|
|
|
+ channels: ['whatsapp'],
|
|
|
+ status: 'Ativo',
|
|
|
+ availableForEscalation: true,
|
|
|
+ todayAttendances: 29,
|
|
|
+ avgResponseTime: '4m 15s',
|
|
|
+ responseTimeTrend: 'down',
|
|
|
+ slaPct: 91
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 7,
|
|
|
+ name: 'Carlos Mendes',
|
|
|
+ initials: 'CM',
|
|
|
+ department: 'SAC',
|
|
|
+ channels: ['whatsapp', 'instagram'],
|
|
|
+ status: 'Ativo',
|
|
|
+ availableForEscalation: false,
|
|
|
+ todayAttendances: 20,
|
|
|
+ avgResponseTime: '6m 00s',
|
|
|
+ responseTimeTrend: 'stable',
|
|
|
+ slaPct: 83
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 8,
|
|
|
+ name: 'Patrícia Nunes',
|
|
|
+ initials: 'PN',
|
|
|
+ department: 'Vendas',
|
|
|
+ channels: ['whatsapp'],
|
|
|
+ status: 'Disponível',
|
|
|
+ availableForEscalation: true,
|
|
|
+ todayAttendances: 8,
|
|
|
+ avgResponseTime: '3m 50s',
|
|
|
+ responseTimeTrend: 'down',
|
|
|
+ slaPct: 96
|
|
|
+ }
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // ── Filters ───────────────────────────────────────────────────────────────
|
|
|
+ let searchTerm = $state('');
|
|
|
+ let filterDept = $state('Todos');
|
|
|
+ let filterChannel = $state('Todos');
|
|
|
+ let filterStatus = $state('Todos');
|
|
|
+
|
|
|
+ const filteredAgents = $derived(
|
|
|
+ agents.filter((a) => {
|
|
|
+ if (searchTerm && !a.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
|
|
+ if (filterDept !== 'Todos' && a.department !== filterDept) return false;
|
|
|
+ if (filterChannel === 'WhatsApp' && !a.channels.includes('whatsapp')) return false;
|
|
|
+ if (filterChannel === 'Instagram' && !a.channels.includes('instagram')) return false;
|
|
|
+ if (filterStatus === 'Disponível para Escalonamento' && !a.availableForEscalation)
|
|
|
+ return false;
|
|
|
+ if (
|
|
|
+ filterStatus !== 'Todos' &&
|
|
|
+ filterStatus !== 'Disponível para Escalonamento' &&
|
|
|
+ a.status !== filterStatus
|
|
|
+ )
|
|
|
+ return false;
|
|
|
+ return true;
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ // ── Stats ─────────────────────────────────────────────────────────────────
|
|
|
+ const stats = $derived({
|
|
|
+ total: agents.length,
|
|
|
+ active: agents.filter((a) => a.status === 'Ativo' || a.status === 'Em Atendimento' || a.status === 'Disponível').length,
|
|
|
+ inAttendance: agents.filter((a) => a.status === 'Em Atendimento').length,
|
|
|
+ availableForEscalation: agents.filter((a) => a.availableForEscalation).length
|
|
|
+ });
|
|
|
+
|
|
|
+ // ── Actions ───────────────────────────────────────────────────────────────
|
|
|
+ function toggleEscalation(id) {
|
|
|
+ const agent = agents.find((a) => a.id === id);
|
|
|
+ if (agent) agent.availableForEscalation = !agent.availableForEscalation;
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleAgentActive(id) {
|
|
|
+ const agent = agents.find((a) => a.id === id);
|
|
|
+ if (agent) agent.status = agent.status === 'Inativo' ? 'Ativo' : 'Inativo';
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── New Agent Modal ───────────────────────────────────────────────────────
|
|
|
+ let showModal = $state(false);
|
|
|
+ let editingId = $state(null);
|
|
|
+
|
|
|
+ const emptyForm = () => ({
|
|
|
+ name: '',
|
|
|
+ email: '',
|
|
|
+ department: 'SAC',
|
|
|
+ channels: [],
|
|
|
+ status: 'Ativo',
|
|
|
+ availableForEscalation: true
|
|
|
+ });
|
|
|
+
|
|
|
+ let form = $state(emptyForm());
|
|
|
+
|
|
|
+ function openNewAgent() {
|
|
|
+ form = emptyForm();
|
|
|
+ editingId = null;
|
|
|
+ showModal = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ function openEditAgent(agent) {
|
|
|
+ form = {
|
|
|
+ name: agent.name,
|
|
|
+ email: agent.email ?? '',
|
|
|
+ department: agent.department,
|
|
|
+ channels: [...agent.channels],
|
|
|
+ status: agent.status,
|
|
|
+ availableForEscalation: agent.availableForEscalation
|
|
|
+ };
|
|
|
+ editingId = agent.id;
|
|
|
+ showModal = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ function toggleChannel(ch) {
|
|
|
+ if (form.channels.includes(ch)) {
|
|
|
+ form.channels = form.channels.filter((c) => c !== ch);
|
|
|
+ } else {
|
|
|
+ form.channels = [...form.channels, ch];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function saveAgent() {
|
|
|
+ if (!form.name.trim()) return;
|
|
|
+ if (editingId !== null) {
|
|
|
+ const agent = agents.find((a) => a.id === editingId);
|
|
|
+ if (agent) {
|
|
|
+ agent.name = form.name;
|
|
|
+ agent.initials = form.name
|
|
|
+ .split(' ')
|
|
|
+ .slice(0, 2)
|
|
|
+ .map((n) => n[0])
|
|
|
+ .join('')
|
|
|
+ .toUpperCase();
|
|
|
+ agent.department = form.department;
|
|
|
+ agent.channels = [...form.channels];
|
|
|
+ agent.status = form.status;
|
|
|
+ agent.availableForEscalation = form.availableForEscalation;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const id = Math.max(0, ...agents.map((a) => a.id)) + 1;
|
|
|
+ agents = [
|
|
|
+ ...agents,
|
|
|
+ {
|
|
|
+ id,
|
|
|
+ name: form.name,
|
|
|
+ initials: form.name
|
|
|
+ .split(' ')
|
|
|
+ .slice(0, 2)
|
|
|
+ .map((n) => n[0])
|
|
|
+ .join('')
|
|
|
+ .toUpperCase(),
|
|
|
+ department: form.department,
|
|
|
+ channels: [...form.channels],
|
|
|
+ status: form.status,
|
|
|
+ availableForEscalation: form.availableForEscalation,
|
|
|
+ todayAttendances: 0,
|
|
|
+ avgResponseTime: '—',
|
|
|
+ responseTimeTrend: 'stable',
|
|
|
+ slaPct: 100
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ showModal = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
+ function statusConfig(status) {
|
|
|
+ const map = {
|
|
|
+ Ativo: { dot: 'bg-emerald-500', text: 'text-emerald-700 dark:text-emerald-400' },
|
|
|
+ 'Em Atendimento': { dot: 'bg-sky-500', text: 'text-sky-700 dark:text-sky-400' },
|
|
|
+ Inativo: { dot: 'bg-slate-400', text: 'text-slate-500 dark:text-slate-400' },
|
|
|
+ Disponível: { dot: 'bg-indigo-500', text: 'text-indigo-700 dark:text-indigo-400' }
|
|
|
+ };
|
|
|
+ return map[status] ?? map['Inativo'];
|
|
|
+ }
|
|
|
+
|
|
|
+ function deptBadgeClass(dept) {
|
|
|
+ const map = {
|
|
|
+ SAC: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-400/10 dark:text-sky-400 dark:border-sky-400/20',
|
|
|
+ Vendas:
|
|
|
+ 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-400/10 dark:text-emerald-400 dark:border-emerald-400/20',
|
|
|
+ Suporte:
|
|
|
+ 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-400/10 dark:text-purple-400 dark:border-purple-400/20'
|
|
|
+ };
|
|
|
+ return (
|
|
|
+ map[dept] ??
|
|
|
+ 'bg-slate-50 text-slate-700 border-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:border-slate-600'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function trendArrow(trend) {
|
|
|
+ if (trend === 'down') return { arrow: '↓', cls: 'text-emerald-600 dark:text-emerald-400' };
|
|
|
+ if (trend === 'up') return { arrow: '↑', cls: 'text-red-600 dark:text-red-400' };
|
|
|
+ return { arrow: '→', cls: 'text-slate-400' };
|
|
|
+ }
|
|
|
+
|
|
|
+ function slaBarClass(pct) {
|
|
|
+ if (pct >= 90) return 'bg-emerald-500';
|
|
|
+ if (pct >= 70) return 'bg-amber-500';
|
|
|
+ return 'bg-red-500';
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<svelte:head>
|
|
|
+ <title>Agentes - Nettown Analytics</title>
|
|
|
+</svelte:head>
|
|
|
+
|
|
|
+<!-- New Agent Modal -->
|
|
|
+{#if showModal}
|
|
|
+ <div
|
|
|
+ class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 p-4"
|
|
|
+ onclick={(e) => e.target === e.currentTarget && (showModal = false)}
|
|
|
+ onkeydown={(e) => e.key === 'Escape' && (showModal = false)}
|
|
|
+ role="dialog"
|
|
|
+ aria-modal="true"
|
|
|
+ tabindex="-1"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="w-full max-w-lg 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">
|
|
|
+ {editingId !== null ? 'Editar Agente' : 'Cadastrar Novo Agente'}
|
|
|
+ </h2>
|
|
|
+ <button
|
|
|
+ onclick={() => (showModal = 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">
|
|
|
+ <!-- Nome -->
|
|
|
+ <div>
|
|
|
+ <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
|
+ Nome completo
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ bind:value={form.name}
|
|
|
+ placeholder="Ex: Carolina Ribeiro"
|
|
|
+ class="w-full rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-sm text-slate-900 placeholder-slate-400 transition-colors focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:placeholder-slate-500 dark:focus:bg-slate-900"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- E-mail -->
|
|
|
+ <div>
|
|
|
+ <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
|
+ E-mail
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="email"
|
|
|
+ bind:value={form.email}
|
|
|
+ placeholder="agente@empresa.com.br"
|
|
|
+ class="w-full rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-sm text-slate-900 placeholder-slate-400 transition-colors focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:placeholder-slate-500 dark:focus:bg-slate-900"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Departamento -->
|
|
|
+ <div>
|
|
|
+ <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
|
+ Departamento
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ bind:value={form.department}
|
|
|
+ class="w-full rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 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"
|
|
|
+ >
|
|
|
+ <option>SAC</option>
|
|
|
+ <option>Vendas</option>
|
|
|
+ <option>Suporte</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Canal -->
|
|
|
+ <div>
|
|
|
+ <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
|
+ Canal (multi-seleção)
|
|
|
+ </label>
|
|
|
+ <div class="flex gap-2">
|
|
|
+ {#each ['whatsapp', 'instagram'] as ch}
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onclick={() => toggleChannel(ch)}
|
|
|
+ class="rounded-lg border px-4 py-2 text-sm font-medium transition-colors {form.channels.includes(ch)
|
|
|
+ ? 'border-indigo-600 bg-indigo-600 text-white dark:border-indigo-500 dark:bg-indigo-500'
|
|
|
+ : 'border-slate-300 bg-white text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800'}"
|
|
|
+ >
|
|
|
+ {ch === 'whatsapp' ? 'WhatsApp' : 'Instagram'}
|
|
|
+ </button>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Status -->
|
|
|
+ <div>
|
|
|
+ <label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
|
|
+ Status inicial
|
|
|
+ </label>
|
|
|
+ <select
|
|
|
+ bind:value={form.status}
|
|
|
+ class="w-full rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 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"
|
|
|
+ >
|
|
|
+ <option>Ativo</option>
|
|
|
+ <option>Inativo</option>
|
|
|
+ <option>Disponível</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Disponível para escalonamento -->
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <div class="text-sm font-medium text-slate-900 dark:text-white">
|
|
|
+ Disponível para escalonamento
|
|
|
+ </div>
|
|
|
+ <div class="text-xs text-slate-500 dark:text-slate-400">
|
|
|
+ Permite receber conversas escaladas de outros agentes
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onclick={() => (form.availableForEscalation = !form.availableForEscalation)}
|
|
|
+ class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none {form.availableForEscalation
|
|
|
+ ? 'bg-indigo-600 dark:bg-indigo-500'
|
|
|
+ : 'bg-slate-200 dark:bg-slate-700'}"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 {form.availableForEscalation
|
|
|
+ ? 'translate-x-5'
|
|
|
+ : 'translate-x-0'}"
|
|
|
+ ></span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ class="flex justify-end gap-3 border-t border-slate-200 p-5 dark:border-slate-700"
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ onclick={() => (showModal = 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={saveAgent}
|
|
|
+ disabled={!form.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:cursor-not-allowed disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
|
|
+ >
|
|
|
+ Salvar
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+{/if}
|
|
|
+
|
|
|
+<div class="mx-auto max-w-[1600px] space-y-6">
|
|
|
+ <!-- Page header -->
|
|
|
+ <div
|
|
|
+ class="flex flex-wrap items-center justify-between gap-4 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"
|
|
|
+ >
|
|
|
+ <Users size={20} strokeWidth={2.5} />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <h1 class="text-xl font-bold text-slate-900 dark:text-white">Gestão de Agentes</h1>
|
|
|
+ <p class="text-sm text-slate-500 dark:text-slate-400">
|
|
|
+ Gerencie a equipe de atendimento e configure escalonamentos
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ onclick={openNewAgent}
|
|
|
+ class="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:-translate-y-0.5 hover:bg-indigo-700 hover:shadow-md dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
|
|
+ >
|
|
|
+ <UserPlus size={16} strokeWidth={2.5} />
|
|
|
+ + Cadastrar Agente
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Stats row -->
|
|
|
+ {#if isLoading}
|
|
|
+ <div class="grid grid-cols-2 gap-4 xl:grid-cols-4">
|
|
|
+ {#each [1, 2, 3, 4] as _}
|
|
|
+ <div
|
|
|
+ class="h-20 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="grid grid-cols-2 gap-4 xl:grid-cols-4">
|
|
|
+ {#each [
|
|
|
+ { label: 'Total de Agentes', value: stats.total, color: 'text-slate-900 dark:text-white', icon: Users, bg: 'bg-slate-100 dark:bg-slate-800', iconCls: 'text-slate-600 dark:text-slate-400' },
|
|
|
+ { label: 'Ativos agora', value: stats.active, color: 'text-emerald-600 dark:text-emerald-400', icon: CheckCircle, bg: 'bg-emerald-50 dark:bg-emerald-400/10', iconCls: 'text-emerald-600 dark:text-emerald-400' },
|
|
|
+ { label: 'Em atendimento', value: stats.inAttendance, color: 'text-sky-600 dark:text-sky-400', icon: MessageSquare, bg: 'bg-sky-50 dark:bg-sky-400/10', iconCls: 'text-sky-600 dark:text-sky-400' },
|
|
|
+ { label: 'Disp. para escalonamento', value: stats.availableForEscalation, color: 'text-indigo-600 dark:text-indigo-400', icon: ShieldCheck, bg: 'bg-indigo-50 dark:bg-indigo-400/10', iconCls: 'text-indigo-600 dark:text-indigo-400' }
|
|
|
+ ] as stat}
|
|
|
+ {@const Icon = stat.icon}
|
|
|
+ <div
|
|
|
+ class="flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-colors dark:border-slate-800 dark:bg-[#1e293b]"
|
|
|
+ >
|
|
|
+ <div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg {stat.bg} {stat.iconCls}">
|
|
|
+ <Icon size={18} strokeWidth={2.5} />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="text-2xl font-bold {stat.color}">{stat.value}</div>
|
|
|
+ <div class="text-xs text-slate-500 dark:text-slate-400">{stat.label}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+
|
|
|
+ <!-- Filter bar -->
|
|
|
+ <div
|
|
|
+ class="flex flex-wrap items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 text-sm shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
|
|
|
+ >
|
|
|
+ <div class="relative flex-1 min-w-48">
|
|
|
+ <Search size={16} class="absolute top-1/2 left-3 -translate-y-1/2 text-slate-400" />
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ placeholder="Buscar por nome..."
|
|
|
+ bind:value={searchTerm}
|
|
|
+ class="w-full rounded-lg border border-slate-300 bg-slate-50 py-1.5 pr-3 pl-9 text-slate-900 placeholder-slate-400 focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:placeholder-slate-500"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="text-xs font-medium text-slate-500 dark:text-slate-400">Departamento:</span>
|
|
|
+ <select
|
|
|
+ bind:value={filterDept}
|
|
|
+ class="rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 text-slate-900 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"
|
|
|
+ >
|
|
|
+ {#each ['Todos', 'SAC', 'Vendas', 'Suporte'] as opt}
|
|
|
+ <option>{opt}</option>
|
|
|
+ {/each}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="text-xs font-medium text-slate-500 dark:text-slate-400">Canal:</span>
|
|
|
+ <select
|
|
|
+ bind:value={filterChannel}
|
|
|
+ class="rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 text-slate-900 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"
|
|
|
+ >
|
|
|
+ {#each ['Todos', 'WhatsApp', 'Instagram'] as opt}
|
|
|
+ <option>{opt}</option>
|
|
|
+ {/each}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="text-xs font-medium text-slate-500 dark:text-slate-400">Status:</span>
|
|
|
+ <select
|
|
|
+ bind:value={filterStatus}
|
|
|
+ class="rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 text-slate-900 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"
|
|
|
+ >
|
|
|
+ {#each ['Todos', 'Ativo', 'Inativo', 'Em Atendimento', 'Disponível para Escalonamento'] as opt}
|
|
|
+ <option>{opt}</option>
|
|
|
+ {/each}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Agents table -->
|
|
|
+ <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]"
|
|
|
+ >
|
|
|
+ {#if isLoading}
|
|
|
+ <div class="space-y-3 p-4">
|
|
|
+ {#each [1, 2, 3, 4, 5] as _}
|
|
|
+ <div
|
|
|
+ class="h-14 animate-pulse rounded-lg bg-slate-100 dark:bg-slate-800"
|
|
|
+ ></div>
|
|
|
+ {/each}
|
|
|
+ </div>
|
|
|
+ {:else}
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="w-full min-w-[960px] text-sm">
|
|
|
+ <thead>
|
|
|
+ <tr class="border-b border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900/50">
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Agente</th>
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Depto</th>
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Canal</th>
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Status</th>
|
|
|
+ <th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Escalonamento</th>
|
|
|
+ <th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Hoje</th>
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">T. Médio Resp.</th>
|
|
|
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">SLA Cumprido</th>
|
|
|
+ <th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Ações</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
|
|
+ {#if filteredAgents.length === 0}
|
|
|
+ <tr>
|
|
|
+ <td colspan="9" class="py-16 text-center">
|
|
|
+ <div class="flex flex-col items-center gap-3">
|
|
|
+ <div class="flex h-12 w-12 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800">
|
|
|
+ <Users size={20} class="text-slate-400" />
|
|
|
+ </div>
|
|
|
+ <div class="text-sm font-medium text-slate-500 dark:text-slate-400">Nenhum agente encontrado</div>
|
|
|
+ <div class="text-xs text-slate-400 dark:text-slate-500">Tente ajustar os filtros acima</div>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ {:else}
|
|
|
+ {#each filteredAgents as agent (agent.id)}
|
|
|
+ {@const sc = statusConfig(agent.status)}
|
|
|
+ {@const trend = trendArrow(agent.responseTimeTrend)}
|
|
|
+ <tr
|
|
|
+ class="transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/40 {agent.status === 'Inativo'
|
|
|
+ ? 'opacity-60'
|
|
|
+ : ''}"
|
|
|
+ >
|
|
|
+ <!-- Nome -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
+ <div
|
|
|
+ class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-xs font-bold text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300"
|
|
|
+ >
|
|
|
+ {agent.initials}
|
|
|
+ </div>
|
|
|
+ <span class="font-medium text-slate-900 dark:text-white">{agent.name}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Departamento -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <span
|
|
|
+ class="rounded-md border px-2 py-0.5 text-xs font-semibold {deptBadgeClass(agent.department)}"
|
|
|
+ >
|
|
|
+ {agent.department}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Canal -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex flex-wrap gap-1">
|
|
|
+ {#if agent.channels.includes('whatsapp')}
|
|
|
+ <span
|
|
|
+ class="rounded-md border border-emerald-200 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-bold text-emerald-700 dark:border-emerald-400/20 dark:bg-emerald-400/10 dark:text-emerald-400"
|
|
|
+ >WA</span
|
|
|
+ >
|
|
|
+ {/if}
|
|
|
+ {#if agent.channels.includes('instagram')}
|
|
|
+ <span
|
|
|
+ class="rounded-md border border-purple-200 bg-purple-50 px-1.5 py-0.5 text-[10px] font-bold text-purple-700 dark:border-purple-400/20 dark:bg-purple-400/10 dark:text-purple-400"
|
|
|
+ >IG</span
|
|
|
+ >
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Status -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span
|
|
|
+ class="inline-block h-2 w-2 shrink-0 rounded-full {sc.dot}"
|
|
|
+ ></span>
|
|
|
+ <span class="text-xs font-medium {sc.text}">{agent.status}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Escalonamento toggle -->
|
|
|
+ <td class="px-4 py-3 text-center">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onclick={() => toggleEscalation(agent.id)}
|
|
|
+ class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none {agent.availableForEscalation
|
|
|
+ ? 'bg-indigo-600 dark:bg-indigo-500'
|
|
|
+ : 'bg-slate-200 dark:bg-slate-700'}"
|
|
|
+ title={agent.availableForEscalation
|
|
|
+ ? 'Disponível para escalonamento'
|
|
|
+ : 'Indisponível para escalonamento'}
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ class="pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 {agent.availableForEscalation
|
|
|
+ ? 'translate-x-4'
|
|
|
+ : 'translate-x-0'}"
|
|
|
+ ></span>
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Atendimentos hoje -->
|
|
|
+ <td class="px-4 py-3 text-center">
|
|
|
+ <span class="font-semibold text-slate-900 dark:text-white"
|
|
|
+ >{agent.todayAttendances}</span
|
|
|
+ >
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Tempo médio -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <Clock size={13} class="shrink-0 text-slate-400" />
|
|
|
+ <span class="text-slate-700 dark:text-slate-300">{agent.avgResponseTime}</span>
|
|
|
+ <span class="text-xs font-semibold {trend.cls}">{trend.arrow}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- SLA progress -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <div class="h-2 w-20 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
|
|
|
+ <div
|
|
|
+ class="h-full rounded-full {slaBarClass(agent.slaPct)}"
|
|
|
+ style="width: {agent.slaPct}%"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ <span
|
|
|
+ class="text-xs font-semibold {agent.slaPct >= 90
|
|
|
+ ? 'text-emerald-600 dark:text-emerald-400'
|
|
|
+ : agent.slaPct >= 70
|
|
|
+ ? 'text-amber-600 dark:text-amber-400'
|
|
|
+ : 'text-red-600 dark:text-red-400'}"
|
|
|
+ >
|
|
|
+ {agent.slaPct}%
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+
|
|
|
+ <!-- Ações -->
|
|
|
+ <td class="px-4 py-3">
|
|
|
+ <div class="flex items-center justify-center gap-2">
|
|
|
+ <button
|
|
|
+ onclick={() => openEditAgent(agent)}
|
|
|
+ class="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
|
|
+ title="Editar agente"
|
|
|
+ >
|
|
|
+ <Edit size={15} />
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onclick={() => toggleAgentActive(agent.id)}
|
|
|
+ class="rounded-md p-1.5 transition-colors {agent.status === 'Inativo'
|
|
|
+ ? 'text-emerald-500 hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-400/10'
|
|
|
+ : 'text-slate-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-400/10 dark:hover:text-red-400'}"
|
|
|
+ title={agent.status === 'Inativo' ? 'Reativar agente' : 'Desativar agente'}
|
|
|
+ >
|
|
|
+ <Power size={15} />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ {/each}
|
|
|
+ {/if}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+</div>
|