Sfoglia il codice sorgente

add the chart redirect

gdias 1 mese fa
parent
commit
f078189e1a

+ 22 - 4
src/lib/core/models/mock-data.js

@@ -111,10 +111,10 @@ export const mockSentimentDistribution = [
 ];
 
 export const mockAspectsData = [
-    { aspect: 'Atendimento', positive: 350, negative: 20 },
-    { aspect: 'Produto', positive: 130, negative: 40 },
-    { aspect: 'Entrega', positive: 60, negative: 40 },
-    { aspect: 'Monetário', positive: 30, negative: 20 },
+    { aspect: 'Atendimento', positive: 350, neutral: 50, negative: 20 },
+    { aspect: 'Produto', positive: 130, neutral: 30, negative: 40 },
+    { aspect: 'Entrega', positive: 60, neutral: 15, negative: 40 },
+    { aspect: 'Monetário', positive: 30, neutral: 10, negative: 20 },
 ];
 
 export const mockAspectsDrilldown = {
@@ -128,6 +128,11 @@ export const mockAspectsDrilldown = {
             { label: 'Empatia', value: 7 },
             { label: 'Acompanhamento ate fechar', value: 5 },
         ],
+        neutral: [
+            { label: 'Atendimento padrao', value: 30 },
+            { label: 'Informacoes basicas passadas', value: 15 },
+            { label: 'Tempo de espera normal', value: 5 },
+        ],
         negative: [
             { label: 'Demora no retorno', value: 13 },
             { label: 'Falta de continuidade', value: 3 },
@@ -145,6 +150,11 @@ export const mockAspectsDrilldown = {
             { label: 'Durabilidade', value: 7 },
             { label: 'Disponibilidade em estoque', value: 6 },
         ],
+        neutral: [
+            { label: 'Produto ok', value: 18 },
+            { label: 'Caimento aceitavel', value: 8 },
+            { label: 'Dentro do esperado', value: 4 },
+        ],
         negative: [
             { label: 'Preco alto', value: 16 },
             { label: 'Falta de tamanho/cor', value: 11 },
@@ -161,6 +171,10 @@ export const mockAspectsDrilldown = {
             { label: 'Embalagem elogiada', value: 6 },
             { label: 'Retirada facilitada', value: 4 },
         ],
+        neutral: [
+            { label: 'Chegou no prazo limite', value: 10 },
+            { label: 'Embalagem padrao', value: 5 },
+        ],
         negative: [
             { label: 'Atraso na entrega', value: 22 },
             { label: 'Falta de rastreio', value: 8 },
@@ -177,6 +191,10 @@ export const mockAspectsDrilldown = {
             { label: 'Cashback percebido', value: 3 },
             { label: 'Transparencia no valor final', value: 2 },
         ],
+        neutral: [
+            { label: 'Preco na media', value: 6 },
+            { label: 'Condicoes padrao', value: 4 },
+        ],
         negative: [
             { label: 'Frete elevado', value: 8 },
             { label: 'Preco final acima esperado', value: 6 },

+ 9 - 7
src/lib/features/sentiment/ui/AspectFeedbackPanel.svelte

@@ -1,8 +1,5 @@
 <script>
-	let { aspects = [] } = $props();
-
-	let selectedSentiment = $state('positive');
-	let selectedAspectId = $state(null);
+	let { aspects = [], selectedAspectId = $bindable(null), selectedSentiment = $bindable('positive') } = $props();
 
 	const sentimentOptions = [
 		{ id: 'positive', label: 'Positivo' },
@@ -10,16 +7,21 @@
 		{ id: 'negative', label: 'Negativo' }
 	];
 
+	// Make sure selectedSentiment is a valid option, otherwise fallback to positive
+	const validSentiment = $derived(
+		sentimentOptions.some(opt => opt.id === selectedSentiment) ? selectedSentiment : 'positive'
+	);
+
 	const activeAspectId = $derived(selectedAspectId ?? aspects[0]?.id ?? null);
 	const selectedAspect = $derived(aspects.find((aspect) => aspect.id === activeAspectId) ?? null);
-	const selectedQuotes = $derived(selectedAspect ? selectedAspect[selectedSentiment] ?? [] : []);
+	const selectedQuotes = $derived(selectedAspect ? selectedAspect[validSentiment] ?? [] : []);
 
 	function getSentimentCount(aspect, sentiment) {
 		return aspect[sentiment]?.length ?? 0;
 	}
 </script>
 
-<section class="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#1e293b] p-5 shadow-sm transition-colors duration-200">
+<section id="aspects-panel" class="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="flex flex-wrap items-center justify-between gap-4 border-b border-slate-100 dark:border-slate-800/50 pb-4">
 		<div>
 			<h2 class="text-sm font-bold tracking-wider text-slate-500 dark:text-slate-400 uppercase">
@@ -34,7 +36,7 @@
 				<button
 					type="button"
 					class={`rounded-md px-4 py-1.5 text-xs font-bold transition-colors ${
-						selectedSentiment === option.id
+						validSentiment === option.id
 							? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white'
 							: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
 					}`}

+ 49 - 22
src/routes/(app)/dashboard/+page.svelte

@@ -12,6 +12,7 @@
 	import { scaleTime, scaleLinear, scaleBand } from 'd3-scale';
 	import { format } from 'date-fns';
 	import { ptBR } from 'date-fns/locale';
+	import { goto } from '$app/navigation';
 
 	import {
 		mockKpis,
@@ -29,6 +30,11 @@
 		return 'Neutro';
 	}
 
+	function navigateToAnalytics(aspect, tone) {
+		const aspectId = aspect.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+		goto(`/dashboard/analytics?aspect=${aspectId}&sentiment=${tone}#aspects-panel`);
+	}
+
 	let selectedAspectDrilldown = $state(null);
 
 	function openAspectDrilldown(aspect, tone) {
@@ -48,12 +54,15 @@
 			: []
 	);
 
-	const maxDrilldownValue = $derived(Math.max(1, ...drilldownItems.map((item) => item.value)));
+	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' : 'Pontos Negativos'
+					selectedAspectDrilldown.tone === 'positive' ? 'Pontos Positivos' :
+					selectedAspectDrilldown.tone === 'neutral' ? 'Pontos Neutros' : 'Pontos Negativos'
 				}`
 			: 'Distribuição de Aspectos'
 	);
@@ -503,7 +512,7 @@
 							<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.negative} interações</span>
+									<span>{aspect.positive + aspect.neutral + aspect.negative} interações</span>
 								</div>
 								<div
 									class="flex h-7 w-full overflow-hidden rounded bg-slate-100 shadow-inner dark:bg-slate-800"
@@ -511,14 +520,21 @@
 									<button
 										type="button"
 										class="h-full bg-emerald-500 transition-opacity hover:opacity-90"
-										style="width: {(aspect.positive / (aspect.positive + aspect.negative)) * 100}%"
+										style="width: {(aspect.positive / (aspect.positive + aspect.neutral + aspect.negative)) * 100}%"
 										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}%"
+										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.negative)) * 100}%"
+										style="width: {(aspect.negative / (aspect.positive + aspect.neutral + aspect.negative)) * 100}%"
 										onclick={() => openAspectDrilldown(aspect.aspect, 'negative')}
 										aria-label={`Ver pontos negativos de ${aspect.aspect}`}
 									></button>
@@ -527,26 +543,37 @@
 						{/each}
 					</div>
 				{:else}
-					<div class="space-y-3">
-						{#each drilldownItems as item}
-							<div>
-								<div class="mb-1 flex justify-between text-xs text-slate-600 dark:text-slate-300">
-									<span class="font-medium">{item.label}</span>
-									<span>{item.value}</span>
-								</div>
-								<div class="h-5 w-full rounded bg-slate-100 dark:bg-slate-800">
-									<div
-										class="h-full rounded {selectedAspectDrilldown.tone === 'positive'
-											? 'bg-emerald-500'
-											: 'bg-red-500'}"
-										style="width: {(item.value / maxDrilldownValue) * 100}%"
-									></div>
+					<div class="flex flex-col h-full">
+						<div class="space-y-3 flex-1 overflow-y-auto custom-scrollbar pr-2 mb-4">
+							{#each drilldownItems as item}
+								<div>
+									<div class="mb-1 flex justify-between text-xs text-slate-600 dark:text-slate-300">
+										<span class="font-medium">{item.label}</span>
+										<span>{item.value}</span>
+									</div>
+									<div class="h-5 w-full rounded bg-slate-100 dark:bg-slate-800">
+										<div
+											class="h-full rounded {selectedAspectDrilldown.tone === 'positive'
+												? 'bg-emerald-500'
+												: selectedAspectDrilldown.tone === 'neutral'
+												? 'bg-slate-400'
+												: 'bg-red-500'}"
+											style="width: {(item.value / maxDrilldownValue) * 100}%"
+										></div>
+									</div>
 								</div>
-							</div>
-						{/each}
+							{/each}
+						</div>
+						<button
+							type="button"
+							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
+						</button>
 					</div>
 				{/if}
 			</div>
 		</div>
 	</div>
-</div>
+</div>

+ 22 - 1
src/routes/(app)/dashboard/analytics/+page.svelte

@@ -1,4 +1,5 @@
 <script>
+	import { page } from '$app/stores';
 	import SummaryCards from '$lib/features/sentiment/ui/SummaryCards.svelte';
 	import AlertsList from '$lib/features/sentiment/ui/AlertsList.svelte';
 	import GainLossChart from '$lib/features/sentiment/ui/GainLossChart.svelte';
@@ -9,6 +10,7 @@
 		getAlertInsight,
 		getTimelineInsight
 	} from '$lib/features/sentiment/domain/sentiment-dashboard.service.js';
+	import { onMount } from 'svelte';
 
 	const dashboardData = getSentimentDashboardViewModel();
 	const defaultInsight = {
@@ -26,6 +28,21 @@
 	let selectedPeriod = $state(null);
 	let selectedInsight = $state(defaultInsight);
 
+	// Read initial state from URL query parameters
+	let panelAspectId = $state($page.url.searchParams.get('aspect') ?? null);
+	let panelSentiment = $state($page.url.searchParams.get('sentiment') ?? 'positive');
+
+	onMount(() => {
+		if (window.location.hash === '#aspects-panel') {
+			setTimeout(() => {
+				const element = document.getElementById('aspects-panel');
+				if (element) {
+					element.scrollIntoView({ behavior: 'smooth' });
+				}
+			}, 100);
+		}
+	});
+
 	function handleCardSelect(card) {
 		selectedCardId = card.id;
 		selectedAlertId = null;
@@ -102,5 +119,9 @@
 		</div>
 	</section>
 
-	<AspectFeedbackPanel aspects={dashboardData.aspects} />
+	<AspectFeedbackPanel 
+		aspects={dashboardData.aspects} 
+		bind:selectedAspectId={panelAspectId}
+		bind:selectedSentiment={panelSentiment}
+	/>
 </div>