+page.svelte 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <script>
  2. import {
  3. Users,
  4. AlertTriangle,
  5. DollarSign,
  6. Compass,
  7. UserRound,
  8. TrendingUp,
  9. HeartHandshake
  10. } from 'lucide-svelte';
  11. import { mockPersonasKpis, mockPersonas } from '$lib/core/models/mock-data.js';
  12. let selectedPersona = $state(mockPersonas[0] ?? null);
  13. const periodOptions = [
  14. { id: 'week', label: 'Últimos 7 dias', multiplier: 1 },
  15. { id: 'month', label: 'Últimos 30 dias', multiplier: 1.25 },
  16. { id: 'quarter', label: 'Último trimestre', multiplier: 1.6 }
  17. ];
  18. const unitOptions = [
  19. { id: 'all', label: 'Todas unidades', multiplier: 1 },
  20. { id: 'flagship', label: 'Loja Flagship', multiplier: 1.4 },
  21. { id: 'digital', label: 'Digital', multiplier: 1.2 },
  22. { id: 'franquias', label: 'Franquias', multiplier: 0.85 }
  23. ];
  24. const areaOptions = [
  25. { id: 'any', label: 'Sem segmento de setor', multiplier: 1 },
  26. { id: 'atendimento', label: 'Atendimento', multiplier: 1.15 },
  27. { id: 'produto', label: 'Produto', multiplier: 1.05 },
  28. { id: 'logistica', label: 'Logística', multiplier: 0.9 }
  29. ];
  30. const sentimentOptions = [
  31. { id: 'all', label: 'Todos', bias: { churn: 1, volume: 1, potential: 0 } },
  32. { id: 'positive', label: 'Positivo', bias: { churn: 0.85, volume: 1.25, potential: 0.4 } },
  33. { id: 'neutral', label: 'Neutro', bias: { churn: 1, volume: 1, potential: 0 } },
  34. { id: 'negative', label: 'Negativo', bias: { churn: 1.25, volume: 0.85, potential: -0.5 } }
  35. ];
  36. let selectedPeriod = $state(periodOptions[0].id);
  37. let selectedUnit = $state(unitOptions[0].id);
  38. let selectedArea = $state(areaOptions[0].id);
  39. let selectedSentiment = $state(sentimentOptions[0].id);
  40. const baseKpis = {
  41. active: Number(mockPersonasKpis.ativas) || 5,
  42. churn: parseFloat(String(mockPersonasKpis.riscoChurn).replace(',', '.')) || 60.1,
  43. loss:
  44. Number(
  45. String(mockPersonasKpis.perdaMensalEst)
  46. .replace(/[^0-9,.-]/g, '')
  47. .replace(',', '.')
  48. ) || 7521.27,
  49. potentialScore: mockPersonasKpis.potencialExpansao === 'Neutro' ? 0 : 0.2
  50. };
  51. const baseStats = {
  52. identified: 28,
  53. messages: 1840,
  54. aspects: 18,
  55. subaspects: 56
  56. };
  57. const periodSelection = $derived.by(
  58. () => periodOptions.find((opt) => opt.id === selectedPeriod) ?? periodOptions[0]
  59. );
  60. const unitSelection = $derived.by(
  61. () => unitOptions.find((opt) => opt.id === selectedUnit) ?? unitOptions[0]
  62. );
  63. const areaSelection = $derived.by(
  64. () => areaOptions.find((opt) => opt.id === selectedArea) ?? areaOptions[0]
  65. );
  66. const sentimentSelection = $derived.by(
  67. () => sentimentOptions.find((opt) => opt.id === selectedSentiment) ?? sentimentOptions[0]
  68. );
  69. const combinedMultiplier = $derived.by(() => {
  70. const periodMultiplier = periodSelection?.multiplier ?? 1;
  71. const unitMultiplier = unitSelection?.multiplier ?? 1;
  72. const areaMultiplier = areaSelection?.multiplier ?? 1;
  73. return periodMultiplier * unitMultiplier * areaMultiplier;
  74. });
  75. const kpis = $derived.by(() => {
  76. const sentimentBias = sentimentSelection?.bias ?? sentimentOptions[0].bias;
  77. const multiplier = Number.isFinite(combinedMultiplier) ? combinedMultiplier : 1;
  78. const active = Math.max(1, Math.round(baseKpis.active * multiplier * sentimentBias.volume));
  79. const churn = Math.min(
  80. 100,
  81. Math.max(5, Number((baseKpis.churn * sentimentBias.churn).toFixed(1)))
  82. );
  83. const loss = Math.max(500, baseKpis.loss * multiplier * sentimentBias.churn * 0.9);
  84. const potentialScore = Math.max(
  85. -1,
  86. Math.min(1.5, baseKpis.potentialScore + sentimentBias.potential)
  87. );
  88. const potentialLabel =
  89. potentialScore >= 0.8
  90. ? 'Muito alto'
  91. : potentialScore >= 0.3
  92. ? 'Alto'
  93. : potentialScore >= -0.2
  94. ? 'Neutro'
  95. : 'Baixo';
  96. return {
  97. active,
  98. churn,
  99. loss,
  100. potentialLabel,
  101. potentialScore
  102. };
  103. });
  104. const personaStats = $derived.by(() => {
  105. const multiplier = Number.isFinite(combinedMultiplier) ? combinedMultiplier : 1;
  106. return {
  107. identified: Math.round(baseStats.identified * multiplier),
  108. messages: Math.round(baseStats.messages * multiplier * 1.1),
  109. aspects: Math.round(baseStats.aspects * (0.9 + multiplier / 4)),
  110. subaspects: Math.round(baseStats.subaspects * (0.85 + multiplier / 5))
  111. };
  112. });
  113. function formatCurrency(value) {
  114. const safeValue = Number.isFinite(value) ? value : 0;
  115. return safeValue.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
  116. }
  117. function formatNumber(value) {
  118. return Number.isFinite(value) ? value.toLocaleString('pt-BR') : '0';
  119. }
  120. function openPersonaDetails(persona) {
  121. selectedPersona = persona;
  122. }
  123. function closePersonaDetails() {
  124. selectedPersona = null;
  125. }
  126. </script>
  127. <svelte:head>
  128. <title>Laboratório de Personas - Nettown Analytics</title>
  129. </svelte:head>
  130. <div class="mx-auto max-w-[1600px] space-y-6">
  131. <!-- Top Section: Header, Filters, KPIs -->
  132. <div
  133. 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]"
  134. >
  135. <div class="mb-8 flex items-center gap-3">
  136. <div
  137. class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-500/20 text-amber-500"
  138. >
  139. <Compass size={20} strokeWidth={2.5} />
  140. </div>
  141. <h1 class="text-xl font-bold text-slate-900 dark:text-white">
  142. Expansão: Laboratório de Personas
  143. </h1>
  144. </div>
  145. <div class="mb-12 grid w-full grid-cols-1 gap-6 text-sm md:grid-cols-2 lg:grid-cols-4">
  146. <div class="flex items-center justify-center gap-2">
  147. <span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Período:</span>
  148. <select
  149. 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"
  150. bind:value={selectedPeriod}
  151. >
  152. {#each periodOptions as option}
  153. <option value={option.id}>{option.label}</option>
  154. {/each}
  155. </select>
  156. </div>
  157. <div class="flex items-center justify-center gap-2">
  158. <span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Unidade:</span>
  159. <select
  160. 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"
  161. bind:value={selectedUnit}
  162. >
  163. {#each unitOptions as option}
  164. <option value={option.id}>{option.label}</option>
  165. {/each}
  166. </select>
  167. </div>
  168. <div class="flex items-center justify-center gap-2">
  169. <span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Área:</span>
  170. <select
  171. 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"
  172. bind:value={selectedArea}
  173. >
  174. {#each areaOptions as option}
  175. <option value={option.id}>{option.label}</option>
  176. {/each}
  177. </select>
  178. </div>
  179. <div class="flex items-center justify-center gap-2">
  180. <span class="whitespace-nowrap text-slate-500 dark:text-slate-400">Sentimento:</span>
  181. <select
  182. 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"
  183. bind:value={selectedSentiment}
  184. >
  185. {#each sentimentOptions as option}
  186. <option value={option.id}>{option.label}</option>
  187. {/each}
  188. </select>
  189. </div>
  190. </div>
  191. <!-- KPIs -->
  192. <div class="grid w-full grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-4">
  193. <div class="flex flex-col items-center justify-center text-center">
  194. <UserRound size={24} class="mx-auto mb-3 text-amber-500" />
  195. <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">{kpis.active}</div>
  196. <div
  197. class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  198. >
  199. Personas Ativas
  200. </div>
  201. </div>
  202. <div class="flex flex-col items-center justify-center text-center">
  203. <AlertTriangle size={24} class="mx-auto mb-3 text-red-500" />
  204. <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">{kpis.churn}</div>
  205. <div
  206. class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  207. >
  208. Risco de Churn
  209. </div>
  210. </div>
  211. <div class="flex flex-col items-center justify-center text-center">
  212. <DollarSign size={24} class="mx-auto mb-3 text-red-500" />
  213. <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
  214. {formatCurrency(kpis.loss)}
  215. </div>
  216. <div
  217. class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  218. >
  219. Perda Mensal Est.
  220. </div>
  221. </div>
  222. <div class="flex flex-col items-center justify-center text-center">
  223. <DollarSign size={24} class="mx-auto mb-3 text-amber-500" />
  224. <div class="mb-1 text-3xl font-bold text-slate-900 dark:text-white">
  225. {kpis.potentialLabel}
  226. </div>
  227. <div
  228. class="text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  229. >
  230. Potencial de Expansão
  231. </div>
  232. </div>
  233. </div>
  234. </div>
  235. <!-- Personas Grid & Details -->
  236. <div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
  237. <div
  238. 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]"
  239. >
  240. <div class="mb-8 flex flex-wrap items-center justify-between gap-4">
  241. <div class="flex items-center gap-3">
  242. <div class="flex items-center justify-center text-slate-700 dark:text-slate-300">
  243. <UserRound size={20} />
  244. </div>
  245. <h2 class="text-lg font-bold text-slate-900 dark:text-white">Minhas Personas</h2>
  246. <span class="text-sm text-slate-500 dark:text-slate-400"
  247. >Conheça sua base de forma humana</span
  248. >
  249. </div>
  250. <!-- <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">
  251. Meus Clientes
  252. </button> -->
  253. </div>
  254. <!-- Header Stats -->
  255. <div
  256. 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"
  257. >
  258. <div class="flex flex-col items-start text-left">
  259. <div
  260. class="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  261. >
  262. Personas Identificadas
  263. </div>
  264. <div class="text-3xl font-bold text-slate-900 dark:text-white">
  265. {personaStats.identified}
  266. </div>
  267. </div>
  268. <div class="flex flex-col items-start text-left">
  269. <div
  270. class="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  271. >
  272. Volume Total de Mensagens Analisadas
  273. </div>
  274. <div class="text-3xl font-bold text-slate-900 dark:text-white">
  275. {formatNumber(personaStats.messages)}
  276. </div>
  277. </div>
  278. <div class="flex flex-col items-start text-left">
  279. <div
  280. class="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  281. >
  282. Aspectos Únicos
  283. </div>
  284. <div class="text-3xl font-bold text-slate-900 dark:text-white">
  285. {formatNumber(personaStats.aspects)}
  286. </div>
  287. </div>
  288. <div class="flex flex-col items-start text-left">
  289. <div
  290. class="mb-2 text-[11px] font-semibold tracking-wider text-slate-500 uppercase dark:text-slate-400"
  291. >
  292. Subaspectos Únicos
  293. </div>
  294. <div class="text-3xl font-bold text-slate-900 dark:text-white">
  295. {formatNumber(personaStats.subaspects)}
  296. </div>
  297. </div>
  298. </div>
  299. <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
  300. {#each mockPersonas as persona}
  301. <button
  302. type="button"
  303. onclick={() => openPersonaDetails(persona)}
  304. class={`flex flex-col items-center justify-start rounded-xl border p-6 text-center transition-all duration-200 ${
  305. selectedPersona?.id === persona.id
  306. ? '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)]'
  307. : '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'
  308. }`}
  309. >
  310. <span
  311. 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"
  312. >
  313. {persona.tipo}
  314. </span>
  315. <UserRound
  316. size={36}
  317. strokeWidth={1.5}
  318. class="mx-auto mb-4 text-slate-400 dark:text-slate-500"
  319. />
  320. <h3
  321. class="mb-2 text-center text-sm leading-snug font-bold text-slate-900 dark:text-white"
  322. >
  323. {persona.nome}
  324. </h3>
  325. <p
  326. class="text-center text-xs leading-relaxed font-medium text-slate-500 dark:text-slate-400"
  327. >
  328. {persona.descricao}
  329. </p>
  330. </button>
  331. {/each}
  332. </div>
  333. </div>
  334. <!-- Next Best Action Panel -->
  335. <div
  336. 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]"
  337. >
  338. <div class="mb-4">
  339. <h2 class="text-lg font-bold text-slate-900 dark:text-white">Next Best Action</h2>
  340. <p class="text-sm text-slate-500 dark:text-slate-400">
  341. Recomendações estratégicas por persona
  342. </p>
  343. </div>
  344. <div class="custom-scrollbar flex-1 overflow-y-auto pr-2">
  345. {#if selectedPersona}
  346. <div class="space-y-6">
  347. <div>
  348. <div class="mb-2 flex items-center gap-2">
  349. <UserRound size={18} class="text-indigo-500" />
  350. <h3 class="font-bold text-slate-900 dark:text-white">{selectedPersona.nome}</h3>
  351. </div>
  352. <p
  353. 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"
  354. >
  355. {selectedPersona.detalhes}
  356. </p>
  357. </div>
  358. <div class="space-y-4">
  359. <div
  360. class="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/20 dark:bg-emerald-500/5"
  361. >
  362. <h4
  363. class="mb-2 flex items-center gap-2 font-bold text-emerald-800 dark:text-emerald-400"
  364. >
  365. <TrendingUp size={16} />
  366. Expansão
  367. </h4>
  368. <p class="text-sm text-emerald-700 dark:text-emerald-300">
  369. {selectedPersona.expansao}
  370. </p>
  371. </div>
  372. <div
  373. class="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-500/20 dark:bg-red-500/5"
  374. >
  375. <h4 class="mb-2 flex items-center gap-2 font-bold text-red-800 dark:text-red-400">
  376. <HeartHandshake size={16} />
  377. Engajamento (Salvar Churn)
  378. </h4>
  379. <p class="text-sm text-red-700 dark:text-red-300">
  380. {selectedPersona.engajamento}
  381. </p>
  382. </div>
  383. </div>
  384. </div>
  385. {:else}
  386. <div
  387. class="flex h-full flex-col items-center justify-center text-center text-slate-500 dark:text-slate-400"
  388. >
  389. <UserRound size={48} class="mb-4 opacity-20" />
  390. <p>Selecione uma persona ao lado para ver o Next Best Action.</p>
  391. </div>
  392. {/if}
  393. </div>
  394. </div>
  395. </div>
  396. </div>