|
|
@@ -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>
|