|
@@ -1,6 +1,6 @@
|
|
|
<script>
|
|
<script>
|
|
|
import { Search, Eye, X, MessageCircle } from 'lucide-svelte';
|
|
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 { format } from 'date-fns';
|
|
|
import { onMount } from 'svelte';
|
|
import { onMount } from 'svelte';
|
|
|
import { api } from '$lib/core/api/client.js';
|
|
import { api } from '$lib/core/api/client.js';
|
|
@@ -121,21 +121,49 @@
|
|
|
}, 300);
|
|
}, 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 chartData = $derived.by(() => {
|
|
|
const thread = details?.thread ?? [];
|
|
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`);
|
|
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);
|
|
const report = $derived(details?.report ?? null);
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -407,38 +435,55 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="custom-scrollbar flex-1 space-y-8 overflow-y-auto p-6">
|
|
<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>
|
|
</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>
|
|
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
|
|
<div class="grid grid-cols-2 gap-x-4 gap-y-6">
|