@snksergio/design-system 0.62.0 → 0.63.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 (135) hide show
  1. package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
  2. package/dist-lib/ai/blocos/indice.md +43 -0
  3. package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
  4. package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
  5. package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
  6. package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
  7. package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
  8. package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
  9. package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
  10. package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
  11. package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
  12. package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
  13. package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
  14. package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
  15. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
  16. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
  17. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
  18. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
  19. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
  20. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
  21. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
  22. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
  23. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
  24. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
  25. package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
  26. package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
  27. package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
  28. package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
  29. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
  30. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
  31. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
  32. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
  33. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
  34. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
  35. package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
  36. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
  37. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
  38. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
  39. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
  40. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
  41. package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
  42. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
  43. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
  44. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
  45. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
  46. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
  47. package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
  48. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
  49. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
  50. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
  51. package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
  52. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
  53. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
  54. package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
  55. package/dist-lib/ai/exemplos/chat/index.ts +1 -0
  56. package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
  57. package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
  58. package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
  59. package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
  60. package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
  61. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
  62. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
  63. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
  64. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
  65. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
  66. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  67. package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
  68. package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
  69. package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
  70. package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
  71. package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
  72. package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
  73. package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
  74. package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
  75. package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
  76. package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
  77. package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
  78. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
  79. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
  80. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
  81. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
  82. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
  83. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
  84. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
  85. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  86. package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
  87. package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
  88. package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
  89. package/dist-lib/ai/exemplos/finance/index.ts +1 -0
  90. package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
  91. package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
  92. package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
  93. package/dist-lib/ai/exemplos/login/index.ts +1 -0
  94. package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
  95. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
  96. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
  97. package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
  98. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
  99. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
  100. package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
  101. package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
  102. package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
  103. package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
  104. package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
  105. package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
  106. package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
  107. package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
  108. package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
  109. package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
  110. package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
  111. package/dist-lib/ai/indice.json +30 -15
  112. package/dist-lib/ai/manifest.json +31 -5
  113. package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
  114. package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
  115. package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
  116. package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
  117. package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
  118. package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
  119. package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
  120. package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
  121. package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
  122. package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
  123. package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
  124. package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
  125. package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
  126. package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
  127. package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
  128. package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
  129. package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
  130. package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
  131. package/package.json +1 -1
  132. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/blueprint.md +0 -0
  133. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/entrevista.md +0 -0
  134. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/geracao.md +0 -0
  135. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/roteiro.md +0 -0
@@ -0,0 +1,505 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ import {
3
+ Archive,
4
+ Clock,
5
+ Copy,
6
+ Download,
7
+ Maximize2,
8
+ MessageSquare,
9
+ MoreHorizontal,
10
+ Pencil,
11
+ Plus,
12
+ RotateCcw,
13
+ Trash2,
14
+ Upload,
15
+ } from "lucide-react";
16
+ import {
17
+ CATEGORIES,
18
+ AGENTS,
19
+ type ClientRow,
20
+ } from "./_table-data";
21
+ import {
22
+ DataTable,
23
+ presetView,
24
+ type DataTableColumnDef,
25
+ type DataTableKanbanConfig,
26
+ type DataTablePresetView,
27
+ type DataTableRef,
28
+ type DataTableViewMode,
29
+ type FilterModel,
30
+ } from "@/components/ui/DataTable";
31
+ import { Button } from "@/components/ui/Button/button";
32
+ import { Chip } from "@/components/ui/Chip";
33
+ import { PageHeader } from "@/components/ui/PageHeader";
34
+ import { BulkActionButton } from "@/components/ui/TableToolbar";
35
+ import { Avatar, AvatarFallback } from "@/components/shadcn/avatar";
36
+ import {
37
+ CLIENTS_87,
38
+ STATUS_OPTIONS,
39
+ CATEGORY_OPTIONS,
40
+ AGENT_OPTIONS,
41
+ AGENTS_LOOKUP,
42
+ KANBAN_COLUMNS,
43
+ CATEGORY_KIND,
44
+ formatBRL,
45
+ formatShortDate,
46
+ } from "./clientes-showcase-mocks";
47
+ import { NovoClienteDrawer } from "./components/NovoClienteDrawer";
48
+ import { DetailDrawer } from "./components/DetailDrawer";
49
+ import { AlertModal } from "@/components/ui/AlertModal";
50
+
51
+ /* ── Columns declarativas (type-based) ─────────────────────────── */
52
+
53
+ type ColumnHandlers = {
54
+ /** Pede confirmação de exclusão (abre o AlertModal). */
55
+ onAskDelete: (row: ClientRow) => void;
56
+ };
57
+
58
+ function buildColumns(handlers: ColumnHandlers): DataTableColumnDef<ClientRow>[] {
59
+ return [
60
+ { field: "id", headerName: "ID", type: "text", width: 130 },
61
+ {
62
+ field: "name",
63
+ headerName: "Nome",
64
+ type: "user",
65
+ sortable: true,
66
+ width: 220,
67
+ typeOptions: {
68
+ users: Object.fromEntries(
69
+ CLIENTS_87.map((c) => [
70
+ c.name,
71
+ {
72
+ name: c.name,
73
+ initials: c.initials,
74
+ color: c.avatarColor,
75
+ },
76
+ ]),
77
+ ),
78
+ },
79
+ valueGetter: (row) => row.name,
80
+ },
81
+ {
82
+ field: "email",
83
+ headerName: "Email",
84
+ type: "email",
85
+ enableColumnFilter: true,
86
+ filterType: "text",
87
+ },
88
+ {
89
+ field: "phone",
90
+ headerName: "Telefone",
91
+ type: "phone",
92
+ },
93
+ {
94
+ field: "statusId",
95
+ headerName: "Status",
96
+ type: "status",
97
+ enableColumnFilter: true,
98
+ filterType: "multiSelect",
99
+ filterOptions: STATUS_OPTIONS,
100
+ },
101
+ {
102
+ field: "categoryId",
103
+ headerName: "Categoria",
104
+ type: "badge",
105
+ enableColumnFilter: true,
106
+ filterType: "multiSelect",
107
+ filterOptions: CATEGORY_OPTIONS,
108
+ },
109
+ {
110
+ field: "agentId",
111
+ headerName: "Atribuído",
112
+ type: "user",
113
+ enableColumnFilter: true,
114
+ filterType: "select",
115
+ filterOptions: AGENT_OPTIONS,
116
+ typeOptions: { users: AGENTS_LOOKUP },
117
+ },
118
+ {
119
+ field: "value",
120
+ headerName: "Valor",
121
+ type: "currency",
122
+ sortable: true,
123
+ enableColumnFilter: true,
124
+ filterType: "number",
125
+ aggregate: "sum",
126
+ aggregateFormatter: (v) =>
127
+ new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v),
128
+ },
129
+ {
130
+ field: "createdAt",
131
+ headerName: "Criado em",
132
+ type: "date",
133
+ sortable: true,
134
+ enableColumnFilter: true,
135
+ filterType: "date",
136
+ width: 130,
137
+ },
138
+ {
139
+ field: "lastContact",
140
+ headerName: "Último contato",
141
+ type: "date",
142
+ sortable: true,
143
+ width: 150,
144
+ },
145
+ {
146
+ field: "location",
147
+ headerName: "Localização",
148
+ type: "text",
149
+ minWidth: 150,
150
+ },
151
+ {
152
+ field: "_actions",
153
+ headerName: "",
154
+ type: "actions",
155
+ // Sem `width` e sem `pinned`, de propósito — o `type` já entrega os dois:
156
+ // a largura é DERIVADA do nº de ações (1→44px · 2→74 · 3→104) e acima de 3 elas
157
+ // colapsam no "…" sozinhas; o pin à direita sai de
158
+ // `col.pinned ?? (isActions ? "right" : undefined)`. Prop redundante aqui vira prop
159
+ // copiada em toda tela gerada — o exemplo é a fonte de maior precedência das skills.
160
+ getActions: ({ row }) => [
161
+ {
162
+ id: "edit",
163
+ label: "Editar",
164
+ icon: <Pencil />,
165
+ onClick: () => console.log("Editar", row.id),
166
+ },
167
+ {
168
+ id: "whatsapp",
169
+ label: "Abrir atendimento (WhatsApp)",
170
+ icon: <MessageSquare />,
171
+ onClick: () => console.log("Atendimento", row.id),
172
+ },
173
+ {
174
+ id: "archive",
175
+ label: "Arquivar",
176
+ icon: <Archive />,
177
+ onClick: () => console.log("Arquivar", row.id),
178
+ },
179
+ {
180
+ id: "delete",
181
+ label: "Excluir cliente",
182
+ icon: <Trash2 />,
183
+ destructive: true,
184
+ onClick: () => handlers.onAskDelete(row),
185
+ },
186
+ ],
187
+ },
188
+ ];
189
+ }
190
+
191
+ /* ── Saved views / Presets — tabs do toolbar ───────────────────── */
192
+
193
+ const DEFAULT_VIEWS: DataTablePresetView[] = [
194
+ presetView({
195
+ id: "preset:meus",
196
+ name: "Meus",
197
+ filters: [{ field: "agentId", value: "you" }],
198
+ }),
199
+ presetView({
200
+ id: "preset:ativos",
201
+ name: "Ativos",
202
+ filters: [{ field: "statusId", value: "active" }],
203
+ }),
204
+ presetView({
205
+ id: "preset:royals",
206
+ name: "Royals",
207
+ filters: [{ field: "categoryId", value: "royal" }],
208
+ }),
209
+ presetView({
210
+ id: "preset:ultimos-7-dias",
211
+ name: "Últimos 7 dias",
212
+ sort: [{ field: "createdAt", direction: "desc" }],
213
+ }),
214
+ ];
215
+
216
+ /* ── Filtros iniciais (Status=Ativo + Categoria=Royal — print) ─── */
217
+
218
+ // statusId e categoryId são filterType="multiSelect" — operator correto é "isAnyOf"
219
+ // (not "equals"). Sem isso, o popover Filtros mostrava o Select de operador VAZIO
220
+ // porque "equals" não está nos operators de MultiSelectColumnType.
221
+ const INITIAL_FILTERS: FilterModel = {
222
+ items: [
223
+ { id: "filter-status-ativo", field: "statusId", operator: "isAnyOf", value: "active" },
224
+ { id: "filter-categoria-royal", field: "categoryId", operator: "isAnyOf", value: "royal" },
225
+ ],
226
+ logicOperator: "AND",
227
+ };
228
+
229
+ /* ── Kanban config (renderCard com JSX) ────────────────────────── */
230
+
231
+ const KANBAN_CONFIG: DataTableKanbanConfig<ClientRow> = {
232
+ groupByField: "statusId",
233
+ columns: KANBAN_COLUMNS,
234
+ renderCard: ({ row }) => {
235
+ const cat = CATEGORIES[row.categoryId as keyof typeof CATEGORIES];
236
+ const agent = AGENTS[row.agentId as keyof typeof AGENTS];
237
+ const agentInitials = agent?.name.split(" ").map((s) => s[0]).join("").slice(0, 2) ?? "?";
238
+ return {
239
+ title: row.name,
240
+ subtitle: row.id,
241
+ avatar: (
242
+ <Avatar className="size-[28px]" style={{ background: row.avatarColor }}>
243
+ <AvatarFallback className="bg-transparent text-white text-caption-sm font-bold">
244
+ {row.initials}
245
+ </AvatarFallback>
246
+ </Avatar>
247
+ ),
248
+ chip: cat ? (
249
+ <Chip
250
+ color={CATEGORY_KIND[row.categoryId] ?? "info"}
251
+ variant="soft"
252
+ size="sm"
253
+ shape="pill"
254
+ >
255
+ {cat.label}
256
+ </Chip>
257
+ ) : null,
258
+ value: formatBRL(row.value),
259
+ footerLeft: agent ? (
260
+ <span className="inline-flex items-center gap-gp-sm min-w-0 flex-1">
261
+ <Avatar className="size-[22px]" style={{ background: agent.color }}>
262
+ <AvatarFallback className="bg-transparent text-white text-caption-xs font-bold">
263
+ {agentInitials}
264
+ </AvatarFallback>
265
+ </Avatar>
266
+ <span className="text-body-xs font-normal text-fg-muted whitespace-nowrap overflow-hidden text-ellipsis">
267
+ {agent.name}
268
+ </span>
269
+ </span>
270
+ ) : null,
271
+ footerRight: (
272
+ <span className="inline-flex items-center gap-[4px] text-caption-sm text-fg-muted [font-variant-numeric:tabular-nums] shrink-0">
273
+ <Clock size={11} strokeWidth={1.8} aria-hidden />
274
+ {formatShortDate(row.lastContact)}
275
+ </span>
276
+ ),
277
+ };
278
+ },
279
+ };
280
+
281
+ /* ═══════════════════════════════════════════════════════════════════════════
282
+ Page — Clientes (showcase usando DataTable real)
283
+ ═══════════════════════════════════════════════════════════════════════════ */
284
+
285
+ type ConfirmDeleteState = {
286
+ ids: string[];
287
+ label: string;
288
+ /** Callback opcional após confirmar — usado pelo bulk pra limpar seleção. */
289
+ onAfter?: () => void;
290
+ } | null;
291
+
292
+ export function ClientesScreen() {
293
+ const [rows, setRows] = useState<ClientRow[]>(() => CLIENTS_87);
294
+ const [filterModel, setFilterModel] = useState<FilterModel>(INITIAL_FILTERS);
295
+ const [viewMode, setViewMode] = useState<DataTableViewMode>("table");
296
+ const [novoClienteOpen, setNovoClienteOpen] = useState(false);
297
+ const [detailRowId, setDetailRowId] = useState<string | null>(null);
298
+ const [confirmDelete, setConfirmDelete] = useState<ConfirmDeleteState>(null);
299
+ const detailRow = detailRowId ? rows.find((r) => r.id === detailRowId) ?? null : null;
300
+ const tableRef = useRef<DataTableRef>(null);
301
+
302
+ const askDelete = useCallback(
303
+ (row: ClientRow) =>
304
+ setConfirmDelete({ ids: [row.id], label: row.name }),
305
+ [],
306
+ );
307
+
308
+ const askBulkDelete = useCallback(
309
+ (ids: string[], onAfter: () => void) =>
310
+ setConfirmDelete({
311
+ ids,
312
+ label: `${ids.length} cliente${ids.length === 1 ? "" : "s"}`,
313
+ onAfter,
314
+ }),
315
+ [],
316
+ );
317
+
318
+ const handleConfirmDelete = useCallback(() => {
319
+ if (!confirmDelete) return;
320
+ const ids = new Set(confirmDelete.ids);
321
+ setRows((prev) => prev.filter((r) => !ids.has(r.id)));
322
+ if (detailRowId && ids.has(detailRowId)) setDetailRowId(null);
323
+ confirmDelete.onAfter?.();
324
+ setConfirmDelete(null);
325
+ }, [confirmDelete, detailRowId]);
326
+
327
+ const columns = useMemo(
328
+ () => buildColumns({ onAskDelete: askDelete }),
329
+ [askDelete],
330
+ );
331
+
332
+ return (
333
+ // Conteúdo da página (sem shell). Embrulhe no seu AppShell/layout — o
334
+ // parent precisa dar altura (h-full/flex) pra DataTable preencher.
335
+ <div className="flex flex-col h-full min-h-0 gap-gp-2xl">
336
+ <PageHeader
337
+ title="Clientes"
338
+ description="Gerencie sua base de clientes, acompanhe status de cadastro e abra atendimentos diretos pelo WhatsApp."
339
+ badge={
340
+ <Chip color="primary" variant="soft" size="sm" shape="rounded">
341
+ {rows.length.toLocaleString("pt-BR")} registros
342
+ </Chip>
343
+ }
344
+ actions={
345
+ <>
346
+ <Button
347
+ variant="outline"
348
+ color="secondary"
349
+ size="icon-md"
350
+ aria-label="Mais ações"
351
+ onClick={() => alert("Mais ações")}
352
+ >
353
+ <MoreHorizontal />
354
+ </Button>
355
+ <Button
356
+ variant="filled"
357
+ color="primary"
358
+ size="md"
359
+ iconLeft={<Plus />}
360
+ onClick={() => setNovoClienteOpen(true)}
361
+ >
362
+ Novo cliente
363
+ </Button>
364
+ </>
365
+ }
366
+ />
367
+
368
+ {/* DataTable — ocupa o resto do body ──────────────────────── */}
369
+ <DataTable<ClientRow>
370
+ ref={tableRef}
371
+ rows={rows}
372
+ columns={columns}
373
+ getRowId={(r) => r.id}
374
+ persistId="showcase-clientes-crud"
375
+ defaultViews={DEFAULT_VIEWS}
376
+ /* Chips de filtro VAZIOS já visíveis no load ("Atribuído") — a afordância
377
+ aparece sem abrir o menu; o usuário clica e preenche. Filtro é recurso
378
+ da tabela (nativo), nunca form/select acima dela. */
379
+ showEmptyFilterChips={["agentId"]}
380
+ filterModel={filterModel}
381
+ onFilterModelChange={setFilterModel}
382
+ viewMode={viewMode}
383
+ onViewModeChange={setViewMode}
384
+ kanbanConfig={KANBAN_CONFIG}
385
+ toolbar={{
386
+ enableSearch: true,
387
+ enableFilters: true,
388
+ enableColumns: true,
389
+ enableDensity: true,
390
+ enableExport: true,
391
+ moreMenu: {
392
+ items: [
393
+ {
394
+ id: "fullscreen",
395
+ label: "Tela cheia",
396
+ icon: <Maximize2 />,
397
+ onSelect: () => alert("Tela cheia"),
398
+ },
399
+ {
400
+ id: "duplicate",
401
+ label: "Duplicar visualização",
402
+ icon: <Copy />,
403
+ onSelect: () => alert("Duplicar visualização"),
404
+ },
405
+ {
406
+ id: "import",
407
+ label: "Importar CSV",
408
+ icon: <Upload />,
409
+ onSelect: () => alert("Importar CSV"),
410
+ },
411
+ {
412
+ id: "reset",
413
+ label: "Resetar visualização",
414
+ icon: <RotateCcw />,
415
+ destructive: true,
416
+ onSelect: () => tableRef.current?.resetPersistedState(),
417
+ },
418
+ ],
419
+ },
420
+ }}
421
+ paginationConfig={{
422
+ enabled: true,
423
+ initialPageSize: 10,
424
+ pageSizeOptions: [10, 25, 50, 100],
425
+ }}
426
+ selectionConfig={{
427
+ enabled: true,
428
+ enableGlobal: true,
429
+ actions: (selectedIds, clearSelection) => (
430
+ <>
431
+ <BulkActionButton
432
+ icon={<Download />}
433
+ onClick={() => tableRef.current?.exportCsv("selected")}
434
+ >
435
+ Exportar
436
+ </BulkActionButton>
437
+ <BulkActionButton
438
+ icon={<Trash2 />}
439
+ variant="danger"
440
+ onClick={() => askBulkDelete(selectedIds.map(String), clearSelection)}
441
+ >
442
+ Excluir
443
+ </BulkActionButton>
444
+ </>
445
+ ),
446
+ }}
447
+ onCellEditCommit={async ({ id, field, value }) => {
448
+ await new Promise((res) => setTimeout(res, 400));
449
+ setRows((prev) =>
450
+ prev.map((r) => (r.id === id ? { ...r, [field]: value } : r)),
451
+ );
452
+ }}
453
+ onRowClick={(row) => setDetailRowId(row.id)}
454
+ className="flex-1 min-h-0"
455
+ />
456
+
457
+ {/* Drawer "Novo cliente" — controlado pelo botão do header */}
458
+ <NovoClienteDrawer
459
+ open={novoClienteOpen}
460
+ onOpenChange={setNovoClienteOpen}
461
+ onSubmit={(data) => {
462
+ console.log("Novo cliente:", data);
463
+ alert(`Cliente "${data.name}" criado (mock)`);
464
+ }}
465
+ />
466
+
467
+ {/* DetailDrawer — abre ao clicar numa row, slide-in da direita SEM
468
+ backdrop modal (tabela continua interativa atrás). */}
469
+ <DetailDrawer
470
+ row={detailRow}
471
+ onClose={() => setDetailRowId(null)}
472
+ onEdit={(r) => console.log("Editar", r.id)}
473
+ onDelete={(r) => askDelete(r)}
474
+ onSave={(r) => alert(`Salvar alterações em ${r.name} (mock)`)}
475
+ />
476
+
477
+ {/* AlertModal de confirmação de exclusão — compartilhado entre
478
+ row action menu, bulk action e DetailDrawer.onDelete. */}
479
+ <AlertModal
480
+ open={confirmDelete !== null}
481
+ onOpenChange={(open) => {
482
+ if (!open) setConfirmDelete(null);
483
+ }}
484
+ tone="danger"
485
+ title={
486
+ confirmDelete && confirmDelete.ids.length > 1
487
+ ? `Excluir ${confirmDelete.label}?`
488
+ : "Excluir cliente?"
489
+ }
490
+ description={
491
+ confirmDelete
492
+ ? confirmDelete.ids.length > 1
493
+ ? `Esta ação não pode ser desfeita. ${confirmDelete.label} serão removidos permanentemente.`
494
+ : `Esta ação não pode ser desfeita. "${confirmDelete.label}" será removido permanentemente.`
495
+ : null
496
+ }
497
+ confirmLabel="Excluir"
498
+ cancelLabel="Cancelar"
499
+ onConfirm={handleConfirmDelete}
500
+ />
501
+ </div>
502
+ );
503
+ }
504
+
505
+ export default ClientesScreen;
@@ -0,0 +1,117 @@
1
+ import {
2
+ CLIENTS_MOCK,
3
+ STATUSES,
4
+ CATEGORIES,
5
+ AGENTS,
6
+ type ClientRow,
7
+ } from "./_table-data";
8
+
9
+ /* ── Dataset 87 clientes (bate com badge do print) ─────────────── */
10
+
11
+ export const CLIENTS_87: ClientRow[] = Array.from({ length: 9 }, (_, batch) =>
12
+ CLIENTS_MOCK.map((row, i) => ({
13
+ ...row,
14
+ id: `CLI-${2400 + batch * 10 + i}`,
15
+ })),
16
+ )
17
+ .flat()
18
+ .slice(0, 87);
19
+
20
+ /* ── Lookups por categoria de status (column color mapping) ────── */
21
+
22
+ export const STATUS_COLOR_MAP: Record<string, string> = {
23
+ active: "success",
24
+ pending: "warning",
25
+ paused: "info",
26
+ inactive: "neutral",
27
+ };
28
+
29
+ export const STATUS_DOT: Record<string, string> = {
30
+ active: "var(--color-fg-success)",
31
+ pending: "var(--color-fg-warning)",
32
+ paused: "var(--color-fg-info)",
33
+ inactive: "var(--color-fg-muted)",
34
+ };
35
+
36
+ export const CATEGORY_KIND: Record<string, "warning" | "info" | "success"> = {
37
+ royal: "warning",
38
+ licenciado: "info",
39
+ lead: "success",
40
+ };
41
+
42
+ /* ── Options pra filters/selects ───────────────────────────────── */
43
+
44
+ export const STATUS_OPTIONS = Object.entries(STATUSES).map(([v, m]) => ({
45
+ value: v,
46
+ label: m.label,
47
+ color: STATUS_COLOR_MAP[v] ?? "neutral",
48
+ }));
49
+
50
+ export const CATEGORY_OPTIONS = Object.entries(CATEGORIES).map(([v, m]) => ({
51
+ value: v,
52
+ label: m.label,
53
+ color: m.kind === "neutral" ? "neutral" : m.kind,
54
+ }));
55
+
56
+ export const AGENT_OPTIONS = Object.entries(AGENTS).map(([v, a]) => ({
57
+ value: v,
58
+ label: a.name,
59
+ }));
60
+
61
+ /* ── Avatar lookup pro column type "user" ──────────────────────── */
62
+
63
+ export const AGENTS_LOOKUP = Object.fromEntries(
64
+ Object.entries(AGENTS).map(([id, a]) => [
65
+ id,
66
+ {
67
+ name: a.name,
68
+ initials: a.name
69
+ .split(" ")
70
+ .map((s) => s[0])
71
+ .join("")
72
+ .slice(0, 2),
73
+ color: a.color,
74
+ },
75
+ ]),
76
+ );
77
+
78
+ /* ── Kanban columns (1 por status) ─────────────────────────────── */
79
+
80
+ export const KANBAN_COLUMNS = Object.entries(STATUSES).map(([id, s]) => ({
81
+ id,
82
+ label: s.label,
83
+ dotColor: STATUS_DOT[id] ?? "var(--color-fg-muted)",
84
+ }));
85
+
86
+ /* ── Cidades pro select do drawer ──────────────────────────────── */
87
+
88
+ export const CITIES = [
89
+ "São Paulo, SP",
90
+ "Rio de Janeiro, RJ",
91
+ "Belo Horizonte, MG",
92
+ "Porto Alegre, RS",
93
+ "Curitiba, PR",
94
+ "Recife, PE",
95
+ "Salvador, BA",
96
+ "Fortaleza, CE",
97
+ "Brasília, DF",
98
+ "Manaus, AM",
99
+ ];
100
+
101
+ /* ── Helpers de formatação ─────────────────────────────────────── */
102
+
103
+ export function formatBRL(value: number) {
104
+ return value.toLocaleString("pt-BR", {
105
+ style: "currency",
106
+ currency: "BRL",
107
+ minimumFractionDigits: 2,
108
+ });
109
+ }
110
+
111
+ export function formatShortDate(ms: number) {
112
+ if (!ms) return "—";
113
+ const d = new Date(ms);
114
+ const day = d.getDate().toString().padStart(2, "0");
115
+ const months = ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"];
116
+ return `${day} ${months[d.getMonth()]}`;
117
+ }
@@ -0,0 +1,13 @@
1
+ import { tv } from "@/utils/tv";
2
+
3
+ /**
4
+ * Layout em grid de 2 colunas usado dentro do drawer pra pares de campos
5
+ * (Email + WhatsApp, Cidade + Valor).
6
+ * `gap-form-gap` (20px) = token DS pra spacing entre FormField units (L-024).
7
+ */
8
+ export const drawerFormStyles = tv({
9
+ slots: {
10
+ twoCols: "grid grid-cols-2 gap-form-gap",
11
+ statusDot: "inline-block size-[8px] rounded-radius-full mr-gp-xs shrink-0",
12
+ },
13
+ });
@@ -0,0 +1,16 @@
1
+ import type { STATUSES, CATEGORIES } from "./_table-data";
2
+
3
+ /**
4
+ * Payload do formulário do drawer "Novo cliente".
5
+ * Consumido por <NovoClienteDrawer onSubmit={...}> no main page.
6
+ */
7
+ export type NovoClienteFormData = {
8
+ name: string;
9
+ email: string;
10
+ whatsapp: string;
11
+ statusId: keyof typeof STATUSES;
12
+ categoryId: keyof typeof CATEGORIES;
13
+ city: string;
14
+ value: number;
15
+ notes: string;
16
+ };
@@ -0,0 +1,28 @@
1
+ import { tv } from "@/utils/tv";
2
+
3
+ /**
4
+ * Styles do DetailDrawer — apenas as partes internas (sections collapsibles
5
+ * + DetailField). O shell flutuante (aside, header com avatar+meta, footer
6
+ * com botões) é delegado ao `<FloatingPanel>` do DS.
7
+ */
8
+ export const detailDrawerStyles = tv({
9
+ slots: {
10
+ section: "border-b border-border-default last:border-b-0",
11
+ sectionHead: [
12
+ "flex items-center justify-between w-full",
13
+ "px-[18px] py-[14px]",
14
+ "bg-transparent border-0 cursor-pointer text-left",
15
+ "text-body-sm font-semibold text-fg-default",
16
+ "transition-colors duration-150",
17
+ "hover:bg-bg-muted",
18
+ "focus-visible:outline-none focus-visible:bg-bg-muted",
19
+ ],
20
+ sectionChev: "size-[14px] text-fg-muted transition-transform duration-200",
21
+ sectionChevOpen: "rotate-180",
22
+ sectionBody: "flex flex-col gap-gp-md px-[18px] pb-pad-2xl",
23
+
24
+ field: "flex items-baseline justify-between gap-gp-md text-body-sm font-normal",
25
+ fieldLabel: "text-body-xs font-normal text-fg-muted shrink-0",
26
+ fieldValue: "text-fg-default text-right break-words min-w-0",
27
+ },
28
+ });