Pārlūkot izejas kodu

implement feature 2 lines in the conversation grafic

gdias 5 dienas atpakaļ
vecāks
revīzija
6a9fe95513
1 mainītis faili ar 86 papildinājumiem un 41 dzēšanām
  1. 86 41
      src/routes/(app)/dashboard/interactions/+page.svelte

+ 86 - 41
src/routes/(app)/dashboard/interactions/+page.svelte

@@ -1,6 +1,6 @@
 <script>
 	import { Search, Eye, X, MessageCircle } from 'lucide-svelte';
-	import { Chart, Svg, Axis, Spline, Highlight } from 'layerchart';
+	import { Chart, Svg, Axis, Spline } from 'layerchart';
 	import { format } from 'date-fns';
 	import { onMount } from 'svelte';
 	import { api } from '$lib/core/api/client.js';
@@ -121,21 +121,49 @@
 		}, 300);
 	}
 
-	// Gráfico funcional: volume acumulado de mensagens ao longo do tempo,
-	// construído a partir das mensagens reais retornadas em details.thread.
+	// Gráfico funcional: volume de mensagens ao longo do tempo, separado por autor
+	// (operador x cliente), construído a partir das mensagens reais de details.thread.
+	// A granularidade do agrupamento é adaptativa para conversas longas (hora) ou
+	// curtas (5 min / 1 min), evitando colapsar tudo num único ponto.
 	const chartData = $derived.by(() => {
 		const thread = details?.thread ?? [];
-		return thread
-			.map((message, index) => {
+		const points = thread
+			.map((message) => {
 				const stamp = new Date(`${message.date ?? ''}T${message.time ?? '00:00'}:00`);
-				return {
-					date: Number.isNaN(stamp.getTime()) ? null : stamp,
-					value: index + 1
-				};
+				return Number.isNaN(stamp.getTime())
+					? null
+					: { t: stamp.getTime(), isAgent: Boolean(message.isAgent) };
 			})
-			.filter((point) => point.date !== null);
+			.filter((point) => point !== null)
+			.sort((a, b) => a.t - b.t);
+
+		if (points.length === 0) return [];
+
+		const MIN = 60_000;
+		const HOUR = 60 * MIN;
+		const span = points[points.length - 1].t - points[0].t;
+		const bucketMs = span >= 2 * HOUR ? HOUR : span >= 20 * MIN ? 5 * MIN : MIN;
+
+		const buckets = new Map();
+		for (const point of points) {
+			const key = Math.floor(point.t / bucketMs) * bucketMs;
+			const entry = buckets.get(key) ?? { date: new Date(key), operador: 0, cliente: 0 };
+			if (point.isAgent) entry.operador += 1;
+			else entry.cliente += 1;
+			buckets.set(key, entry);
+		}
+
+		return [...buckets.values()].sort((a, b) => a.date - b.date);
 	});
 
+	// Limite superior do eixo Y: maior volume de mensagens num único intervalo.
+	const chartMaxY = $derived(
+		Math.max(1, ...chartData.map((point) => Math.max(point.operador, point.cliente)))
+	);
+
+	const CLIENT_COLOR = '#38bdf8'; // sky-400 — cliente
+	const AGENT_COLOR = '#6366f1'; // indigo-500 — operador (mesma cor das bolhas do agente)
+
 	const report = $derived(details?.report ?? null);
 </script>
 
@@ -407,38 +435,55 @@
 					</div>
 
 					<div class="custom-scrollbar flex-1 space-y-8 overflow-y-auto p-6">
-						<!-- Gráfico funcional: mensagens acumuladas ao longo do tempo -->
-						<div
-							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"
-						>
-							{#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.
+						<!-- Gráfico funcional: volume de mensagens por horário, operador x cliente -->
+						<div>
+							<div
+								class="mb-2 flex items-center justify-between text-[10px] font-bold tracking-wider text-slate-500 uppercase dark:text-slate-400"
+							>
+								<span>Mensagens por horário</span>
+								<div class="flex items-center gap-3 normal-case">
+									<div class="flex items-center gap-1.5">
+										<span class="h-2.5 w-2.5 rounded-sm" style="background-color: {CLIENT_COLOR}"></span>
+										Cliente
+									</div>
+									<div class="flex items-center gap-1.5">
+										<span class="h-2.5 w-2.5 rounded-sm" style="background-color: {AGENT_COLOR}"></span>
+										Operador
+									</div>
 								</div>
-							{/if}
+							</div>
+							<div
+								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"
+							>
+								{#if chartData.length >= 2}
+									<Chart
+										data={chartData}
+										x={(d) => d.date}
+										yDomain={[0, chartMaxY]}
+										padding={{ top: 10, right: 10, bottom: 18, left: 22 }}
+									>
+										<Svg>
+											<Axis
+												placement="left"
+												ticks={chartMaxY <= 4 ? chartMaxY : 4}
+												format={(d) => (Number.isInteger(d) ? d : '')}
+												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 y={(d) => d.cliente} stroke={CLIENT_COLOR} strokeWidth={2} />
+											<Spline y={(d) => d.operador} stroke={AGENT_COLOR} 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>
 
 						<div class="grid grid-cols-2 gap-x-4 gap-y-6">