gdias 2 долоо хоног өмнө
parent
commit
33bb147b1a

+ 33 - 0
src/lib/features/sentiment/domain/sentiment-dashboard.service.js

@@ -44,6 +44,39 @@ export function getSentimentDashboardViewModel() {
 	};
 }
 
+/**
+ * Mapeia a resposta de GET /v1/analytics/sentiment/dashboard para o view model
+ * usado pelas telas. O backend já entrega summaryCards/alerts/timelineViews/aspects
+ * no formato esperado; aqui apenas anexamos ícone/cor aos cards (que são visuais e
+ * não vêm da API) e normalizamos defaults.
+ */
+export function mapSentimentDashboardData(data) {
+	const timelineViews = data?.timelineViews ?? { day: [], week: [], month: [] };
+	return {
+		summaryCards: mapSummaryCardsFromApi(data?.summaryCards ?? []),
+		alerts: data?.alerts ?? [],
+		timeline: timelineViews.week ?? [],
+		timelineViews,
+		aspects: data?.aspects ?? []
+	};
+}
+
+function mapSummaryCardsFromApi(cards) {
+	const configById = Object.fromEntries(summaryCardsConfig.map((config) => [config.key, config]));
+	return cards.map((card) => {
+		const config = configById[card.id] ?? {};
+		return {
+			id: card.id,
+			label: card.label ?? config.label ?? card.id,
+			value: card.value,
+			image: card.image ?? null,
+			icon: config.icon,
+			color: config.color,
+			bg: config.bg
+		};
+	});
+}
+
 export function getSummaryInsight(cardId, viewModel) {
 	const card = viewModel.summaryCards.find((item) => item.id === cardId);
 	if (!card) return createFallbackInsight();

+ 548 - 0
src/lib/features/sla/SlaConfigManager.svelte

@@ -0,0 +1,548 @@
+<script>
+	import { Clock, CheckCircle, AlertTriangle, XCircle, Plus, X } from 'lucide-svelte';
+	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
+
+	// ── Estado (GET /v1/sla/configs) ────────────────────────────────────────────
+	let isLoading = $state(true);
+	let loadError = $state('');
+	let departments = $state([]);
+	let savedDeptIds = $state([]);
+	let savingDeptIds = $state([]);
+
+	async function loadConfigs() {
+		isLoading = true;
+		loadError = '';
+		try {
+			const data = await api.get('/v1/sla/configs');
+			departments = data.items ?? [];
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar as configurações de SLA.';
+			departments = [];
+		} finally {
+			isLoading = false;
+		}
+	}
+
+	onMount(loadConfigs);
+
+	async function saveDepartment(index) {
+		const dept = departments[index];
+		if (!dept) return;
+		savingDeptIds = [...savingDeptIds, dept.id];
+		loadError = '';
+		try {
+			const updated = await api.post('/v1/sla/configs', {
+				body: {
+					id: dept.id,
+					name: dept.name,
+					firstResponseH: Number(dept.firstResponseH) || 0,
+					firstResponseM: Number(dept.firstResponseM) || 0,
+					resolutionH: Number(dept.resolutionH) || 1,
+					alertPct: Number(dept.alertPct) || 80
+				}
+			});
+			departments[index] = { ...departments[index], ...updated };
+			savedDeptIds = [...savedDeptIds, dept.id];
+			setTimeout(() => {
+				savedDeptIds = savedDeptIds.filter((id) => id !== dept.id);
+			}, 3000);
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao salvar o SLA do departamento.';
+		} finally {
+			savingDeptIds = savingDeptIds.filter((id) => id !== dept.id);
+		}
+	}
+
+	// ── Modal de adicionar ──────────────────────────────────────────────────────
+	let showAddModal = $state(false);
+	let addError = $state('');
+	let adding = $state(false);
+	let newDept = $state({ name: '', firstResponseH: 1, firstResponseM: 0, resolutionH: 24, alertPct: 80 });
+
+	function openAddModal() {
+		newDept = { name: '', firstResponseH: 1, firstResponseM: 0, resolutionH: 24, alertPct: 80 };
+		addError = '';
+		showAddModal = true;
+	}
+
+	async function addDepartment() {
+		if (!newDept.name.trim()) return;
+		adding = true;
+		addError = '';
+		try {
+			const created = await api.post('/v1/sla/configs', {
+				body: {
+					name: newDept.name.trim(),
+					firstResponseH: Number(newDept.firstResponseH) || 0,
+					firstResponseM: Number(newDept.firstResponseM) || 0,
+					resolutionH: Number(newDept.resolutionH) || 1,
+					alertPct: Number(newDept.alertPct) || 80
+				}
+			});
+			departments = [...departments, created];
+			showAddModal = false;
+		} catch (err) {
+			addError = err?.message ?? 'Falha ao adicionar o departamento.';
+		} finally {
+			adding = false;
+		}
+	}
+
+	// ── Helpers de status ────────────────────────────────────────────────────────
+	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>
+
+<!-- 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">
+				{#if addError}
+					<div
+						class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+					>
+						{addError}
+					</div>
+				{/if}
+				<div>
+					<label
+						for="sla-new-name"
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+						>Nome do departamento</label
+					>
+					<input
+						id="sla-new-name"
+						type="text"
+						bind:value={newDept.name}
+						placeholder="Ex: Financeiro"
+						class={inputCls}
+					/>
+				</div>
+				<div class="grid grid-cols-2 gap-4">
+					<div>
+						<label
+							for="sla-new-fr-h"
+							class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+							>Primeira resposta (h)</label
+						>
+						<input
+							id="sla-new-fr-h"
+							type="number"
+							min="0"
+							max="72"
+							bind:value={newDept.firstResponseH}
+							class={inputCls}
+						/>
+					</div>
+					<div>
+						<label
+							for="sla-new-fr-m"
+							class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+							>Primeira resposta (min)</label
+						>
+						<input
+							id="sla-new-fr-m"
+							type="number"
+							min="0"
+							max="59"
+							bind:value={newDept.firstResponseM}
+							class={inputCls}
+						/>
+					</div>
+				</div>
+				<div>
+					<label
+						for="sla-new-res"
+						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
+						id="sla-new-res"
+						type="number"
+						min="1"
+						max="720"
+						bind:value={newDept.resolutionH}
+						class={inputCls}
+					/>
+				</div>
+				<div>
+					<label
+						for="sla-new-alert"
+						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
+						id="sla-new-alert"
+						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() || adding}
+					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"
+				>
+					{adding ? 'Adicionando…' : 'Adicionar'}
+				</button>
+			</div>
+		</div>
+	</div>
+{/if}
+
+<div class="space-y-6">
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
+	<!-- 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 (dept.id)}
+				{@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
+					)}"
+				>
+					<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>
+
+					<div class="grid grid-cols-1 gap-6 p-5 sm:grid-cols-2 xl:grid-cols-4">
+						<div>
+							<label
+								for="sla-fr-h-{dept.id}"
+								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
+									id="sla-fr-h-{dept.id}"
+									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>
+
+						<div>
+							<label
+								for="sla-fr-m-{dept.id}"
+								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
+									id="sla-fr-m-{dept.id}"
+									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>
+
+						<div>
+							<label
+								for="sla-res-{dept.id}"
+								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
+									id="sla-res-{dept.id}"
+									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>
+
+						<div>
+							<label
+								for="sla-alert-{dept.id}"
+								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
+									id="sla-alert-{dept.id}"
+									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>
+
+					<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(i)}
+								disabled={savingDeptIds.includes(dept.id)}
+								class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-all hover:bg-indigo-700 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-600"
+							>
+								{savingDeptIds.includes(dept.id) ? 'Salvando…' : 'Salvar'}
+							</button>
+						</div>
+					</div>
+				</div>
+			{/each}
+
+			{#if departments.length === 0}
+				<div
+					class="rounded-xl border border-dashed border-slate-300 p-8 text-center text-sm text-slate-400 dark:border-slate-700"
+				>
+					Nenhum departamento configurado ainda.
+				</div>
+			{/if}
+		</div>
+	{/if}
+
+	<!-- Add Department button -->
+	{#if !isLoading}
+		<div class="flex justify-center">
+			<button
+				onclick={openAddModal}
+				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 && departments.length > 0}
+		<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 (dept.id)}
+							{@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>

+ 1 - 1
src/routes/(app)/+layout.svelte

@@ -67,7 +67,7 @@
 		{ name: 'Evolução', href: '/dashboard/evolucao', icon: TrendingUp },
 		{ name: 'Configurações', href: '/dashboard/settings', icon: Settings },
 		{ name: 'Dashboard Executivo', href: '/dashboard/executive', icon: PieChart },
-		{ name: 'Agentes', href: '/dashboard/agents', icon: Users },
+		{ name: 'Agentes', href: '/dashboard/operators', icon: Users },
 		{ name: 'Config. SLA', href: '/dashboard/sla', icon: ShieldCheck }
 	];
 

+ 248 - 276
src/routes/(app)/dashboard/+page.svelte

@@ -8,21 +8,11 @@
 		UserRoundCog,
 		UserX
 	} from 'lucide-svelte';
-	import { Chart, Svg, Axis, Bar, Line, Spline, Highlight, Group } from 'layerchart';
-	import { scaleTime, scaleLinear, scaleBand } from 'd3-scale';
-	import { format, addDays } from 'date-fns';
-	import { ptBR } from 'date-fns/locale';
+	import { Chart, Svg, Axis, Spline, Highlight } from 'layerchart';
+	import { format } from 'date-fns';
 	import { goto } from '$app/navigation';
-
-	import {
-		mockKpis,
-		mockPriorityQueue,
-		mockRadarData,
-		mockVolumeData,
-		mockSentimentDistribution,
-		mockAspectsData,
-		mockAspectsDrilldown
-	} from '$lib/core/models/mock-data.js';
+	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
 	function getSentimentLabel(sentimentScore) {
 		if (sentimentScore >= 0.35) return 'Positivo';
@@ -31,39 +21,25 @@
 	}
 
 	function navigateToAnalytics(aspect, tone) {
-		const aspectId = aspect.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+		const aspectId = aspect
+			.toLowerCase()
+			.normalize('NFD')
+			.replace(/[̀-ͯ]/g, '');
 		goto(`/dashboard/analytics?aspect=${aspectId}&sentiment=${tone}#aspects-panel`);
 	}
 
-	let selectedAspectDrilldown = $state(null);
-
+	// ── Filtros (enviados como query params ao backend) ────────────────────────
 	const periodOptions = [
-		{ id: 'today', label: 'Hoje (24h)', multiplier: 0.9 },
-		{ id: 'yesterday', label: 'Ontem', multiplier: 0.7 },
-		{ id: 'week', label: 'Últimos 7 dias', multiplier: 1.45 }
-	];
-
-	const unitOptions = [
-		{ id: 'all', label: 'Todas as unidades', multiplier: 1 },
-		{ id: 'flagship', label: 'Loja Flagship', multiplier: 1.35 },
-		{ id: 'franquias', label: 'Franquias', multiplier: 0.75 },
-		{ id: 'pop-up', label: 'Pop-up Stores', multiplier: 1.1 },
-		{ id: 'digital', label: 'Operação Digital', multiplier: 1.6 }
-	];
-
-	const areaOptions = [
-		{ id: 'all', label: 'Todas as áreas', multiplier: 1 },
-		{ id: 'atendimento', label: 'Atendimento', multiplier: 1.25 },
-		{ id: 'produto', label: 'Produto', multiplier: 0.8 },
-		{ id: 'logistica', label: 'Logística & Entrega', multiplier: 1.3 },
-		{ id: 'marketing', label: 'Marketing & Campanhas', multiplier: 1.4 }
+		{ id: 'today', label: 'Hoje (24h)' },
+		{ id: 'yesterday', label: 'Ontem' },
+		{ id: 'week', label: 'Últimos 7 dias' }
 	];
 
 	const sentimentFilterOptions = [
-		{ id: 'all', label: 'Todos', multiplier: 1, weights: { positive: 1, neutral: 1, negative: 1 } },
-		{ id: 'positive', label: 'Positivo', multiplier: 1.35, weights: { positive: 1.5, neutral: 0.7, negative: 0.4 } },
-		{ id: 'neutral', label: 'Neutro', multiplier: 0.95, weights: { positive: 0.8, neutral: 1.3, negative: 0.8 } },
-		{ id: 'negative', label: 'Negativo', multiplier: 0.6, weights: { positive: 0.4, neutral: 0.7, negative: 1.7 } }
+		{ id: 'all', label: 'Todos' },
+		{ id: 'positive', label: 'Positivo' },
+		{ id: 'neutral', label: 'Neutro' },
+		{ id: 'negative', label: 'Negativo' }
 	];
 
 	const volumeViewOptions = [
@@ -71,230 +47,149 @@
 		{ id: 'day', label: 'Por Dia', format: 'dd/MM' }
 	];
 
-	let selectedPeriod = $state(periodOptions[0].id);
-	let selectedUnit = $state(unitOptions[0].id);
-	let selectedArea = $state(areaOptions[0].id);
-	let selectedSentimentFilter = $state(sentimentFilterOptions[0].id);
-	let volumeView = $state(volumeViewOptions[0].id);
+	let selectedPeriod = $state('week');
+	let selectedSentimentFilter = $state('all');
+	let volumeView = $state('day');
 	let queueTab = $state('all');
 
-	function openAspectDrilldown(aspect, tone) {
-		selectedAspectDrilldown = { aspect, tone };
-	}
-
-	function clearAspectDrilldown() {
-		selectedAspectDrilldown = null;
+	// ── Dados vindos da API ─────────────────────────────────────────────────────
+	let overview = $state(null);
+	let slaLive = $state([]);
+	let loading = $state(true);
+	let loadError = $state('');
+
+	async function loadDashboard() {
+		loading = true;
+		loadError = '';
+		try {
+			const [overviewData, slaData] = await Promise.all([
+				api.get('/v1/dashboard/overview', {
+					query: {
+						period: selectedPeriod,
+						sentiment: selectedSentimentFilter,
+						volume_view: volumeView
+					}
+				}),
+				api.get('/v1/sla/live-status').catch(() => ({ items: [] }))
+			]);
+			overview = overviewData;
+			slaLive = slaData?.items ?? [];
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar o dashboard.';
+		} finally {
+			loading = false;
+		}
 	}
 
-	const drilldownItems = $derived(
-		selectedAspectDrilldown
-			? (mockAspectsDrilldown[selectedAspectDrilldown.aspect]?.[selectedAspectDrilldown.tone] ?? []).slice(
-					0,
-					6
-				)
-			: []
-	);
-
-	const maxDrilldownValue = $derived(
-		drilldownItems.length > 0 ? Math.max(1, ...drilldownItems.map((item) => item.value)) : 1
-	);
-
-	const drilldownTitle = $derived(
-		selectedAspectDrilldown
-			? `${selectedAspectDrilldown.aspect} • ${
-					selectedAspectDrilldown.tone === 'positive' ? 'Pontos Positivos' :
-					selectedAspectDrilldown.tone === 'neutral' ? 'Pontos Neutros' : 'Pontos Negativos'
-				}`
-			: 'Distribuição de Aspectos'
-	);
-
-	const selectedFilterContext = $derived({
-		period: periodOptions.find((opt) => opt.id === selectedPeriod) ?? periodOptions[0],
-		unit: unitOptions.find((opt) => opt.id === selectedUnit) ?? unitOptions[0],
-		area: areaOptions.find((opt) => opt.id === selectedArea) ?? areaOptions[0],
-		sentiment: sentimentFilterOptions.find((opt) => opt.id === selectedSentimentFilter) ?? sentimentFilterOptions[0]
+	onMount(loadDashboard);
+
+	// Recarrega ao mudar filtros (a 1ª execução já é feita no onMount).
+	let firstRun = true;
+	$effect(() => {
+		// leitura para registrar dependências reativas
+		void selectedPeriod;
+		void selectedSentimentFilter;
+		void volumeView;
+		if (firstRun) {
+			firstRun = false;
+			return;
+		}
+		loadDashboard();
 	});
 
-	const combinedMultiplier = $derived(
-		selectedFilterContext.period.multiplier *
-		selectedFilterContext.unit.multiplier *
-		selectedFilterContext.area.multiplier
-	);
-
-	const sentimentWeights = $derived(selectedFilterContext.sentiment.weights);
-
-	const kpiDefinitions = [
+	// ── KPIs ────────────────────────────────────────────────────────────────────
+	const kpiCards = [
 		{
+			key: 'registeredUsers',
 			title: 'Usuários Cadastrados',
 			icon: Users,
 			color: 'text-emerald-600 dark:text-emerald-400',
 			bg: 'bg-emerald-50 dark:bg-emerald-400/10',
 			border: 'border-emerald-200 dark:border-emerald-400/20',
-			compute: ({ multiplier }) => Math.max(5, Math.round(mockKpis.newPeople.new * multiplier)),
-			format: (value) => value.toLocaleString('pt-BR')
+			format: (v) => Number(v ?? 0).toLocaleString('pt-BR')
 		},
 		{
+			key: 'activeAgents',
 			title: 'Atendentes Ativos',
 			icon: UserRoundCog,
 			color: 'text-purple-600 dark:text-purple-400',
 			bg: 'bg-purple-50 dark:bg-purple-400/10',
 			border: 'border-purple-200 dark:border-purple-400/20',
-			compute: ({ multiplier }) => Math.max(1, Math.round(mockKpis.activeService.value * (0.6 + multiplier / 1.5))),
-			format: (value) => value.toLocaleString('pt-BR')
+			format: (v) => Number(v ?? 0).toLocaleString('pt-BR')
 		},
 		{
+			key: 'totalConversations',
 			title: 'Total de conversas',
 			icon: MessageSquare,
 			color: 'text-sky-600 dark:text-sky-400',
 			bg: 'bg-sky-50 dark:bg-sky-400/10',
 			border: 'border-sky-200 dark:border-sky-400/20',
-			compute: ({ multiplier }) => Math.max(0, Math.round(mockKpis.whatsappMessages.current * (multiplier * 0.9 + selectedFilterContext.sentiment.multiplier))),
-			format: (value) => value.toLocaleString('pt-BR')
+			format: (v) => Number(v ?? 0).toLocaleString('pt-BR')
 		},
 		{
+			key: 'generalSentimentScore',
 			title: 'Sentimento Geral',
 			icon: ThumbsUp,
 			color: 'text-amber-600 dark:text-amber-400',
 			bg: 'bg-amber-50 dark:bg-amber-400/10',
 			border: 'border-amber-200 dark:border-amber-400/20',
-			compute: ({ sentimentBias, multiplier }) => {
-				const base = mockKpis.generalSentiment.value * sentimentBias + (multiplier - 1) * 0.4;
-				const adjusted = Math.max(-1, Math.min(1, base));
-				return adjusted;
-			},
-			format: (value) => getSentimentLabel(value)
+			format: (v) => getSentimentLabel(Number(v ?? 0))
 		},
 		{
+			key: 'unregisteredUsers',
 			title: 'Usuários Não Cadastrados',
 			icon: UserX,
 			color: 'text-indigo-600 dark:text-indigo-400',
 			bg: 'bg-indigo-50 dark:bg-indigo-400/10',
 			border: 'border-indigo-200 dark:border-indigo-400/20',
-			compute: ({ multiplier }) => Math.max(1000, Math.round(mockKpis.crmLines.total * (1 + (1 - multiplier) / 2))),
-			format: (value) => value.toLocaleString('pt-BR')
+			format: (v) => Number(v ?? 0).toLocaleString('pt-BR')
 		}
 	];
 
 	const kpis = $derived(
-		kpiDefinitions.map((kpi) => ({
-			...kpi,
-			value: kpi.format(
-				kpi.compute({
-					multiplier: combinedMultiplier,
-					sentimentBias: selectedFilterContext.sentiment.multiplier
-				})
-			)
+		kpiCards.map((card) => ({
+			...card,
+			value: card.format(overview?.kpis?.[card.key])
 		}))
 	);
 
-	const aspectsData = $derived(
-		mockAspectsData.map((aspect) => ({
-			...aspect,
-			positive: Math.round(aspect.positive * sentimentWeights.positive * combinedMultiplier),
-			neutral: Math.round(aspect.neutral * sentimentWeights.neutral * combinedMultiplier * 0.95),
-			negative: Math.round(aspect.negative * sentimentWeights.negative * combinedMultiplier)
-		}))
+	// ── Fila priorizada (overview.priorityQueue já traz lastMessage e slaStatus) ─
+	const queueAll = $derived(
+		(overview?.priorityQueue ?? []).map((item) => {
+			const isSeller = String(item.status ?? '')
+				.toLowerCase()
+				.includes('vendedora');
+			return { ...item, isSellerFault: isSeller, isClientFault: !isSeller };
+		})
 	);
 
-	const baseDailyVolume = (() => {
-		const grouped = new Map();
-		for (const record of mockVolumeData) {
-			const key = `${record.date.getFullYear()}-${record.date.getMonth()}-${record.date.getDate()}`;
-			if (!grouped.has(key)) {
-				grouped.set(key, { date: new Date(record.date.getFullYear(), record.date.getMonth(), record.date.getDate()), whatsapp: 0 });
-			}
-			grouped.get(key).whatsapp += record.whatsapp;
-		}
-
-		let series = Array.from(grouped.values()).sort((a, b) => a.date - b.date);
-
-		if (series.length <= 1) {
-			const seedDate = series[0]?.date ?? new Date();
-			const seedValue = series[0]?.whatsapp ?? 120;
-			series = Array.from({ length: 5 }, (_, idx) => {
-				const offset = 4 - idx;
-				return {
-					date: addDays(seedDate, -offset),
-					whatsapp: Math.max(5, Math.round(seedValue * (0.5 + idx * 0.2)))
-				};
-			});
-		}
-
-		return series;
-	})();
-
-	const selectedVolumeOption = $derived(volumeViewOptions.find((opt) => opt.id === volumeView) ?? volumeViewOptions[0]);
-
-	const volumeData = $derived.by(() => {
-		const base = volumeView === 'day' ? baseDailyVolume : mockVolumeData;
-		return base.map((entry, idx) => ({
-			date: entry.date,
-			whatsapp: Math.max(0, Math.round(entry.whatsapp * (combinedMultiplier + idx * 0.02)))
-		}));
-	});
-
-	const maxVolume = $derived(Math.max(60, ...volumeData.map((item) => item.whatsapp)));
-
-	const filteredQueueItems = $derived.by(() => {
-		const periodMult = selectedFilterContext.period.multiplier;
-		const sentimentMult = selectedFilterContext.sentiment.multiplier;
-		const areaMult = selectedFilterContext.area.multiplier;
-
-		return mockPriorityQueue.map((item) => {
-			const weight = periodMult * sentimentMult * areaMult;
-			return {
-				...item,
-				impact: Math.round(item.impact * weight),
-				chance: Math.min(100, Math.max(10, Math.round(item.chance * sentimentMult))),
-				isSellerFault: item.status.toLowerCase().includes('vendedora'),
-				isClientFault: !item.status.toLowerCase().includes('vendedora')
-			};
-		});
-	});
-
 	const queueItems = $derived.by(() => {
-		if (queueTab === 'seller') return filteredQueueItems.filter(i => i.isSellerFault);
-		if (queueTab === 'client') return filteredQueueItems.filter(i => i.isClientFault);
-		return filteredQueueItems;
+		if (queueTab === 'seller') return queueAll.filter((i) => i.isSellerFault);
+		if (queueTab === 'client') return queueAll.filter((i) => i.isClientFault);
+		return queueAll;
 	});
 
 	const queueCounts = $derived({
-		all: filteredQueueItems.length,
-		seller: filteredQueueItems.filter(i => i.isSellerFault).length,
-		client: filteredQueueItems.filter(i => i.isClientFault).length
+		all: queueAll.length,
+		seller: queueAll.filter((i) => i.isSellerFault).length,
+		client: queueAll.filter((i) => i.isClientFault).length
 	});
 
-	const totalPotential = $derived(
-		filteredQueueItems.reduce((sum, item) => sum + item.impact, 0)
-	);
+	const totalPotential = $derived(queueAll.reduce((sum, item) => sum + (item.impact ?? 0), 0));
+
+	// SLA ao vivo: nº de departamentos com SLA estourado (de /v1/sla/live-status).
+	const slaBreachCount = $derived(slaLive.filter((s) => s.liveStatus === 'breach').length);
 
+	// ── Radar (humor da base) ────────────────────────────────────────────────────
 	const radarRadius = 80;
 	const radarCenter = 100;
-
-	const radarData = $derived.by(() => {
-		const periodBias = selectedFilterContext.period.multiplier;
-		const unitBias = selectedFilterContext.unit.multiplier;
-		const areaBias = selectedFilterContext.area.multiplier;
-		const sentimentBias = selectedFilterContext.sentiment.multiplier;
-		return mockRadarData.map((metric, idx) => {
-			const wave = Math.sin((idx + 1) * periodBias) * 8;
-			const adjusted = Math.max(
-				5,
-				Math.min(
-					100,
-					Math.round(metric.value * sentimentBias * (0.7 + unitBias / 3 + areaBias / 4) + wave)
-				)
-			);
-			return { ...metric, value: adjusted };
-		});
-	});
-
-	const angleStep = $derived((Math.PI * 2) / radarData.length);
+	const radarData = $derived(overview?.radarData ?? []);
+	const angleStep = $derived(radarData.length > 0 ? (Math.PI * 2) / radarData.length : 0);
 
 	const radarPoints = $derived(
 		radarData.map((d, i) => {
 			const angle = i * angleStep - Math.PI / 2;
-			const r = (d.value / 100) * radarRadius;
+			const r = (Number(d.value ?? 0) / 100) * radarRadius;
 			return {
 				...d,
 				x: radarCenter + r * Math.cos(angle),
@@ -310,15 +205,66 @@
 	const radarPath = $derived(radarPoints.map((p) => `${p.x},${p.y}`).join(' '));
 
 	const gridLevels = $derived(
-		[0.2, 0.4, 0.6, 0.8, 1].map((level) => {
-			return radarData
+		[0.2, 0.4, 0.6, 0.8, 1].map((level) =>
+			radarData
 				.map((_, i) => {
 					const angle = i * angleStep - Math.PI / 2;
 					const r = level * radarRadius;
 					return `${radarCenter + r * Math.cos(angle)},${radarCenter + r * Math.sin(angle)}`;
 				})
-				.join(' ');
-		})
+				.join(' ')
+		)
+	);
+
+	// ── Volume por canal ──────────────────────────────────────────────────────────
+	const selectedVolumeOption = $derived(
+		volumeViewOptions.find((opt) => opt.id === volumeView) ?? volumeViewOptions[1]
+	);
+
+	const volumeData = $derived(
+		(overview?.volumeData ?? []).map((entry) => ({
+			date: new Date(entry.date),
+			whatsapp: Number(entry.whatsapp ?? 0)
+		}))
+	);
+
+	const maxVolume = $derived(Math.max(60, ...volumeData.map((item) => item.whatsapp), 0));
+
+	// ── Aspectos + drilldown ────────────────────────────────────────────────────
+	const aspectsData = $derived(overview?.aspectsData ?? []);
+
+	let selectedAspectDrilldown = $state(null);
+	function openAspectDrilldown(aspect, tone) {
+		selectedAspectDrilldown = { aspect, tone };
+	}
+	function clearAspectDrilldown() {
+		selectedAspectDrilldown = null;
+	}
+
+	const drilldownItems = $derived(
+		selectedAspectDrilldown
+			? (
+					overview?.aspectsDrilldown?.[selectedAspectDrilldown.aspect]?.[
+						selectedAspectDrilldown.tone
+					] ?? []
+				).slice(0, 6)
+			: []
+	);
+
+	const maxDrilldownValue = $derived(
+		drilldownItems.length > 0 ? Math.max(1, ...drilldownItems.map((item) => item.value)) : 1
+	);
+
+	const drilldownTitle = $derived(
+		selectedAspectDrilldown
+			? `${selectedAspectDrilldown.aspect} • ${
+					selectedAspectDrilldown.tone === 'positive'
+						? 'Pontos Positivos'
+						: selectedAspectDrilldown.tone === 'neutral'
+							? 'Pontos Neutros'
+							: 'Pontos Negativos'
+				}`
+			: 'Distribuição de Aspectos'
 	);
 </script>
 
@@ -342,28 +288,6 @@
 				{/each}
 			</select>
 		</div>
-		<div class="flex items-center gap-2">
-			<span class="font-medium text-slate-600 dark:text-slate-400">Unidade:</span>
-			<select
-				class="rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 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"
-				bind:value={selectedUnit}
-			>
-				{#each unitOptions as option}
-					<option value={option.id}>{option.label}</option>
-				{/each}
-			</select>
-		</div>
-		<div class="flex items-center gap-2">
-			<span class="font-medium text-slate-600 dark:text-slate-400">Área:</span>
-			<select
-				class="rounded-lg border border-slate-300 bg-slate-50 px-3 py-1.5 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"
-				bind:value={selectedArea}
-			>
-				{#each areaOptions as option}
-					<option value={option.id}>{option.label}</option>
-				{/each}
-			</select>
-		</div>
 		<div class="flex items-center gap-2">
 			<span class="font-medium text-slate-600 dark:text-slate-400">Sentimento:</span>
 			<select
@@ -375,8 +299,19 @@
 				{/each}
 			</select>
 		</div>
+		{#if loading}
+			<span class="text-xs text-slate-400">Carregando…</span>
+		{/if}
 	</div>
 
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
 	<!-- KPIs -->
 	<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
 		{#each kpis as kpi}
@@ -391,7 +326,11 @@
 						<Icon size={20} strokeWidth={2.5} />
 					</div>
 					<div class="space-y-1">
-						<div class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">{kpi.title}</div>
+						<div
+							class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+						>
+							{kpi.title}
+						</div>
 						<div class="text-2xl font-bold text-slate-900 dark:text-white">{kpi.value}</div>
 					</div>
 				</div>
@@ -419,6 +358,12 @@
 						class="rounded-md border border-red-200 bg-red-50 px-2.5 py-1 text-xs font-medium text-red-600 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
 						>{queueCounts.all} fora da janela</span
 					>
+					{#if slaBreachCount > 0}
+						<span
+							class="rounded-md border border-orange-200 bg-orange-50 px-2.5 py-1 text-xs font-medium text-orange-700 dark:border-orange-500/20 dark:bg-orange-500/10 dark:text-orange-400"
+							>{slaBreachCount} SLA estourado</span
+						>
+					{/if}
 					<span
 						class="rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-400"
 						>Potencial: R$ {totalPotential.toLocaleString('pt-BR')}</span
@@ -431,19 +376,28 @@
 			>
 				<button
 					type="button"
-					onclick={() => queueTab = 'all'}
-					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'all' ? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white' : 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
-				>Todas ({queueCounts.all})</button>
+					onclick={() => (queueTab = 'all')}
+					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'all'
+						? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white'
+						: 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
+					>Todas ({queueCounts.all})</button
+				>
 				<button
 					type="button"
-					onclick={() => queueTab = 'seller'}
-					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'seller' ? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white' : 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
-				>Pela vendedora ({queueCounts.seller})</button>
+					onclick={() => (queueTab = 'seller')}
+					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'seller'
+						? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white'
+						: 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
+					>Pela vendedora ({queueCounts.seller})</button
+				>
 				<button
 					type="button"
-					onclick={() => queueTab = 'client'}
-					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'client' ? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white' : 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
-				>Pelo cliente ({queueCounts.client})</button>
+					onclick={() => (queueTab = 'client')}
+					class="rounded-lg px-4 py-1.5 text-sm font-medium transition-colors {queueTab === 'client'
+						? 'border border-slate-300 bg-white text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white'
+						: 'border border-transparent bg-transparent text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white'}"
+					>Pelo cliente ({queueCounts.client})</button
+				>
 			</div>
 
 			<div class="flex-1 space-y-4 overflow-y-auto p-4">
@@ -518,6 +472,11 @@
 						</div>
 					</div>
 				{/each}
+				{#if !loading && queueItems.length === 0}
+					<div class="py-10 text-center text-sm text-slate-400">
+						Nenhuma conversa na fila para os filtros atuais.
+					</div>
+				{/if}
 			</div>
 		</div>
 
@@ -635,34 +594,40 @@
 			</div>
 
 			<div class="h-full w-full flex-1">
-				<Chart
-					data={volumeData}
-					x={d => d.date}
-					y={d => d.whatsapp}
-					yDomain={[0, maxVolume]}
-					padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
-				>
-					<Svg>
-						<Axis
-							placement="left"
-							grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-						/>
-						<Axis
-							placement="bottom"
-							format={(d) => format(d, selectedVolumeOption.format)}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-						/>
-						<Spline stroke="#38bdf8" strokeWidth={2} />
-						<Highlight
-							points={{
-								fill: '#38bdf8',
-								class: 'stroke-white dark:stroke-slate-900',
-								strokeWidth: 2
-							}}
-						/>
-					</Svg>
-				</Chart>
+				{#if volumeData.length > 0}
+					<Chart
+						data={volumeData}
+						x={(d) => d.date}
+						y={(d) => d.whatsapp}
+						yDomain={[0, maxVolume]}
+						padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
+					>
+						<Svg>
+							<Axis
+								placement="left"
+								grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+							/>
+							<Axis
+								placement="bottom"
+								format={(d) => format(d, selectedVolumeOption.format)}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+							/>
+							<Spline stroke="#38bdf8" strokeWidth={2} />
+							<Highlight
+								points={{
+									fill: '#38bdf8',
+									class: 'stroke-white dark:stroke-slate-900',
+									strokeWidth: 2
+								}}
+							/>
+						</Svg>
+					</Chart>
+				{:else}
+					<div class="flex h-full items-center justify-center text-sm text-slate-400">
+						Sem dados de volume no período.
+					</div>
+				{/if}
 			</div>
 		</div>
 
@@ -689,10 +654,11 @@
 				{#if !selectedAspectDrilldown}
 					<div class="space-y-4">
 						{#each aspectsData as aspect}
+							{@const total = aspect.positive + aspect.neutral + aspect.negative}
 							<div>
 								<div class="mb-1 flex justify-between text-xs text-slate-600 dark:text-slate-300">
 									<span class="font-medium">{aspect.aspect}</span>
-									<span>{aspect.positive + aspect.neutral + aspect.negative} interações</span>
+									<span>{total} interações</span>
 								</div>
 								<div
 									class="flex h-7 w-full overflow-hidden rounded bg-slate-100 shadow-inner dark:bg-slate-800"
@@ -700,31 +666,36 @@
 									<button
 										type="button"
 										class="h-full bg-emerald-500 transition-opacity hover:opacity-90"
-										style="width: {(aspect.positive / (aspect.positive + aspect.neutral + aspect.negative)) * 100}%"
+										style="width: {total > 0 ? (aspect.positive / total) * 100 : 0}%"
 										onclick={() => openAspectDrilldown(aspect.aspect, 'positive')}
 										aria-label={`Ver pontos positivos de ${aspect.aspect}`}
 									></button>
 									<button
 										type="button"
 										class="h-full bg-slate-400 transition-opacity hover:opacity-90"
-										style="width: {(aspect.neutral / (aspect.positive + aspect.neutral + aspect.negative)) * 100}%"
+										style="width: {total > 0 ? (aspect.neutral / total) * 100 : 0}%"
 										onclick={() => openAspectDrilldown(aspect.aspect, 'neutral')}
 										aria-label={`Ver pontos neutros de ${aspect.aspect}`}
 									></button>
 									<button
 										type="button"
 										class="h-full bg-red-500 transition-opacity hover:opacity-90"
-										style="width: {(aspect.negative / (aspect.positive + aspect.neutral + aspect.negative)) * 100}%"
+										style="width: {total > 0 ? (aspect.negative / total) * 100 : 0}%"
 										onclick={() => openAspectDrilldown(aspect.aspect, 'negative')}
 										aria-label={`Ver pontos negativos de ${aspect.aspect}`}
 									></button>
 								</div>
 							</div>
 						{/each}
+						{#if !loading && aspectsData.length === 0}
+							<div class="py-10 text-center text-sm text-slate-400">
+								Sem aspectos para os filtros atuais.
+							</div>
+						{/if}
 					</div>
 				{:else}
-					<div class="flex flex-col h-full">
-						<div class="space-y-3 flex-1 overflow-y-auto custom-scrollbar pr-2 mb-4">
+					<div class="flex h-full flex-col">
+						<div class="custom-scrollbar mb-4 flex-1 space-y-3 overflow-y-auto pr-2">
 							{#each drilldownItems as item}
 								<div>
 									<div class="mb-1 flex justify-between text-xs text-slate-600 dark:text-slate-300">
@@ -736,8 +707,8 @@
 											class="h-full rounded {selectedAspectDrilldown.tone === 'positive'
 												? 'bg-emerald-500'
 												: selectedAspectDrilldown.tone === 'neutral'
-												? 'bg-slate-400'
-												: 'bg-red-500'}"
+													? 'bg-slate-400'
+													: 'bg-red-500'}"
 											style="width: {(item.value / maxDrilldownValue) * 100}%"
 										></div>
 									</div>
@@ -746,7 +717,8 @@
 						</div>
 						<button
 							type="button"
-							onclick={() => navigateToAnalytics(selectedAspectDrilldown.aspect, selectedAspectDrilldown.tone)}
+							onclick={() =>
+								navigateToAnalytics(selectedAspectDrilldown.aspect, selectedAspectDrilldown.tone)}
 							class="w-full shrink-0 rounded-lg border border-indigo-200 bg-indigo-50 px-4 py-2.5 text-sm font-semibold text-indigo-700 transition-colors hover:bg-indigo-100 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-400 dark:hover:bg-indigo-500/20"
 						>
 							Saber mais detalhes
@@ -756,4 +728,4 @@
 			</div>
 		</div>
 	</div>
-</div>
+</div>

+ 0 - 146
src/routes/(app)/dashboard/ajuda/+page.svelte

@@ -1,146 +0,0 @@
-<script>
-	import { HelpCircle, MessageCircle, Mail, Book, Globe, FileText, MoreHorizontal, ChevronDown } from 'lucide-svelte';
-	let expandedFaq = null;
-
-	const supportChannels = [
-		{
-			title: 'Central de Ajuda',
-			description: 'Acesse nossa base de conhecimento com artigos e tutoriais',
-			icon: Book,
-			action: 'Acessar',
-			link: '#'
-		},
-		{
-			title: 'E-mail de Suporte',
-			description: 'Envie suas dúvidas para nossa equipe de suporte',
-			icon: Mail,
-			action: 'Enviar',
-			link: 'mailto:support@nettown.com'
-		},
-		{
-			title: 'Chat ao Vivo',
-			description: 'Converse com nossa equipe em tempo real',
-			icon: MessageCircle,
-			action: 'Iniciar Chat',
-			link: '#'
-		},
-		{
-			title: 'Documentação',
-			description: 'Leia a documentação técnica e guias de integração',
-			icon: FileText,
-			action: 'Ler Docs',
-			link: '#'
-		}
-	];
-
-	const faqs = [
-		{
-			question: 'Como funciona a análise de sentimento?',
-			answer: 'Nossa análise de sentimento utiliza inteligência artificial para avaliar o tom e a emoção em conversas. O sistema analisa mensagens de texto e categoriza o sentimento em positivo, negativo ou neutro.'
-		},
-		{
-			question: 'Quais plataformas são suportadas?',
-			answer: 'Atualmente suportamos integração com WhatsApp Business. Estamos trabalhando em suporte para outras plataformas como Messenger, Telegram e SMS.'
-		},
-		{
-			question: 'Como faço para integrar minha conta do WhatsApp?',
-			answer: 'Você pode integrar sua conta do WhatsApp Business através das configurações de integrações. Será necessário ter um número de telefone comercial e acesso à sua conta do Meta Business.'
-		},
-		{
-			question: 'Posso exportar meus dados?',
-			answer: 'Sim! Você pode exportar seus dados em formato CSV ou Excel a qualquer momento. Esta opção está disponível nas configurações de conta.'
-		},
-		{
-			question: 'Qual é o tempo de resposta do suporte?',
-			answer: 'Nosso tempo médio de resposta é de 24 horas para suporte por email. Se você tem um plano Profissional ou Empresa, oferecemos suporte prioritário com tempo de resposta de até 4 horas.'
-		},
-		{
-			question: 'Como posso cancelar minha assinatura?',
-			answer: 'Você pode cancelar sua assinatura a qualquer momento através da seção de Assinatura em seu perfil. Seu acesso será mantido até o final do ciclo de faturamento atual.'
-		}
-	];
-
-	function toggleFaq(index) {
-		expandedFaq = expandedFaq === index ? null : index;
-	}
-</script>
-
-<div class="mx-auto max-w-4xl space-y-6">
-	<!-- Header -->
-	<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="flex items-center gap-3">
-			<div class="h-8 w-8 shrink-0 rounded-lg bg-indigo-500/20 text-indigo-500 flex items-center justify-center">
-				<HelpCircle size={20} strokeWidth={2.5} />
-			</div>
-			<h1 class="text-xl font-bold text-slate-900 dark:text-white">Ajuda e Suporte</h1>
-		</div>
-		<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
-			Encontre respostas para suas dúvidas e entre em contato com nosso suporte
-		</p>
-	</div>
-
-	<!-- Support Channels -->
-	<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-		{#each supportChannels as channel}
-			{@const Icon = channel.icon}
-			<a
-				href={channel.link}
-				class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-all duration-200 dark:border-slate-800 dark:bg-[#161f30] hover:border-indigo-400 dark:hover:border-indigo-500 hover:shadow-md"
-			>
-				<div class="flex items-start justify-between mb-3">
-					<div class="h-10 w-10 rounded-lg bg-indigo-500/10 text-indigo-600 flex items-center justify-center dark:bg-indigo-900/30 dark:text-indigo-400">
-						<Icon size={20} strokeWidth={2} />
-					</div>
-					<button class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
-						<MoreHorizontal size={16} />
-					</button>
-				</div>
-				<h3 class="font-semibold text-slate-900 dark:text-white mb-1">{channel.title}</h3>
-				<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">{channel.description}</p>
-				<button class="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300">
-					{channel.action} →
-				</button>
-			</a>
-		{/each}
-	</div>
-
-	<!-- FAQ Section -->
-	<div class="space-y-4">
-		<h2 class="text-lg font-semibold text-slate-900 dark:text-white">Perguntas Frequentes</h2>
-
-		<div class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30] divide-y divide-slate-200 dark:divide-slate-800">
-			{#each faqs as faq, index}
-				<div class="p-5 hover:bg-slate-50 transition-colors duration-150 dark:hover:bg-slate-800/50">
-					<button
-						onclick={() => toggleFaq(index)}
-						class="w-full flex items-center justify-between"
-					>
-						<h3 class="text-sm font-medium text-slate-900 dark:text-white text-left">{faq.question}</h3>
-						<ChevronDown
-							size={18}
-							class="text-slate-400 shrink-0 transition-transform duration-200 {expandedFaq === index ? 'rotate-180' : ''}"
-						/>
-					</button>
-
-					{#if expandedFaq === index}
-						<p class="mt-3 text-sm text-slate-600 dark:text-slate-400 text-left">
-							{faq.answer}
-						</p>
-					{/if}
-				</div>
-			{/each}
-		</div>
-	</div>
-
-	<!-- Contact Info -->
-	<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4">Não encontrou o que procura?</h2>
-		<p class="text-slate-600 dark:text-slate-400 mb-4">
-			Entre em contato com nossa equipe de suporte através de qualquer um dos canais acima. Estamos aqui para ajudá-lo!
-		</p>
-		<div class="space-y-2 text-sm text-slate-600 dark:text-slate-400">
-			<p><strong class="text-slate-900 dark:text-white">E-mail:</strong> support@nettown.com</p>
-			<p><strong class="text-slate-900 dark:text-white">Horário de atendimento:</strong> Segunda à Sexta, 9:00 às 18:00</p>
-		</div>
-	</div>
-</div>

+ 113 - 6
src/routes/(app)/dashboard/analytics/+page.svelte

@@ -5,19 +5,27 @@
 	import GainLossChart from '$lib/features/sentiment/ui/GainLossChart.svelte';
 	import AspectFeedbackPanel from '$lib/features/sentiment/ui/AspectFeedbackPanel.svelte';
 	import {
-		getSentimentDashboardViewModel,
+		mapSentimentDashboardData,
 		getSummaryInsight,
 		getAlertInsight,
 		getTimelineInsight
 	} from '$lib/features/sentiment/domain/sentiment-dashboard.service.js';
 	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
-	const dashboardData = getSentimentDashboardViewModel();
 	const timeframeOptions = [
 		{ id: 'day', label: 'Últimos 7 dias' },
 		{ id: 'week', label: 'Últimas 6 semanas' },
-		{ id: 'month', label: 'Últimos 6 meses' }
+		{ id: 'month', label: 'Últimos 6 meses' },
+		{ id: 'custom', label: 'Personalizado' }
 	];
+	const emptyViewModel = {
+		summaryCards: [],
+		alerts: [],
+		timeline: [],
+		timelineViews: { day: [], week: [], month: [] },
+		aspects: []
+	};
 	const defaultInsight = {
 		title: 'Painel de decisao',
 		context: 'Clique em um card, alerta ou ponto do grafico para entender o que esta acontecendo e qual acao executar.',
@@ -28,18 +36,65 @@
 		]
 	};
 
+	let dashboardData = $state(emptyViewModel);
+	let loading = $state(true);
+	let loadError = $state('');
+
 	let selectedCardId = $state(null);
 	let selectedAlertId = $state(null);
 	let selectedPeriod = $state(null);
 	let selectedTimeframe = $state('week');
 	let selectedInsight = $state(defaultInsight);
-	const timelineData = $derived(dashboardData.timelineViews?.[selectedTimeframe] ?? dashboardData.timeline);
+	let customStart = $state('');
+	let customEnd = $state('');
+
+	const timelineData = $derived(
+		dashboardData.timelineViews?.[selectedTimeframe] ?? dashboardData.timeline
+	);
 
-	// Read initial state from URL query parameters
-	let panelAspectId = $state($page.url.searchParams.get('aspect') ?? null);
+	// Estado inicial via query params (vindo do drilldown do dashboard).
+	// aspect é normalizado para lowercase para casar com o slug do backend.
+	let panelAspectId = $state(($page.url.searchParams.get('aspect') ?? '').toLowerCase() || null);
 	let panelSentiment = $state($page.url.searchParams.get('sentiment') ?? 'positive');
 
+	async function loadDashboard() {
+		// Para timeframe personalizado, só busca quando as duas datas existem.
+		if (selectedTimeframe === 'custom' && (!customStart || !customEnd)) {
+			return;
+		}
+
+		loading = true;
+		loadError = '';
+		try {
+			const query = {};
+			if (selectedTimeframe === 'custom') {
+				query.timeframe = 'custom';
+				query.start_date = customStart;
+				query.end_date = customEnd;
+			} else {
+				query.timeframe = selectedTimeframe;
+			}
+			if (panelAspectId) {
+				query.aspect = panelAspectId.toLowerCase();
+			}
+			const data = await api.get('/v1/analytics/sentiment/dashboard', { query });
+			dashboardData = mapSentimentDashboardData(data);
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar a análise de sentimento.';
+			dashboardData = emptyViewModel;
+		} finally {
+			loading = false;
+		}
+	}
+
+	function applyCustomRange() {
+		if (customStart && customEnd) {
+			loadDashboard();
+		}
+	}
+
 	onMount(() => {
+		loadDashboard();
 		if (window.location.hash === '#aspects-panel') {
 			setTimeout(() => {
 				const element = document.getElementById('aspects-panel');
@@ -75,6 +130,10 @@
 		selectedTimeframe = value;
 		selectedPeriod = null;
 		selectedInsight = defaultInsight;
+		// "custom" só dispara o fetch quando as duas datas estiverem preenchidas.
+		if (value !== 'custom') {
+			loadDashboard();
+		}
 	}
 </script>
 
@@ -83,6 +142,54 @@
 </svelte:head>
 
 <div class="mx-auto max-w-[1600px] space-y-6 transition-colors duration-200">
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
+	{#if selectedTimeframe === 'custom'}
+		<div
+			class="flex flex-wrap items-end gap-3 rounded-xl border border-slate-200 bg-white p-4 text-sm shadow-sm dark:border-slate-800 dark:bg-[#1e293b]"
+		>
+			<div class="flex flex-col gap-1">
+				<label for="custom-start" class="text-xs font-medium text-slate-500 dark:text-slate-400"
+					>Data inicial</label
+				>
+				<input
+					id="custom-start"
+					type="date"
+					bind:value={customStart}
+					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"
+				/>
+			</div>
+			<div class="flex flex-col gap-1">
+				<label for="custom-end" class="text-xs font-medium text-slate-500 dark:text-slate-400"
+					>Data final</label
+				>
+				<input
+					id="custom-end"
+					type="date"
+					bind:value={customEnd}
+					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"
+				/>
+			</div>
+			<button
+				type="button"
+				onclick={applyCustomRange}
+				disabled={!customStart || !customEnd}
+				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"
+			>
+				Aplicar
+			</button>
+			{#if loading}
+				<span class="pb-2 text-xs text-slate-400">Carregando…</span>
+			{/if}
+		</div>
+	{/if}
+
 	<SummaryCards
 		cards={dashboardData.summaryCards}
 		onCardSelect={handleCardSelect}

+ 0 - 164
src/routes/(app)/dashboard/assinatura/+page.svelte

@@ -1,164 +0,0 @@
-<script>
-	import { CreditCard, Calendar, AlertCircle, Check } from 'lucide-svelte';
-
-	let currentPlan = {
-		name: 'Profissional',
-		price: 299,
-		billingCycle: 'mensal',
-		status: 'ativo',
-		nextBillingDate: '15/06/2026'
-	};
-
-	const plans = [
-		{
-			name: 'Starter',
-			price: 99,
-			description: 'Perfeito para começar',
-			features: [
-				'Até 5 clientes',
-				'Análise básica de sentimento',
-				'Relatórios mensais',
-				'Suporte por email'
-			],
-			popular: false
-		},
-		{
-			name: 'Profissional',
-			price: 299,
-			description: 'Mais recursos e análises',
-			features: [
-				'Até 50 clientes',
-				'Análise avançada de sentimento',
-				'Relatórios em tempo real',
-				'API access',
-				'Suporte prioritário',
-				'Integrações WhatsApp'
-			],
-			popular: true
-		},
-		{
-			name: 'Empresa',
-			price: 999,
-			description: 'Recursos completos',
-			features: [
-				'Clientes ilimitados',
-				'Análise completa de IA',
-				'Dashboards personalizados',
-				'API completa',
-				'Suporte 24/7',
-				'Todas as integrações',
-				'Usuários ilimitados'
-			],
-			popular: false
-		}
-	];
-
-	function handleUpgrade(plan) {
-		console.log(`Upgrading to ${plan.name} plan`);
-	}
-
-	function handleCancelSubscription() {
-		if (confirm('Tem certeza que deseja cancelar sua assinatura?')) {
-			console.log('Cancelando assinatura...');
-		}
-	}
-</script>
-
-<div class="mx-auto max-w-4xl space-y-6">
-	<!-- Header -->
-	<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="flex items-center gap-3">
-			<div class="h-8 w-8 shrink-0 rounded-lg bg-indigo-500/20 text-indigo-500 flex items-center justify-center">
-				<CreditCard size={20} strokeWidth={2.5} />
-			</div>
-			<h1 class="text-xl font-bold text-slate-900 dark:text-white">Assinatura</h1>
-		</div>
-		<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
-			Gerencie seu plano, faturamento e métodos de pagamento
-		</p>
-	</div>
-
-	<!-- Current Plan -->
-	<div class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="border-b border-slate-200 p-5 dark:border-slate-800">
-			<h2 class="text-lg font-semibold text-slate-900 dark:text-white">Seu Plano Atual</h2>
-		</div>
-
-		<div class="p-5 md:p-6">
-			<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
-				<div>
-					<p class="text-sm text-slate-600 dark:text-slate-400 mb-1">Plano</p>
-					<p class="text-2xl font-bold text-slate-900 dark:text-white">{currentPlan.name}</p>
-				</div>
-				<div>
-					<p class="text-sm text-slate-600 dark:text-slate-400 mb-1">Preço</p>
-					<p class="text-2xl font-bold text-slate-900 dark:text-white">R$ {currentPlan.price}</p>
-					<p class="text-xs text-slate-500 dark:text-slate-400">por mês</p>
-				</div>
-				<div>
-					<p class="text-sm text-slate-600 dark:text-slate-400 mb-1">Próxima cobrança</p>
-					<div class="flex items-center gap-2">
-						<Calendar size={18} class="text-indigo-600 dark:text-indigo-400" />
-						<span class="text-lg font-semibold text-slate-900 dark:text-white">{currentPlan.nextBillingDate}</span>
-					</div>
-				</div>
-			</div>
-
-			<div class="flex flex-col md:flex-row gap-3 pt-4 border-t border-slate-200 dark:border-slate-800">
-				<button class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors duration-150">
-					Atualizar Método de Pagamento
-				</button>
-				<button
-					onclick={handleCancelSubscription}
-					class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-red-300 text-red-600 hover:bg-red-50 transition-colors duration-150 dark:border-red-800 dark:text-red-400 dark:hover:bg-slate-800"
-				>
-					Cancelar Assinatura
-				</button>
-			</div>
-		</div>
-	</div>
-
-	<!-- Available Plans -->
-	<div class="space-y-4">
-		<h2 class="text-lg font-semibold text-slate-900 dark:text-white">Outros Planos Disponíveis</h2>
-
-		<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
-			{#each plans as plan}
-				<div class="rounded-xl border {plan.popular ? 'border-indigo-500 ring-2 ring-indigo-100 dark:ring-indigo-900/30' : 'border-slate-200 dark:border-slate-800'} bg-white shadow-sm transition-colors duration-200 dark:bg-[#161f30] overflow-hidden">
-					{#if plan.popular}
-						<div class="bg-indigo-600 px-4 py-2 text-center">
-							<span class="text-xs font-semibold text-white uppercase tracking-wider">Mais Popular</span>
-						</div>
-					{/if}
-
-					<div class="p-5 md:p-6">
-						<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">{plan.name}</h3>
-						<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">{plan.description}</p>
-
-						<div class="mb-6">
-							<span class="text-3xl font-bold text-slate-900 dark:text-white">R$ {plan.price}</span>
-							<span class="text-sm text-slate-600 dark:text-slate-400">/mês</span>
-						</div>
-
-						<button
-							onclick={() => handleUpgrade(plan)}
-							disabled={plan.name === currentPlan.name}
-							class="w-full px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-150 {plan.popular ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700'} {plan.name === currentPlan.name ? 'opacity-50 cursor-not-allowed' : ''}"
-						>
-							{plan.name === currentPlan.name ? 'Plano Atual' : 'Escolher Plano'}
-						</button>
-
-						<div class="mt-6 space-y-3 pt-6 border-t border-slate-200 dark:border-slate-800">
-							{#each plan.features as feature}
-								<div class="flex items-center gap-3">
-									<Check size={16} class="text-green-600 dark:text-green-400 shrink-0" />
-									<span class="text-sm text-slate-700 dark:text-slate-300">{feature}</span>
-								</div>
-							{/each}
-						</div>
-					</div>
-				</div>
-			{/each}
-		</div>
-	</div>
-</div>

+ 205 - 314
src/routes/(app)/dashboard/evolucao/+page.svelte

@@ -1,119 +1,58 @@
 <script>
-	import { TrendingUp, Shield, DollarSign, CheckCircle, Heart, Activity } from 'lucide-svelte';
+	import { TrendingUp, Shield, DollarSign, CheckCircle, Heart } from 'lucide-svelte';
 	import { Chart, Svg, Axis, Spline, Highlight } from 'layerchart';
 	import { format } from 'date-fns';
-	import { mockEvolucaoSentimentosData, mockMonitoramentoPlaybooksData } from '$lib/core/models/mock-data.js';
+	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
 	const periodOptions = [
-		{ id: 'week', label: 'Últimos 7 dias', multiplier: 1, sentimentBoost: 0 },
-		{ id: 'month', label: 'Últimos 30 dias', multiplier: 1.25, sentimentBoost: 0.05 },
-		{ id: 'quarter', label: 'Último trimestre', multiplier: 1.5, sentimentBoost: 0.08 }
+		{ id: 'week', label: 'Últimos 7 dias' },
+		{ id: 'month', label: 'Últimos 30 dias' },
+		{ id: 'quarter', label: 'Último trimestre' }
 	];
+	let selectedPeriod = $state('week');
 
-	const unitOptions = [
-		{ id: 'all', label: 'Sem segmento', multiplier: 1 },
-		{ id: 'flagship', label: 'Loja Flagship', multiplier: 1.2 },
-		{ id: 'digital', label: 'Digital', multiplier: 1.1 },
-		{ id: 'atacado', label: 'Atacado', multiplier: 0.85 }
-	];
-
-	const areaOptions = [
-		{ id: 'any', label: 'Sem segmento de setor', multiplier: 1 },
-		{ id: 'atendimento', label: 'Atendimento', multiplier: 1.15 },
-		{ id: 'produto', label: 'Produto', multiplier: 1.05 },
-		{ id: 'logistica', label: 'Logística', multiplier: 0.95 }
-	];
-
-	const lineOptions = [
-		{ id: 'all', label: 'Todas', multiplier: 1 },
-		{ id: 'luxo', label: 'Linha Luxo', multiplier: 1.3 },
-		{ id: 'casual', label: 'Linha Casual', multiplier: 1.1 },
-		{ id: 'basicos', label: 'Linha Básicos', multiplier: 0.9 }
-	];
+	// ── Dados (GET /v1/evolution/overview) ──────────────────────────────────────
+	let kpis = $state({ churnEvitado: 0, roiUpsell: 0, scoreMedio: 0, taxaEvolucao: 0, conversaoEmocao: 0 });
+	let evolucaoChartData = $state([]);
+	let playbooksChartData = $state([]);
+	let playbooksTotals = $state({ novos: 0, convertidos: 0 });
+	let playbooksMaxY = $state(60);
+	let loading = $state(true);
+	let loadError = $state('');
 
-	const sentimentOptions = [
-		{
-			id: 'all',
-			label: 'Todos',
-			bias: { churn: 1, roi: 1, satisfaction: 1, trend: 1, conversion: 1, sentimentShift: 1, detection: 1, conversionRate: 1 }
-		},
-		{
-			id: 'positive',
-			label: 'Positivo',
-			bias: { churn: 0.85, roi: 1.15, satisfaction: 1.1, trend: 1.2, conversion: 1.2, sentimentShift: 1.15, detection: 1.05, conversionRate: 1.1 }
-		},
-		{
-			id: 'neutral',
-			label: 'Neutro',
-			bias: { churn: 1, roi: 1, satisfaction: 1, trend: 1, conversion: 1, sentimentShift: 1, detection: 1, conversionRate: 1 }
-		},
-		{
-			id: 'negative',
-			label: 'Negativo',
-			bias: { churn: 1.2, roi: 0.85, satisfaction: 0.9, trend: 0.75, conversion: 0.8, sentimentShift: 0.8, detection: 0.9, conversionRate: 0.75 }
+	async function loadEvolution() {
+		loading = true;
+		loadError = '';
+		try {
+			const data = await api.get('/v1/evolution/overview', { query: { period: selectedPeriod } });
+			kpis = data.kpis ?? { churnEvitado: 0, roiUpsell: 0, scoreMedio: 0, taxaEvolucao: 0, conversaoEmocao: 0 };
+			evolucaoChartData = (data.sentimentSeries ?? []).map((point) => ({
+				date: new Date(point.date),
+				value: Number(point.value)
+			}));
+			playbooksChartData = (data.playbooksSeries ?? []).map((point) => ({
+				date: new Date(point.date),
+				novos: Number(point.novos),
+				convertidos: Number(point.convertidos)
+			}));
+			playbooksTotals = data.playbooksTotals ?? { novos: 0, convertidos: 0 };
+			const datasetMax = playbooksChartData.reduce(
+				(max, point) => Math.max(max, point.novos, point.convertidos),
+				0
+			);
+			playbooksMaxY = Math.max(10, Math.ceil(datasetMax / 10) * 10 + 10);
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar a evolução.';
+		} finally {
+			loading = false;
 		}
-	];
-
-	const sourceOptions = [
-		{ id: 'all', label: 'Todas', multiplier: 1 },
-		{ id: 'whatsapp', label: 'WhatsApp', multiplier: 1.1 },
-		{ id: 'crm', label: 'CRM', multiplier: 1.05 },
-		{ id: 'email', label: 'E-mail', multiplier: 0.95 }
-	];
-
-	let selectedPeriod = $state(periodOptions[0].id);
-	let selectedUnit = $state(unitOptions[0].id);
-	let selectedArea = $state(areaOptions[0].id);
-	let selectedLine = $state(lineOptions[0].id);
-	let selectedSentiment = $state(sentimentOptions[0].id);
-	let selectedSource = $state(sourceOptions[0].id);
-
-	const filterContext = $derived({
-		period: periodOptions.find((opt) => opt.id === selectedPeriod) ?? periodOptions[0],
-		unit: unitOptions.find((opt) => opt.id === selectedUnit) ?? unitOptions[0],
-		area: areaOptions.find((opt) => opt.id === selectedArea) ?? areaOptions[0],
-		line: lineOptions.find((opt) => opt.id === selectedLine) ?? lineOptions[0],
-		sentiment: sentimentOptions.find((opt) => opt.id === selectedSentiment) ?? sentimentOptions[0],
-		source: sourceOptions.find((opt) => opt.id === selectedSource) ?? sourceOptions[0]
-	});
-
-	const combinedMultiplier = $derived(
-		(filterContext.period?.multiplier ?? 1) *
-			(filterContext.unit?.multiplier ?? 1) *
-			(filterContext.area?.multiplier ?? 1) *
-			(filterContext.line?.multiplier ?? 1) *
-			(filterContext.source?.multiplier ?? 1)
-	);
-
-	const baseKpis = {
-		churnEvitado: 85000,
-		roiUpsell: 3.1,
-		scoreMedio: 74,
-		taxaEvolucao: 21,
-		conversaoEmocao: 47
-	};
+	}
 
-	const kpis = $derived.by(() => {
-		const bias = filterContext.sentiment?.bias ?? sentimentOptions[0].bias;
-		const multiplier = Number.isFinite(combinedMultiplier) ? combinedMultiplier : 1;
-		const churnEvitado = Math.round(baseKpis.churnEvitado * multiplier * bias.churn);
-		const roiUpsell = Number((baseKpis.roiUpsell * bias.roi).toFixed(1));
-		const scoreMedio = clamp(
-			Math.round(baseKpis.scoreMedio * bias.satisfaction + (multiplier - 1) * 18),
-			0,
-			100
-		);
-		const taxaEvolucao = Number((baseKpis.taxaEvolucao * multiplier * bias.trend).toFixed(1));
-		const conversaoEmocao = clamp(
-			Math.round(baseKpis.conversaoEmocao * bias.conversion + (multiplier - 1) * 10),
-			0,
-			100
-		);
-		return { churnEvitado, roiUpsell, scoreMedio, taxaEvolucao, conversaoEmocao };
-	});
+	onMount(loadEvolution);
 
-	function clamp(value, min, max) {
-		return Math.min(max, Math.max(min, value));
+	function applyFilters() {
+		loadEvolution();
 	}
 
 	function formatCurrency(value) {
@@ -133,53 +72,6 @@
 		return Number.isFinite(value) ? value.toFixed(1) : '0.0';
 	}
 
-	let evolucaoChartData = $state([...mockEvolucaoSentimentosData]);
-	let playbooksChartData = $state([...mockMonitoramentoPlaybooksData]);
-	let playbooksTotals = $state(
-		[...mockMonitoramentoPlaybooksData].reduce(
-			(acc, point) => {
-				acc.novos += point.novos;
-				acc.convertidos += point.convertidos;
-				return acc;
-			},
-			{ novos: 0, convertidos: 0 }
-		)
-	);
-	let playbooksMaxY = $state(60);
-
-	$effect(() => {
-		const bias = filterContext.sentiment.bias;
-		const adjustedData = mockEvolucaoSentimentosData.map((point) => {
-			const adjusted = clamp(
-				point.value * bias.sentimentShift + (combinedMultiplier - 1) * 0.12 + filterContext.period.sentimentBoost,
-				-1,
-				1
-			);
-			return { ...point, value: Number(adjusted.toFixed(2)) };
-		});
-		evolucaoChartData = adjustedData;
-	});
-
-	$effect(() => {
-		const bias = filterContext.sentiment.bias;
-		const adjustedPlaybooks = mockMonitoramentoPlaybooksData.map((point) => {
-			const novos = Math.max(0, Math.round(point.novos * combinedMultiplier * bias.detection));
-			const convertidos = Math.max(0, Math.round(point.convertidos * combinedMultiplier * bias.conversionRate));
-			return { ...point, novos, convertidos };
-		});
-		playbooksChartData = adjustedPlaybooks;
-		playbooksTotals = adjustedPlaybooks.reduce(
-			(acc, point) => {
-				acc.novos += point.novos;
-				acc.convertidos += point.convertidos;
-				return acc;
-			},
-			{ novos: 0, convertidos: 0 }
-		);
-		const datasetMax = adjustedPlaybooks.reduce((max, point) => Math.max(max, point.novos, point.convertidos), 0);
-		playbooksMaxY = Math.max(10, Math.ceil(datasetMax / 10) * 10 + 10);
-	});
-
 	function formatSentimentAxis(val) {
 		if (val === 1) return 'Pos.';
 		if (val === 0) return 'Neutro';
@@ -194,110 +86,99 @@
 
 <div class="mx-auto max-w-[1600px] space-y-6">
 	<!-- Top Section: Header, Filters, KPIs -->
-	<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-8 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="flex items-center gap-3 mb-8">
-			<div class="h-8 w-8 shrink-0 rounded-lg bg-blue-500/20 text-blue-500 flex items-center justify-center">
+	<div
+		class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 md:p-8 dark:border-slate-800 dark:bg-[#161f30]"
+	>
+		<div class="mb-8 flex items-center gap-3">
+			<div
+				class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-500/20 text-blue-500"
+			>
 				<TrendingUp size={20} strokeWidth={2.5} />
 			</div>
-			<h1 class="text-xl font-bold text-slate-900 dark:text-white">Evolução: Observatório de Resultados</h1>
+			<h1 class="text-xl font-bold text-slate-900 dark:text-white">
+				Evolução: Observatório de Resultados
+			</h1>
 		</div>
 
-		<!-- Filters -->
-		<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 w-full text-sm mb-12">
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Período</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedPeriod}
-				>
-					{#each periodOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Unidade</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedUnit}
-				>
-					{#each unitOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Área</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedArea}
-				>
-					{#each areaOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Linha</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedLine}
-				>
-					{#each lineOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Sentimento</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedSentiment}
-				>
-					{#each sentimentOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex flex-col gap-1.5">
-				<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Fonte</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedSource}
-				>
-					{#each sourceOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
+		<!-- Filtro de período -->
+		<div class="mb-12 flex w-full items-center gap-2 text-sm">
+			<span class="text-xs font-semibold text-slate-500 dark:text-slate-400">Período</span>
+			<select
+				class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
+				bind:value={selectedPeriod}
+				onchange={applyFilters}
+			>
+				{#each periodOptions as option}
+					<option value={option.id}>{option.label}</option>
+				{/each}
+			</select>
+			{#if loading}
+				<span class="text-xs text-slate-400">Carregando…</span>
+			{/if}
 		</div>
 
+		{#if loadError}
+			<div
+				class="mb-6 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+			>
+				{loadError}
+			</div>
+		{/if}
+
 		<!-- KPIs -->
-		<div class="grid grid-cols-2 md:grid-cols-5 gap-6 w-full pb-4">
+		<div class="grid w-full grid-cols-2 gap-6 pb-4 md:grid-cols-5">
 			<div class="flex flex-col items-center justify-center text-center">
-				<Shield size={24} class="text-red-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{formatCurrency(kpis.churnEvitado)}</div>
-				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Churn Evitado (Valor)</div>
+				<Shield size={24} class="mx-auto mb-3 text-red-500" />
+				<div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
+					{formatCurrency(kpis.churnEvitado)}
+				</div>
+				<div
+					class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+				>
+					Churn Evitado (Valor)
+				</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
-				<DollarSign size={24} class="text-emerald-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{formatRatio(kpis.roiUpsell)}x</div>
-				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">ROI de Upsell</div>
+				<DollarSign size={24} class="mx-auto mb-3 text-emerald-500" />
+				<div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
+					{formatRatio(kpis.roiUpsell)}x
+				</div>
+				<div
+					class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+				>
+					ROI de Upsell
+				</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
-				<CheckCircle size={24} class="text-amber-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{kpis.scoreMedio}</div>
-				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Score Médio Geral</div>
+				<CheckCircle size={24} class="mx-auto mb-3 text-amber-500" />
+				<div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">{kpis.scoreMedio}</div>
+				<div
+					class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+				>
+					Score Médio Geral
+				</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
-				<TrendingUp size={24} class="text-blue-400 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{formatPercent(kpis.taxaEvolucao)}</div>
-				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Taxa de Evolução</div>
+				<TrendingUp size={24} class="mx-auto mb-3 text-blue-400" />
+				<div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
+					{formatPercent(kpis.taxaEvolucao)}
+				</div>
+				<div
+					class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+				>
+					Taxa de Evolução
+				</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
-				<Heart size={24} class="text-pink-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{formatPercent(kpis.conversaoEmocao)}</div>
-				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Conversão de Emoção</div>
+				<Heart size={24} class="mx-auto mb-3 text-pink-500" />
+				<div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
+					{formatPercent(kpis.conversaoEmocao)}
+				</div>
+				<div
+					class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+				>
+					Conversão de Emoção
+				</div>
 			</div>
 		</div>
 	</div>
@@ -305,55 +186,63 @@
 	<!-- Charts Area -->
 	<div class="grid grid-cols-1 gap-6">
 		<!-- Chart 1: Evolucao dos Sentimentos Geral -->
-		<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30] h-[360px] flex flex-col">
-			<h2 class="text-base font-bold text-slate-900 dark:text-white mb-4">Evolução dos Sentimentos Geral</h2>
+		<div
+			class="flex h-[360px] flex-col rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 md:p-6 dark:border-slate-800 dark:bg-[#161f30]"
+		>
+			<h2 class="mb-4 text-base font-bold text-slate-900 dark:text-white">
+				Evolução dos Sentimentos Geral
+			</h2>
 			<div class="h-full w-full flex-1">
-				<Chart
-					data={evolucaoChartData}
-					x={(d) => d.date}
-					y={(d) => d.value}
-					yDomain={[-1, 1]}
-					padding={{ top: 10, right: 10, bottom: 20, left: 40 }}
-				>
-					<Svg>
-						<Axis
-							placement="left"
-							grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-							format={formatSentimentAxis}
-							ticks={[-1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1]}
-						/>
-						<Axis
-							placement="bottom"
-							format={(d) => format(d, 'dd/MM')}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-						/>
-						<Spline stroke="#6366f1" strokeWidth={3} />
-						<Highlight
-							points={{
-								fill: '#6366f1',
-								class: 'stroke-white dark:stroke-slate-900',
-								strokeWidth: 2
-							}}
-						/>
-					</Svg>
-				</Chart>
+				{#if evolucaoChartData.length > 0}
+					<Chart
+						data={evolucaoChartData}
+						x={(d) => d.date}
+						y={(d) => d.value}
+						yDomain={[-1, 1]}
+						padding={{ top: 10, right: 10, bottom: 20, left: 40 }}
+					>
+						<Svg>
+							<Axis
+								placement="left"
+								grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+								format={formatSentimentAxis}
+								ticks={[-1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1]}
+							/>
+							<Axis
+								placement="bottom"
+								format={(d) => format(d, 'dd/MM')}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+							/>
+							<Spline stroke="#6366f1" strokeWidth={3} />
+							<Highlight
+								points={{ fill: '#6366f1', class: 'stroke-white dark:stroke-slate-900', strokeWidth: 2 }}
+							/>
+						</Svg>
+					</Chart>
+				{:else}
+					<div class="flex h-full items-center justify-center text-sm text-slate-400">
+						Sem dados de sentimento no período.
+					</div>
+				{/if}
 			</div>
 		</div>
 
 		<!-- Chart 2: Monitoramento de Playbooks -->
-		<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30] h-[360px] flex flex-col">
-			<div class="flex items-center justify-between mb-4">
+		<div
+			class="flex h-[360px] flex-col rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 md:p-6 dark:border-slate-800 dark:bg-[#161f30]"
+		>
+			<div class="mb-4 flex items-center justify-between">
 				<div class="flex items-center gap-4">
-					<h2 class="text-base font-bold text-slate-900 dark:text-white">Monitoramento de Playbooks (Prospecção Anônima)</h2>
-					<div class="flex items-center text-sm font-medium">
-						<button class="px-3 py-1 text-slate-400 hover:text-white transition-colors">Gráfico</button>
-						<button class="px-3 py-1 text-slate-400 hover:text-white transition-colors">Lista</button>
-					</div>
+					<h2 class="text-base font-bold text-slate-900 dark:text-white">
+						Monitoramento de Playbooks (Prospecção Anônima)
+					</h2>
 				</div>
 				<div class="flex items-center gap-6">
 					<div class="flex flex-col items-end">
-						<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Total do Período</span>
+						<span class="text-[10px] font-bold tracking-wider text-slate-500 uppercase"
+							>Total do Período</span
+						>
 						<div class="flex gap-1 text-sm font-bold">
 							<span class="text-indigo-400">{formatNumber(playbooksTotals.novos)}</span>
 							<span class="text-slate-500">/</span>
@@ -362,49 +251,51 @@
 					</div>
 					<div class="flex items-center gap-4 text-xs font-medium text-slate-400">
 						<div class="flex items-center gap-1.5">
-							<div class="w-3 h-3 rounded-sm bg-indigo-500"></div>
+							<div class="h-3 w-3 rounded-sm bg-indigo-500"></div>
 							Novos Detectados
 						</div>
 						<div class="flex items-center gap-1.5">
-							<div class="w-3 h-3 rounded-sm bg-emerald-500"></div>
+							<div class="h-3 w-3 rounded-sm bg-emerald-500"></div>
 							Cadastrados Convertidos
 						</div>
 					</div>
 				</div>
 			</div>
-			
+
 			<div class="h-full w-full flex-1">
-				<Chart
-					data={playbooksChartData}
-					x={(d) => d.date}
-					yDomain={[0, playbooksMaxY]}
-					padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
-				>
-					<Svg>
-						<Axis
-							placement="left"
-							grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-							ticks={[0, playbooksMaxY * 0.25, playbooksMaxY * 0.5, playbooksMaxY * 0.75, playbooksMaxY]}
-						/>
-						<Axis
-							placement="bottom"
-							format={(d) => format(d, 'dd/MM')}
-							class="fill-slate-500 text-xs dark:fill-slate-400"
-						/>
-						
-						<Spline y={d => d.novos} stroke="#6366f1" strokeWidth={3} />
-						<Spline y={d => d.convertidos} stroke="#10b981" strokeWidth={3} />
-						
-						<Highlight
-							points={{
-								fill: 'white',
-								class: 'stroke-slate-400 dark:stroke-slate-500',
-								strokeWidth: 2
-							}}
-						/>
-					</Svg>
-				</Chart>
+				{#if playbooksChartData.length > 0}
+					<Chart
+						data={playbooksChartData}
+						x={(d) => d.date}
+						yDomain={[0, playbooksMaxY]}
+						padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
+					>
+						<Svg>
+							<Axis
+								placement="left"
+								grid={{ class: 'stroke-slate-200 dark:stroke-slate-700', strokeDasharray: '2 2' }}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+								ticks={[0, playbooksMaxY * 0.25, playbooksMaxY * 0.5, playbooksMaxY * 0.75, playbooksMaxY]}
+							/>
+							<Axis
+								placement="bottom"
+								format={(d) => format(d, 'dd/MM')}
+								class="fill-slate-500 text-xs dark:fill-slate-400"
+							/>
+
+							<Spline y={(d) => d.novos} stroke="#6366f1" strokeWidth={3} />
+							<Spline y={(d) => d.convertidos} stroke="#10b981" strokeWidth={3} />
+
+							<Highlight
+								points={{ fill: 'white', class: 'stroke-slate-400 dark:stroke-slate-500', strokeWidth: 2 }}
+							/>
+						</Svg>
+					</Chart>
+				{:else}
+					<div class="flex h-full items-center justify-center text-sm text-slate-400">
+						Sem dados de playbooks no período.
+					</div>
+				{/if}
 			</div>
 		</div>
 	</div>

+ 179 - 172
src/routes/(app)/dashboard/executive/+page.svelte

@@ -13,74 +13,68 @@
 		BookOpen
 	} from 'lucide-svelte';
 	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
 	let isLoading = $state(true);
-	onMount(() => {
-		setTimeout(() => {
+	let loadError = $state('');
+	let data = $state(null);
+
+	async function loadExecutive() {
+		isLoading = true;
+		loadError = '';
+		try {
+			data = await api.get('/v1/executive/dashboard');
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar o dashboard executivo.';
+		} finally {
 			isLoading = false;
-		}, 700);
-	});
+		}
+	}
+
+	onMount(loadExecutive);
 
-	// ── Section 1: Top KPIs ──────────────────────────────────────────────────
-	const topKpis = [
+	// ── Section 1: Top KPIs (ordem fixa do backend) ──────────────────────────────
+	const topKpiVisuals = [
 		{
-			title: 'Venda Atual',
-			value: 'R$ 879',
-			trendLabel: '↑ +12% vs ontem',
-			trendColor: 'text-emerald-600 dark:text-emerald-400',
 			icon: DollarSign,
-			danger: false,
 			color: 'text-emerald-600 dark:text-emerald-400',
 			bg: 'bg-emerald-50 dark:bg-emerald-400/10',
-			border: 'border-emerald-200 dark:border-emerald-400/20'
+			border: 'border-emerald-200 dark:border-emerald-400/20',
+			trendColor: 'text-emerald-600 dark:text-emerald-400'
 		},
 		{
-			title: 'Ticket Médio',
-			value: 'R$ 655,00',
-			trendLabel: '→ estável',
-			trendColor: 'text-slate-500 dark:text-slate-400',
 			icon: BarChart2,
-			danger: false,
 			color: 'text-sky-600 dark:text-sky-400',
 			bg: 'bg-sky-50 dark:bg-sky-400/10',
-			border: 'border-sky-200 dark:border-sky-400/20'
+			border: 'border-sky-200 dark:border-sky-400/20',
+			trendColor: 'text-slate-500 dark:text-slate-400'
 		},
 		{
-			title: 'Lifetime em Risco',
-			value: 'R$ 8.639',
-			trendLabel: '↑ +5% esta semana',
-			trendColor: 'text-red-600 dark:text-red-400',
 			icon: AlertTriangle,
-			danger: true,
 			color: 'text-red-600 dark:text-red-400',
 			bg: 'bg-red-50 dark:bg-red-400/10',
-			border: 'border-red-200 dark:border-red-400/20'
+			border: 'border-red-200 dark:border-red-400/20',
+			trendColor: 'text-red-600 dark:text-red-400'
 		},
 		{
-			title: 'Clientes em Risco Crítico',
-			value: '34',
-			trendLabel: '↑ +3 desde ontem',
-			trendColor: 'text-red-600 dark:text-red-400',
 			icon: Users,
-			danger: true,
 			color: 'text-red-600 dark:text-red-400',
 			bg: 'bg-red-50 dark:bg-red-400/10',
-			border: 'border-red-200 dark:border-red-400/20'
+			border: 'border-red-200 dark:border-red-400/20',
+			trendColor: 'text-red-600 dark:text-red-400'
 		}
 	];
 
-	// ── Section 2: Churn donut ───────────────────────────────────────────────
-	const churnData = [
-		{ label: 'Baixo', value: 45, color: '#10b981' },
-		{ label: 'Moderado', value: 30, color: '#f59e0b' },
-		{ label: 'Alto', value: 15, color: '#f97316' },
-		{ label: 'Crítico', value: 10, color: '#ef4444' }
-	];
+	const topKpis = $derived(
+		(data?.topKpis ?? []).map((kpi, i) => ({ ...(topKpiVisuals[i] ?? topKpiVisuals[0]), ...kpi }))
+	);
 
-	function buildDonut(data, cx, cy, r, ri) {
-		const total = data.reduce((s, d) => s + d.value, 0);
+	// ── Section 2: Churn donut ───────────────────────────────────────────────────
+	function buildDonut(items, cx, cy, r, ri) {
+		const total = items.reduce((s, d) => s + d.value, 0);
+		if (total <= 0) return [];
 		let a = -Math.PI / 2;
-		return data.map((d) => {
+		return items.map((d) => {
 			const sweep = (d.value / total) * 2 * Math.PI;
 			const a1 = a;
 			const a2 = a + sweep;
@@ -102,100 +96,111 @@
 		});
 	}
 
-	const donutPaths = buildDonut(churnData, 60, 60, 50, 28);
+	const churnData = $derived(data?.churnDistribution ?? []);
+	const donutPaths = $derived(buildDonut(churnData, 60, 60, 50, 28));
 
-	// ── Section 2: LTV em Risco ──────────────────────────────────────────────
-	const ltvTotal = 285000;
-	const ltvAtRisk = 34556;
-	const ltvRiskPct = Math.round((ltvAtRisk / ltvTotal) * 100);
+	// ── Section 2: LTV em Risco ──────────────────────────────────────────────────
+	const ltv = $derived(
+		data?.ltvRisk ?? {
+			ltvTotal: 0,
+			ltvAtRisk: 0,
+			ltvRiskPct: 0,
+			criticalClients: 0,
+			avgTicket: 0,
+			trendText: ''
+		}
+	);
 
-	// ── Section 2: SLA ──────────────────────────────────────────────────────
-	const slaWithinPct = 78;
-	const slaBreachPct = 22;
+	// ── Section 2: SLA ────────────────────────────────────────────────────────────
+	const sla = $derived(data?.sla ?? { withinPct: 0, breachPct: 0, byDepartment: [] });
 
-	// ── Section 3: Sentiment ─────────────────────────────────────────────────
-	const emotionData = [
-		{ label: 'Alegria', value: 38, color: '#10b981', count: 1524 },
-		{ label: 'Confiança', value: 22, color: '#6366f1', count: 882 },
-		{ label: 'Medo', value: 12, color: '#f97316', count: 481 },
-		{ label: 'Raiva', value: 5, color: '#ef4444', count: 201 },
-		{ label: 'Surpresa', value: 15, color: '#f59e0b', count: 602 },
-		{ label: 'Tristeza', value: 8, color: '#64748b', count: 321 }
-	];
+	function deptSlaColor(value) {
+		if (value >= 90) return { bg: 'bg-emerald-50 dark:bg-emerald-400/10', text: 'text-emerald-700 dark:text-emerald-400' };
+		if (value >= 70) return { bg: 'bg-amber-50 dark:bg-amber-400/10', text: 'text-amber-700 dark:text-amber-400' };
+		return { bg: 'bg-red-50 dark:bg-red-400/10', text: 'text-red-700 dark:text-red-400' };
+	}
 
-	const avgSentimentScore = 0.28;
-	const scorePosition = ((avgSentimentScore + 1) / 2) * 100;
+	// ── Section 3: Sentiment ───────────────────────────────────────────────────────
+	const emotionData = $derived(data?.emotions?.items ?? []);
+	const avgSentimentScore = $derived(data?.emotions?.avgSentimentScore ?? 0);
+	const scorePosition = $derived(Math.max(0, Math.min(100, ((avgSentimentScore + 1) / 2) * 100)));
+	const sentimentLabel = $derived(
+		avgSentimentScore >= 0.15 ? 'Positivo ↑' : avgSentimentScore <= -0.15 ? 'Negativo ↓' : 'Neutro'
+	);
 	let hoveredEmotion = $state(null);
 
-	// ── Section 4: Quick access ──────────────────────────────────────────────
-	const quickAccessItems = [
-		{
-			label: 'Conversas',
-			href: '/dashboard/interactions',
-			icon: MessageSquare,
-			metric: '418 hoje',
-			trend: '↑',
-			trendColor: 'text-emerald-600 dark:text-emerald-400',
-			color: 'text-sky-600 dark:text-sky-400',
-			bg: 'bg-sky-50 dark:bg-sky-400/10',
-			border: 'border-sky-200 dark:border-sky-400/20'
-		},
-		{
-			label: 'Personas',
-			href: '/dashboard/personas',
-			icon: UserRound,
-			metric: '5 ativas',
-			trend: '→',
-			trendColor: 'text-slate-500 dark:text-slate-400',
-			color: 'text-purple-600 dark:text-purple-400',
-			bg: 'bg-purple-50 dark:bg-purple-400/10',
-			border: 'border-purple-200 dark:border-purple-400/20'
-		},
-		{
-			label: 'Agentes',
-			href: '/dashboard/agents',
-			icon: Users,
-			metric: '12 ativos',
-			trend: '↑',
-			trendColor: 'text-emerald-600 dark:text-emerald-400',
-			color: 'text-emerald-600 dark:text-emerald-400',
-			bg: 'bg-emerald-50 dark:bg-emerald-400/10',
-			border: 'border-emerald-200 dark:border-emerald-400/20'
-		},
-		{
-			label: 'Playbooks',
-			href: '/dashboard/evolucao',
-			icon: BookOpen,
-			metric: '3 ativos',
-			trend: '→',
-			trendColor: 'text-slate-500 dark:text-slate-400',
-			color: 'text-amber-600 dark:text-amber-400',
-			bg: 'bg-amber-50 dark:bg-amber-400/10',
-			border: 'border-amber-200 dark:border-amber-400/20'
-		},
-		{
-			label: 'Configurações',
-			href: '/dashboard/settings',
-			icon: Settings,
-			metric: '2 pendentes',
-			trend: '↓',
-			trendColor: 'text-amber-600 dark:text-amber-400',
-			color: 'text-slate-600 dark:text-slate-400',
-			bg: 'bg-slate-100 dark:bg-slate-700/50',
-			border: 'border-slate-200 dark:border-slate-700'
-		},
-		{
-			label: 'Evolução',
-			href: '/dashboard/evolucao',
-			icon: TrendingUp,
-			metric: '+12% vs ontem',
-			trend: '↑',
-			trendColor: 'text-emerald-600 dark:text-emerald-400',
-			color: 'text-indigo-600 dark:text-indigo-400',
-			bg: 'bg-indigo-50 dark:bg-indigo-400/10',
-			border: 'border-indigo-200 dark:border-indigo-400/20'
-		}
-	];
+	// ── Section 4: Quick access ─────────────────────────────────────────────────────
+	const quickAccessItems = $derived.by(() => {
+		const qa = data?.quickAccess ?? {};
+		return [
+			{
+				label: 'Conversas',
+				href: '/dashboard/interactions',
+				icon: MessageSquare,
+				metric: `${qa.conversationsToday ?? 0} hoje`,
+				trend: '↑',
+				trendColor: 'text-emerald-600 dark:text-emerald-400',
+				color: 'text-sky-600 dark:text-sky-400',
+				bg: 'bg-sky-50 dark:bg-sky-400/10',
+				border: 'border-sky-200 dark:border-sky-400/20'
+			},
+			{
+				label: 'Personas',
+				href: '/dashboard/personas',
+				icon: UserRound,
+				metric: `${qa.activePersonas ?? 0} ativas`,
+				trend: '→',
+				trendColor: 'text-slate-500 dark:text-slate-400',
+				color: 'text-purple-600 dark:text-purple-400',
+				bg: 'bg-purple-50 dark:bg-purple-400/10',
+				border: 'border-purple-200 dark:border-purple-400/20'
+			},
+			{
+				label: 'Agentes',
+				href: '/dashboard/operators',
+				icon: Users,
+				metric: `${qa.activeAgents ?? 0} ativos`,
+				trend: '↑',
+				trendColor: 'text-emerald-600 dark:text-emerald-400',
+				color: 'text-emerald-600 dark:text-emerald-400',
+				bg: 'bg-emerald-50 dark:bg-emerald-400/10',
+				border: 'border-emerald-200 dark:border-emerald-400/20'
+			},
+			{
+				label: 'Playbooks',
+				href: '/dashboard/evolucao',
+				icon: BookOpen,
+				metric: `${qa.activePlaybooks ?? 0} ativos`,
+				trend: '→',
+				trendColor: 'text-slate-500 dark:text-slate-400',
+				color: 'text-amber-600 dark:text-amber-400',
+				bg: 'bg-amber-50 dark:bg-amber-400/10',
+				border: 'border-amber-200 dark:border-amber-400/20'
+			},
+			{
+				label: 'Configurações',
+				href: '/dashboard/settings',
+				icon: Settings,
+				metric: `${qa.pendingSettings ?? 0} pendentes`,
+				trend: '↓',
+				trendColor: 'text-amber-600 dark:text-amber-400',
+				color: 'text-slate-600 dark:text-slate-400',
+				bg: 'bg-slate-100 dark:bg-slate-700/50',
+				border: 'border-slate-200 dark:border-slate-700'
+			},
+			{
+				label: 'Evolução',
+				href: '/dashboard/evolucao',
+				icon: TrendingUp,
+				metric: `${qa.evolutionDelta ?? '—'} vs ontem`,
+				trend: '↑',
+				trendColor: 'text-emerald-600 dark:text-emerald-400',
+				color: 'text-indigo-600 dark:text-indigo-400',
+				bg: 'bg-indigo-50 dark:bg-indigo-400/10',
+				border: 'border-indigo-200 dark:border-indigo-400/20'
+			}
+		];
+	});
 </script>
 
 <svelte:head>
@@ -216,11 +221,18 @@
 			<h1 class="text-xl font-bold text-slate-900 dark:text-white">Executive Dashboard</h1>
 		</div>
 		<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
-			Visão executiva do desempenho comercial e saúde da base de clientes — atualizado agora há
-			pouco
+			Visão executiva do desempenho comercial e saúde da base de clientes
 		</p>
 	</div>
 
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
 	<!-- Section 1: KPI Bar -->
 	{#if isLoading}
 		<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
@@ -244,13 +256,13 @@
 							<Icon size={20} strokeWidth={2.5} />
 						</div>
 						<div class="min-w-0 flex-1 space-y-1">
-							<div class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+							<div
+								class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+							>
 								{kpi.title}
 							</div>
 							<div
-								class="text-2xl font-bold {kpi.danger
-									? kpi.color
-									: 'text-slate-900 dark:text-white'}"
+								class="text-2xl font-bold {kpi.danger ? kpi.color : 'text-slate-900 dark:text-white'}"
 							>
 								{kpi.value}
 							</div>
@@ -301,16 +313,16 @@
 							dominant-baseline="middle"
 							class="fill-slate-900 dark:fill-white"
 							font-size="11"
-							font-weight="bold"
-						>Churn</text>
+							font-weight="bold">Churn</text
+						>
 						<text
 							x="60"
 							y="67"
 							text-anchor="middle"
 							dominant-baseline="middle"
 							class="fill-slate-500 dark:fill-slate-400"
-							font-size="8"
-						>Relacional</text>
+							font-size="8">Relacional</text
+						>
 					</svg>
 					<div class="flex-1 space-y-2.5">
 						{#each donutPaths as seg}
@@ -342,21 +354,20 @@
 				<div class="space-y-4">
 					<div>
 						<div class="text-2xl font-bold text-red-600 dark:text-red-400">
-							R$ {ltvAtRisk.toLocaleString('pt-BR')}
+							R$ {ltv.ltvAtRisk.toLocaleString('pt-BR')}
 						</div>
 						<div class="text-xs text-slate-500 dark:text-slate-400">
-							de R$ {ltvTotal.toLocaleString('pt-BR')} em LTV total
+							de R$ {ltv.ltvTotal.toLocaleString('pt-BR')} em LTV total
 						</div>
 					</div>
 					<div>
 						<div class="mb-1.5 flex items-center justify-between text-xs">
-							<span class="text-slate-600 dark:text-slate-300">{ltvRiskPct}% da base em risco</span>
-							<span class="font-semibold text-red-600 dark:text-red-400">↑ aumentando</span>
+							<span class="text-slate-600 dark:text-slate-300">{ltv.ltvRiskPct}% da base em risco</span>
 						</div>
 						<div class="h-3 w-full overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
 							<div
 								class="h-full rounded-full bg-red-500 transition-all"
-								style="width: {ltvRiskPct}%"
+								style="width: {ltv.ltvRiskPct}%"
 							></div>
 						</div>
 					</div>
@@ -364,11 +375,13 @@
 						class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-400/20 dark:bg-amber-400/10"
 					>
 						<div class="text-xs font-medium text-amber-700 dark:text-amber-400">
-							34 clientes críticos · Ticket médio R$ 1.016
+							{ltv.criticalClients} clientes críticos · Ticket médio R$ {ltv.avgTicket.toLocaleString(
+								'pt-BR'
+							)}
 						</div>
 					</div>
 					<div class="text-xs text-slate-500 dark:text-slate-400">
-						↑ +3 clientes entraram em risco crítico desde ontem
+						{ltv.trendText}
 					</div>
 				</div>
 			</div>
@@ -389,35 +402,32 @@
 					<div class="flex items-end gap-6">
 						<div>
 							<div class="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
-								{slaWithinPct}%
+								{sla.withinPct}%
 							</div>
 							<div class="text-xs text-slate-500 dark:text-slate-400">dentro do SLA</div>
 						</div>
 						<div>
-							<div class="text-2xl font-bold text-red-600 dark:text-red-400">{slaBreachPct}%</div>
+							<div class="text-2xl font-bold text-red-600 dark:text-red-400">{sla.breachPct}%</div>
 							<div class="text-xs text-slate-500 dark:text-slate-400">estourado</div>
 						</div>
 					</div>
 					<div class="h-3 w-full overflow-hidden rounded-full bg-red-100 dark:bg-red-400/20">
 						<div
 							class="h-full rounded-full bg-emerald-500 transition-all"
-							style="width: {slaWithinPct}%"
+							style="width: {sla.withinPct}%"
 						></div>
 					</div>
-					<div class="grid grid-cols-3 gap-2 text-center">
-						<div class="rounded-lg bg-emerald-50 p-2.5 dark:bg-emerald-400/10">
-							<div class="text-xs font-bold text-emerald-700 dark:text-emerald-400">SAC</div>
-							<div class="text-sm font-semibold text-slate-900 dark:text-white">72%</div>
+					{#if sla.byDepartment.length > 0}
+						<div class="grid grid-cols-3 gap-2 text-center">
+							{#each sla.byDepartment as dept}
+								{@const c = deptSlaColor(dept.value)}
+								<div class="rounded-lg p-2.5 {c.bg}">
+									<div class="text-xs font-bold {c.text}">{dept.department}</div>
+									<div class="text-sm font-semibold text-slate-900 dark:text-white">{dept.value}%</div>
+								</div>
+							{/each}
 						</div>
-						<div class="rounded-lg bg-amber-50 p-2.5 dark:bg-amber-400/10">
-							<div class="text-xs font-bold text-amber-700 dark:text-amber-400">Vendas</div>
-							<div class="text-sm font-semibold text-slate-900 dark:text-white">85%</div>
-						</div>
-						<div class="rounded-lg bg-emerald-50 p-2.5 dark:bg-emerald-400/10">
-							<div class="text-xs font-bold text-emerald-700 dark:text-emerald-400">Suporte</div>
-							<div class="text-sm font-semibold text-slate-900 dark:text-white">91%</div>
-						</div>
-					</div>
+					{/if}
 				</div>
 			</div>
 		</div>
@@ -444,9 +454,7 @@
 			<!-- Stacked bar with hover tooltip -->
 			<div class="relative mb-2 pt-10">
 				{#if hoveredEmotion}
-					<div
-						class="pointer-events-none absolute top-0 left-0 right-0 flex justify-center"
-					>
+					<div class="pointer-events-none absolute top-0 right-0 left-0 flex justify-center">
 						<div
 							class="rounded-lg border border-slate-200 bg-white px-3 py-2 shadow-lg dark:border-slate-700 dark:bg-[#0f172a]"
 						>
@@ -481,7 +489,7 @@
 			</div>
 
 			<!-- Legend -->
-			<div class="mb-6 mt-3 flex flex-wrap gap-4">
+			<div class="mt-3 mb-6 flex flex-wrap gap-4">
 				{#each emotionData as emotion}
 					<div class="flex items-center gap-1.5 text-xs">
 						<span
@@ -498,9 +506,11 @@
 			<!-- Sentiment score scale -->
 			<div class="space-y-2">
 				<div class="flex items-center justify-between text-xs">
-					<span class="font-medium text-slate-600 dark:text-slate-400">Score médio de sentimento</span>
+					<span class="font-medium text-slate-600 dark:text-slate-400"
+						>Score médio de sentimento</span
+					>
 					<span class="font-bold text-emerald-600 dark:text-emerald-400">
-						+{avgSentimentScore} — Positivo ↑
+						{avgSentimentScore >= 0 ? '+' : ''}{avgSentimentScore} — {sentimentLabel}
 					</span>
 				</div>
 				<div
@@ -508,13 +518,10 @@
 					style="background: linear-gradient(to right, #ef4444 0%, #f59e0b 35%, #10b981 100%)"
 				>
 					<div
-						class="absolute top-1/2 h-5 w-5 -translate-y-1/2 -translate-x-1/2 rounded-full border-2 border-white bg-white shadow-md dark:border-slate-900 dark:bg-slate-900"
+						class="absolute top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-white shadow-md dark:border-slate-900 dark:bg-slate-900"
 						style="left: {scorePosition}%"
 					>
-						<div
-							class="absolute inset-0.5 rounded-full"
-							style="background-color: #10b981"
-						></div>
+						<div class="absolute inset-0.5 rounded-full" style="background-color: #10b981"></div>
 					</div>
 				</div>
 				<div class="flex justify-between text-[10px] text-slate-500 dark:text-slate-400">

+ 216 - 197
src/routes/(app)/dashboard/interactions/+page.svelte

@@ -1,112 +1,81 @@
 <script>
-	import { Search, Download, Eye, X, MessageCircle } from 'lucide-svelte';
-	import { mockInteractions } from '$lib/core/models/mock-data.js';
+	import { Search, Eye, X, MessageCircle } from 'lucide-svelte';
+	import { Chart, Svg, Axis, Spline, Highlight } from 'layerchart';
+	import { format } from 'date-fns';
+	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
 	let searchQuery = $state('');
-	let selectedInteraction = $state(null);
-	let isChatModalOpen = $state(false);
-
-	// Filters state
 	let activeFilter = $state('all'); // all, my_clients, new, unfinished
+	let page = $state(1);
+	const perPage = 20;
+
+	// ── Lista de interações (GET /v1/interactions) ──────────────────────────────
+	let items = $state([]);
+	let pagination = $state({ page: 1, per_page: perPage, total: 0, total_pages: 0 });
+	let loading = $state(true);
+	let loadError = $state('');
+	let searchTimer;
+
+	async function loadInteractions() {
+		loading = true;
+		loadError = '';
+		try {
+			const data = await api.get('/v1/interactions', {
+				query: {
+					page,
+					per_page: perPage,
+					search: searchQuery.trim(),
+					filter: activeFilter
+				}
+			});
+			items = data.items ?? [];
+			pagination = data.pagination ?? { page, per_page: perPage, total: 0, total_pages: 0 };
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar interações.';
+			items = [];
+		} finally {
+			loading = false;
+		}
+	}
 
-	const conversationTemplates = [
-		[
-			{
-				isAgent: false,
-				text: 'Oi, queria saber se esse look chega na loja física ou é só on-line?'
-			},
-			{
-				isAgent: true,
-				text: 'Oi {client}! Aqui é {agent}. Posso te mandar combinações com o que chegou hoje?'
-			},
-			{
-				isAgent: false,
-				text: 'Quero sim! Tive dúvida porque o tecido parece quente, o que você acha?'
-			},
-			{
-				isAgent: true,
-				text: 'Ele é leve, perfeito para eventos. Posso reservar dois tamanhos pra você testar?'
-			}
-		],
-		[
-			{
-				isAgent: false,
-				text: 'Boa tarde! O prazo de entrega está mantido? Preciso para um evento.'
-			},
-			{
-				isAgent: true,
-				text: 'Está sim! Consigo acompanhar junto com a logística e te aviso qualquer mudança.'
-			},
-			{
-				isAgent: false,
-				text: 'Perfeito, fiquei tranquila. Pode confirmar se a cor é igual da foto?'
-			},
-			{
-				isAgent: true,
-				text: 'É idêntica! Vou te mandar mais fotos com detalhes da costura.'
-			}
-		],
-		[
-			{
-				isAgent: false,
-				text: 'Oi, recebi o link mas não consegui finalizar o pagamento.'
-			},
-			{
-				isAgent: true,
-				text: 'Sem problemas, {client}. Gereiem um novo link com cashback extra, vale tentar novamente?'
-			},
-			{
-				isAgent: false,
-				text: 'Agora foi! Consegue separar junto com uma calça flare 38?'
-			},
-			{
-				isAgent: true,
-				text: 'Já separei e mando atualizações de retirada. Obrigada por confiar!'
-			}
-		]
-	];
+	onMount(loadInteractions);
 
-	function applyFilter(interaction) {
-		const normalizedQuery = searchQuery.trim();
-		if (normalizedQuery && !interaction.client.includes(normalizedQuery)) return false;
+	function setFilter(filter) {
+		activeFilter = activeFilter === filter ? 'all' : filter;
+		page = 1;
+		loadInteractions();
+	}
 
-		if (activeFilter === 'my_clients') {
-			return interaction.agent !== '-';
-		}
+	function onSearchInput() {
+		clearTimeout(searchTimer);
+		searchTimer = setTimeout(() => {
+			page = 1;
+			loadInteractions();
+		}, 400);
+	}
 
-		if (activeFilter === 'new') {
-			return interaction.sentiment === 'CONTENTAMENTO' || interaction.score > 0.4;
+	function prevPage() {
+		if (page > 1) {
+			page -= 1;
+			loadInteractions();
 		}
+	}
 
-		if (activeFilter === 'unfinished') {
-			return interaction.sentiment === 'FRUSTRAÇÃO' || interaction.score < 0;
+	function nextPage() {
+		if (page < pagination.total_pages) {
+			page += 1;
+			loadInteractions();
 		}
-
-		return true;
 	}
 
-	const filteredInteractions = $derived(mockInteractions.filter(applyFilter));
-
-	function buildConversation(interaction, idx) {
-		const template = conversationTemplates[idx % conversationTemplates.length];
-		const agentName = interaction.agent === '-' ? 'Time Nettown' : interaction.agent;
-		return template.map((message, msgIdx) => ({
-			...message,
-			id: `${interaction.client}-${msgIdx}`,
-			text: message.text
-				.replace('{client}', interaction.client.slice(-4))
-				.replace('{agent}', agentName),
-			time: interaction.datetime.split(',')[1]?.trim() ?? '00:00'
-		}));
+	function formatDateTime(iso) {
+		if (!iso) return '—';
+		const date = new Date(iso);
+		if (Number.isNaN(date.getTime())) return iso;
+		return format(date, 'dd/MM/yy, HH:mm');
 	}
 
-	const conversationThreads = $derived(
-		filteredInteractions.map((interaction, idx) => ({
-			...interaction,
-			thread: buildConversation(interaction, idx)
-		}))
-	);
-
 	function getSentimentColor(sentiment) {
 		switch (sentiment) {
 			case 'CONTENTAMENTO':
@@ -120,17 +89,54 @@
 		}
 	}
 
-	function openChat(interaction) {
+	// ── Modal de detalhes (GET /v1/interactions/details) ───────────────────────
+	let isChatModalOpen = $state(false);
+	let selectedInteraction = $state(null);
+	let details = $state(null);
+	let detailsLoading = $state(false);
+	let detailsError = $state('');
+
+	async function openChat(interaction) {
 		selectedInteraction = interaction;
 		isChatModalOpen = true;
+		details = null;
+		detailsError = '';
+		detailsLoading = true;
+		try {
+			details = await api.get('/v1/interactions/details', {
+				query: { conversation_id: interaction.conversationId }
+			});
+		} catch (err) {
+			detailsError = err?.message ?? 'Falha ao carregar os detalhes da conversa.';
+		} finally {
+			detailsLoading = false;
+		}
 	}
 
 	function closeChat() {
 		isChatModalOpen = false;
 		setTimeout(() => {
 			selectedInteraction = null;
+			details = null;
 		}, 300);
 	}
+
+	// Gráfico funcional: volume acumulado de mensagens ao longo do tempo,
+	// construído a partir das mensagens reais retornadas em details.thread.
+	const chartData = $derived.by(() => {
+		const thread = details?.thread ?? [];
+		return thread
+			.map((message, index) => {
+				const stamp = new Date(`${message.date ?? ''}T${message.time ?? '00:00'}:00`);
+				return {
+					date: Number.isNaN(stamp.getTime()) ? null : stamp,
+					value: index + 1
+				};
+			})
+			.filter((point) => point.date !== null);
+	});
+
+	const report = $derived(details?.report ?? null);
 </script>
 
 <svelte:head>
@@ -150,7 +156,7 @@
 				'my_clients'
 					? 'border-indigo-200 bg-indigo-50 text-indigo-700 shadow-inner dark:border-indigo-500/30 dark:bg-indigo-500/20 dark:text-indigo-400'
 					: 'border-slate-200 bg-white text-slate-700 shadow-sm hover:border-slate-300 dark:border-slate-700 dark:bg-[#1e293b] dark:text-slate-300 dark:shadow-none dark:hover:border-slate-500'}"
-				onclick={() => (activeFilter = activeFilter === 'my_clients' ? 'all' : 'my_clients')}
+				onclick={() => setFilter('my_clients')}
 			>
 				Meus Clientes
 			</button>
@@ -159,7 +165,7 @@
 				'new'
 					? 'border-emerald-200 bg-emerald-50 text-emerald-700 shadow-inner dark:border-emerald-500/30 dark:bg-emerald-500/20 dark:text-emerald-400'
 					: 'border-slate-200 bg-white text-slate-700 shadow-sm hover:border-slate-300 dark:border-slate-700 dark:bg-[#1e293b] dark:text-slate-300 dark:shadow-none dark:hover:border-slate-500'}"
-				onclick={() => (activeFilter = activeFilter === 'new' ? 'all' : 'new')}
+				onclick={() => setFilter('new')}
 			>
 				Relatório Pessoas Novas
 			</button>
@@ -168,18 +174,21 @@
 				'unfinished'
 					? 'border-amber-200 bg-amber-50 text-amber-700 shadow-inner dark:border-amber-500/30 dark:bg-amber-500/20 dark:text-amber-400'
 					: 'border-slate-200 bg-white text-slate-700 shadow-sm hover:border-slate-300 dark:border-slate-700 dark:bg-[#1e293b] dark:text-slate-300 dark:shadow-none dark:hover:border-slate-500'}"
-				onclick={() => (activeFilter = activeFilter === 'unfinished' ? 'all' : 'unfinished')}
+				onclick={() => setFilter('unfinished')}
 			>
 				Conversas Inacabadas
 			</button>
-
-			<!-- <button class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-600 dark:hover:bg-indigo-500 text-white transition-colors shadow-sm dark:shadow-none ml-auto sm:ml-0">
-                <Download size={16} />
-                Download
-            </button> -->
 		</div>
 	</div>
 
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
 	<!-- Table Container -->
 	<div
 		class="flex flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
@@ -195,12 +204,13 @@
 				<input
 					type="text"
 					bind:value={searchQuery}
-					placeholder="Buscar cliente..."
-					class="w-64 rounded-lg border border-slate-300 bg-white py-2 pr-4 pl-9 text-sm text-slate-900 placeholder-slate-400 shadow-sm transition-all 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 dark:placeholder-slate-500 dark:shadow-none"
+					oninput={onSearchInput}
+					placeholder="Buscar cliente, telefone ou agente..."
+					class="w-72 rounded-lg border border-slate-300 bg-white py-2 pr-4 pl-9 text-sm text-slate-900 placeholder-slate-400 shadow-sm transition-all 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 dark:placeholder-slate-500 dark:shadow-none"
 				/>
 			</div>
 			<div class="text-sm font-medium text-slate-500 dark:text-slate-400">
-				Exibindo {conversationThreads.length} conversas
+				{loading ? 'Carregando…' : `${pagination.total} conversas`}
 			</div>
 		</div>
 
@@ -223,13 +233,13 @@
 				<tbody
 					class="divide-y divide-slate-100 transition-colors duration-200 dark:divide-slate-800/50"
 				>
-					{#each conversationThreads as item}
+					{#each items as item (item.conversationId)}
 						<tr class="group transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/30">
 							<td class="p-4">
 								<span class="font-bold text-slate-900 dark:text-white">{item.client}</span>
 							</td>
 							<td class="p-4 font-medium text-slate-600 dark:text-slate-300"
-								>{item.agent === '-' ? 'Time Nettown' : item.agent}</td
+								>{item.agent || 'Time Nettown'}</td
 							>
 							<td class="p-4">
 								<span
@@ -241,10 +251,10 @@
 								</span>
 							</td>
 							<td class="p-4 font-bold text-slate-700 dark:text-slate-300">{item.score}</td>
-							<td class="p-4 font-medium text-slate-600 dark:text-slate-300">{item.aspect}</td>
-							<td class="p-4 text-slate-500 dark:text-slate-400">{item.subaspect}</td>
+							<td class="p-4 font-medium text-slate-600 dark:text-slate-300">{item.aspect || '—'}</td>
+							<td class="p-4 text-slate-500 dark:text-slate-400">{item.subaspect || '—'}</td>
 							<td class="p-4 text-sm font-medium text-slate-500 dark:text-slate-400"
-								>{item.datetime}</td
+								>{formatDateTime(item.datetime)}</td
 							>
 							<td class="p-4 text-right">
 								<button
@@ -258,7 +268,7 @@
 						</tr>
 					{/each}
 
-					{#if conversationThreads.length === 0}
+					{#if !loading && items.length === 0}
 						<tr>
 							<td colspan="8" class="p-8 text-center font-medium text-slate-500">
 								Nenhuma interação encontrada.
@@ -269,16 +279,23 @@
 			</table>
 		</div>
 
+		<!-- Paginação -->
 		<div
 			class="flex items-center justify-center gap-4 border-t border-slate-200 bg-slate-50 p-4 transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
 		>
 			<button
-				class="text-sm font-medium text-slate-400 hover:text-slate-600 disabled:opacity-50 dark:text-slate-500 dark:hover:text-slate-300"
-				disabled>Anterior</button
+				onclick={prevPage}
+				disabled={page <= 1 || loading}
+				class="text-sm font-medium text-indigo-600 hover:text-indigo-700 disabled:cursor-not-allowed disabled:text-slate-400 disabled:opacity-50 dark:text-indigo-400 dark:hover:text-indigo-300 dark:disabled:text-slate-500"
+				>Anterior</button
 			>
-			<span class="text-sm font-bold text-slate-900 dark:text-white">Página 1</span>
+			<span class="text-sm font-bold text-slate-900 dark:text-white">
+				Página {pagination.page} de {Math.max(1, pagination.total_pages)}
+			</span>
 			<button
-				class="text-sm font-medium text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
+				onclick={nextPage}
+				disabled={page >= pagination.total_pages || loading}
+				class="text-sm font-medium text-indigo-600 hover:text-indigo-700 disabled:cursor-not-allowed disabled:text-slate-400 disabled:opacity-50 dark:text-indigo-400 dark:hover:text-indigo-300 dark:disabled:text-slate-500"
 				>Próxima</button
 			>
 		</div>
@@ -321,9 +338,11 @@
 							</div>
 							<div>
 								<div class="font-bold text-slate-900 dark:text-white">
-									{selectedInteraction?.client}
+									{details?.conversation?.client ?? selectedInteraction?.client}
+								</div>
+								<div class="text-xs font-medium text-slate-500 dark:text-slate-400">
+									{details?.conversation?.channel ?? 'WhatsApp'}
 								</div>
-								<div class="text-xs font-medium text-slate-500 dark:text-slate-400">WhatsApp</div>
 							</div>
 						</div>
 						<button
@@ -336,35 +355,38 @@
 
 					<!-- Chat Messages -->
 					<div class="custom-scrollbar flex-1 space-y-6 overflow-y-auto p-6">
-						<div class="flex justify-center">
-							<span
-								class="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-bold text-slate-500 shadow-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:shadow-none"
-							>
-								ONTEM
-							</span>
-						</div>
-
-						{#each selectedInteraction?.thread ?? [] as msg}
-							<div class="flex flex-col {msg.isAgent ? 'items-end' : 'items-start'}">
-								<div
-									class="max-w-[80%] {msg.isAgent
-										? 'rounded-2xl rounded-tr-sm bg-indigo-600 text-white shadow-md shadow-indigo-600/20'
-										: 'rounded-2xl rounded-tl-sm border border-slate-200 bg-white text-slate-700 shadow-sm dark:border-transparent dark:bg-slate-800 dark:text-slate-200'} p-3.5"
-								>
-									{#if msg.isAgent}
-										<div class="mb-1 text-[10px] font-bold text-indigo-200">Ativadora</div>
-									{/if}
-									<p class="text-sm leading-relaxed whitespace-pre-wrap">{msg.text}</p>
+						{#if detailsLoading}
+							<div class="py-10 text-center text-sm text-slate-400">Carregando conversa…</div>
+						{:else if detailsError}
+							<div class="py-10 text-center text-sm text-red-500">{detailsError}</div>
+						{:else}
+							{#each details?.thread ?? [] as msg (msg.id)}
+								<div class="flex flex-col {msg.isAgent ? 'items-end' : 'items-start'}">
 									<div
-										class="mt-1.5 text-right text-[10px] {msg.isAgent
-											? 'opacity-70'
-											: 'font-medium text-slate-400'}"
+										class="max-w-[80%] {msg.isAgent
+											? 'rounded-2xl rounded-tr-sm bg-indigo-600 text-white shadow-md shadow-indigo-600/20'
+											: 'rounded-2xl rounded-tl-sm border border-slate-200 bg-white text-slate-700 shadow-sm dark:border-transparent dark:bg-slate-800 dark:text-slate-200'} p-3.5"
 									>
-										{msg.time}
+										{#if msg.isAgent}
+											<div class="mb-1 text-[10px] font-bold text-indigo-200">Ativadora</div>
+										{/if}
+										<p class="text-sm leading-relaxed whitespace-pre-wrap">{msg.text}</p>
+										<div
+											class="mt-1.5 text-right text-[10px] {msg.isAgent
+												? 'opacity-70'
+												: 'font-medium text-slate-400'}"
+										>
+											{msg.time}
+										</div>
 									</div>
 								</div>
-							</div>
-						{/each}
+							{/each}
+							{#if (details?.thread ?? []).length === 0}
+								<div class="py-10 text-center text-sm text-slate-400">
+									Nenhuma mensagem nesta conversa.
+								</div>
+							{/if}
+						{/if}
 					</div>
 				</div>
 
@@ -385,43 +407,38 @@
 					</div>
 
 					<div class="custom-scrollbar flex-1 space-y-8 overflow-y-auto p-6">
-						<!-- Mini chart placeholder -->
+						<!-- Gráfico funcional: mensagens acumuladas ao longo do tempo -->
 						<div
-							class="relative flex h-32 items-end overflow-hidden rounded-lg border border-slate-200 bg-slate-50 p-4 shadow-inner dark:border-slate-800 dark:bg-slate-900"
+							class="relative h-32 overflow-hidden rounded-lg border border-slate-200 bg-slate-50 p-2 shadow-inner dark:border-slate-800 dark:bg-slate-900"
 						>
-							<!-- Decorative chart lines matching the UI -->
-							<svg class="absolute inset-0 h-full w-full" preserveAspectRatio="none">
-								<path
-									d="M0,20 L40,100 L200,100 L300,100 L350,60"
-									fill="none"
-									stroke="#38bdf8"
-									stroke-width="2"
-									vector-effect="non-scaling-stroke"
-								/>
-								<path
-									d="M0,60 L40,100 L200,100 L300,100 L350,40"
-									fill="none"
-									stroke="#10b981"
-									stroke-width="2"
-									vector-effect="non-scaling-stroke"
-								/>
-								<!-- Dots -->
-								<circle cx="0" cy="20" r="3" fill="#38bdf8" />
-								<circle cx="40" cy="100" r="3" fill="#38bdf8" />
-								<circle cx="120" cy="100" r="3" fill="#38bdf8" />
-								<circle cx="200" cy="100" r="3" fill="#38bdf8" />
-								<circle cx="300" cy="100" r="3" fill="#38bdf8" />
-								<circle cx="350" cy="60" r="3" fill="#38bdf8" />
-							</svg>
-							<div
-								class="absolute bottom-2 left-0 flex w-full justify-between px-4 text-[10px] font-medium text-slate-400 dark:text-slate-500"
-							>
-								<span>22:58</span>
-								<span>23:00</span>
-								<span>23:02</span>
-								<span>23:04</span>
-								<span>23:06</span>
-							</div>
+							{#if chartData.length >= 2}
+								<Chart
+									data={chartData}
+									x={(d) => d.date}
+									y={(d) => d.value}
+									padding={{ top: 10, right: 10, bottom: 18, left: 22 }}
+								>
+									<Svg>
+										<Axis
+											placement="left"
+											class="fill-slate-400 text-[9px] dark:fill-slate-500"
+										/>
+										<Axis
+											placement="bottom"
+											format={(d) => format(d, 'HH:mm')}
+											class="fill-slate-400 text-[9px] dark:fill-slate-500"
+										/>
+										<Spline stroke="#38bdf8" strokeWidth={2} />
+										<Highlight
+											points={{ fill: '#38bdf8', class: 'stroke-white dark:stroke-slate-900', strokeWidth: 2 }}
+										/>
+									</Svg>
+								</Chart>
+							{:else}
+								<div class="flex h-full items-center justify-center text-xs text-slate-400">
+									Mensagens insuficientes para o gráfico.
+								</div>
+							{/if}
 						</div>
 
 						<div class="grid grid-cols-2 gap-x-4 gap-y-6">
@@ -431,7 +448,9 @@
 								>
 									MÉDIA RESPOSTA
 								</div>
-								<div class="text-lg font-extrabold text-slate-900 dark:text-white">04:03</div>
+								<div class="text-lg font-extrabold text-slate-900 dark:text-white">
+									{report?.avgResponse ?? '—'}
+								</div>
 							</div>
 							<div>
 								<div
@@ -439,7 +458,9 @@
 								>
 									DURAÇÃO TOTAL
 								</div>
-								<div class="text-lg font-extrabold text-slate-900 dark:text-white">08:28</div>
+								<div class="text-lg font-extrabold text-slate-900 dark:text-white">
+									{report?.totalDuration ?? '—'}
+								</div>
 							</div>
 							<div>
 								<div
@@ -447,7 +468,9 @@
 								>
 									MÉDIA AGENTE
 								</div>
-								<div class="text-lg font-extrabold text-slate-900 dark:text-white">00:00</div>
+								<div class="text-lg font-extrabold text-slate-900 dark:text-white">
+									{report?.avgAgent ?? '—'}
+								</div>
 							</div>
 							<div>
 								<div
@@ -455,7 +478,9 @@
 								>
 									MÉDIA CLIENTE
 								</div>
-								<div class="text-lg font-extrabold text-slate-900 dark:text-white">08:05</div>
+								<div class="text-lg font-extrabold text-slate-900 dark:text-white">
+									{report?.avgClient ?? '—'}
+								</div>
 							</div>
 						</div>
 
@@ -467,7 +492,7 @@
 									ASPECTO PRINCIPAL
 								</div>
 								<div class="text-base font-semibold text-indigo-600 dark:text-indigo-400">
-									{selectedInteraction?.aspect || 'Atendimento'}
+									{report?.mainAspect || selectedInteraction?.aspect || '—'}
 								</div>
 							</div>
 							<div>
@@ -477,21 +502,21 @@
 									SUBASPECTO
 								</div>
 								<div class="text-base font-semibold text-slate-700 dark:text-slate-200">
-									{selectedInteraction?.subaspect || 'Informativo'}
+									{report?.subAspect || selectedInteraction?.subaspect || '—'}
 								</div>
 							</div>
 						</div>
 
-						<div
-							class="grid grid-cols-2 gap-4 border-t border-slate-200 pt-4 dark:border-slate-800"
-						>
+						<div class="grid grid-cols-2 gap-4 border-t border-slate-200 pt-4 dark:border-slate-800">
 							<div>
 								<div
 									class="mb-1 text-xs font-bold tracking-wider text-slate-500 uppercase dark:text-slate-400"
 								>
 									ÚLTIMA MENSAGEM
 								</div>
-								<div class="text-sm font-semibold text-slate-900 dark:text-white">Cliente</div>
+								<div class="text-sm font-semibold text-slate-900 dark:text-white">
+									{report?.lastMessageAuthor ?? '—'}
+								</div>
 							</div>
 							<div>
 								<div
@@ -499,17 +524,11 @@
 								>
 									CONSECUTIVAS
 								</div>
-								<div class="text-sm font-semibold text-slate-900 dark:text-white">Não</div>
+								<div class="text-sm font-semibold text-slate-900 dark:text-white">
+									{report ? (report.consecutiveMessages ? 'Sim' : 'Não') : '—'}
+								</div>
 							</div>
 						</div>
-
-						<div class="pt-4 text-center">
-							<button
-								class="text-sm font-bold text-indigo-600 transition-colors hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
-							>
-								Detalhes Extraídos
-							</button>
-						</div>
 					</div>
 				</div>
 			</div>

+ 243 - 238
src/routes/(app)/dashboard/agents/+page.svelte → src/routes/(app)/dashboard/operators/+page.svelte

@@ -4,7 +4,6 @@
 		UserPlus,
 		Search,
 		CheckCircle,
-		XCircle,
 		Clock,
 		Edit,
 		Power,
@@ -13,168 +12,81 @@
 		MessageSquare
 	} from 'lucide-svelte';
 	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
+	// ── Estado de dados (GET /v1/agents) ────────────────────────────────────────
 	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
-		}
-	]);
+	let loadError = $state('');
+	let agents = $state([]);
+	let stats = $state({ total: 0, active: 0, inAttendance: 0, availableForEscalation: 0 });
 
-	// ── Filters ───────────────────────────────────────────────────────────────
+	// ── Filtros (enviados ao backend; "Todos" omite o parâmetro) ────────────────
 	let searchTerm = $state('');
 	let filterDept = $state('Todos');
 	let filterChannel = $state('Todos');
 	let filterStatus = $state('Todos');
+	let searchTimer;
+
+	function buildQuery() {
+		const query = {};
+		if (searchTerm.trim()) query.search = searchTerm.trim();
+		if (filterDept !== 'Todos') query.department = filterDept;
+		// O backend espera o canal em minúsculo (whatsapp/instagram).
+		if (filterChannel !== 'Todos') query.channel = filterChannel.toLowerCase();
+		if (filterStatus !== 'Todos') query.status = filterStatus;
+		return query;
+	}
 
-	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
-	});
+	async function loadAgents() {
+		isLoading = true;
+		loadError = '';
+		try {
+			const data = await api.get('/v1/agents', { query: buildQuery() });
+			agents = data.items ?? [];
+			stats = data.stats ?? { total: 0, active: 0, inAttendance: 0, availableForEscalation: 0 };
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar os agentes.';
+			agents = [];
+		} finally {
+			isLoading = false;
+		}
+	}
+
+	onMount(loadAgents);
+
+	function applyFilters() {
+		loadAgents();
+	}
+
+	function onSearchInput() {
+		clearTimeout(searchTimer);
+		searchTimer = setTimeout(loadAgents, 400);
+	}
 
-	// ── Actions ───────────────────────────────────────────────────────────────
-	function toggleEscalation(id) {
-		const agent = agents.find((a) => a.id === id);
-		if (agent) agent.availableForEscalation = !agent.availableForEscalation;
+	// ── Ações (POST status / escalation) ─────────────────────────────────────────
+	async function toggleEscalation(id) {
+		try {
+			await api.post('/v1/agents/escalation', { body: { id } });
+			await loadAgents();
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao atualizar escalonamento.';
+		}
 	}
 
-	function toggleAgentActive(id) {
-		const agent = agents.find((a) => a.id === id);
-		if (agent) agent.status = agent.status === 'Inativo' ? 'Ativo' : 'Inativo';
+	async function toggleAgentActive(id) {
+		try {
+			await api.post('/v1/agents/status', { body: { id } });
+			await loadAgents();
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao atualizar status.';
+		}
 	}
 
-	// ── New Agent Modal ───────────────────────────────────────────────────────
+	// ── Modal de cadastro/edição (POST /v1/agents) ──────────────────────────────
 	let showModal = $state(false);
 	let editingId = $state(null);
+	let saving = $state(false);
+	let saveError = $state('');
 
 	const emptyForm = () => ({
 		name: '',
@@ -190,6 +102,7 @@
 	function openNewAgent() {
 		form = emptyForm();
 		editingId = null;
+		saveError = '';
 		showModal = true;
 	}
 
@@ -203,6 +116,7 @@
 			availableForEscalation: agent.availableForEscalation
 		};
 		editingId = agent.id;
+		saveError = '';
 		showModal = true;
 	}
 
@@ -214,51 +128,34 @@
 		}
 	}
 
-	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
-				}
-			];
+	async function saveAgent() {
+		if (!form.name.trim() || !form.email.trim()) {
+			saveError = 'Nome e e-mail são obrigatórios.';
+			return;
+		}
+		saving = true;
+		saveError = '';
+		try {
+			const body = {
+				name: form.name,
+				email: form.email,
+				department: form.department,
+				channels: form.channels,
+				status: form.status,
+				availableForEscalation: form.availableForEscalation
+			};
+			if (editingId !== null) body.id = editingId;
+			await api.post('/v1/agents', { body });
+			showModal = false;
+			await loadAgents();
+		} catch (err) {
+			saveError = err?.message ?? 'Falha ao salvar o agente.';
+		} finally {
+			saving = false;
 		}
-		showModal = false;
 	}
 
-	// ── Helpers ───────────────────────────────────────────────────────────────
+	// ── Helpers visuais ─────────────────────────────────────────────────────────
 	function statusConfig(status) {
 		const map = {
 			Ativo: { dot: 'bg-emerald-500', text: 'text-emerald-700 dark:text-emerald-400' },
@@ -294,10 +191,45 @@
 		if (pct >= 70) return 'bg-amber-500';
 		return 'bg-red-500';
 	}
+
+	const statCards = $derived([
+		{
+			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'
+		}
+	]);
 </script>
 
 <svelte:head>
-	<title>Agentes - Nettown Analytics</title>
+	<title>Operadores - Nettown Analytics</title>
 </svelte:head>
 
 <!-- New Agent Modal -->
@@ -328,12 +260,24 @@
 			</div>
 
 			<div class="space-y-4 p-5">
+				{#if saveError}
+					<div
+						class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+					>
+						{saveError}
+					</div>
+				{/if}
+
 				<!-- Nome -->
 				<div>
-					<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+					<label
+						for="op-name"
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+					>
 						Nome completo
 					</label>
 					<input
+						id="op-name"
 						type="text"
 						bind:value={form.name}
 						placeholder="Ex: Carolina Ribeiro"
@@ -343,10 +287,14 @@
 
 				<!-- E-mail -->
 				<div>
-					<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+					<label
+						for="op-email"
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+					>
 						E-mail
 					</label>
 					<input
+						id="op-email"
 						type="email"
 						bind:value={form.email}
 						placeholder="agente@empresa.com.br"
@@ -356,10 +304,14 @@
 
 				<!-- Departamento -->
 				<div>
-					<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+					<label
+						for="op-department"
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+					>
 						Departamento
 					</label>
 					<select
+						id="op-department"
 						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"
 					>
@@ -371,15 +323,19 @@
 
 				<!-- Canal -->
 				<div>
-					<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+					<span
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+					>
 						Canal (multi-seleção)
-					</label>
+					</span>
 					<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)
+								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'}"
 							>
@@ -391,10 +347,14 @@
 
 				<!-- Status -->
 				<div>
-					<label class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
+					<label
+						for="op-status"
+						class="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"
+					>
 						Status inicial
 					</label>
 					<select
+						id="op-status"
 						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"
 					>
@@ -417,6 +377,8 @@
 					<button
 						type="button"
 						onclick={() => (form.availableForEscalation = !form.availableForEscalation)}
+						aria-label="Alternar disponibilidade para escalonamento"
+						title="Disponível para escalonamento"
 						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'}"
@@ -430,9 +392,7 @@
 				</div>
 			</div>
 
-			<div
-				class="flex justify-end gap-3 border-t border-slate-200 p-5 dark:border-slate-700"
-			>
+			<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"
@@ -441,10 +401,10 @@
 				</button>
 				<button
 					onclick={saveAgent}
-					disabled={!form.name.trim()}
+					disabled={!form.name.trim() || saving}
 					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
+					{saving ? 'Salvando…' : 'Salvar'}
 				</button>
 			</div>
 		</div>
@@ -478,6 +438,14 @@
 		</button>
 	</div>
 
+	{#if loadError}
+		<div
+			class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+		>
+			{loadError}
+		</div>
+	{/if}
+
 	<!-- Stats row -->
 	{#if isLoading}
 		<div class="grid grid-cols-2 gap-4 xl:grid-cols-4">
@@ -489,17 +457,14 @@
 		</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}
+			{#each statCards 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}">
+					<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>
@@ -515,12 +480,13 @@
 	<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">
+		<div class="relative min-w-48 flex-1">
 			<Search size={16} class="absolute top-1/2 left-3 -translate-y-1/2 text-slate-400" />
 			<input
 				type="text"
-				placeholder="Buscar por nome..."
+				placeholder="Buscar por nome ou e-mail..."
 				bind:value={searchTerm}
+				oninput={onSearchInput}
 				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>
@@ -529,6 +495,7 @@
 			<span class="text-xs font-medium text-slate-500 dark:text-slate-400">Departamento:</span>
 			<select
 				bind:value={filterDept}
+				onchange={applyFilters}
 				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}
@@ -541,6 +508,7 @@
 			<span class="text-xs font-medium text-slate-500 dark:text-slate-400">Canal:</span>
 			<select
 				bind:value={filterChannel}
+				onchange={applyFilters}
 				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}
@@ -553,6 +521,7 @@
 			<span class="text-xs font-medium text-slate-500 dark:text-slate-400">Status:</span>
 			<select
 				bind:value={filterStatus}
+				onchange={applyFilters}
 				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}
@@ -569,46 +538,80 @@
 		{#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>
+					<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
+							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}
+						{#if agents.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">
+										<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 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)}
+							{#each agents 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'
+									class="transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/40 {agent.status ===
+									'Inativo'
 										? 'opacity-60'
 										: ''}"
 								>
@@ -627,7 +630,9 @@
 									<!-- Departamento -->
 									<td class="px-4 py-3">
 										<span
-											class="rounded-md border px-2 py-0.5 text-xs font-semibold {deptBadgeClass(agent.department)}"
+											class="rounded-md border px-2 py-0.5 text-xs font-semibold {deptBadgeClass(
+												agent.department
+											)}"
 										>
 											{agent.department}
 										</span>
@@ -654,9 +659,7 @@
 									<!-- 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="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>
@@ -700,7 +703,9 @@
 									<!-- 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-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}%"
@@ -710,8 +715,8 @@
 												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'}"
+														? 'text-amber-600 dark:text-amber-400'
+														: 'text-red-600 dark:text-red-400'}"
 											>
 												{agent.slaPct}%
 											</span>

+ 0 - 190
src/routes/(app)/dashboard/perfil/+page.svelte

@@ -1,190 +0,0 @@
-<script>
-	import { User, Mail, Phone, MapPin, Building, Key, ChevronRight } from 'lucide-svelte';
-	import { goto } from '$app/navigation';
-	let isEditingPassword = false;
-	let passwordForm = {
-		currentPassword: '',
-		newPassword: '',
-		confirmPassword: ''
-	};
-
-	function handleChangePassword() {
-		isEditingPassword = true;
-	}
-
-	function handleSavePassword() {
-		if (passwordForm.newPassword !== passwordForm.confirmPassword) {
-			alert('As senhas não conferem');
-			return;
-		}
-		console.log('Alterando senha...');
-		isEditingPassword = false;
-		passwordForm = { currentPassword: '', newPassword: '', confirmPassword: '' };
-	}
-
-	function handleCancel() {
-		isEditingPassword = false;
-		passwordForm = { currentPassword: '', newPassword: '', confirmPassword: '' };
-	}
-</script>
-
-<div class="mx-auto max-w-3xl space-y-6">
-	<!-- Header -->
-	<div class="rounded-xl border border-slate-200 bg-white p-5 md:p-6 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="flex items-center gap-3">
-			<div class="h-8 w-8 shrink-0 rounded-lg bg-indigo-500/20 text-indigo-500 flex items-center justify-center">
-				<User size={20} strokeWidth={2.5} />
-			</div>
-			<h1 class="text-xl font-bold text-slate-900 dark:text-white">Meu Perfil</h1>
-		</div>
-		<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
-			Gerencie suas informações de perfil e preferências de segurança
-		</p>
-	</div>
-
-	<!-- Profile Information -->
-	<div class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="border-b border-slate-200 p-5 dark:border-slate-800">
-			<h2 class="text-lg font-semibold text-slate-900 dark:text-white">Informações Pessoais</h2>
-		</div>
-
-		<div class="p-5 space-y-4">
-			<!-- Name -->
-			<div>
-				<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-					Nome
-				</label>
-				<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700">
-					<User size={18} class="text-slate-400 shrink-0" />
-					<span class="text-slate-900 dark:text-slate-200">Admin</span>
-				</div>
-			</div>
-
-			<!-- Email -->
-			<div>
-				<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-					E-mail
-				</label>
-				<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700">
-					<Mail size={18} class="text-slate-400 shrink-0" />
-					<span class="text-slate-900 dark:text-slate-200">admin@nettown.com</span>
-				</div>
-			</div>
-
-			<!-- Phone -->
-			<div>
-				<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-					Telefone
-				</label>
-				<input
-					type="tel"
-					placeholder="(11) 99999-9999"
-					class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
-				/>
-			</div>
-
-			<!-- Company -->
-			<div>
-				<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-					Empresa
-				</label>
-				<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700">
-					<Building size={18} class="text-slate-400 shrink-0" />
-					<span class="text-slate-900 dark:text-slate-200">NetTown Analytics</span>
-				</div>
-			</div>
-
-			<!-- Save Button -->
-			<div class="flex gap-3 pt-4">
-				<button class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors duration-150">
-					Salvar Alterações
-				</button>
-			</div>
-		</div>
-	</div>
-
-	<!-- Security Section -->
-	<div class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]">
-		<div class="border-b border-slate-200 p-5 dark:border-slate-800">
-			<h2 class="text-lg font-semibold text-slate-900 dark:text-white">Segurança</h2>
-		</div>
-
-		{#if !isEditingPassword}
-			<div class="divide-y divide-slate-200 dark:divide-slate-800">
-				<div class="p-5 hover:bg-slate-50 transition-colors duration-150 dark:hover:bg-slate-800/50">
-					<div class="flex items-center justify-between">
-						<div class="flex items-center gap-3">
-							<div class="h-10 w-10 rounded-lg bg-slate-100 text-slate-600 flex items-center justify-center dark:bg-slate-800 dark:text-slate-400">
-								<Key size={18} />
-							</div>
-							<div>
-								<h3 class="text-sm font-medium text-slate-900 dark:text-white">Alterar Senha</h3>
-								<p class="mt-0.5 text-xs text-slate-600 dark:text-slate-400">Atualize sua senha com regularidade</p>
-							</div>
-						</div>
-						<button
-							onclick={handleChangePassword}
-							class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md bg-indigo-600 text-white hover:bg-indigo-700 transition-colors duration-150"
-						>
-							Alterar
-							<ChevronRight size={14} />
-						</button>
-					</div>
-				</div>
-			</div>
-		{:else}
-			<div class="p-5 space-y-4">
-				<div>
-					<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-						Senha Atual
-					</label>
-					<input
-						type="password"
-						placeholder="Insira sua senha atual"
-						bind:value={passwordForm.currentPassword}
-						class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
-					/>
-				</div>
-
-				<div>
-					<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-						Nova Senha
-					</label>
-					<input
-						type="password"
-						placeholder="Insira sua nova senha"
-						bind:value={passwordForm.newPassword}
-						class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
-					/>
-				</div>
-
-				<div>
-					<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
-						Confirmar Nova Senha
-					</label>
-					<input
-						type="password"
-						placeholder="Confirme sua nova senha"
-						bind:value={passwordForm.confirmPassword}
-						class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
-					/>
-				</div>
-
-				<div class="flex gap-3 pt-4">
-					<button
-						onclick={handleSavePassword}
-						class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors duration-150"
-					>
-						Salvar Senha
-					</button>
-					<button
-						onclick={handleCancel}
-						class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-slate-200 text-slate-900 hover:bg-slate-300 transition-colors duration-150 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
-					>
-						Cancelar
-					</button>
-				</div>
-			</div>
-		{/if}
-	</div>
-</div>

+ 59 - 127
src/routes/(app)/dashboard/personas/+page.svelte

@@ -8,120 +8,59 @@
 		TrendingUp,
 		HeartHandshake
 	} from 'lucide-svelte';
-	import { mockPersonasKpis, mockPersonas } from '$lib/core/models/mock-data.js';
-
-	let selectedPersona = $state(mockPersonas[0] ?? null);
+	import { onMount } from 'svelte';
+	import { api } from '$lib/core/api/client.js';
 
 	const periodOptions = [
-		{ id: 'week', label: 'Últimos 7 dias', multiplier: 1 },
-		{ id: 'month', label: 'Últimos 30 dias', multiplier: 1.25 },
-		{ id: 'quarter', label: 'Último trimestre', multiplier: 1.6 }
-	];
-
-	const unitOptions = [
-		{ id: 'all', label: 'Todas unidades', multiplier: 1 },
-		{ id: 'flagship', label: 'Loja Flagship', multiplier: 1.4 },
-		{ id: 'digital', label: 'Digital', multiplier: 1.2 },
-		{ id: 'franquias', label: 'Franquias', multiplier: 0.85 }
-	];
-
-	const areaOptions = [
-		{ id: 'any', label: 'Sem segmento de setor', multiplier: 1 },
-		{ id: 'atendimento', label: 'Atendimento', multiplier: 1.15 },
-		{ id: 'produto', label: 'Produto', multiplier: 1.05 },
-		{ id: 'logistica', label: 'Logística', multiplier: 0.9 }
+		{ id: 'week', label: 'Últimos 7 dias' },
+		{ id: 'month', label: 'Últimos 30 dias' },
+		{ id: 'quarter', label: 'Último trimestre' }
 	];
 
 	const sentimentOptions = [
-		{ id: 'all', label: 'Todos', bias: { churn: 1, volume: 1, potential: 0 } },
-		{ id: 'positive', label: 'Positivo', bias: { churn: 0.85, volume: 1.25, potential: 0.4 } },
-		{ id: 'neutral', label: 'Neutro', bias: { churn: 1, volume: 1, potential: 0 } },
-		{ id: 'negative', label: 'Negativo', bias: { churn: 1.25, volume: 0.85, potential: -0.5 } }
+		{ id: 'all', label: 'Todos' },
+		{ id: 'positive', label: 'Positivo' },
+		{ id: 'neutral', label: 'Neutro' },
+		{ id: 'negative', label: 'Negativo' }
 	];
 
-	let selectedPeriod = $state(periodOptions[0].id);
-	let selectedUnit = $state(unitOptions[0].id);
-	let selectedArea = $state(areaOptions[0].id);
-	let selectedSentiment = $state(sentimentOptions[0].id);
-
-	const baseKpis = {
-		active: Number(mockPersonasKpis.ativas) || 5,
-		churn: parseFloat(String(mockPersonasKpis.riscoChurn).replace(',', '.')) || 60.1,
-		loss:
-			Number(
-				String(mockPersonasKpis.perdaMensalEst)
-					.replace(/[^0-9,.-]/g, '')
-					.replace(',', '.')
-			) || 7521.27,
-		potentialScore: mockPersonasKpis.potencialExpansao === 'Neutro' ? 0 : 0.2
-	};
+	let selectedPeriod = $state('week');
+	let selectedSentiment = $state('all');
 
-	const baseStats = {
-		identified: 28,
-		messages: 1840,
-		aspects: 18,
-		subaspects: 56
-	};
+	// ── Dados (GET /v1/personas/overview) ───────────────────────────────────────
+	let kpis = $state({ active: 0, churn: 0, loss: 0, potentialLabel: '—' });
+	let personaStats = $state({ identified: 0, messages: 0, aspects: 0, subaspects: 0 });
+	let personas = $state([]);
+	let selectedPersona = $state(null);
+	let loading = $state(true);
+	let loadError = $state('');
 
-	const periodSelection = $derived.by(
-		() => periodOptions.find((opt) => opt.id === selectedPeriod) ?? periodOptions[0]
-	);
-	const unitSelection = $derived.by(
-		() => unitOptions.find((opt) => opt.id === selectedUnit) ?? unitOptions[0]
-	);
-	const areaSelection = $derived.by(
-		() => areaOptions.find((opt) => opt.id === selectedArea) ?? areaOptions[0]
-	);
-	const sentimentSelection = $derived.by(
-		() => sentimentOptions.find((opt) => opt.id === selectedSentiment) ?? sentimentOptions[0]
-	);
-
-	const combinedMultiplier = $derived.by(() => {
-		const periodMultiplier = periodSelection?.multiplier ?? 1;
-		const unitMultiplier = unitSelection?.multiplier ?? 1;
-		const areaMultiplier = areaSelection?.multiplier ?? 1;
-		return periodMultiplier * unitMultiplier * areaMultiplier;
-	});
+	async function loadPersonas() {
+		loading = true;
+		loadError = '';
+		try {
+			const data = await api.get('/v1/personas/overview', {
+				query: { period: selectedPeriod, sentiment: selectedSentiment }
+			});
+			kpis = data.kpis ?? { active: 0, churn: 0, loss: 0, potentialLabel: '—' };
+			personaStats = data.stats ?? { identified: 0, messages: 0, aspects: 0, subaspects: 0 };
+			personas = data.personas ?? [];
+			if (!selectedPersona || !personas.some((p) => p.id === selectedPersona.id)) {
+				selectedPersona = personas[0] ?? null;
+			}
+		} catch (err) {
+			loadError = err?.message ?? 'Falha ao carregar personas.';
+			personas = [];
+		} finally {
+			loading = false;
+		}
+	}
 
-	const kpis = $derived.by(() => {
-		const sentimentBias = sentimentSelection?.bias ?? sentimentOptions[0].bias;
-		const multiplier = Number.isFinite(combinedMultiplier) ? combinedMultiplier : 1;
-		const active = Math.max(1, Math.round(baseKpis.active * multiplier * sentimentBias.volume));
-		const churn = Math.min(
-			100,
-			Math.max(5, Number((baseKpis.churn * sentimentBias.churn).toFixed(1)))
-		);
-		const loss = Math.max(500, baseKpis.loss * multiplier * sentimentBias.churn * 0.9);
-		const potentialScore = Math.max(
-			-1,
-			Math.min(1.5, baseKpis.potentialScore + sentimentBias.potential)
-		);
-		const potentialLabel =
-			potentialScore >= 0.8
-				? 'Muito alto'
-				: potentialScore >= 0.3
-					? 'Alto'
-					: potentialScore >= -0.2
-						? 'Neutro'
-						: 'Baixo';
-		return {
-			active,
-			churn,
-			loss,
-			potentialLabel,
-			potentialScore
-		};
-	});
+	onMount(loadPersonas);
 
-	const personaStats = $derived.by(() => {
-		const multiplier = Number.isFinite(combinedMultiplier) ? combinedMultiplier : 1;
-		return {
-			identified: Math.round(baseStats.identified * multiplier),
-			messages: Math.round(baseStats.messages * multiplier * 1.1),
-			aspects: Math.round(baseStats.aspects * (0.9 + multiplier / 4)),
-			subaspects: Math.round(baseStats.subaspects * (0.85 + multiplier / 5))
-		};
-	});
+	function applyFilters() {
+		loadPersonas();
+	}
 
 	function formatCurrency(value) {
 		const safeValue = Number.isFinite(value) ? value : 0;
@@ -161,45 +100,25 @@
 			</h1>
 		</div>
 
-		<div class="mb-12 grid w-full grid-cols-1 gap-6 text-sm md:grid-cols-2 lg:grid-cols-4">
+		<div class="mb-12 grid w-full grid-cols-1 gap-6 text-sm md:grid-cols-2">
 			<div class="flex items-center justify-center gap-2">
 				<span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Período:</span>
 				<select
 					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
 					bind:value={selectedPeriod}
+					onchange={applyFilters}
 				>
 					{#each periodOptions as option}
 						<option value={option.id}>{option.label}</option>
 					{/each}
 				</select>
 			</div>
-			<div class="flex items-center justify-center gap-2">
-				<span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Unidade:</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedUnit}
-				>
-					{#each unitOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
-			<div class="flex items-center justify-center gap-2">
-				<span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Área:</span>
-				<select
-					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
-					bind:value={selectedArea}
-				>
-					{#each areaOptions as option}
-						<option value={option.id}>{option.label}</option>
-					{/each}
-				</select>
-			</div>
 			<div class="flex items-center justify-center gap-2">
 				<span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Sentimento:</span>
 				<select
 					class="rounded-md border border-slate-200 bg-white px-3 py-1.5 text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-[#0f172a] dark:text-slate-200"
 					bind:value={selectedSentiment}
+					onchange={applyFilters}
 				>
 					{#each sentimentOptions as option}
 						<option value={option.id}>{option.label}</option>
@@ -208,6 +127,14 @@
 			</div>
 		</div>
 
+		{#if loadError}
+			<div
+				class="mb-6 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-400"
+			>
+				{loadError}
+			</div>
+		{/if}
+
 		<!-- KPIs -->
 		<div class="grid w-full grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-4">
 			<div class="flex flex-col items-center justify-center text-center">
@@ -320,7 +247,7 @@
 			</div>
 
 			<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
-				{#each mockPersonas as persona}
+				{#each personas as persona (persona.id)}
 					<button
 						type="button"
 						onclick={() => openPersonaDetails(persona)}
@@ -352,6 +279,11 @@
 						</p>
 					</button>
 				{/each}
+				{#if !loading && personas.length === 0}
+					<div class="col-span-full py-10 text-center text-sm text-slate-400">
+						Nenhuma persona encontrada para os filtros atuais.
+					</div>
+				{/if}
 			</div>
 		</div>
 

+ 26 - 2
src/routes/(app)/dashboard/settings/+page.svelte

@@ -1,6 +1,6 @@
 <script>
-	import { Settings, Phone, Bell, HelpCircle } from 'lucide-svelte';
-	import { goto } from '$app/navigation';
+	import { Settings, Phone, Bell, HelpCircle, ShieldCheck } from 'lucide-svelte';
+	import SlaConfigManager from '$lib/features/sla/SlaConfigManager.svelte';
 
 	// Settings sections
 	const settingsSections = [
@@ -149,4 +149,28 @@
 			</div>
 		{/each}
 	</div>
+
+	<!-- SLA / Tempo de Resposta -->
+	<div
+		class="rounded-xl border border-slate-200 bg-white shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#161f30]"
+	>
+		<div class="border-b border-slate-200 p-5 dark:border-slate-800">
+			<div class="flex items-center gap-3">
+				<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"
+				>
+					<ShieldCheck size={18} strokeWidth={2} />
+				</div>
+				<div>
+					<h2 class="text-lg font-semibold text-slate-900 dark:text-white">SLA por Departamento</h2>
+					<p class="text-sm text-slate-500 dark:text-slate-400">
+						Tempos de primeira resposta e resolução, com status em tempo real
+					</p>
+				</div>
+			</div>
+		</div>
+		<div class="p-5">
+			<SlaConfigManager />
+		</div>
+	</div>
 </div>

+ 3 - 462
src/routes/(app)/dashboard/sla/+page.svelte

@@ -1,210 +1,12 @@
 <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';
+	import { ShieldCheck } from 'lucide-svelte';
+	import SlaConfigManager from '$lib/features/sla/SlaConfigManager.svelte';
 </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
@@ -224,266 +26,5 @@
 		</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}
+	<SlaConfigManager />
 </div>

+ 51 - 0
src/routes/+error.svelte

@@ -0,0 +1,51 @@
+<script>
+	import { page } from '$app/state';
+	import { goto } from '$app/navigation';
+	import { Compass, ArrowLeft } from 'lucide-svelte';
+
+	const isNotFound = $derived(page.status === 404);
+	const title = $derived(isNotFound ? 'Página não encontrada' : 'Algo deu errado');
+	const description = $derived(
+		isNotFound
+			? 'A página que você procura não existe ou foi movida.'
+			: (page.error?.message ?? 'Ocorreu um erro inesperado. Tente novamente.')
+	);
+</script>
+
+<svelte:head>
+	<title>{page.status} • Nettown Analytics</title>
+</svelte:head>
+
+<div
+	class="flex min-h-screen items-center justify-center bg-slate-50 p-6 dark:bg-[#0f172a]"
+>
+	<div class="w-full max-w-md text-center">
+		<div
+			class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-indigo-500/10 text-indigo-500"
+		>
+			<Compass size={32} strokeWidth={2} />
+		</div>
+
+		<div class="text-6xl font-extrabold tracking-tight text-slate-900 dark:text-white">
+			{page.status}
+		</div>
+		<h1 class="mt-3 text-xl font-bold text-slate-900 dark:text-white">{title}</h1>
+		<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">{description}</p>
+
+		<div class="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
+			<button
+				onclick={() => goto('/dashboard')}
+				class="inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600"
+			>
+				Ir para o Dashboard
+			</button>
+			<button
+				onclick={() => history.back()}
+				class="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-[#1e293b] dark:text-slate-200 dark:hover:bg-slate-800"
+			>
+				<ArrowLeft size={16} />
+				Voltar
+			</button>
+		</div>
+	</div>
+</div>

+ 5 - 0
src/routes/+layout.js

@@ -0,0 +1,5 @@
+// O app é uma SPA client-side (dados vindos da API em runtime, sessão via JWT).
+// Desliga SSR/prerender: o servidor estático serve o fallback (index.html) e o
+// roteamento + render acontecem no navegador.
+export const ssr = false;
+export const prerender = false;

+ 3 - 1
svelte.config.js

@@ -6,7 +6,9 @@ const config = {
 		// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
 		runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
 	},
-	kit: { adapter: adapter() }
+	// Fallback (SPA) porque o app é totalmente client-side e protegido por sessão:
+	// as páginas buscam dados da API em runtime, então não são pré-renderizadas.
+	kit: { adapter: adapter({ fallback: 'index.html' }) }
 };
 
 export default config;