| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- <script>
- import {
- Users,
- AlertTriangle,
- DollarSign,
- Compass,
- UserRound,
- TrendingUp,
- HeartHandshake
- } from 'lucide-svelte';
- import { mockPersonasKpis, mockPersonas } from '$lib/core/models/mock-data.js';
- 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;
- }
- function closePersonaDetails() {
- selectedPersona = null;
- }
- </script>
- <svelte:head>
- <title>Laboratório de Personas - Nettown Analytics</title>
- </svelte:head>
- <div class="mx-auto max-w-[1600px] space-y-6">
- <!-- Top Section: Header, Filters, KPIs -->
- <div
- class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 md:p-8 dark:border-slate-800 dark:bg-[#161f30]"
- >
- <div class="mb-8 flex items-center gap-3">
- <div
- class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-500/20 text-amber-500"
- >
- <Compass size={20} strokeWidth={2.5} />
- </div>
- <h1 class="text-xl font-bold text-slate-900 dark:text-white">
- Expansão: Laboratório de Personas
- </h1>
- </div>
- <div class="mb-12 grid w-full grid-cols-1 gap-6 text-sm md:grid-cols-2 lg:grid-cols-4">
- <div class="flex items-center justify-center gap-2">
- <span class="whitespace-nowrap 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"
- 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="whitespace-nowrap 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"
- 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="whitespace-nowrap 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"
- 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="whitespace-nowrap 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"
- bind:value={selectedSentiment}
- >
- {#each sentimentOptions as option}
- <option value={option.id}>{option.label}</option>
- {/each}
- </select>
- </div>
- </div>
- <!-- KPIs -->
- <div class="grid w-full grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-4">
- <div class="flex flex-col items-center justify-center text-center">
- <UserRound size={24} class="mx-auto mb-3 text-amber-500" />
- <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">{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="mx-auto mb-3 text-red-500" />
- <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">{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="mx-auto mb-3 text-red-500" />
- <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
- {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="mx-auto mb-3 text-amber-500" />
- <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
- {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>
- </div>
- <!-- Personas Grid & Details -->
- <div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
- <div
- class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 md:p-6 xl:col-span-2 dark:border-slate-800 dark:bg-[#161f30]"
- >
- <div class="mb-8 flex flex-wrap items-center justify-between gap-4">
- <div class="flex items-center gap-3">
- <div class="flex items-center justify-center text-slate-700 dark:text-slate-300">
- <UserRound size={20} />
- </div>
- <h2 class="text-lg font-bold text-slate-900 dark:text-white">Minhas Personas</h2>
- <span class="text-sm text-slate-500 dark:text-slate-400"
- >Conheça sua base de forma humana</span
- >
- </div>
- <!-- <button class="rounded-lg bg-indigo-600 px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-700 shadow-sm whitespace-nowrap">
- Meus Clientes
- </button> -->
- </div>
- <!-- Header Stats -->
- <div
- class="mb-8 grid grid-cols-2 gap-4 border-b border-slate-100 pb-8 md:grid-cols-4 dark:border-slate-800/60"
- >
- <div class="flex flex-col items-start text-left">
- <div
- class="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
- >
- Personas Identificadas
- </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="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
- >
- Volume Total de Mensagens Analisadas
- </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="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
- >
- Aspectos Únicos
- </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="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
- >
- Subaspectos Únicos
- </div>
- <div class="text-3xl font-bold text-slate-900 dark:text-white">
- {formatNumber(personaStats.subaspects)}
- </div>
- </div>
- </div>
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
- {#each mockPersonas as persona}
- <button
- type="button"
- onclick={() => openPersonaDetails(persona)}
- class={`flex flex-col items-center justify-start rounded-xl border p-6 text-center transition-all duration-200 ${
- selectedPersona?.id === persona.id
- ? 'border-indigo-400 bg-indigo-50/50 shadow-[0_0_15px_rgba(99,102,241,0.15)] dark:border-indigo-500/80 dark:bg-[#1e293b] dark:shadow-[0_0_20px_rgba(99,102,241,0.1)]'
- : 'border-slate-200 bg-white hover:border-slate-300 dark:border-slate-700/60 dark:bg-[#1e293b]/40 dark:hover:border-slate-600 dark:hover:bg-[#1e293b]/60'
- }`}
- >
- <span
- class="mb-6 inline-block rounded-full bg-indigo-100 px-4 py-1 text-[10px] font-bold tracking-wider text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-400"
- >
- {persona.tipo}
- </span>
- <UserRound
- size={36}
- strokeWidth={1.5}
- class="mx-auto mb-4 text-slate-400 dark:text-slate-500"
- />
- <h3
- class="mb-2 text-center text-sm leading-snug font-bold text-slate-900 dark:text-white"
- >
- {persona.nome}
- </h3>
- <p
- class="text-center text-xs leading-relaxed font-medium text-slate-500 dark:text-slate-400"
- >
- {persona.descricao}
- </p>
- </button>
- {/each}
- </div>
- </div>
- <!-- Next Best Action Panel -->
- <div
- class="flex h-[600px] flex-col rounded-xl border border-slate-200 bg-white p-5 shadow-sm transition-colors duration-200 dark:border-slate-800 dark:bg-[#1e293b]"
- >
- <div class="mb-4">
- <h2 class="text-lg font-bold text-slate-900 dark:text-white">Next Best Action</h2>
- <p class="text-sm text-slate-500 dark:text-slate-400">
- Recomendações estratégicas por persona
- </p>
- </div>
- <div class="custom-scrollbar flex-1 overflow-y-auto pr-2">
- {#if selectedPersona}
- <div class="space-y-6">
- <div>
- <div class="mb-2 flex items-center gap-2">
- <UserRound size={18} class="text-indigo-500" />
- <h3 class="font-bold text-slate-900 dark:text-white">{selectedPersona.nome}</h3>
- </div>
- <p
- class="rounded-lg border border-slate-100 bg-slate-50 p-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
- >
- {selectedPersona.detalhes}
- </p>
- </div>
- <div class="space-y-4">
- <div
- class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/20 dark:bg-emerald-500/5"
- >
- <h4
- class="mb-2 flex items-center gap-2 font-bold text-emerald-800 dark:text-emerald-400"
- >
- <TrendingUp size={16} />
- Expansão
- </h4>
- <p class="text-sm text-emerald-700 dark:text-emerald-300">
- {selectedPersona.expansao}
- </p>
- </div>
- <div
- class="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-500/20 dark:bg-red-500/5"
- >
- <h4 class="mb-2 flex items-center gap-2 font-bold text-red-800 dark:text-red-400">
- <HeartHandshake size={16} />
- Engajamento (Salvar Churn)
- </h4>
- <p class="text-sm text-red-700 dark:text-red-300">
- {selectedPersona.engajamento}
- </p>
- </div>
- </div>
- </div>
- {:else}
- <div
- class="flex h-full flex-col items-center justify-center text-center text-slate-500 dark:text-slate-400"
- >
- <UserRound size={48} class="mb-4 opacity-20" />
- <p>Selecione uma persona ao lado para ver o Next Best Action.</p>
- </div>
- {/if}
- </div>
- </div>
- </div>
- </div>
|