浏览代码

add function in all screens and update charts

gdias 1 月之前
父节点
当前提交
64111181ea

+ 3 - 0
.gitignore

@@ -21,3 +21,6 @@ Thumbs.db
 # Vite
 vite.config.js.timestamp-*
 vite.config.ts.timestamp-*
+.windsurfrules
+.cursor
+claude.md

+ 29 - 31
src/lib/features/sentiment/domain/sentiment-dashboard.service.js

@@ -2,36 +2,31 @@ import { sentimentDashboardMockSource } from '../data/sentiment-dashboard.mock.j
 import { AlertTriangle, CheckCircle, MessageSquare, TrendingUp } from 'lucide-svelte';
 
 const summaryCardsConfig = [
-	{ 
-		key: 'atRiskClients', 
-		label: 'Clientes em risco',
-		icon: AlertTriangle,
-		color: 'text-red-600 dark:text-red-400',
-		bg: 'bg-red-50 dark:bg-red-400/10'
-	},
-	{ 
-		key: 'opportunities', 
-		label: 'Oportunidades',
-		icon: CheckCircle,
-		color: 'text-emerald-600 dark:text-emerald-400',
-		bg: 'bg-emerald-50 dark:bg-emerald-400/10'
-	},
-	{ 
-		key: 'recentInteractions', 
-		label: 'Interacoes recentes',
-		icon: MessageSquare,
-		color: 'text-sky-600 dark:text-sky-400',
-		bg: 'bg-sky-50 dark:bg-sky-400/10'
-	},
-	{ 
-		key: 'netTrend', 
-		label: 'Tendencia liquida',
-		icon: TrendingUp,
-		color: 'text-indigo-600 dark:text-indigo-400',
-		bg: 'bg-indigo-50 dark:bg-indigo-400/10'
-	}
+	{ key: 'atRiskClients', label: 'Clientes em risco', icon: AlertTriangle, color: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-400/10' },
+	{ key: 'opportunities', label: 'Oportunidades', icon: CheckCircle, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-400/10' },
+	{ key: 'recentInteractions', label: 'Interacoes recentes', icon: MessageSquare, color: 'text-sky-600 dark:text-sky-400', bg: 'bg-sky-50 dark:bg-sky-400/10' },
+	{ key: 'netTrend', label: 'Tendencia liquida', icon: TrendingUp, color: 'text-indigo-600 dark:text-indigo-400', bg: 'bg-indigo-50 dark:bg-indigo-400/10' }
 ];
 
+function buildTimelineViews(baseWeekTimeline) {
+	const week = baseWeekTimeline;
+	const day = Array.from({ length: 7 }, (_, idx) => ({
+		period: `Dia ${idx + 1}`,
+		gains: Math.round(28 + idx * 3 + (idx % 2 === 0 ? 4 : 0)),
+		losses: Math.round(12 + idx + (idx % 2 === 0 ? -1 : 1))
+	}));
+	const month = [
+		{ period: 'Jan', gains: 180, losses: 75 },
+		{ period: 'Fev', gains: 195, losses: 70 },
+		{ period: 'Mar', gains: 210, losses: 82 },
+		{ period: 'Abr', gains: 225, losses: 79 },
+		{ period: 'Mai', gains: 238, losses: 84 },
+		{ period: 'Jun', gains: 252, losses: 88 }
+	];
+
+	return { day, week, month };
+}
+
 const priorityLabel = {
 	high: 'Alta prioridade',
 	medium: 'Media prioridade',
@@ -39,10 +34,12 @@ const priorityLabel = {
 };
 
 export function getSentimentDashboardViewModel() {
+	const timelineViews = buildTimelineViews(sentimentDashboardMockSource.timeline);
 	return {
 		summaryCards: mapSummaryCards(sentimentDashboardMockSource.summary),
 		alerts: mapAlerts(sentimentDashboardMockSource.alerts),
-		timeline: sentimentDashboardMockSource.timeline,
+		timeline: timelineViews.week,
+		timelineViews,
 		aspects: mapAspects(sentimentDashboardMockSource.aspects)
 	};
 }
@@ -122,8 +119,9 @@ export function getAlertInsight(alertId, viewModel) {
 	};
 }
 
-export function getTimelineInsight(period, viewModel) {
-	const timelinePoint = viewModel.timeline.find((item) => item.period === period);
+export function getTimelineInsight(period, viewModel, timeframe = 'week') {
+	const timelineCollection = viewModel.timelineViews?.[timeframe] ?? viewModel.timeline ?? [];
+	const timelinePoint = timelineCollection.find((item) => item.period === period);
 	if (!timelinePoint) return createFallbackInsight();
 
 	const balance = timelinePoint.gains - timelinePoint.losses;

+ 39 - 12
src/lib/features/sentiment/ui/GainLossChart.svelte

@@ -1,5 +1,18 @@
 <script>
-	let { data = [], onPointSelect = () => {}, selectedPeriod = null } = $props();
+	const defaultTimeframeOptions = [
+		{ id: 'day', label: 'Diário' },
+		{ id: 'week', label: 'Semanal' },
+		{ id: 'month', label: 'Mensal' }
+	];
+
+	let {
+		data = [],
+		onPointSelect = () => {},
+		selectedPeriod = null,
+		timeframeOptions = defaultTimeframeOptions,
+		selectedTimeframe = 'week',
+		onTimeframeChange = () => {}
+	} = $props();
 
 	const chartWidth = 760;
 	const chartHeight = 320;
@@ -25,18 +38,32 @@
 </script>
 
 <section class="h-[460px] flex flex-col rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#1e293b] p-5 shadow-sm transition-colors duration-200">
-	<div class="mb-6 flex items-center justify-between border-b border-slate-100 dark:border-slate-800/50 pb-3 shrink-0">
-		<h2 class="text-sm font-bold tracking-wider text-slate-500 dark:text-slate-400 uppercase">
-			Opinião do Público
-		</h2>
-		<div class="flex items-center gap-4 text-xs font-bold">
-			<span class="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
-				<i class="h-2.5 w-2.5 rounded-full bg-emerald-500"></i>Positivo
-			</span>
-			<span class="flex items-center gap-1.5 text-red-600 dark:text-red-400">
-				<i class="h-2.5 w-2.5 rounded-full bg-red-500"></i>Negativo
-			</span>
+	<div class="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-slate-100 dark:border-slate-800/50 pb-3 shrink-0">
+		<div>
+			<h2 class="text-sm font-bold tracking-wider text-slate-500 dark:text-slate-400 uppercase">
+				Opinião do Público
+			</h2>
+			<div class="flex items-center gap-4 text-xs font-bold mt-2">
+				<span class="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
+					<i class="h-2.5 w-2.5 rounded-full bg-emerald-500"></i>Positivo
+				</span>
+				<span class="flex items-center gap-1.5 text-red-600 dark:text-red-400">
+					<i class="h-2.5 w-2.5 rounded-full bg-red-500"></i>Negativo
+				</span>
+			</div>
 		</div>
+		<label class="flex items-center gap-2 text-xs font-semibold text-slate-500 dark:text-slate-400">
+			<span>Período</span>
+			<select
+				class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm transition-colors focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none dark:border-slate-600 dark:bg-[#0f172a] dark:text-slate-200"
+				value={selectedTimeframe}
+				onchange={(event) => onTimeframeChange(event.currentTarget.value)}
+			>
+				{#each timeframeOptions as option}
+					<option value={option.id}>{option.label}</option>
+				{/each}
+			</select>
+		</label>
 	</div>
 
 	<div class="flex-1 w-full flex items-center justify-center pt-2">

+ 11 - 2
src/routes/(app)/+layout.svelte

@@ -40,6 +40,14 @@
         isSidebarCollapsed = !isSidebarCollapsed;
     }
 
+    function isNavItemActive(href) {
+        const currentPath = $page.url.pathname;
+        if (href === '/dashboard') {
+            return currentPath === '/dashboard';
+        }
+        return currentPath === href || currentPath.startsWith(`${href}/`);
+    }
+
     function toggleUserMenu() {
         isUserMenuOpen = !isUserMenuOpen;
     }
@@ -113,13 +121,14 @@
             </div>
             {#each navItems as item}
                 {@const Icon = item.icon}
+                {@const isActive = isNavItemActive(item.href)}
                 <a 
                     href={item.href} 
                     title={isSidebarCollapsed ? item.name : ''}
-                    class="flex items-center gap-3 py-2.5 rounded-lg transition-colors {$page.url.pathname === item.href || $page.url.pathname.startsWith(item.href + '/') ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-200'} {isSidebarCollapsed ? 'justify-center px-0' : 'px-3'}"
+                    class="flex items-center gap-3 py-2.5 rounded-lg transition-colors {isActive ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-200'} {isSidebarCollapsed ? 'justify-center px-0' : 'px-3'}"
                     onclick={() => isMobileMenuOpen = false}
                 >
-                    <Icon size={isSidebarCollapsed ? 22 : 18} strokeWidth={$page.url.pathname === item.href ? 2.5 : 2} class="shrink-0" />
+                    <Icon size={isSidebarCollapsed ? 22 : 18} strokeWidth={isActive ? 2.5 : 2} class="shrink-0" />
                     {#if !isSidebarCollapsed}
                         <span class="truncate">{item.name}</span>
                     {/if}

+ 244 - 65
src/routes/(app)/dashboard/+page.svelte

@@ -10,7 +10,7 @@
 	} from 'lucide-svelte';
 	import { Chart, Svg, Axis, Bar, Line, Spline, Highlight, Group } from 'layerchart';
 	import { scaleTime, scaleLinear, scaleBand } from 'd3-scale';
-	import { format } from 'date-fns';
+	import { format, addDays } from 'date-fns';
 	import { ptBR } from 'date-fns/locale';
 	import { goto } from '$app/navigation';
 
@@ -37,6 +37,47 @@
 
 	let selectedAspectDrilldown = $state(null);
 
+	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 }
+	];
+
+	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 } }
+	];
+
+	const volumeViewOptions = [
+		{ id: 'hour', label: 'Por Hora', format: 'HH:mm' },
+		{ 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 queueTab = $state('all');
+
 	function openAspectDrilldown(aspect, tone) {
 		selectedAspectDrilldown = { aspect, tone };
 	}
@@ -67,62 +108,191 @@
 			: 'Distribuição de Aspectos'
 	);
 
-	// Map KPIs to UI format
-	const kpis = [
+	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]
+	});
+
+	const combinedMultiplier = $derived(
+		selectedFilterContext.period.multiplier *
+		selectedFilterContext.unit.multiplier *
+		selectedFilterContext.area.multiplier
+	);
+
+	const sentimentWeights = $derived(selectedFilterContext.sentiment.weights);
+
+	const kpiDefinitions = [
 		{
 			title: 'Usuários Cadastrados',
-			value: mockKpis.newPeople.new,
-			// subvalue: `${mockKpis.newPeople.recurring} entrada / 0 saída`,
 			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'
+			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')
 		},
 		{
 			title: 'Atendentes Ativos',
-			value: mockKpis.activeService.value,
-			// subvalue: 'Com conversas no período',
 			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'
+			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')
 		},
 		{
 			title: 'Total de conversas',
-			value: mockKpis.whatsappMessages.current,
-			// subvalue: `${mockKpis.whatsappMessages.total} total geral`,
 			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'
+			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')
 		},
 		{
 			title: 'Sentimento Geral',
-			value: getSentimentLabel(mockKpis.generalSentiment.value),
-			// subvalue: `Score ${mockKpis.generalSentiment.value.toFixed(1).replace('.', ',')}`,
 			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'
+			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)
 		},
 		{
 			title: 'Usuários Não Cadastrados',
-			value: mockKpis.crmLines.total,
-			// subvalue: 'Base sem cadastro no CRM',
 			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'
+			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')
 		}
 	];
 
-	// Data-driven Radar Chart logic
+	const kpis = $derived(
+		kpiDefinitions.map((kpi) => ({
+			...kpi,
+			value: kpi.format(
+				kpi.compute({
+					multiplier: combinedMultiplier,
+					sentimentBias: selectedFilterContext.sentiment.multiplier
+				})
+			)
+		}))
+	);
+
+	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)
+		}))
+	);
+
+	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;
+	});
+
+	const queueCounts = $derived({
+		all: filteredQueueItems.length,
+		seller: filteredQueueItems.filter(i => i.isSellerFault).length,
+		client: filteredQueueItems.filter(i => i.isClientFault).length
+	});
+
+	const totalPotential = $derived(
+		filteredQueueItems.reduce((sum, item) => sum + item.impact, 0)
+	);
+
 	const radarRadius = 80;
 	const radarCenter = 100;
-	const angleStep = (Math.PI * 2) / mockRadarData.length;
+
+	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 radarPoints = $derived(
-		mockRadarData.map((d, i) => {
+		radarData.map((d, i) => {
 			const angle = i * angleStep - Math.PI / 2;
 			const r = (d.value / 100) * radarRadius;
 			return {
@@ -139,15 +309,17 @@
 
 	const radarPath = $derived(radarPoints.map((p) => `${p.x},${p.y}`).join(' '));
 
-	const gridLevels = [0.2, 0.4, 0.6, 0.8, 1].map((level) => {
-		return mockRadarData
-			.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(' ');
-	});
+	const gridLevels = $derived(
+		[0.2, 0.4, 0.6, 0.8, 1].map((level) => {
+			return 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(' ');
+		})
+	);
 </script>
 
 <svelte:head>
@@ -163,37 +335,44 @@
 			<span class="font-medium text-slate-600 dark:text-slate-400">Período:</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={selectedPeriod}
 			>
-				<option>Hoje (24h)</option>
-				<option>Ontem</option>
-				<option>Últimos 7 dias</option>
+				{#each periodOptions 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">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}
 			>
-				<option>Sem segmento</option>
+				{#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}
 			>
-				<option>Sem segmento de setor</option>
+				{#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
 				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={selectedSentimentFilter}
 			>
-				<option>Todos</option>
-				<option>Positivo</option>
-				<option>Neutro</option>
-				<option>Negativo</option>
+				{#each sentimentFilterOptions as option}
+					<option value={option.id}>{option.label}</option>
+				{/each}
 			</select>
 		</div>
 	</div>
@@ -212,13 +391,7 @@
 						<Icon size={20} strokeWidth={2.5} />
 					</div>
 					<div>
-						<div
-							class="mb-1 text-xs font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
-						>
-							{kpi.title}
-						</div>
-						<div class="mb-0.5 text-2xl font-bold text-slate-900 dark:text-white">{kpi.value}</div>
-						<div class="text-xs text-slate-500">{kpi.subvalue}</div>
+						<div class="text-2xl font-bold text-slate-900 dark:text-white">{kpi.value}</div>
 					</div>
 				</div>
 			</div>
@@ -243,11 +416,11 @@
 				<div class="flex gap-2">
 					<span
 						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"
-						>22 fora da janela</span
+						>{queueCounts.all} fora da janela</span
 					>
 					<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$ 81.004</span
+						>Potencial: R$ {totalPotential.toLocaleString('pt-BR')}</span
 					>
 				</div>
 			</div>
@@ -256,21 +429,24 @@
 				class="flex gap-2 border-b border-slate-200 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-900"
 			>
 				<button
-					class="rounded-lg border border-slate-300 bg-white px-4 py-1.5 text-sm font-medium text-slate-900 shadow-sm dark:border-slate-700 dark:bg-[#1e293b] dark:text-white dark:shadow-none"
-					>Todas (22)</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>
 				<button
-					class="rounded-lg border border-transparent bg-transparent px-4 py-1.5 text-sm font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white"
-					>Pela vendedora (13)</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>
 				<button
-					class="rounded-lg border border-transparent bg-transparent px-4 py-1.5 text-sm font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-white"
-					>Pelo cliente (9)</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>
 			</div>
 
 			<div class="flex-1 space-y-4 overflow-y-auto p-4">
-				{#each mockPriorityQueue as item}
+				{#each queueItems as item (item.id)}
 					<div
 						class="group relative overflow-hidden rounded-lg border border-red-200 bg-white p-4 shadow-sm transition-colors hover:border-red-300 dark:border-red-500/30 dark:bg-slate-900 dark:shadow-none dark:hover:border-red-500/60"
 					>
@@ -449,17 +625,20 @@
 				<h2 class="text-base font-bold text-slate-900 dark:text-white">Volume por Canal</h2>
 				<select
 					class="rounded-lg border border-slate-300 bg-slate-50 px-2 py-1 text-xs text-slate-900 focus:border-indigo-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
+					bind:value={volumeView}
 				>
-					<option>Por Hora</option>
+					{#each volumeViewOptions as option}
+						<option value={option.id}>{option.label}</option>
+					{/each}
 				</select>
 			</div>
 
 			<div class="h-full w-full flex-1">
 				<Chart
-					data={mockVolumeData}
-					x="date"
-					y="whatsapp"
-					yDomain={[0, 250]}
+					data={volumeData}
+					x={d => d.date}
+					y={d => d.whatsapp}
+					yDomain={[0, maxVolume]}
 					padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
 				>
 					<Svg>
@@ -470,7 +649,7 @@
 						/>
 						<Axis
 							placement="bottom"
-							format={(d) => format(d, 'HH:mm')}
+							format={(d) => format(d, selectedVolumeOption.format)}
 							class="fill-slate-500 text-xs dark:fill-slate-400"
 						/>
 						<Spline stroke="#38bdf8" strokeWidth={2} />
@@ -508,7 +687,7 @@
 			<div class="custom-scrollbar relative h-full w-full flex-1 overflow-y-auto pr-2">
 				{#if !selectedAspectDrilldown}
 					<div class="space-y-4">
-						{#each mockAspectsData as aspect}
+						{#each aspectsData as aspect}
 							<div>
 								<div class="mb-1 flex justify-between text-xs text-slate-600 dark:text-slate-300">
 									<span class="font-medium">{aspect.aspect}</span>

+ 18 - 2
src/routes/(app)/dashboard/analytics/+page.svelte

@@ -13,6 +13,11 @@
 	import { onMount } from 'svelte';
 
 	const dashboardData = getSentimentDashboardViewModel();
+	const timeframeOptions = [
+		{ id: 'day', label: 'Últimos 7 dias' },
+		{ id: 'week', label: 'Últimas 6 semanas' },
+		{ id: 'month', label: 'Últimos 6 meses' }
+	];
 	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.',
@@ -26,7 +31,9 @@
 	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);
 
 	// Read initial state from URL query parameters
 	let panelAspectId = $state($page.url.searchParams.get('aspect') ?? null);
@@ -61,7 +68,13 @@
 		selectedCardId = null;
 		selectedAlertId = null;
 		selectedPeriod = point.period;
-		selectedInsight = getTimelineInsight(point.period, dashboardData);
+		selectedInsight = getTimelineInsight(point.period, dashboardData, selectedTimeframe);
+	}
+
+	function handleTimeframeChange(value) {
+		selectedTimeframe = value;
+		selectedPeriod = null;
+		selectedInsight = defaultInsight;
 	}
 </script>
 
@@ -86,9 +99,12 @@
 		</div>
 		<div class="2xl:col-span-4 flex flex-col">
 			<GainLossChart
-				data={dashboardData.timeline}
+				data={timelineData}
 				onPointSelect={handleTimelinePointSelect}
 				selectedPeriod={selectedPeriod}
+				timeframeOptions={timeframeOptions}
+				selectedTimeframe={selectedTimeframe}
+				onTimeframeChange={handleTimeframeChange}
 			/>
 		</div>
 		<div class="2xl:col-span-2 flex flex-col">

+ 231 - 35
src/routes/(app)/dashboard/evolucao/+page.svelte

@@ -1,19 +1,185 @@
 <script>
 	import { TrendingUp, Shield, DollarSign, CheckCircle, Heart, Activity } from 'lucide-svelte';
-	import { Chart, Svg, Axis, Line, Spline, Highlight } from 'layerchart';
+	import { Chart, Svg, Axis, Spline, Highlight } from 'layerchart';
 	import { format } from 'date-fns';
-	import { ptBR } from 'date-fns/locale';
 	import { mockEvolucaoSentimentosData, mockMonitoramentoPlaybooksData } from '$lib/core/models/mock-data.js';
 
-	// Mock Data for KPIs
-	const mockEvolucaoKpis = {
-		churnEvitado: '--',
-		roiUpsell: '--',
-		scoreMedio: '--',
-		taxaEvolucao: '--',
-		conversaoEmocao: '--'
+	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 }
+	];
+
+	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 }
+	];
+
+	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 }
+		}
+	];
+
+	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 };
+	});
+
+	function clamp(value, min, max) {
+		return Math.min(max, Math.max(min, value));
+	}
+
+	function formatCurrency(value) {
+		const safeValue = Number.isFinite(value) ? value : 0;
+		return safeValue.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
+	}
+
+	function formatPercent(value) {
+		return `${Number.isFinite(value) ? value.toFixed(1) : '0.0'}%`;
+	}
+
+	function formatNumber(value) {
+		return Number.isFinite(value) ? value.toLocaleString('pt-BR') : '0';
+	}
+
+	function formatRatio(value) {
+		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';
@@ -40,38 +206,68 @@
 		<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">
-					<option>Últimos 7 dias</option>
+				<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">
-					<option>Sem segmento</option>
+				<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">
-					<option>Sem segmento de setor</option>
+				<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">
-					<option>Todas</option>
+				<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">
-					<option>Todos</option>
+				<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">
-					<option>Todas</option>
+				<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>
 		</div>
@@ -80,27 +276,27 @@
 		<div class="grid grid-cols-2 md:grid-cols-5 gap-6 w-full pb-4">
 			<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">{mockEvolucaoKpis.churnEvitado}</div>
+				<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>
 			</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">{mockEvolucaoKpis.roiUpsell}</div>
+				<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>
 			</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">{mockEvolucaoKpis.scoreMedio}</div>
+				<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>
 			</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">{mockEvolucaoKpis.taxaEvolucao}</div>
+				<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>
 			</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">{mockEvolucaoKpis.conversaoEmocao}</div>
+				<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>
 			</div>
 		</div>
@@ -113,9 +309,9 @@
 			<h2 class="text-base font-bold text-slate-900 dark:text-white mb-4">Evolução dos Sentimentos Geral</h2>
 			<div class="h-full w-full flex-1">
 				<Chart
-					data={mockEvolucaoSentimentosData}
-					x={d => d.date}
-					y={d => d.value}
+					data={evolucaoChartData}
+					x={(d) => d.date}
+					y={(d) => d.value}
 					yDomain={[-1, 1]}
 					padding={{ top: 10, right: 10, bottom: 20, left: 40 }}
 				>
@@ -159,9 +355,9 @@
 					<div class="flex flex-col items-end">
 						<span class="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Total do Período</span>
 						<div class="flex gap-1 text-sm font-bold">
-							<span class="text-indigo-400">148</span>
+							<span class="text-indigo-400">{formatNumber(playbooksTotals.novos)}</span>
 							<span class="text-slate-500">/</span>
-							<span class="text-emerald-400">5</span>
+							<span class="text-emerald-400">{formatNumber(playbooksTotals.convertidos)}</span>
 						</div>
 					</div>
 					<div class="flex items-center gap-4 text-xs font-medium text-slate-400">
@@ -179,9 +375,9 @@
 			
 			<div class="h-full w-full flex-1">
 				<Chart
-					data={mockMonitoramentoPlaybooksData}
-					x={d => d.date}
-					yDomain={[0, 60]}
+					data={playbooksChartData}
+					x={(d) => d.date}
+					yDomain={[0, playbooksMaxY]}
 					padding={{ top: 10, right: 10, bottom: 20, left: 30 }}
 				>
 					<Svg>
@@ -189,7 +385,7 @@
 							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, 10, 20, 30, 40, 50, 60]}
+							ticks={[0, playbooksMaxY * 0.25, playbooksMaxY * 0.5, playbooksMaxY * 0.75, playbooksMaxY]}
 						/>
 						<Axis
 							placement="bottom"

+ 171 - 83
src/routes/(app)/dashboard/interactions/+page.svelte

@@ -1,6 +1,6 @@
 <script>
-    import { Search, Download, Filter, Eye, X, MessageCircle } from 'lucide-svelte';
-    import { mockInteractions, mockChatLog } from '$lib/core/models/mock-data.js';
+    import { Search, Download, Eye, X, MessageCircle } from 'lucide-svelte';
+    import { mockInteractions } from '$lib/core/models/mock-data.js';
 
     let searchQuery = $state('');
     let selectedInteraction = $state(null);
@@ -9,11 +9,102 @@
     // Filters state
     let activeFilter = $state('all'); // all, my_clients, new, unfinished
 
-    const filteredInteractions = $derived(
-        mockInteractions.filter(interaction => {
-            if (searchQuery && !interaction.client.includes(searchQuery)) return false;
-            return true;
-        })
+    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!'
+            }
+        ]
+    ];
+
+    function applyFilter(interaction) {
+        const normalizedQuery = searchQuery.trim();
+        if (normalizedQuery && !interaction.client.includes(normalizedQuery)) return false;
+
+        if (activeFilter === 'my_clients') {
+            return interaction.agent !== '-';
+        }
+
+        if (activeFilter === 'new') {
+            return interaction.sentiment === 'CONTENTAMENTO' || interaction.score > 0.4;
+        }
+
+        if (activeFilter === 'unfinished') {
+            return interaction.sentiment === 'FRUSTRAÇÃO' || interaction.score < 0;
+        }
+
+        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'
+        }));
+    }
+
+    const conversationThreads = $derived(
+        filteredInteractions.map((interaction, idx) => ({
+            ...interaction,
+            thread: buildConversation(interaction, idx)
+        }))
     );
 
     function getSentimentColor(sentiment) {
@@ -76,8 +167,7 @@
 
     <!-- Table Container -->
     <div class="bg-white dark:bg-[#1e293b] rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden flex flex-col transition-colors duration-200">
-        <!-- Table Toolbar -->
-        <div class="p-4 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center bg-slate-50 dark:bg-[#1e293b] transition-colors duration-200">
+        <div class="p-4 border-b border-slate-200 dark:border-slate-800 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between bg-slate-50 dark:bg-[#1e293b]">
             <div class="relative">
                 <Search size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
                 <input 
@@ -88,11 +178,10 @@
                 />
             </div>
             <div class="text-sm text-slate-500 dark:text-slate-400 font-medium">
-                Exibindo 0 - {filteredInteractions.length} (Página 1)
+                Exibindo {conversationThreads.length} conversas
             </div>
         </div>
 
-        <!-- Table -->
         <div class="overflow-x-auto custom-scrollbar">
             <table class="w-full text-left border-collapse">
                 <thead>
@@ -108,12 +197,12 @@
                     </tr>
                 </thead>
                 <tbody class="divide-y divide-slate-100 dark:divide-slate-800/50 transition-colors duration-200">
-                    {#each filteredInteractions as item}
+                    {#each conversationThreads as item}
                         <tr class="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors group">
                             <td class="p-4">
                                 <span class="font-bold text-slate-900 dark:text-white">{item.client}</span>
                             </td>
-                            <td class="p-4 text-slate-600 dark:text-slate-300 font-medium">{item.agent}</td>
+                            <td class="p-4 text-slate-600 dark:text-slate-300 font-medium">{item.agent === '-' ? 'Time Nettown' : item.agent}</td>
                             <td class="p-4">
                                 <span class="px-2.5 py-1 rounded-md text-xs font-bold border {getSentimentColor(item.sentiment)} transition-colors duration-200">
                                     {item.sentiment}
@@ -134,8 +223,8 @@
                             </td>
                         </tr>
                     {/each}
-                    
-                    {#if filteredInteractions.length === 0}
+
+                    {#if conversationThreads.length === 0}
                         <tr>
                             <td colspan="8" class="p-8 text-center text-slate-500 font-medium">
                                 Nenhuma interação encontrada.
@@ -145,88 +234,85 @@
                 </tbody>
             </table>
         </div>
-        
-        <!-- Pagination -->
+
         <div class="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-[#1e293b] flex items-center justify-center gap-4 transition-colors duration-200">
             <button class="text-sm font-medium text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300 disabled:opacity-50" disabled>Anterior</button>
             <span class="text-sm font-bold text-slate-900 dark:text-white">Página 1</span>
             <button class="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300">Próxima</button>
         </div>
     </div>
-</div>
 
-<!-- Chat Modal -->
-{#if isChatModalOpen}
-    <div class="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6 transition-colors duration-200" role="dialog" aria-modal="true">
-        <!-- Backdrop -->
-        <div 
-            class="absolute inset-0 bg-slate-900/60 dark:bg-slate-950/80 backdrop-blur-sm transition-opacity" 
-            onclick={closeChat}
-            onkeydown={(e) => e.key === 'Escape' && closeChat()}
-            tabindex="0"
-            role="button"
-            aria-label="Close modal"
-        ></div>
-        
-        <!-- Modal Content -->
-        <div class="relative bg-white dark:bg-[#0f172a] rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl w-full max-w-6xl max-h-[90vh] flex flex-col md:flex-row overflow-hidden animate-in fade-in zoom-in-95 duration-200">
+    <!-- Chat Modal -->
+    {#if isChatModalOpen}
+        <div class="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6 transition-colors duration-200" role="dialog" aria-modal="true">
+            <!-- Backdrop -->
+            <div 
+                class="absolute inset-0 bg-slate-900/60 dark:bg-slate-950/80 backdrop-blur-sm transition-opacity" 
+                onclick={closeChat}
+                onkeydown={(e) => e.key === 'Escape' && closeChat()}
+                tabindex="0"
+                role="button"
+                aria-label="Close modal"
+            ></div>
             
-            <!-- Chat Area -->
-            <div class="flex-1 flex flex-col border-r border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-[#0b1120] min-h-[50vh] md:min-h-[600px] transition-colors duration-200">
-                <!-- Chat Header -->
-                <div class="h-16 px-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-[#1e293b] transition-colors duration-200">
-                    <div class="flex items-center gap-3">
-                        <div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-500 dark:text-slate-400">
-                            <MessageCircle size={20} />
+            <!-- Modal Content -->
+            <div class="relative bg-white dark:bg-[#0f172a] rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl w-full max-w-6xl max-h-[90vh] flex flex-col md:flex-row overflow-hidden animate-in fade-in zoom-in-95 duration-200">
+                <!-- Chat Area -->
+                <div class="flex-1 flex flex-col border-r border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-[#0b1120] min-h-[50vh] md:min-h-[600px] transition-colors duration-200">
+                    <!-- Chat Header -->
+                    <div class="h-16 px-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-[#1e293b] transition-colors duration-200">
+                        <div class="flex items-center gap-3">
+                            <div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-500 dark:text-slate-400">
+                                <MessageCircle size={20} />
+                            </div>
+                            <div>
+                                <div class="font-bold text-slate-900 dark:text-white">{selectedInteraction?.client}</div>
+                                <div class="text-xs font-medium text-slate-500 dark:text-slate-400">WhatsApp</div>
+                            </div>
                         </div>
-                        <div>
-                            <div class="font-bold text-slate-900 dark:text-white">{selectedInteraction?.client}</div>
-                            <div class="text-xs font-medium text-slate-500 dark:text-slate-400">WhatsApp</div>
+                        <button 
+                            onclick={closeChat}
+                            class="md:hidden p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
+                        >
+                            <X size={20} />
+                        </button>
+                    </div>
+
+                    <!-- Chat Messages -->
+                    <div class="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
+                        <div class="flex justify-center">
+                            <span class="px-3 py-1 rounded-full bg-white dark:bg-slate-800 text-xs font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 shadow-sm 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 ? 'bg-indigo-600 text-white rounded-2xl rounded-tr-sm shadow-md shadow-indigo-600/20' : 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-transparent rounded-2xl rounded-tl-sm shadow-sm'} p-3.5">
+                                    {#if msg.isAgent}
+                                        <div class="text-[10px] font-bold text-indigo-200 mb-1">Ativadora</div>
+                                    {/if}
+                                    <p class="text-sm leading-relaxed whitespace-pre-wrap">{msg.text}</p>
+                                    <div class="text-[10px] text-right mt-1.5 {msg.isAgent ? 'opacity-70' : 'text-slate-400 font-medium'}">{msg.time}</div>
+                                </div>
+                            </div>
+                        {/each}
                     </div>
-                    <button 
-                        onclick={closeChat}
-                        class="md:hidden p-2 text-slate-400 hover:text-slate-700 dark:hover:text-white rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
-                    >
-                        <X size={20} />
-                    </button>
                 </div>
-                
-                <!-- Chat Messages -->
-                <div class="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
-                    <div class="flex justify-center">
-                        <span class="px-3 py-1 rounded-full bg-white dark:bg-slate-800 text-xs font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 shadow-sm dark:shadow-none">
-                            ONTEM
-                        </span>
+
+                <!-- Analysis Area -->
+                <div class="w-full md:w-[400px] flex flex-col bg-white dark:bg-[#1e293b] shrink-0 transition-colors duration-200">
+                    <div class="h-16 px-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
+                        <h2 class="font-bold text-slate-900 dark:text-white text-lg">Relatório</h2>
+                        <button 
+                            onclick={closeChat}
+                            class="hidden md:flex px-3 py-1.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-md text-sm font-bold transition-colors"
+                        >
+                            Fechar
+                        </button>
                     </div>
                     
-                    {#each mockChatLog as msg}
-                        <div class="flex flex-col {msg.isAgent ? 'items-end' : 'items-start'}">
-                            <div class="max-w-[80%] {msg.isAgent ? 'bg-indigo-600 text-white rounded-2xl rounded-tr-sm shadow-md shadow-indigo-600/20' : 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-transparent rounded-2xl rounded-tl-sm shadow-sm'} p-3.5">
-                                {#if msg.isAgent}
-                                    <div class="text-[10px] font-bold text-indigo-200 mb-1">Ativadora</div>
-                                {/if}
-                                <p class="text-sm leading-relaxed whitespace-pre-wrap">{msg.text}</p>
-                                <div class="text-[10px] text-right mt-1.5 {msg.isAgent ? 'opacity-70' : 'text-slate-400 font-medium'}">{msg.time}</div>
-                            </div>
-                        </div>
-                    {/each}
-                </div>
-            </div>
-            
-            <!-- Analysis Area -->
-            <div class="w-full md:w-[400px] flex flex-col bg-white dark:bg-[#1e293b] shrink-0 transition-colors duration-200">
-                <div class="h-16 px-6 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
-                    <h2 class="font-bold text-slate-900 dark:text-white text-lg">Relatório</h2>
-                    <button 
-                        onclick={closeChat}
-                        class="hidden md:flex px-3 py-1.5 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-md text-sm font-bold transition-colors"
-                    >
-                        Fechar
-                    </button>
-                </div>
-                
-                <div class="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
+                    <div class="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
                     <!-- Mini chart placeholder -->
                     <div class="h-32 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 relative p-4 flex items-end overflow-hidden shadow-inner">
                         <!-- Decorative chart lines matching the UI -->
@@ -301,3 +387,5 @@
         </div>
     </div>
 {/if}
+
+</div>

+ 133 - 17
src/routes/(app)/dashboard/personas/+page.svelte

@@ -2,7 +2,103 @@
 	import { Users, AlertTriangle, DollarSign, Compass, UserRound, TrendingUp, HeartHandshake } from 'lucide-svelte';
 	import { mockPersonasKpis, mockPersonas } from '$lib/core/models/mock-data.js';
 
-	let selectedPersona = $state(null);
+	let selectedPersona = $state(mockPersonas[0] ?? null);
+
+	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 }
+	];
+
+	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 } }
+	];
+
+	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
+	};
+
+	const baseStats = {
+		identified: 28,
+		messages: 1840,
+		aspects: 18,
+		subaspects: 56
+	};
+
+	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;
+	});
+
+	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
+		};
+	});
+
+	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 formatCurrency(value) {
+		const safeValue = Number.isFinite(value) ? value : 0;
+		return safeValue.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
+	}
+
+	function formatNumber(value) {
+		return Number.isFinite(value) ? value.toLocaleString('pt-BR') : '0';
+	}
 
 	function openPersonaDetails(persona) {
 		selectedPersona = persona;
@@ -30,26 +126,46 @@
 		<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full text-sm mb-12">
 			<div class="flex items-center justify-center gap-2">
 				<span class="text-slate-500 dark:text-slate-400 whitespace-nowrap">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">
-					<option>Últimos 7 dias</option>
+				<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 items-center justify-center gap-2">
 				<span class="text-slate-500 dark:text-slate-400 whitespace-nowrap">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">
-					<option>Sem segmento</option>
+				<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="text-slate-500 dark:text-slate-400 whitespace-nowrap">Á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">
-					<option>Sem segmento de setor</option>
+				<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="text-slate-500 dark:text-slate-400 whitespace-nowrap">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">
-					<option>Todos</option>
+				<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>
@@ -58,22 +174,22 @@
 		<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full pb-4">
 			<div class="flex flex-col items-center justify-center text-center">
 				<UserRound size={24} class="text-amber-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{mockPersonasKpis.ativas}</div>
+				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{kpis.active}</div>
 				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Personas Ativas</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
 				<AlertTriangle size={24} class="text-red-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{mockPersonasKpis.riscoChurn}</div>
+				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{kpis.churn}</div>
 				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Risco de Churn</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
 				<DollarSign size={24} class="text-red-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{mockPersonasKpis.perdaMensalEst}</div>
+				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{formatCurrency(kpis.loss)}</div>
 				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Perda Mensal Est.</div>
 			</div>
 			<div class="flex flex-col items-center justify-center text-center">
 				<DollarSign size={24} class="text-amber-500 mb-3 mx-auto" />
-				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{mockPersonasKpis.potencialExpansao}</div>
+				<div class="text-3xl font-bold text-slate-900 dark:text-white mb-1">{kpis.potentialLabel}</div>
 				<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400">Potencial de Expansão</div>
 			</div>
 		</div>
@@ -102,25 +218,25 @@
 					<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400 mb-2">
 						Personas Identificadas
 					</div>
-					<div class="text-3xl font-bold text-slate-900 dark:text-white">0</div>
+					<div class="text-3xl font-bold text-slate-900 dark:text-white">{personaStats.identified}</div>
 				</div>
 				<div class="flex flex-col items-start text-left">
 					<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400 mb-2">
 						Volume Total de Mensagens Analisadas
 					</div>
-					<div class="text-3xl font-bold text-slate-900 dark:text-white">0</div>
+					<div class="text-3xl font-bold text-slate-900 dark:text-white">{formatNumber(personaStats.messages)}</div>
 				</div>
 				<div class="flex flex-col items-start text-left">
 					<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400 mb-2">
 						Aspectos Únicos
 					</div>
-					<div class="text-3xl font-bold text-slate-900 dark:text-white">0</div>
+					<div class="text-3xl font-bold text-slate-900 dark:text-white">{formatNumber(personaStats.aspects)}</div>
 				</div>
 				<div class="flex flex-col items-start text-left">
 					<div class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400 mb-2">
 						Subaspectos Únicos
 					</div>
-					<div class="text-3xl font-bold text-slate-900 dark:text-white">0</div>
+					<div class="text-3xl font-bold text-slate-900 dark:text-white">{formatNumber(personaStats.subaspects)}</div>
 				</div>
 			</div>