@snksergio/design-system 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist-lib/ai/componentes/AlertModal.md +47 -0
  2. package/dist-lib/ai/componentes/AppShell.md +117 -0
  3. package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
  4. package/dist-lib/ai/componentes/Button.md +116 -0
  5. package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
  6. package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
  7. package/dist-lib/ai/componentes/CardOption.md +133 -0
  8. package/dist-lib/ai/componentes/Chart.md +93 -0
  9. package/dist-lib/ai/componentes/Chip.md +68 -0
  10. package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
  11. package/dist-lib/ai/componentes/ColorPicker.md +70 -0
  12. package/dist-lib/ai/componentes/Combobox.md +51 -0
  13. package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
  14. package/dist-lib/ai/componentes/DataList.md +111 -0
  15. package/dist-lib/ai/componentes/DataTable.md +867 -0
  16. package/dist-lib/ai/componentes/DatePicker.md +84 -0
  17. package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
  18. package/dist-lib/ai/componentes/EmptyState.md +72 -0
  19. package/dist-lib/ai/componentes/FileUploadField.md +95 -0
  20. package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
  21. package/dist-lib/ai/componentes/FooterTable.md +62 -0
  22. package/dist-lib/ai/componentes/FormField.md +110 -0
  23. package/dist-lib/ai/componentes/Gantt.md +552 -0
  24. package/dist-lib/ai/componentes/Header.md +98 -0
  25. package/dist-lib/ai/componentes/Icon.md +65 -0
  26. package/dist-lib/ai/componentes/Kanban.md +343 -0
  27. package/dist-lib/ai/componentes/Kpi.md +103 -0
  28. package/dist-lib/ai/componentes/List.md +61 -0
  29. package/dist-lib/ai/componentes/MarkdownText.md +59 -0
  30. package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
  31. package/dist-lib/ai/componentes/MessageAck.md +53 -0
  32. package/dist-lib/ai/componentes/MessageBubble.md +115 -0
  33. package/dist-lib/ai/componentes/MessageComposer.md +80 -0
  34. package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
  35. package/dist-lib/ai/componentes/Modal.md +88 -0
  36. package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
  37. package/dist-lib/ai/componentes/PageHeader.md +129 -0
  38. package/dist-lib/ai/componentes/Panel.md +84 -0
  39. package/dist-lib/ai/componentes/Scheduler.md +421 -0
  40. package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
  41. package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
  42. package/dist-lib/ai/componentes/Spinner.md +52 -0
  43. package/dist-lib/ai/componentes/Table.md +192 -0
  44. package/dist-lib/ai/componentes/TableToolbar.md +87 -0
  45. package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
  46. package/dist-lib/ai/componentes/Toast.md +49 -0
  47. package/dist-lib/ai/componentes/_primitivos.md +74 -0
  48. package/dist-lib/ai/componentes/avatar-ig.md +181 -0
  49. package/dist-lib/ai/componentes/indice.json +49 -0
  50. package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
  51. package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
  52. package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
  53. package/dist-lib/ai/global/componentes.md +217 -0
  54. package/dist-lib/ai/global/composicao.md +182 -0
  55. package/dist-lib/ai/indice.json +177 -0
  56. package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
  57. package/dist-lib/ai/manifest.json +16 -0
  58. package/dist-lib/ai/regras/design.md +88 -0
  59. package/dist-lib/ai/regras/temas.md +192 -0
  60. package/dist-lib/ai/regras-por-componente.json +103 -0
  61. package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
  62. package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
  63. package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
  64. package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
  65. package/package.json +4 -1
@@ -0,0 +1,1110 @@
1
+ import { useState } from "react";
2
+ import type { LucideIcon } from "@/lib/lucide-types";
3
+ import {
4
+ ArrowRight,
5
+ Bot,
6
+ Calendar,
7
+ CheckCircle2,
8
+ ChevronDown,
9
+ Clock,
10
+ Headphones,
11
+ Mail,
12
+ MessageSquare,
13
+ MoreVertical,
14
+ Phone,
15
+ Search,
16
+ Send,
17
+ Star,
18
+ Sun,
19
+ Timer,
20
+ TrendingUp,
21
+ UserPlus,
22
+ } from "lucide-react";
23
+ import {
24
+ Area,
25
+ AreaChart,
26
+ Bar,
27
+ BarChart,
28
+ CartesianGrid,
29
+ Cell,
30
+ Pie,
31
+ PieChart,
32
+ PolarAngleAxis,
33
+ RadialBar,
34
+ RadialBarChart,
35
+ ResponsiveContainer,
36
+ Tooltip,
37
+ XAxis,
38
+ YAxis,
39
+ } from "recharts";
40
+ import { cn } from "@/lib/utils";
41
+ import { BRAZIL_PATHS, BRAZIL_VIEWBOX } from "./dashboard-brazil-map";
42
+ import { Button } from "@/components/ui/Button/button";
43
+ import { Chip } from "@/components/ui/Chip";
44
+ import { PageHeader } from "@/components/ui/PageHeader";
45
+ import { Kpi, KpiGroup, KpiDelta } from "@/components/ui/Kpi";
46
+ import { Avatar, AvatarFallback } from "@/components/shadcn/avatar";
47
+
48
+ /* ─────────────────────────────────────────────────────────────
49
+ * Tipos + mock data — call center + analytics
50
+ * ───────────────────────────────────────────────────────────── */
51
+
52
+ type KpiTone = "brand" | "success" | "warning" | "info" | "danger" | "neutral";
53
+
54
+ type KpiCardData = {
55
+ id: string;
56
+ title: string;
57
+ value: string;
58
+ delta?: { value: string; direction: "up" | "down"; positive: boolean; label: string };
59
+ icon: LucideIcon;
60
+ tone: KpiTone;
61
+ };
62
+
63
+ const KPIS_PRIMARY: KpiCardData[] = [
64
+ { id: "in-conv", title: "Em atendimento", value: "12",
65
+ delta: { value: "+3", direction: "up", positive: true, label: "vs ontem" },
66
+ icon: Phone, tone: "success" },
67
+ { id: "waiting", title: "Em espera", value: "4",
68
+ delta: { value: "+1", direction: "up", positive: false, label: "vs ontem" },
69
+ icon: Timer, tone: "warning" },
70
+ { id: "finished", title: "Resolvidos hoje", value: "87",
71
+ delta: { value: "+12%", direction: "up", positive: true, label: "vs semana" },
72
+ icon: CheckCircle2, tone: "info" },
73
+ { id: "new", title: "Novos contatos", value: "260",
74
+ delta: { value: "+24", direction: "up", positive: true, label: "vs ontem" },
75
+ icon: UserPlus, tone: "brand" },
76
+ { id: "talk", title: "Tempo médio atend.", value: "4m 32s",
77
+ delta: { value: "-12s", direction: "down", positive: true, label: "melhorou" },
78
+ icon: Clock, tone: "neutral" },
79
+ { id: "wait", title: "Tempo médio espera", value: "1m 18s",
80
+ delta: { value: "-8s", direction: "down", positive: true, label: "melhorou" },
81
+ icon: Timer, tone: "neutral" },
82
+ ];
83
+
84
+ const KPIS_QUALITY: KpiCardData[] = [
85
+ { id: "frt", title: "First Response Time", value: "42s",
86
+ delta: { value: "-5s", direction: "down", positive: true, label: "melhorou" },
87
+ icon: TrendingUp, tone: "info" },
88
+ { id: "csat", title: "CSAT", value: "4.7",
89
+ delta: { value: "+0.2", direction: "up", positive: true, label: "vs semana" },
90
+ icon: Star, tone: "warning" },
91
+ { id: "nps", title: "NPS", value: "68",
92
+ delta: { value: "+5", direction: "up", positive: true, label: "vs mês" },
93
+ icon: Headphones, tone: "success" },
94
+ { id: "ai", title: "Resolvido por IA", value: "32%",
95
+ delta: { value: "+4%", direction: "up", positive: true, label: "vs semana" },
96
+ icon: Bot, tone: "brand" },
97
+ ];
98
+
99
+ /* Volume — stacked bars (2 séries: humano + IA) */
100
+ const VOLUME_BY_MONTH = [
101
+ { m: "Jan", human: 2400, ai: 1200 },
102
+ { m: "Feb", human: 4000, ai: 2800 },
103
+ { m: "Mar", human: 3500, ai: 2200 },
104
+ { m: "Apr", human: 3800, ai: 2900 },
105
+ { m: "May", human: 2700, ai: 2300 },
106
+ { m: "Jun", human: 3200, ai: 2700 },
107
+ { m: "Jul", human: 2600, ai: 2700 },
108
+ { m: "Aug", human: 4500, ai: 3200 },
109
+ { m: "Sep", human: 2800, ai: 2700 },
110
+ { m: "Oct", human: 4200, ai: 2300 },
111
+ { m: "Nov", human: 2600, ai: 2700 },
112
+ { m: "Dec", human: 3000, ai: 4000 },
113
+ ];
114
+
115
+ /* Current Visits — origem dos atendimentos (regiões/cidades) */
116
+ const REGIONS = [
117
+ { name: "São Paulo", value: 1650, color: "var(--color-fg-brand)", delta: "+4.7%", positive: true },
118
+ { name: "Rio de Janeiro", value: 350, color: "var(--color-fg-warning)", delta: "+2.1%", positive: true },
119
+ { name: "Belo Horizonte", value: 498, color: "var(--color-fg-info)", delta: "-1.7%", positive: false },
120
+ ];
121
+
122
+ /* Channels — lista estilo Campaign Performance */
123
+ type ChannelKind = "whatsapp" | "telegram" | "instagram" | "email" | "webchat";
124
+
125
+ const CHANNELS: { id: ChannelKind; name: string; volume: string; color: string; icon: LucideIcon; status: "active" | "paused" | "off" }[] = [
126
+ { id: "whatsapp", name: "WhatsApp", volume: "8.49k", color: "#25d366", icon: MessageSquare, status: "active" },
127
+ { id: "instagram", name: "Instagram", volume: "6.98k", color: "#e1306c", icon: MessageSquare, status: "active" },
128
+ { id: "telegram", name: "Telegram", volume: "3.21k", color: "#0088cc", icon: Send, status: "paused" },
129
+ { id: "email", name: "Email", volume: "2.14k", color: "#8754ec", icon: Mail, status: "active" },
130
+ { id: "webchat", name: "Web Chat", volume: "1.50k", color: "#0a3a2e", icon: MessageSquare, status: "off" },
131
+ ];
132
+
133
+ const CHANNEL_STATUS_LABEL: Record<"active" | "paused" | "off", string> = {
134
+ active: "Ativo",
135
+ paused: "Pausado",
136
+ off: "Desligado",
137
+ };
138
+
139
+ const CHANNEL_STATUS_TONE: Record<"active" | "paused" | "off", "success" | "warning" | "neutral"> = {
140
+ active: "success",
141
+ paused: "warning",
142
+ off: "neutral",
143
+ };
144
+
145
+ /* Open conversations — atendimentos ativos */
146
+ type OpenConv = {
147
+ id: string;
148
+ name: string;
149
+ initials: string;
150
+ hex: string;
151
+ channel: ChannelKind;
152
+ status: "attending" | "waiting" | "ai";
153
+ waitTime: string;
154
+ agent?: { initials: string; name: string; hex: string };
155
+ };
156
+
157
+ const OPEN_CONVS: OpenConv[] = [
158
+ { id: "#A-2453", name: "Maria Hernandez", initials: "MH", hex: "#f59e0b", channel: "whatsapp", status: "attending", waitTime: "2m", agent: { initials: "AC", name: "Aline Castro", hex: "#f59e0b" } },
159
+ { id: "#A-2455", name: "James Johnson", initials: "JJ", hex: "#0a3a2e", channel: "telegram", status: "waiting", waitTime: "8m" },
160
+ { id: "#A-2458", name: "Camila Ribeiro", initials: "CR", hex: "#8754ec", channel: "instagram", status: "attending", waitTime: "1m", agent: { initials: "VC", name: "Você", hex: "#0a3a2e" } },
161
+ { id: "#A-2461", name: "Roberto Souza", initials: "RS", hex: "#0088cc", channel: "whatsapp", status: "ai", waitTime: "3m" },
162
+ { id: "#A-2466", name: "Ana Costa", initials: "AC", hex: "#1cb280", channel: "email", status: "waiting", waitTime: "14m" },
163
+ ];
164
+
165
+ const STATUS_LABEL: Record<OpenConv["status"], string> = {
166
+ attending: "Em atendimento",
167
+ waiting: "Em espera",
168
+ ai: "IA",
169
+ };
170
+
171
+ const STATUS_TONE: Record<OpenConv["status"], "success" | "warning" | "info"> = {
172
+ attending: "success",
173
+ waiting: "warning",
174
+ ai: "info",
175
+ };
176
+
177
+ /* Agents */
178
+ type AgentRow = {
179
+ id: string;
180
+ name: string;
181
+ initials: string;
182
+ hex: string;
183
+ active: boolean;
184
+ resolved: number;
185
+ avgTime: string;
186
+ csat: number;
187
+ };
188
+
189
+ const AGENTS: AgentRow[] = [
190
+ { id: "aline", name: "Aline Castro", initials: "AC", hex: "#f59e0b", active: true, resolved: 24, avgTime: "3m 12s", csat: 4.9 },
191
+ { id: "carlos", name: "Carlos Souza", initials: "CS", hex: "#8754ec", active: true, resolved: 19, avgTime: "4m 45s", csat: 4.7 },
192
+ { id: "maria", name: "Maria Lima", initials: "ML", hex: "#ef4444", active: false, resolved: 17, avgTime: "5m 03s", csat: 4.5 },
193
+ { id: "joao", name: "João Pereira", initials: "JP", hex: "#1cb280", active: true, resolved: 15, avgTime: "3m 58s", csat: 4.8 },
194
+ { id: "you", name: "Você", initials: "VC", hex: "#0a3a2e", active: true, resolved: 12, avgTime: "4m 11s", csat: 4.6 },
195
+ ];
196
+
197
+ /* Traffic Data — origem das conversas (tabela) */
198
+ type TrafficRow = {
199
+ source: string;
200
+ visits: string;
201
+ bounce: string;
202
+ goal: number;
203
+ goalColor: string;
204
+ };
205
+
206
+ const TRAFFIC_ROWS: TrafficRow[] = [
207
+ { source: "Direct", visits: "1.300", bounce: "30%", goal: 80, goalColor: "var(--color-fg-warning)" },
208
+ { source: "Email Campaign", visits: "5.000", bounce: "45%", goal: 40, goalColor: "var(--color-fg-success)" },
209
+ { source: "Organic", visits: "3.000", bounce: "10%", goal: 55, goalColor: "var(--color-fg-warning)" },
210
+ { source: "Paid Search", visits: "2.450", bounce: "22%", goal: 68, goalColor: "var(--color-fg-success)" },
211
+ { source: "Social", visits: "1.870", bounce: "55%", goal: 25, goalColor: "var(--color-fg-danger)" },
212
+ { source: "Referral", visits: "940", bounce: "18%", goal: 72, goalColor: "var(--color-fg-success)" },
213
+ ];
214
+
215
+ /* ─────────────────────────────────────────────────────────────
216
+ * Atoms
217
+ * ───────────────────────────────────────────────────────────── */
218
+
219
+ const TONE_CLASSES: Record<KpiTone, { bg: string; fg: string }> = {
220
+ brand: { bg: "bg-bg-brand-subtle", fg: "text-fg-brand" },
221
+ success: { bg: "bg-bg-success-muted", fg: "text-fg-success" },
222
+ warning: { bg: "bg-bg-warning-muted", fg: "text-fg-warning" },
223
+ info: { bg: "bg-bg-info-muted", fg: "text-fg-info" },
224
+ danger: { bg: "bg-bg-danger-muted", fg: "text-fg-danger" },
225
+ neutral: { bg: "bg-bg-muted", fg: "text-fg-muted" },
226
+ };
227
+
228
+ /** KpiCard — title no topo + icon container + value + Chip delta (padronizado). */
229
+ function KpiCard({ kpi }: { kpi: KpiCardData }) {
230
+ const Icon = kpi.icon;
231
+ const cls = TONE_CLASSES[kpi.tone];
232
+ return (
233
+ <article className="flex flex-col gap-gp-lg p-pad-3xl bg-bg-surface border border-border-subtle rounded-radius-xl shadow-sh-sm">
234
+ <header className="flex items-start justify-between gap-gp-md">
235
+ <h3 className="m-0 text-body-md font-semibold text-fg-default">{kpi.title}</h3>
236
+ <span
237
+ className={cn(
238
+ "grid place-items-center size-form-lg rounded-radius-lg shrink-0",
239
+ cls.bg, cls.fg,
240
+ )}
241
+ aria-hidden
242
+ >
243
+ <Icon className="size-icon-md" strokeWidth={1.8} />
244
+ </span>
245
+ </header>
246
+ <div className="flex flex-col gap-gp-xs">
247
+ <div className="flex items-center gap-gp-md flex-wrap">
248
+ <span className="text-body-2xl font-bold text-fg-default leading-none [font-variant-numeric:tabular-nums]">
249
+ {kpi.value}
250
+ </span>
251
+ {kpi.delta && (
252
+ <Chip
253
+ color={kpi.delta.positive ? "success" : "danger"}
254
+ variant="soft"
255
+ size="sm"
256
+ shape="pill"
257
+ >
258
+ {kpi.delta.value}
259
+ </Chip>
260
+ )}
261
+ </div>
262
+ {kpi.delta && (
263
+ <span className="text-caption-sm text-fg-subtle">{kpi.delta.label}</span>
264
+ )}
265
+ </div>
266
+ </article>
267
+ );
268
+ }
269
+
270
+ /** Welcome banner — brand gradient, ilustração SVG, CTA. */
271
+ function WelcomeBanner() {
272
+ return (
273
+ <article className="relative overflow-hidden bg-bg-brand rounded-radius-xl p-pad-4xl flex items-center gap-gp-2xl flex-1 w-full">
274
+ {/* Texto à esquerda */}
275
+ <div className="flex flex-col gap-gp-md flex-1 min-w-0 z-[1]">
276
+ <h2 className="m-0 flex items-center gap-gp-md text-body-2xl font-bold text-fg-on-brand">
277
+ Boa tarde, Sergio
278
+ <Sun className="size-icon-md text-fg-warning" fill="currentColor" />
279
+ </h2>
280
+ <p className="m-0 text-body-md text-fg-on-brand opacity-80 max-w-[480px]">
281
+ Acompanhe a performance da sua equipe hoje.<br />
282
+ Snapshot rápido das principais métricas do atendimento.
283
+ </p>
284
+ <div>
285
+ <Button
286
+ color="secondary"
287
+ variant="filled"
288
+ size="md"
289
+ iconRight={<ArrowRight />}
290
+ className="bg-bg-surface text-fg-default hover:bg-bg-muted"
291
+ >
292
+ Ver relatório completo
293
+ </Button>
294
+ </div>
295
+ </div>
296
+
297
+ </article>
298
+ );
299
+ }
300
+
301
+ /** Key Insights — all-time revenue + stacked bar + legend (legend no footer via mt-auto). */
302
+ function KeyInsightsCard() {
303
+ return (
304
+ <article className="flex flex-col gap-gp-md p-pad-3xl bg-bg-surface border border-border-subtle rounded-radius-xl shadow-sh-sm h-full">
305
+ <header className="flex items-center justify-between gap-gp-md">
306
+ <h3 className="m-0 text-body-md font-medium text-fg-default">Key Insights</h3>
307
+ <Button color="secondary" variant="ghost" size="icon-sm" aria-label="Mais opções">
308
+ <MoreVertical />
309
+ </Button>
310
+ </header>
311
+ <p className="m-0 text-body-xs font-normal text-fg-muted">Receita acumulada (anual)</p>
312
+ <div className="flex items-center gap-gp-md flex-wrap">
313
+ <span className="text-body-2xl font-bold text-fg-default leading-none [font-variant-numeric:tabular-nums]">
314
+ R$ 395.7k
315
+ </span>
316
+ <Chip color="success" variant="soft" size="sm" shape="pill">
317
+ +2.7%
318
+ </Chip>
319
+ </div>
320
+ {/* Spacer empurra bar + legend pro bottom do card */}
321
+ <div className="mt-auto flex flex-col gap-gp-md">
322
+ {/* Stacked bar 3 segmentos */}
323
+ <div className="flex h-[8px] gap-[2px] rounded-radius-full overflow-hidden">
324
+ <span className="bg-bg-brand" style={{ flex: 4 }} aria-hidden />
325
+ <span className="bg-bg-warning" style={{ flex: 3 }} aria-hidden />
326
+ <span className="bg-bg-info" style={{ flex: 4 }} aria-hidden />
327
+ </div>
328
+ <div className="flex items-center gap-gp-lg flex-wrap text-caption-sm text-fg-muted">
329
+ <span className="inline-flex items-center gap-[5px]">
330
+ <span className="size-[8px] rounded-radius-full bg-bg-brand" aria-hidden />
331
+ São Paulo
332
+ </span>
333
+ <span className="inline-flex items-center gap-[5px]">
334
+ <span className="size-[8px] rounded-radius-full bg-bg-warning" aria-hidden />
335
+ Rio
336
+ </span>
337
+ <span className="inline-flex items-center gap-[5px]">
338
+ <span className="size-[8px] rounded-radius-full bg-bg-info" aria-hidden />
339
+ MG
340
+ </span>
341
+ </div>
342
+ </div>
343
+ </article>
344
+ );
345
+ }
346
+
347
+ function SectionCard({
348
+ title,
349
+ subtitle,
350
+ action,
351
+ children,
352
+ className,
353
+ }: {
354
+ title: string;
355
+ subtitle?: string;
356
+ action?: React.ReactNode;
357
+ children: React.ReactNode;
358
+ className?: string;
359
+ }) {
360
+ return (
361
+ <section
362
+ className={cn(
363
+ "flex flex-col gap-gp-2xl p-pad-3xl bg-bg-surface border border-border-subtle rounded-radius-xl shadow-sh-sm",
364
+ className,
365
+ )}
366
+ >
367
+ <header className="flex items-center justify-between gap-gp-md">
368
+ <div className="flex flex-col gap-[2px] min-w-0">
369
+ <h3 className="m-0 text-body-md font-medium text-fg-default">{title}</h3>
370
+ {subtitle && (
371
+ <p className="m-0 text-body-xs font-normal text-fg-muted">{subtitle}</p>
372
+ )}
373
+ </div>
374
+ {action ?? (
375
+ <Button color="secondary" variant="ghost" size="icon-sm" aria-label="Mais opções">
376
+ <MoreVertical />
377
+ </Button>
378
+ )}
379
+ </header>
380
+ {children}
381
+ </section>
382
+ );
383
+ }
384
+
385
+ /** Tooltip estilizado pros charts — consome tokens DS. */
386
+ const CHART_TOOLTIP_STYLE: React.CSSProperties = {
387
+ borderRadius: 10,
388
+ border: "1px solid var(--color-border-default)",
389
+ background: "var(--color-bg-surface-elevated)",
390
+ color: "var(--color-fg-default)",
391
+ fontSize: 12,
392
+ boxShadow: "rgba(0,0,0,0.18) 0 12px 24px -4px",
393
+ padding: "8px 12px",
394
+ };
395
+
396
+ const CHART_TOOLTIP_LABEL_STYLE: React.CSSProperties = {
397
+ color: "var(--color-fg-muted)",
398
+ fontSize: 11,
399
+ marginBottom: 4,
400
+ };
401
+
402
+ /** Stacked bar chart (humano + IA) com Recharts — tooltip + hover + responsivo. */
403
+ function VolumeStackedChart() {
404
+ return (
405
+ <div className="flex flex-col gap-gp-lg">
406
+ {/* Header com valor + chip delta + legend + selector ano */}
407
+ <div className="flex items-center gap-gp-md flex-wrap">
408
+ <div className="flex items-baseline gap-gp-md flex-wrap">
409
+ <span className="text-body-2xl font-bold text-fg-default leading-none [font-variant-numeric:tabular-nums]">
410
+ R$ 395.7k
411
+ </span>
412
+ <Chip color="success" variant="soft" size="sm" shape="pill">
413
+ +18%
414
+ </Chip>
415
+ <span className="text-body-xs font-normal text-fg-muted">vs ano passado</span>
416
+ </div>
417
+ <div className="ml-auto flex items-center gap-gp-md">
418
+ <span className="inline-flex items-center gap-[4px] text-caption-sm text-fg-muted">
419
+ <span className="size-[8px] rounded-radius-full bg-bg-brand" aria-hidden /> Humano
420
+ </span>
421
+ <span className="inline-flex items-center gap-[4px] text-caption-sm text-fg-muted">
422
+ <span className="size-[8px] rounded-radius-full bg-bg-brand-subtle" aria-hidden /> IA
423
+ </span>
424
+ <Button color="secondary" variant="outline" size="sm" iconRight={<ChevronDown />}>
425
+ 2026
426
+ </Button>
427
+ </div>
428
+ </div>
429
+
430
+ {/* Recharts: ResponsiveContainer + BarChart stacked + Tooltip + grid dashed */}
431
+ <div style={{ width: "100%", height: 280 }}>
432
+ <ResponsiveContainer>
433
+ <BarChart
434
+ data={VOLUME_BY_MONTH}
435
+ margin={{ top: 8, right: 8, left: -16, bottom: 0 }}
436
+ barCategoryGap={18}
437
+ >
438
+ <CartesianGrid
439
+ stroke="var(--color-border-subtle)"
440
+ strokeDasharray="4 4"
441
+ vertical={false}
442
+ />
443
+ <XAxis
444
+ dataKey="m"
445
+ tickLine={false}
446
+ axisLine={false}
447
+ fontSize={11}
448
+ stroke="var(--color-fg-subtle)"
449
+ />
450
+ <YAxis
451
+ tickLine={false}
452
+ axisLine={false}
453
+ fontSize={11}
454
+ stroke="var(--color-fg-subtle)"
455
+ tickFormatter={(v) => (v === 0 ? "0k" : `${v / 1000}k`)}
456
+ ticks={[0, 2000, 4000, 6000, 8000, 10000]}
457
+ domain={[0, 10000]}
458
+ />
459
+ <Tooltip
460
+ cursor={{ fill: "var(--color-bg-muted)", opacity: 0.5 }}
461
+ contentStyle={CHART_TOOLTIP_STYLE}
462
+ labelStyle={CHART_TOOLTIP_LABEL_STYLE}
463
+ formatter={(value, name) => {
464
+ const label = name === "human" ? "Humano" : "IA";
465
+ return [Number(value).toLocaleString("pt-BR"), label];
466
+ }}
467
+ />
468
+ <Bar dataKey="human" stackId="x" fill="var(--color-bg-brand)" radius={[0, 0, 0, 0]} />
469
+ <Bar dataKey="ai" stackId="x" fill="var(--color-bg-brand-subtle)" radius={[6, 6, 0, 0]} />
470
+ </BarChart>
471
+ </ResponsiveContainer>
472
+ </div>
473
+ </div>
474
+ );
475
+ }
476
+
477
+ /** Donut com Recharts PieChart — tooltip + hover + total no centro. */
478
+ function CurrentVisitsCard() {
479
+ const total = REGIONS.reduce((s, r) => s + r.value, 0);
480
+ return (
481
+ <SectionCard title="Origem dos atendimentos" subtitle="Por região" className="h-full">
482
+ {/* Donut — wrapper relativo ancora o center dentro do círculo */}
483
+ <div className="relative flex items-center justify-center" style={{ height: 210 }}>
484
+ <ResponsiveContainer width="100%" height={210}>
485
+ <PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
486
+ <Pie
487
+ data={REGIONS}
488
+ dataKey="value"
489
+ nameKey="name"
490
+ cx="50%"
491
+ cy="50%"
492
+ innerRadius={70}
493
+ outerRadius={92}
494
+ paddingAngle={3}
495
+ startAngle={90}
496
+ endAngle={-270}
497
+ stroke="none"
498
+ isAnimationActive
499
+ >
500
+ {REGIONS.map((r) => (
501
+ <Cell key={r.name} fill={r.color} />
502
+ ))}
503
+ </Pie>
504
+ <Tooltip
505
+ contentStyle={CHART_TOOLTIP_STYLE}
506
+ labelStyle={CHART_TOOLTIP_LABEL_STYLE}
507
+ formatter={(value) => Number(value).toLocaleString("pt-BR")}
508
+ />
509
+ </PieChart>
510
+ </ResponsiveContainer>
511
+ {/* Total absolute no centro do donut */}
512
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
513
+ <span className="text-caption-sm text-fg-muted">Total</span>
514
+ <span className="text-body-2xl font-bold text-fg-default leading-none [font-variant-numeric:tabular-nums]">
515
+ {total.toLocaleString("pt-BR")}
516
+ </span>
517
+ </div>
518
+ </div>
519
+ {/* Lista de regiões — mt-auto empurra pro bottom do card */}
520
+ <div className="mt-auto flex flex-col gap-gp-md">
521
+ {REGIONS.map((r) => (
522
+ <div key={r.name} className="flex items-center gap-gp-md">
523
+ <span
524
+ className="size-[10px] rounded-radius-full shrink-0"
525
+ style={{ background: r.color }}
526
+ aria-hidden
527
+ />
528
+ <span className="flex-1 text-body-xs text-fg-default">{r.name}</span>
529
+ <span className="text-body-xs font-normal text-fg-default [font-variant-numeric:tabular-nums]">
530
+ {r.value.toLocaleString("pt-BR")}
531
+ </span>
532
+ <Chip
533
+ color={r.positive ? "success" : "danger"}
534
+ variant="soft"
535
+ size="sm"
536
+ shape="pill"
537
+ >
538
+ {r.delta}
539
+ </Chip>
540
+ </div>
541
+ ))}
542
+ </div>
543
+ </SectionCard>
544
+ );
545
+ }
546
+
547
+ function PersonAvatar({ initials, hex, size = "md" }: { initials: string; hex: string; size?: "sm" | "md" }) {
548
+ const cls = size === "sm" ? "size-[24px] text-caption-xs" : "size-[32px] text-body-xs font-normal";
549
+ return (
550
+ <Avatar className={cls} style={{ background: hex }}>
551
+ <AvatarFallback className="bg-transparent text-white font-bold">{initials}</AvatarFallback>
552
+ </Avatar>
553
+ );
554
+ }
555
+
556
+ function OpenConversationsList() {
557
+ return (
558
+ <ul className="m-0 p-0 list-none flex flex-col">
559
+ {OPEN_CONVS.map((c) => {
560
+ const ChIcon = CHANNELS.find((ch) => ch.id === c.channel)?.icon ?? MessageSquare;
561
+ return (
562
+ <li
563
+ key={c.id}
564
+ className="flex items-center gap-gp-md py-pad-lg border-b border-border-subtle last:border-b-0 cursor-pointer hover:[&>*]:opacity-90 transition-opacity"
565
+ >
566
+ <PersonAvatar initials={c.initials} hex={c.hex} size="md" />
567
+ <div className="flex-1 min-w-0 flex flex-col gap-[2px]">
568
+ <div className="flex items-center gap-gp-sm">
569
+ <span className="text-body-sm font-semibold text-fg-default whitespace-nowrap overflow-hidden text-ellipsis">
570
+ {c.name}
571
+ </span>
572
+ <span className="text-caption-sm text-fg-subtle [font-variant-numeric:tabular-nums] shrink-0">
573
+ {c.id}
574
+ </span>
575
+ </div>
576
+ <div className="flex items-center gap-gp-sm flex-wrap text-caption-sm text-fg-muted">
577
+ <span className="inline-flex items-center gap-[3px]">
578
+ <ChIcon size={11} strokeWidth={1.8} aria-hidden />
579
+ {CHANNELS.find((ch) => ch.id === c.channel)?.name ?? c.channel}
580
+ </span>
581
+ {c.agent && (
582
+ <>
583
+ <span aria-hidden>·</span>
584
+ <span>{c.agent.name}</span>
585
+ </>
586
+ )}
587
+ </div>
588
+ </div>
589
+ <div className="flex flex-col items-end gap-[4px] shrink-0">
590
+ <Chip color={STATUS_TONE[c.status]} variant="soft" size="sm" shape="pill">
591
+ {STATUS_LABEL[c.status]}
592
+ </Chip>
593
+ <span className="inline-flex items-center gap-[3px] text-caption-sm text-fg-muted [font-variant-numeric:tabular-nums]">
594
+ <Clock size={10} strokeWidth={1.8} aria-hidden />
595
+ {c.waitTime}
596
+ </span>
597
+ </div>
598
+ </li>
599
+ );
600
+ })}
601
+ </ul>
602
+ );
603
+ }
604
+
605
+ function AgentPerformanceList() {
606
+ return (
607
+ <ul className="m-0 p-0 list-none flex flex-col">
608
+ {AGENTS.map((a, i) => (
609
+ <li
610
+ key={a.id}
611
+ className="flex items-center gap-gp-md py-pad-lg border-b border-border-subtle last:border-b-0"
612
+ >
613
+ <span className="grid place-items-center size-[20px] rounded-radius-full bg-bg-muted text-caption-sm font-bold text-fg-muted shrink-0 [font-variant-numeric:tabular-nums]">
614
+ {i + 1}
615
+ </span>
616
+ <div className="relative shrink-0">
617
+ <PersonAvatar initials={a.initials} hex={a.hex} size="md" />
618
+ <span
619
+ className={cn(
620
+ "absolute -bottom-[1px] -right-[1px] size-[8px] rounded-radius-full border border-bg-surface",
621
+ a.active ? "bg-bg-success" : "bg-bg-muted",
622
+ )}
623
+ aria-label={a.active ? "online" : "offline"}
624
+ />
625
+ </div>
626
+ <div className="flex-1 min-w-0 flex flex-col gap-[2px]">
627
+ <span className="text-body-sm font-semibold text-fg-default whitespace-nowrap overflow-hidden text-ellipsis">
628
+ {a.name}
629
+ </span>
630
+ <span className="text-caption-sm text-fg-muted">
631
+ {a.resolved} resolvidos · {a.avgTime} médio
632
+ </span>
633
+ </div>
634
+ <div className="flex items-center gap-[3px] shrink-0 text-body-xs font-semibold text-fg-default">
635
+ <Star size={11} strokeWidth={2} className="text-fg-warning fill-current" aria-hidden />
636
+ <span className="[font-variant-numeric:tabular-nums]">{a.csat.toFixed(1)}</span>
637
+ </div>
638
+ </li>
639
+ ))}
640
+ </ul>
641
+ );
642
+ }
643
+
644
+ /** Canais com volume + status — estilo Campaign Performance. */
645
+ function ChannelPerformanceList() {
646
+ return (
647
+ <ul className="m-0 p-0 list-none flex flex-col">
648
+ {CHANNELS.map((c) => {
649
+ const Icon = c.icon;
650
+ return (
651
+ <li
652
+ key={c.id}
653
+ className="flex items-center gap-gp-lg py-pad-lg border-b border-border-subtle last:border-b-0"
654
+ >
655
+ <span
656
+ className="grid place-items-center size-[36px] rounded-radius-full shrink-0 text-white"
657
+ style={{ background: c.color }}
658
+ aria-hidden
659
+ >
660
+ <Icon size={16} strokeWidth={1.8} />
661
+ </span>
662
+ <div className="flex-1 min-w-0 flex flex-col gap-[2px]">
663
+ <span className="text-body-sm font-semibold text-fg-default">{c.name}</span>
664
+ <span className="text-caption-sm text-fg-muted">
665
+ {c.volume} mensagens
666
+ </span>
667
+ </div>
668
+ <Chip color={CHANNEL_STATUS_TONE[c.status]} variant="soft" size="sm" shape="pill">
669
+ {CHANNEL_STATUS_LABEL[c.status]}
670
+ </Chip>
671
+ </li>
672
+ );
673
+ })}
674
+ </ul>
675
+ );
676
+ }
677
+
678
+ /** Traffic Data — tabela com source/visits/bounce/goal. */
679
+ function TrafficDataTable() {
680
+ return (
681
+ <SectionCard
682
+ title="Origem do tráfego"
683
+ subtitle="Fontes de aquisição"
684
+ className="flex-1 h-full"
685
+ action={
686
+ <div className="relative max-w-[200px]">
687
+ <Search
688
+ size={14}
689
+ strokeWidth={1.8}
690
+ className="absolute left-pad-lg top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none"
691
+ />
692
+ <input
693
+ type="search"
694
+ placeholder="Buscar..."
695
+ className="h-form-md pl-[34px] pr-pad-md w-full rounded-radius-md bg-bg-muted border border-border-input text-body-md text-fg-default placeholder:text-fg-muted focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring-brand focus-visible:border-border-brand"
696
+ />
697
+ </div>
698
+ }
699
+ >
700
+ <div className="overflow-x-auto -mx-pad-2xl px-pad-2xl">
701
+ <table className="w-full text-body-md border-separate border-spacing-0">
702
+ <thead>
703
+ <tr className="text-caption-sm font-bold tracking-[0.04em] text-fg-subtle uppercase">
704
+ <th className="text-left pb-pad-md border-b border-border-subtle font-bold">Fonte</th>
705
+ <th className="text-right pb-pad-md border-b border-border-subtle font-bold">Visitas</th>
706
+ <th className="text-left pb-pad-md pl-pad-2xl border-b border-border-subtle font-bold">Bounce</th>
707
+ <th className="text-left pb-pad-md pl-pad-2xl border-b border-border-subtle font-bold">Meta (%)</th>
708
+ </tr>
709
+ </thead>
710
+ <tbody>
711
+ {TRAFFIC_ROWS.map((r) => (
712
+ <tr key={r.source} className="text-body-xs font-normal">
713
+ <td className="py-pad-lg border-b border-border-subtle text-fg-default font-medium">
714
+ {r.source}
715
+ </td>
716
+ <td className="py-pad-lg border-b border-border-subtle text-right text-fg-default [font-variant-numeric:tabular-nums]">
717
+ {r.visits}
718
+ </td>
719
+ <td className="py-pad-sm pl-pad-2xl border-b border-border-subtle text-fg-muted [font-variant-numeric:tabular-nums]">
720
+ {r.bounce}
721
+ </td>
722
+ <td className="py-pad-sm pl-pad-2xl border-b border-border-subtle">
723
+ <div className="flex items-center gap-gp-md">
724
+ <div className="flex-1 h-[6px] bg-bg-muted rounded-radius-full overflow-hidden min-w-[80px]">
725
+ <div
726
+ className="h-full rounded-radius-full"
727
+ style={{ width: `${r.goal}%`, background: r.goalColor }}
728
+ />
729
+ </div>
730
+ <span className="text-caption-sm font-semibold text-fg-default [font-variant-numeric:tabular-nums] shrink-0 min-w-[32px] text-right">
731
+ {r.goal}%
732
+ </span>
733
+ </div>
734
+ </td>
735
+ </tr>
736
+ ))}
737
+ </tbody>
738
+ </table>
739
+ </div>
740
+ </SectionCard>
741
+ );
742
+ }
743
+
744
+ /* ─────────────────────────────────────────────────────────────
745
+ * KPI evolutivo (LeadKpiCard) — título + valor grande + delta + sparkline. §3
746
+ * ───────────────────────────────────────────────────────────── */
747
+
748
+ type LeadKpi = {
749
+ title: string;
750
+ value: string;
751
+ delta: string;
752
+ down?: boolean;
753
+ note: string;
754
+ footer: string;
755
+ bars: number[];
756
+ hl: number;
757
+ };
758
+
759
+ const LEAD_KPIS: LeadKpi[] = [
760
+ { title: "Receita recorrente", value: "R$ 42,8k", delta: "+12%", note: "vs mês anterior",
761
+ footer: "Maior receita recorrente registrada no trimestre.", bars: [28, 34, 30, 38, 42, 40, 48], hl: 6 },
762
+ { title: "Novos clientes", value: "318", delta: "+8%", note: "vs mês anterior",
763
+ footer: "Volume de novos clientes ativos no período.", bars: [22, 26, 31, 28, 34, 30, 36], hl: 6 },
764
+ ];
765
+
766
+ /** KPI evolutivo — valor + delta + sparkline + FOOTER descritivo (divisória). §3 */
767
+ function LeadKpiCard({ kpi }: { kpi: LeadKpi }) {
768
+ const accent = kpi.down ? "var(--color-fg-danger)" : "var(--color-chart-1)";
769
+ const data = kpi.bars.map((v, i) => ({ i, v }));
770
+ return (
771
+ <article className="flex flex-col rounded-radius-xl border border-border-subtle bg-bg-surface p-pad-3xl shadow-sh-sm">
772
+ <div className="flex items-center justify-between">
773
+ <p className="text-title-md font-semibold text-fg-default">{kpi.title}</p>
774
+ <MoreVertical className="size-icon-sm text-fg-muted" />
775
+ </div>
776
+ <div className="mt-pad-3xl flex items-end justify-between gap-gp-lg">
777
+ <div>
778
+ <div className="flex items-center gap-gp-md">
779
+ <span className="text-[30px] font-bold leading-tight tabular-nums text-fg-default">{kpi.value}</span>
780
+ <KpiDelta value={kpi.delta} signed />
781
+ </div>
782
+ <p className="mt-gp-xs text-body-sm text-fg-muted">{kpi.note}</p>
783
+ </div>
784
+ <div className="h-[64px] w-[150px]">
785
+ <ResponsiveContainer>
786
+ <BarChart data={data} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
787
+ <Bar dataKey="v" radius={4}>
788
+ {data.map((b) => (
789
+ <Cell key={b.i} fill={b.i === kpi.hl ? accent : "var(--color-bg-muted)"} />
790
+ ))}
791
+ </Bar>
792
+ </BarChart>
793
+ </ResponsiveContainer>
794
+ </div>
795
+ </div>
796
+ {/* footer descritivo sob divisória */}
797
+ <div className="mt-auto border-t border-border-subtle pt-pad-lg">
798
+ <p className="text-caption-md text-fg-muted">{kpi.footer}</p>
799
+ </div>
800
+ </article>
801
+ );
802
+ }
803
+
804
+ /* ─────────────────────────────────────────────────────────────
805
+ * Card radial (gauge de meta) — RadialBar + PolarAngleAxis domain.
806
+ * ───────────────────────────────────────────────────────────── */
807
+
808
+ const META_PCT = 78;
809
+
810
+ function RadialGoalCard() {
811
+ return (
812
+ <SectionCard title="Meta do mês" subtitle="Resoluções vs objetivo" className="h-full">
813
+ <div className="flex flex-1 flex-col items-center justify-center">
814
+ <div className="relative">
815
+ <div className="h-[180px] w-[180px]">
816
+ <ResponsiveContainer>
817
+ <RadialBarChart
818
+ data={[{ v: META_PCT }]}
819
+ innerRadius="72%"
820
+ outerRadius="100%"
821
+ startAngle={90}
822
+ endAngle={-270}
823
+ >
824
+ <PolarAngleAxis type="number" domain={[0, 100]} tick={false} axisLine={false} />
825
+ <RadialBar dataKey="v" cornerRadius={8} fill="var(--color-chart-1)" background={{ fill: "var(--color-bg-muted)" }} />
826
+ </RadialBarChart>
827
+ </ResponsiveContainer>
828
+ </div>
829
+ <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
830
+ <span className="text-[30px] font-bold leading-none tabular-nums text-fg-default">{META_PCT}%</span>
831
+ <span className="text-caption-sm text-fg-muted">da meta</span>
832
+ </div>
833
+ </div>
834
+ <p className="mt-gp-lg text-center text-body-sm text-fg-muted">
835
+ <span className="font-semibold text-fg-success">+6%</span> acima do ritmo esperado
836
+ </p>
837
+ </div>
838
+ </SectionCard>
839
+ );
840
+ }
841
+
842
+ /* ─────────────────────────────────────────────────────────────
843
+ * Card dividido em 2 (dados | mapa) — §4.
844
+ * ───────────────────────────────────────────────────────────── */
845
+
846
+ const UF_TICKETS = [
847
+ { uf: "SP", value: 214 }, { uf: "RJ", value: 138 }, { uf: "MG", value: 96 },
848
+ { uf: "PR", value: 74 }, { uf: "BA", value: 61 }, { uf: "RS", value: 52 },
849
+ ];
850
+ // Rampa VERDE monocromática (chart-1 escurecendo) — rankeada por valor (§4).
851
+ const MAP_RAMP = [
852
+ "var(--color-chart-1)",
853
+ "color-mix(in oklch, var(--color-chart-1) 80%, black)",
854
+ "color-mix(in oklch, var(--color-chart-1) 62%, black)",
855
+ "color-mix(in oklch, var(--color-chart-1) 46%, black)",
856
+ "color-mix(in oklch, var(--color-chart-1) 34%, black)",
857
+ ];
858
+ const ufTotal = UF_TICKETS.reduce((s, u) => s + u.value, 0);
859
+ const rankedUf = [...UF_TICKETS].sort((a, b) => b.value - a.value);
860
+ const fillByUf: Record<string, string> = {};
861
+ rankedUf.forEach((u, i) => { fillByUf[u.uf] = MAP_RAMP[Math.min(i, MAP_RAMP.length - 1)]; });
862
+
863
+ /** Card "por UF" — mapa centralizado + legenda 2-col (dots na rampa) embaixo. §4 */
864
+ function RegionMapCard() {
865
+ return (
866
+ <SectionCard title="Tickets por UF" subtitle={`${UF_TICKETS.length} estados · ${ufTotal} no total`} className="h-full">
867
+ <div className="flex min-h-0 flex-1 items-center justify-center">
868
+ <svg viewBox={BRAZIL_VIEWBOX} className="h-full max-h-[300px] w-full" role="img" aria-label="Mapa de tickets por estado">
869
+ {BRAZIL_PATHS.map((p) => (
870
+ <path key={p.uf} d={p.d} fill={fillByUf[p.uf] ?? "var(--color-bg-muted)"} stroke="var(--color-bg-surface)" strokeWidth={1} />
871
+ ))}
872
+ </svg>
873
+ </div>
874
+ <ul className="grid grid-cols-2 gap-x-gp-xl gap-y-gp-xs">
875
+ {rankedUf.map((u, i) => (
876
+ <li key={u.uf} className="flex items-center gap-gp-sm">
877
+ <span className="size-[10px] shrink-0 rounded-radius-full" style={{ background: MAP_RAMP[Math.min(i, MAP_RAMP.length - 1)] }} aria-hidden />
878
+ <span className="flex-1 text-body-sm text-fg-default">{u.uf}</span>
879
+ <span className="text-body-sm font-semibold tabular-nums text-fg-default">{u.value}</span>
880
+ </li>
881
+ ))}
882
+ </ul>
883
+ </SectionCard>
884
+ );
885
+ }
886
+
887
+ /* ─────────────────────────────────────────────────────────────
888
+ * KPI interno no footer (MetricKpi mini-card) + card de crescimento
889
+ * (área + header de KPI grande + footer de 3 KPIs). §2/§3.
890
+ * ───────────────────────────────────────────────────────────── */
891
+
892
+ function MetricKpi({ label, value, sub, tone = "neutral" }: { label: string; value: string; sub?: string; tone?: "up" | "warn" | "neutral" }) {
893
+ return (
894
+ <div className="rounded-radius-lg border border-border-subtle bg-bg-muted px-pad-xl py-pad-lg">
895
+ <p className="truncate text-caption-md text-fg-muted">{label}</p>
896
+ <p className="mt-gp-2xs text-[22px] font-bold leading-tight tabular-nums text-fg-default">{value}</p>
897
+ {sub && (
898
+ <p className={cn("mt-gp-2xs text-caption-sm font-medium",
899
+ tone === "up" ? "text-fg-success" : tone === "warn" ? "text-fg-warning" : "text-fg-muted")}>
900
+ {sub}
901
+ </p>
902
+ )}
903
+ </div>
904
+ );
905
+ }
906
+
907
+ const GROWTH = [
908
+ { m: "Ago", v: 1180 }, { m: "Set", v: 1290 }, { m: "Out", v: 1360 }, { m: "Nov", v: 1440 },
909
+ { m: "Dez", v: 1510 }, { m: "Jan", v: 1560 }, { m: "Fev", v: 1620 }, { m: "Mar", v: 1690 },
910
+ { m: "Abr", v: 1720 }, { m: "Mai", v: 1770 }, { m: "Jun", v: 1804 }, { m: "Jul", v: 1828 },
911
+ ];
912
+
913
+ /** Crescimento da carteira — área + KPI grande interno no topo + 3 KPIs no footer. */
914
+ function GrowthCard() {
915
+ return (
916
+ <SectionCard title="Crescimento da carteira" subtitle="Clientes ativos do segmento ao longo do tempo">
917
+ <div>
918
+ <p className="text-caption-md text-fg-muted">Clientes ativos no segmento</p>
919
+ <p className="text-[34px] font-bold leading-none tabular-nums text-fg-default">1.828</p>
920
+ <p className="mt-gp-2xs text-body-sm font-medium text-fg-success">
921
+ ↑ +124 clientes (8,6%) <span className="font-normal text-fg-muted">no último mês</span>
922
+ </p>
923
+ </div>
924
+ <div className="h-[220px] w-full">
925
+ <ResponsiveContainer>
926
+ <AreaChart data={GROWTH} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
927
+ <defs>
928
+ <linearGradient id="fillGrowth" x1="0" y1="0" x2="0" y2="1">
929
+ <stop offset="0%" stopColor="var(--color-chart-1)" stopOpacity={0.35} />
930
+ <stop offset="100%" stopColor="var(--color-chart-1)" stopOpacity={0.02} />
931
+ </linearGradient>
932
+ </defs>
933
+ <CartesianGrid vertical={false} strokeDasharray="4 4" stroke="var(--color-border-subtle)" />
934
+ <XAxis dataKey="m" tickLine={false} axisLine={false} tickMargin={8} tick={{ fontSize: 11 }} />
935
+ <YAxis tickLine={false} axisLine={false} width={36} tickMargin={2} allowDecimals={false} tick={{ fontSize: 11 }} />
936
+ <Area type="monotone" dataKey="v" stroke="var(--color-chart-1)" strokeWidth={2} fill="url(#fillGrowth)" dot={{ r: 3, fill: "var(--color-chart-1)" }} activeDot={{ r: 5 }} />
937
+ </AreaChart>
938
+ </ResponsiveContainer>
939
+ </div>
940
+ <div className="grid grid-cols-1 gap-gp-lg sm:grid-cols-3">
941
+ <MetricKpi label="Volume total" value="742 MWh" sub="por mês" />
942
+ <MetricKpi label="Economia estimada" value="R$ 122,9 mil" sub="gerada/mês" tone="warn" />
943
+ <MetricKpi label="Ticket médio" value="406 kWh" sub="por cliente" />
944
+ </div>
945
+ </SectionCard>
946
+ );
947
+ }
948
+
949
+ /* ─────────────────────────────────────────────────────────────
950
+ * Page
951
+ * ───────────────────────────────────────────────────────────── */
952
+
953
+ export function DashboardScreen() {
954
+ const [period, setPeriod] = useState<string>("Últimos 7 dias");
955
+
956
+ return (
957
+ // Conteúdo da página (sem shell). Embrulhe no seu AppShell/layout.
958
+ <div className="flex flex-col h-full min-h-0 gap-gp-2xl">
959
+ <PageHeader
960
+ title="Dashboard"
961
+ description="Visão geral do atendimento em tempo real — KPIs, volume, canais e performance da equipe."
962
+ badge={
963
+ <Chip color="primary" variant="soft" size="sm" shape="pill">
964
+ Hoje
965
+ </Chip>
966
+ }
967
+ actions={
968
+ <>
969
+ <Button
970
+ color="secondary"
971
+ variant="outline"
972
+ size="icon-md"
973
+ aria-label="Mais ações"
974
+ >
975
+ <MoreVertical />
976
+ </Button>
977
+ <Button
978
+ color="secondary"
979
+ variant="outline"
980
+ size="md"
981
+ iconLeft={<Calendar />}
982
+ iconRight={<ChevronDown />}
983
+ onClick={() => {
984
+ const next = period === "Últimos 7 dias" ? "Últimos 30 dias" : "Últimos 7 dias";
985
+ setPeriod(next);
986
+ }}
987
+ >
988
+ {period}
989
+ </Button>
990
+ </>
991
+ }
992
+ />
993
+
994
+ {/* Row 1 — Welcome (2/3) + KeyInsights (1/3) — alturas iguais */}
995
+ <section className="grid grid-cols-1 lg:grid-cols-3 gap-gp-2xl items-stretch">
996
+ <div className="lg:col-span-2 flex">
997
+ <WelcomeBanner />
998
+ </div>
999
+ <KeyInsightsCard />
1000
+ </section>
1001
+
1002
+ {/* Row 2 — KPIs primários (6) — padrão "Painel do Líder": card único com
1003
+ divisores via KpiGroup divided + Kpi + KpiDelta (primitivos DS).
1004
+ `tone` colore só o ícone; delta honra `positive` (sinal ≠ bom/ruim aqui:
1005
+ "-12s" é melhora) — por isso tone explícito, não `signed`. */}
1006
+ <section aria-label="KPIs principais">
1007
+ <KpiGroup columns={6} divided>
1008
+ {KPIS_PRIMARY.map((kpi) => (
1009
+ <Kpi
1010
+ key={kpi.id}
1011
+ label={kpi.title}
1012
+ value={kpi.value}
1013
+ icon={<kpi.icon />}
1014
+ tone={kpi.tone}
1015
+ hint={kpi.delta?.label}
1016
+ delta={
1017
+ kpi.delta && (
1018
+ <KpiDelta
1019
+ value={kpi.delta.value}
1020
+ tone={kpi.delta.positive ? "success" : "danger"}
1021
+ direction={kpi.delta.direction}
1022
+ />
1023
+ )
1024
+ }
1025
+ />
1026
+ ))}
1027
+ </KpiGroup>
1028
+ </section>
1029
+
1030
+ {/* Row 3 — Volume stacked (2/3) + Current visits donut (1/3) — alturas iguais */}
1031
+ <section className="grid grid-cols-1 lg:grid-cols-3 gap-gp-2xl items-stretch">
1032
+ <SectionCard
1033
+ title="Volume de atendimentos"
1034
+ subtitle="Humano vs IA — ano corrente"
1035
+ className="lg:col-span-2 h-full"
1036
+ >
1037
+ <VolumeStackedChart />
1038
+ </SectionCard>
1039
+ <CurrentVisitsCard />
1040
+ </section>
1041
+
1042
+ {/* Row 4 — KPIs qualidade (4) — APÓS os charts */}
1043
+ <section
1044
+ aria-label="Qualidade do atendimento"
1045
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gp-2xl"
1046
+ >
1047
+ {KPIS_QUALITY.map((kpi) => (
1048
+ <KpiCard key={kpi.id} kpi={kpi} />
1049
+ ))}
1050
+ </section>
1051
+
1052
+ {/* Row 4.5 — 2 KPIs evolutivos (LeadKpiCard §3) + card radial de meta (§2) */}
1053
+ <section className="grid grid-cols-1 lg:grid-cols-3 gap-gp-2xl items-stretch">
1054
+ {LEAD_KPIS.map((k) => (
1055
+ <LeadKpiCard key={k.title} kpi={k} />
1056
+ ))}
1057
+ <RadialGoalCard />
1058
+ </section>
1059
+
1060
+ {/* Row 4.6 — Crescimento da carteira (área + KPI interno + footer KPIs) (2/3) + mapa por UF (1/3) */}
1061
+ <section className="grid grid-cols-1 gap-gp-2xl lg:grid-cols-3 items-stretch">
1062
+ <div className="lg:col-span-2 flex">
1063
+ <div className="flex-1">
1064
+ <GrowthCard />
1065
+ </div>
1066
+ </div>
1067
+ <RegionMapCard />
1068
+ </section>
1069
+
1070
+ {/* Row 5 — Open + Agents */}
1071
+ <section className="grid grid-cols-1 lg:grid-cols-2 gap-gp-2xl">
1072
+ <SectionCard
1073
+ title="Atendimentos em aberto"
1074
+ subtitle="5 ativos agora"
1075
+ action={
1076
+ <Button color="secondary" variant="ghost" size="sm">
1077
+ Ver todos
1078
+ </Button>
1079
+ }
1080
+ >
1081
+ <OpenConversationsList />
1082
+ </SectionCard>
1083
+ <SectionCard
1084
+ title="Performance da equipe"
1085
+ subtitle="Ranking de hoje"
1086
+ action={
1087
+ <Button color="secondary" variant="ghost" size="sm">
1088
+ Ver equipe
1089
+ </Button>
1090
+ }
1091
+ >
1092
+ <AgentPerformanceList />
1093
+ </SectionCard>
1094
+ </section>
1095
+
1096
+ {/* Row 6 — Channels (1/3) + Traffic Data (2/3). Ambos h-full pra
1097
+ alturas se igualarem dentro da row do grid. */}
1098
+ <section className="grid grid-cols-1 lg:grid-cols-3 gap-gp-2xl items-stretch">
1099
+ <SectionCard title="Canais" subtitle="Volume e status" className="h-full">
1100
+ <ChannelPerformanceList />
1101
+ </SectionCard>
1102
+ <div className="lg:col-span-2 flex">
1103
+ <TrafficDataTable />
1104
+ </div>
1105
+ </section>
1106
+ </div>
1107
+ );
1108
+ }
1109
+
1110
+ export default DashboardScreen;