@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.
- package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
- package/dist-lib/ai/blocos/indice.md +43 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
- package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
- package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
- package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
- package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
- package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
- package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
- package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
- package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
- package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
- package/dist-lib/ai/exemplos/chat/index.ts +1 -0
- package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
- package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
- package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
- package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
- package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
- package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
- package/dist-lib/ai/exemplos/finance/index.ts +1 -0
- package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
- package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
- package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
- package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
- package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
- package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
- package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
- package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
- package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
- package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
- package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
- package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
- package/dist-lib/ai/indice.json +30 -15
- package/dist-lib/ai/manifest.json +31 -5
- package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
- package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
- package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
- package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
- package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
- package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
- package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
- package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
- package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
- package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
- package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
- package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
- package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
- package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
- package/package.json +1 -1
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/blueprint.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/entrevista.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/geracao.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/roteiro.md +0 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
Banknote,
|
|
5
|
+
Download,
|
|
6
|
+
History,
|
|
7
|
+
Lock,
|
|
8
|
+
MoreHorizontal,
|
|
9
|
+
Pause,
|
|
10
|
+
Pencil,
|
|
11
|
+
Plus,
|
|
12
|
+
SquareArrowOutUpRight,
|
|
13
|
+
ShieldAlert,
|
|
14
|
+
TrendingUp,
|
|
15
|
+
Unlock,
|
|
16
|
+
Wallet,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import {
|
|
19
|
+
DataTable,
|
|
20
|
+
presetView,
|
|
21
|
+
type DataTableColumnDef,
|
|
22
|
+
type DataTableKanbanConfig,
|
|
23
|
+
type DataTablePresetView,
|
|
24
|
+
type DataTableRef,
|
|
25
|
+
type DataTableViewMode,
|
|
26
|
+
} from "@/components/ui/DataTable";
|
|
27
|
+
import type { KanbanColumn } from "@/components/ui/Kanban";
|
|
28
|
+
import { Avatar } from "@/components/ui/avatar-ig";
|
|
29
|
+
import { Button } from "@/components/ui/Button/button";
|
|
30
|
+
import { Chip } from "@/components/ui/Chip";
|
|
31
|
+
import { PageHeader } from "@/components/ui/PageHeader";
|
|
32
|
+
import { BulkActionButton } from "@/components/ui/TableToolbar";
|
|
33
|
+
import { Switch } from "@/components/shadcn/switch";
|
|
34
|
+
import { AGENTS } from "./_table-data";
|
|
35
|
+
import { NovoClienteDrawer } from "./components/NovoClienteDrawer";
|
|
36
|
+
import {
|
|
37
|
+
ACCOUNT_STATUS,
|
|
38
|
+
BANKS,
|
|
39
|
+
FINANCE_CLIENTS,
|
|
40
|
+
FINANCE_KPIS,
|
|
41
|
+
formatBRL,
|
|
42
|
+
formatRelativeDays,
|
|
43
|
+
} from "./clientes-financeiro-mocks";
|
|
44
|
+
import { SacarDialog } from "./components/SacarDialog";
|
|
45
|
+
import { ExtratoExpansion } from "./components/ExtratoExpansion";
|
|
46
|
+
import { FinanceDetailPanel } from "./components/FinanceDetailPanel";
|
|
47
|
+
import { EditarFinanceDrawer } from "./components/EditarFinanceDrawer";
|
|
48
|
+
import type {
|
|
49
|
+
AccountStatus,
|
|
50
|
+
FinanceClientRow,
|
|
51
|
+
PaymentMethod,
|
|
52
|
+
} from "./clientes-financeiro.types";
|
|
53
|
+
|
|
54
|
+
/* ── Lookups derivados pros filtros/colunas ─────────────────────── */
|
|
55
|
+
|
|
56
|
+
const ACCOUNT_STATUS_OPTIONS = (
|
|
57
|
+
Object.entries(ACCOUNT_STATUS) as [AccountStatus, (typeof ACCOUNT_STATUS)[AccountStatus]][]
|
|
58
|
+
).map(([value, m]) => ({ value, label: m.label, color: m.color }));
|
|
59
|
+
|
|
60
|
+
const BANK_OPTIONS = [
|
|
61
|
+
{ value: "bb", label: "Banco do Brasil" },
|
|
62
|
+
{ value: "itau", label: "Itaú" },
|
|
63
|
+
{ value: "nubank", label: "Nubank" },
|
|
64
|
+
{ value: "santander", label: "Santander" },
|
|
65
|
+
{ value: "bradesco", label: "Bradesco" },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const AGENT_OPTIONS = Object.entries(AGENTS).map(([value, a]) => ({
|
|
69
|
+
value,
|
|
70
|
+
label: a.name,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
/** typeOptions.users do column-type `user` — id → { name, initials }. */
|
|
74
|
+
const AGENT_USERS = Object.fromEntries(
|
|
75
|
+
Object.entries(AGENTS).map(([id, a]) => [id, { name: a.name, initials: a.initials }]),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const PAYMENT_METHOD_OPTIONS: { value: PaymentMethod; label: string }[] = [
|
|
79
|
+
{ value: "pix", label: "PIX" },
|
|
80
|
+
{ value: "ted", label: "TED" },
|
|
81
|
+
{ value: "boleto", label: "Boleto" },
|
|
82
|
+
];
|
|
83
|
+
const PAYMENT_METHOD_LABEL: Record<PaymentMethod, string> = {
|
|
84
|
+
pix: "PIX",
|
|
85
|
+
ted: "TED",
|
|
86
|
+
boleto: "Boleto",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/* ── Columns financeiras ────────────────────────────────────────── */
|
|
90
|
+
|
|
91
|
+
type ColumnHandlers = {
|
|
92
|
+
onEdit: (row: FinanceClientRow) => void;
|
|
93
|
+
onSacar: (row: FinanceClientRow) => void;
|
|
94
|
+
onToggleBlock: (row: FinanceClientRow) => void;
|
|
95
|
+
onToggleAutoWithdraw: (row: FinanceClientRow, next: boolean) => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function buildColumns(
|
|
99
|
+
handlers: ColumnHandlers,
|
|
100
|
+
): DataTableColumnDef<FinanceClientRow>[] {
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
field: "id",
|
|
104
|
+
headerName: "ID",
|
|
105
|
+
type: "text",
|
|
106
|
+
width: 110,
|
|
107
|
+
expandable: true, // chevron → abre o extrato (ExtratoExpansion)
|
|
108
|
+
aggregate: (rows) => (
|
|
109
|
+
<span className="text-fg-muted">{rows.length} clientes</span>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
field: "name",
|
|
114
|
+
headerName: "Licenciado",
|
|
115
|
+
type: "text",
|
|
116
|
+
sortable: true,
|
|
117
|
+
width: 240,
|
|
118
|
+
isPrimary: true,
|
|
119
|
+
enableColumnFilter: true,
|
|
120
|
+
filterType: "text",
|
|
121
|
+
render: ({ row }) => (
|
|
122
|
+
// Afordância de "abrir detalhe": botão-ícone bordado sempre visível,
|
|
123
|
+
// encostado no fim da célula (estilo o atalho ⌘K do header). Sinaliza
|
|
124
|
+
// que o click na linha abre o painel de detalhe.
|
|
125
|
+
<div className="flex items-center gap-gp-md min-w-0 w-full">
|
|
126
|
+
<Avatar size="md" colorHex={row.avatarColor}>
|
|
127
|
+
{row.initials}
|
|
128
|
+
</Avatar>
|
|
129
|
+
<div className="flex flex-col gap-gp-2xs min-w-0 flex-1">
|
|
130
|
+
<span className="text-body-sm font-medium text-fg-default truncate">
|
|
131
|
+
{row.name}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="text-caption-md text-fg-muted truncate">
|
|
134
|
+
{row.email}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
<span
|
|
138
|
+
className="shrink-0 grid place-items-center size-[24px] rounded-radius-sm border border-border-subtle bg-bg-canvas dark:bg-bg-muted shadow-sh-sm text-fg-muted [&_svg]:size-icon-xs"
|
|
139
|
+
aria-hidden
|
|
140
|
+
>
|
|
141
|
+
<SquareArrowOutUpRight strokeWidth={2} />
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
field: "companyName",
|
|
148
|
+
headerName: "Razão Social",
|
|
149
|
+
type: "text",
|
|
150
|
+
sortable: true,
|
|
151
|
+
enableColumnFilter: true,
|
|
152
|
+
filterType: "text",
|
|
153
|
+
width: 220,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
field: "cnpj",
|
|
157
|
+
headerName: "CNPJ",
|
|
158
|
+
type: "text",
|
|
159
|
+
width: 170,
|
|
160
|
+
enableColumnFilter: true,
|
|
161
|
+
filterType: "text",
|
|
162
|
+
copyable: true, // ícone de copiar no hover da célula + "Copiado!"
|
|
163
|
+
render: ({ value }) => (
|
|
164
|
+
<span className="text-body-sm tabular-nums text-fg-default">
|
|
165
|
+
{value as string}
|
|
166
|
+
</span>
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
// Situação — badge chip + eixo do Kanban.
|
|
170
|
+
{
|
|
171
|
+
field: "accountStatus",
|
|
172
|
+
headerName: "Situação",
|
|
173
|
+
type: "badge",
|
|
174
|
+
width: 150,
|
|
175
|
+
enableColumnFilter: true,
|
|
176
|
+
filterType: "multiSelect",
|
|
177
|
+
filterOptions: ACCOUNT_STATUS_OPTIONS,
|
|
178
|
+
valueFormatter: (v) =>
|
|
179
|
+
ACCOUNT_STATUS[v as AccountStatus]?.label ?? String(v ?? ""),
|
|
180
|
+
render: ({ value }) => {
|
|
181
|
+
const meta = ACCOUNT_STATUS[value as AccountStatus];
|
|
182
|
+
if (!meta) return null;
|
|
183
|
+
return (
|
|
184
|
+
<Chip color={meta.color} variant="soft" size="sm" shape="pill">
|
|
185
|
+
{meta.label}
|
|
186
|
+
</Chip>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
field: "availableBalance",
|
|
192
|
+
headerName: "Saldo disponível",
|
|
193
|
+
type: "currency",
|
|
194
|
+
sortable: true,
|
|
195
|
+
enableColumnFilter: true,
|
|
196
|
+
filterType: "number",
|
|
197
|
+
align: "right",
|
|
198
|
+
width: 160,
|
|
199
|
+
aggregate: "sum",
|
|
200
|
+
aggregateFormatter: (v) => formatBRL(v),
|
|
201
|
+
render: ({ value }) => (
|
|
202
|
+
<span className="font-semibold tabular-nums text-fg-success">
|
|
203
|
+
{formatBRL(value as number)}
|
|
204
|
+
</span>
|
|
205
|
+
),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
field: "monthlyVolume",
|
|
209
|
+
headerName: "Volume mensal",
|
|
210
|
+
type: "currency",
|
|
211
|
+
sortable: true,
|
|
212
|
+
enableColumnFilter: true,
|
|
213
|
+
filterType: "number",
|
|
214
|
+
align: "right",
|
|
215
|
+
width: 150,
|
|
216
|
+
aggregate: "sum",
|
|
217
|
+
aggregateFormatter: (v) => formatBRL(v),
|
|
218
|
+
render: ({ value }) => (
|
|
219
|
+
<span className="tabular-nums text-fg-default">
|
|
220
|
+
{formatBRL(value as number)}
|
|
221
|
+
</span>
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
// Comissão — percentage + inline edit (editType number).
|
|
225
|
+
{
|
|
226
|
+
field: "commissionRate",
|
|
227
|
+
headerName: "Comissão",
|
|
228
|
+
type: "percentage",
|
|
229
|
+
typeOptions: { decimals: 1 },
|
|
230
|
+
align: "right",
|
|
231
|
+
width: 130,
|
|
232
|
+
sortable: true,
|
|
233
|
+
editable: true,
|
|
234
|
+
editType: "number",
|
|
235
|
+
enableColumnFilter: true,
|
|
236
|
+
filterType: "number",
|
|
237
|
+
aggregate: "avg",
|
|
238
|
+
aggregateFormatter: (v) =>
|
|
239
|
+
`${v.toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`,
|
|
240
|
+
},
|
|
241
|
+
// Conta bancária — avatar do banco; valueGetter expõe o id pro filtro/preset.
|
|
242
|
+
{
|
|
243
|
+
field: "bankAccount",
|
|
244
|
+
headerName: "Conta bancária",
|
|
245
|
+
type: "text",
|
|
246
|
+
width: 220,
|
|
247
|
+
enableColumnFilter: true,
|
|
248
|
+
filterType: "select",
|
|
249
|
+
filterOptions: BANK_OPTIONS,
|
|
250
|
+
valueGetter: (row) => row.bankAccount.bank,
|
|
251
|
+
copyable: {
|
|
252
|
+
value: (row) => `Ag ${row.bankAccount.agency} · ${row.bankAccount.account}`,
|
|
253
|
+
label: "Copiar conta",
|
|
254
|
+
},
|
|
255
|
+
render: ({ row }) => {
|
|
256
|
+
const meta = BANKS[row.bankAccount.bank];
|
|
257
|
+
return (
|
|
258
|
+
<div className="flex items-center gap-gp-md min-w-0">
|
|
259
|
+
<Avatar size="lg" colorHex={meta.color} className="text-caption-md font-bold">
|
|
260
|
+
{meta.initials}
|
|
261
|
+
</Avatar>
|
|
262
|
+
<div className="flex flex-col min-w-0">
|
|
263
|
+
<span className="text-body-sm font-medium text-fg-default truncate">
|
|
264
|
+
{row.bankAccount.bankName}
|
|
265
|
+
</span>
|
|
266
|
+
<span className="text-caption-md text-fg-muted tabular-nums">
|
|
267
|
+
Ag {row.bankAccount.agency} · {row.bankAccount.account}
|
|
268
|
+
</span>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
// Saque automático — toggle direto (interação, não inline-edit).
|
|
275
|
+
{
|
|
276
|
+
field: "autoWithdraw",
|
|
277
|
+
headerName: "Saque auto",
|
|
278
|
+
type: "boolean",
|
|
279
|
+
width: 150,
|
|
280
|
+
align: "left",
|
|
281
|
+
enableColumnFilter: true,
|
|
282
|
+
filterType: "boolean",
|
|
283
|
+
render: ({ row, value }) => (
|
|
284
|
+
<Switch
|
|
285
|
+
checked={value as boolean}
|
|
286
|
+
onCheckedChange={(next) => handlers.onToggleAutoWithdraw(row, next)}
|
|
287
|
+
onClick={(e) => e.stopPropagation()}
|
|
288
|
+
aria-label={`Saque automático de ${row.name}`}
|
|
289
|
+
/>
|
|
290
|
+
),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
field: "paymentMethods",
|
|
294
|
+
headerName: "Métodos",
|
|
295
|
+
type: "tags",
|
|
296
|
+
width: 170,
|
|
297
|
+
enableColumnFilter: true,
|
|
298
|
+
filterType: "multiSelect",
|
|
299
|
+
filterOptions: PAYMENT_METHOD_OPTIONS,
|
|
300
|
+
valueFormatter: (v) =>
|
|
301
|
+
Array.isArray(v)
|
|
302
|
+
? (v as PaymentMethod[]).map((m) => PAYMENT_METHOD_LABEL[m]).join(", ")
|
|
303
|
+
: "",
|
|
304
|
+
render: ({ value }) => (
|
|
305
|
+
<div className="flex flex-wrap gap-gp-2xs">
|
|
306
|
+
{(value as PaymentMethod[]).map((m) => (
|
|
307
|
+
<Chip key={m} color="neutral" variant="soft" size="sm" shape="rounded">
|
|
308
|
+
{PAYMENT_METHOD_LABEL[m]}
|
|
309
|
+
</Chip>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
),
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
field: "agentId",
|
|
316
|
+
headerName: "Gestor",
|
|
317
|
+
type: "user",
|
|
318
|
+
typeOptions: { users: AGENT_USERS },
|
|
319
|
+
width: 180,
|
|
320
|
+
enableColumnFilter: true,
|
|
321
|
+
filterType: "select",
|
|
322
|
+
filterOptions: AGENT_OPTIONS,
|
|
323
|
+
valueFormatter: (v) => AGENTS[v as keyof typeof AGENTS]?.name ?? String(v ?? ""),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
field: "lastMovement",
|
|
327
|
+
headerName: "Última mov.",
|
|
328
|
+
type: "datetime",
|
|
329
|
+
sortable: true,
|
|
330
|
+
enableColumnFilter: true,
|
|
331
|
+
filterType: "date",
|
|
332
|
+
width: 150,
|
|
333
|
+
render: ({ value }) => (
|
|
334
|
+
<span className="text-body-sm text-fg-muted tabular-nums">
|
|
335
|
+
{formatRelativeDays(value as number)}
|
|
336
|
+
</span>
|
|
337
|
+
),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
field: "createdAt",
|
|
341
|
+
headerName: "Cliente desde",
|
|
342
|
+
type: "date",
|
|
343
|
+
sortable: true,
|
|
344
|
+
enableColumnFilter: true,
|
|
345
|
+
filterType: "date",
|
|
346
|
+
width: 140,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
field: "_actions",
|
|
350
|
+
headerName: "",
|
|
351
|
+
type: "actions",
|
|
352
|
+
// Sem `width` e sem `pinned`, de propósito — o `type` já entrega os dois:
|
|
353
|
+
// a largura é DERIVADA do nº de ações (1→44px · 2→74 · 3→104) e acima de 3 elas
|
|
354
|
+
// colapsam no "…" sozinhas; o pin à direita sai de
|
|
355
|
+
// `col.pinned ?? (isActions ? "right" : undefined)`. Prop redundante aqui vira prop
|
|
356
|
+
// copiada em toda tela gerada — o exemplo é a fonte de maior precedência das skills.
|
|
357
|
+
pinned: "right",
|
|
358
|
+
getActions: ({ row }) => {
|
|
359
|
+
const blocked = row.accountStatus === "bloqueado";
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
id: "edit",
|
|
363
|
+
label: "Editar dados",
|
|
364
|
+
icon: <Pencil />,
|
|
365
|
+
onClick: () => handlers.onEdit(row),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "sacar",
|
|
369
|
+
label: "Realizar saque",
|
|
370
|
+
icon: <Banknote />,
|
|
371
|
+
disabled: blocked,
|
|
372
|
+
onClick: () => handlers.onSacar(row),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "history",
|
|
376
|
+
label: "Histórico de saques",
|
|
377
|
+
icon: <History />,
|
|
378
|
+
onClick: () =>
|
|
379
|
+
window.alert(`Histórico de ${row.name} — ${row.transactions.length} movimentações (mock).`),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: "block",
|
|
383
|
+
label: blocked ? "Desbloquear conta" : "Bloquear conta",
|
|
384
|
+
icon: blocked ? <Unlock /> : <Lock />,
|
|
385
|
+
destructive: !blocked,
|
|
386
|
+
onClick: () => handlers.onToggleBlock(row),
|
|
387
|
+
},
|
|
388
|
+
];
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ── Preset Views ───────────────────────────────────────────────── */
|
|
395
|
+
|
|
396
|
+
const DEFAULT_VIEWS: DataTablePresetView[] = [
|
|
397
|
+
presetView({
|
|
398
|
+
id: "preset:digitais",
|
|
399
|
+
name: "Digitais",
|
|
400
|
+
filters: [{ field: "bankAccount", value: "nubank" }],
|
|
401
|
+
}),
|
|
402
|
+
presetView({
|
|
403
|
+
id: "preset:alto-valor",
|
|
404
|
+
name: "Alto valor (≥ R$ 5k)",
|
|
405
|
+
filters: [{ field: "availableBalance", operator: "gte", value: 5000 }],
|
|
406
|
+
sort: [{ field: "availableBalance", direction: "desc" }],
|
|
407
|
+
}),
|
|
408
|
+
presetView({
|
|
409
|
+
id: "preset:inadimplentes",
|
|
410
|
+
name: "Inadimplentes",
|
|
411
|
+
// accountStatus é badge → operador isAnyOf (multi-valor).
|
|
412
|
+
filters: [
|
|
413
|
+
{ field: "accountStatus", operator: "isAnyOf", value: ["negociacao", "bloqueado"] },
|
|
414
|
+
],
|
|
415
|
+
}),
|
|
416
|
+
presetView({
|
|
417
|
+
id: "preset:saque-auto",
|
|
418
|
+
name: "Saque automático",
|
|
419
|
+
filters: [{ field: "autoWithdraw", operator: "equals", value: true }],
|
|
420
|
+
}),
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
/* ── Kanban — board por situação da conta ───────────────────────── */
|
|
424
|
+
|
|
425
|
+
const KANBAN_COLUMNS: KanbanColumn[] = [
|
|
426
|
+
{ id: "pendente", label: "Pendente", dotColor: "var(--color-fg-warning)" },
|
|
427
|
+
{ id: "ativo", label: "Ativo", dotColor: "var(--color-fg-success)" },
|
|
428
|
+
{ id: "negociacao", label: "Em negociação", dotColor: "var(--color-fg-info)" },
|
|
429
|
+
{ id: "bloqueado", label: "Bloqueado", dotColor: "var(--color-fg-danger)" },
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* KPIs temporariamente ocultos (Sergio vai retrabalhá-los depois). Mantido o
|
|
434
|
+
* código + a section atrás da flag pra reativar trocando pra `true`.
|
|
435
|
+
*/
|
|
436
|
+
const SHOW_KPIS = false;
|
|
437
|
+
|
|
438
|
+
/* ── KPI Card ───────────────────────────────────────────────────── */
|
|
439
|
+
|
|
440
|
+
type KpiTone = "brand" | "success" | "warning" | "info" | "danger" | "neutral";
|
|
441
|
+
|
|
442
|
+
const KPI_TONE_CLASSES: Record<KpiTone, { bg: string; fg: string }> = {
|
|
443
|
+
brand: { bg: "bg-bg-brand-subtle", fg: "text-fg-brand" },
|
|
444
|
+
success: { bg: "bg-bg-success-muted", fg: "text-fg-success" },
|
|
445
|
+
warning: { bg: "bg-bg-warning-muted", fg: "text-fg-warning" },
|
|
446
|
+
info: { bg: "bg-bg-info-muted", fg: "text-fg-info" },
|
|
447
|
+
danger: { bg: "bg-bg-danger-muted", fg: "text-fg-danger" },
|
|
448
|
+
neutral: { bg: "bg-bg-muted", fg: "text-fg-muted" },
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
function KpiCard({
|
|
452
|
+
icon: Icon,
|
|
453
|
+
title,
|
|
454
|
+
value,
|
|
455
|
+
hint,
|
|
456
|
+
tone = "brand",
|
|
457
|
+
}: {
|
|
458
|
+
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
|
459
|
+
title: string;
|
|
460
|
+
value: string;
|
|
461
|
+
hint?: string;
|
|
462
|
+
tone?: KpiTone;
|
|
463
|
+
}) {
|
|
464
|
+
const cls = KPI_TONE_CLASSES[tone];
|
|
465
|
+
return (
|
|
466
|
+
<article className="flex flex-col gap-gp-lg p-pad-3xl bg-bg-surface border border-border-subtle rounded-radius-xl shadow-sh-sm">
|
|
467
|
+
<header className="flex items-start justify-between gap-gp-md">
|
|
468
|
+
<h3 className="m-0 text-body-md font-semibold text-fg-default">{title}</h3>
|
|
469
|
+
<span
|
|
470
|
+
className={`grid place-items-center size-form-lg rounded-radius-lg shrink-0 ${cls.bg} ${cls.fg}`}
|
|
471
|
+
aria-hidden="true"
|
|
472
|
+
>
|
|
473
|
+
<Icon className="size-icon-md" strokeWidth={1.8} />
|
|
474
|
+
</span>
|
|
475
|
+
</header>
|
|
476
|
+
<div className="flex flex-col gap-gp-xs">
|
|
477
|
+
<span className="text-body-2xl font-bold text-fg-default leading-none [font-variant-numeric:tabular-nums]">
|
|
478
|
+
{value}
|
|
479
|
+
</span>
|
|
480
|
+
{hint && <span className="text-caption-sm text-fg-subtle">{hint}</span>}
|
|
481
|
+
</div>
|
|
482
|
+
</article>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
487
|
+
Page — Clientes Financeiro
|
|
488
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
489
|
+
|
|
490
|
+
export function FinanceScreen() {
|
|
491
|
+
const [rows, setRows] = useState<FinanceClientRow[]>(FINANCE_CLIENTS);
|
|
492
|
+
const [viewMode, setViewMode] = useState<DataTableViewMode>("table");
|
|
493
|
+
const [novoClienteOpen, setNovoClienteOpen] = useState(false);
|
|
494
|
+
const [editingClient, setEditingClient] = useState<FinanceClientRow | null>(null);
|
|
495
|
+
const [sacarClient, setSacarClient] = useState<FinanceClientRow | null>(null);
|
|
496
|
+
const [detailRowId, setDetailRowId] = useState<string | null>(null);
|
|
497
|
+
const tableRef = useRef<DataTableRef>(null);
|
|
498
|
+
|
|
499
|
+
const detailRow = detailRowId
|
|
500
|
+
? rows.find((r) => r.id === detailRowId) ?? null
|
|
501
|
+
: null;
|
|
502
|
+
|
|
503
|
+
const handleEdit = useCallback((row: FinanceClientRow) => {
|
|
504
|
+
setEditingClient(row);
|
|
505
|
+
}, []);
|
|
506
|
+
|
|
507
|
+
const handleSacar = useCallback((row: FinanceClientRow) => {
|
|
508
|
+
setSacarClient(row);
|
|
509
|
+
}, []);
|
|
510
|
+
|
|
511
|
+
const handleToggleBlock = useCallback((row: FinanceClientRow) => {
|
|
512
|
+
setRows((prev) =>
|
|
513
|
+
prev.map((r) =>
|
|
514
|
+
r.id === row.id
|
|
515
|
+
? { ...r, accountStatus: r.accountStatus === "bloqueado" ? "ativo" : "bloqueado" }
|
|
516
|
+
: r,
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
}, []);
|
|
520
|
+
|
|
521
|
+
const handleToggleAutoWithdraw = useCallback(
|
|
522
|
+
(row: FinanceClientRow, next: boolean) => {
|
|
523
|
+
setRows((prev) =>
|
|
524
|
+
prev.map((r) => (r.id === row.id ? { ...r, autoWithdraw: next } : r)),
|
|
525
|
+
);
|
|
526
|
+
},
|
|
527
|
+
[],
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const handleSacarConfirm = useCallback(
|
|
531
|
+
(data: { clientId: string; amount: number; account: { bankName: string } }) => {
|
|
532
|
+
window.alert(
|
|
533
|
+
`Saque de ${formatBRL(data.amount)} confirmado!\n\n` +
|
|
534
|
+
`Cliente: ${data.clientId}\n` +
|
|
535
|
+
`Conta destino: ${data.account.bankName}\n\n` +
|
|
536
|
+
`Disponível em até 1 dia útil.`,
|
|
537
|
+
);
|
|
538
|
+
},
|
|
539
|
+
[],
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Inline edit (commissionRate) — async com latência simulada.
|
|
543
|
+
const handleCellEditCommit = useCallback(
|
|
544
|
+
async ({ id, field, value }: { id: string | number; field: string; value: unknown }) => {
|
|
545
|
+
await new Promise((res) => setTimeout(res, 600));
|
|
546
|
+
setRows((prev) =>
|
|
547
|
+
prev.map((r) => (r.id === id ? { ...r, [field]: Number(value) } : r)),
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
[],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const columns = useMemo(
|
|
554
|
+
() =>
|
|
555
|
+
buildColumns({
|
|
556
|
+
onEdit: handleEdit,
|
|
557
|
+
onSacar: handleSacar,
|
|
558
|
+
onToggleBlock: handleToggleBlock,
|
|
559
|
+
onToggleAutoWithdraw: handleToggleAutoWithdraw,
|
|
560
|
+
}),
|
|
561
|
+
[handleEdit, handleSacar, handleToggleBlock, handleToggleAutoWithdraw],
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const kanbanConfig = useMemo<DataTableKanbanConfig<FinanceClientRow>>(
|
|
565
|
+
() => ({
|
|
566
|
+
groupByField: "accountStatus",
|
|
567
|
+
columns: KANBAN_COLUMNS,
|
|
568
|
+
renderCard: ({ row }) => {
|
|
569
|
+
const bank = BANKS[row.bankAccount.bank];
|
|
570
|
+
return {
|
|
571
|
+
title: row.name,
|
|
572
|
+
subtitle: row.companyName,
|
|
573
|
+
avatar: (
|
|
574
|
+
<Avatar size="sm" colorHex={row.avatarColor}>
|
|
575
|
+
{row.initials}
|
|
576
|
+
</Avatar>
|
|
577
|
+
),
|
|
578
|
+
chip: (
|
|
579
|
+
<Chip color="neutral" variant="soft" size="sm" shape="pill">
|
|
580
|
+
{bank.initials}
|
|
581
|
+
</Chip>
|
|
582
|
+
),
|
|
583
|
+
value: formatBRL(row.availableBalance),
|
|
584
|
+
footerLeft: (
|
|
585
|
+
<span className="inline-flex flex-wrap gap-gp-2xs min-w-0">
|
|
586
|
+
{row.paymentMethods.map((m) => (
|
|
587
|
+
<Chip key={m} color="neutral" variant="soft" size="sm" shape="rounded">
|
|
588
|
+
{PAYMENT_METHOD_LABEL[m]}
|
|
589
|
+
</Chip>
|
|
590
|
+
))}
|
|
591
|
+
</span>
|
|
592
|
+
),
|
|
593
|
+
footerRight: (
|
|
594
|
+
<span className="text-caption-sm text-fg-muted [font-variant-numeric:tabular-nums] shrink-0">
|
|
595
|
+
{formatRelativeDays(row.lastMovement)}
|
|
596
|
+
</span>
|
|
597
|
+
),
|
|
598
|
+
};
|
|
599
|
+
},
|
|
600
|
+
enableDnD: true,
|
|
601
|
+
onCardMove: (cardId, _from, to) => {
|
|
602
|
+
setRows((prev) =>
|
|
603
|
+
prev.map((r) =>
|
|
604
|
+
r.id === cardId ? { ...r, accountStatus: to as AccountStatus } : r,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
609
|
+
[],
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const isTable = viewMode === "table";
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
// Conteúdo da página (sem shell). Embrulhe no seu AppShell/layout — o
|
|
616
|
+
// parent precisa dar altura (h-full/flex) pra DataTable preencher.
|
|
617
|
+
<div className="flex flex-col h-full min-h-0 gap-gp-2xl">
|
|
618
|
+
<PageHeader
|
|
619
|
+
title="Clientes — Financeiro"
|
|
620
|
+
description="Acompanhe saldos, situação das contas e fluxo financeiro dos licenciados. Arraste cards no Kanban pra mudar a situação; edite comissão e saque automático direto na tabela."
|
|
621
|
+
badge={
|
|
622
|
+
<Chip color="primary" variant="soft" size="sm" shape="rounded">
|
|
623
|
+
{rows.length.toLocaleString("pt-BR")} clientes
|
|
624
|
+
</Chip>
|
|
625
|
+
}
|
|
626
|
+
actions={
|
|
627
|
+
<>
|
|
628
|
+
<Button
|
|
629
|
+
variant="outline"
|
|
630
|
+
color="secondary"
|
|
631
|
+
size="icon-md"
|
|
632
|
+
aria-label="Mais ações"
|
|
633
|
+
>
|
|
634
|
+
<MoreHorizontal />
|
|
635
|
+
</Button>
|
|
636
|
+
<Button
|
|
637
|
+
variant="filled"
|
|
638
|
+
color="primary"
|
|
639
|
+
size="md"
|
|
640
|
+
iconLeft={<Plus />}
|
|
641
|
+
onClick={() => setNovoClienteOpen(true)}
|
|
642
|
+
>
|
|
643
|
+
Novo cliente
|
|
644
|
+
</Button>
|
|
645
|
+
</>
|
|
646
|
+
}
|
|
647
|
+
/>
|
|
648
|
+
|
|
649
|
+
{/* KPI grid — 4 cards. Oculto por enquanto (SHOW_KPIS) — Sergio retrabalha depois. */}
|
|
650
|
+
{SHOW_KPIS && (
|
|
651
|
+
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gp-2xl pb-pad-2xl">
|
|
652
|
+
<KpiCard
|
|
653
|
+
icon={Wallet}
|
|
654
|
+
title="Disponível total"
|
|
655
|
+
value={formatBRL(FINANCE_KPIS.totalAvailable)}
|
|
656
|
+
hint="Soma de todos os saldos"
|
|
657
|
+
tone="brand"
|
|
658
|
+
/>
|
|
659
|
+
<KpiCard
|
|
660
|
+
icon={TrendingUp}
|
|
661
|
+
title="High-value (≥ R$ 5k)"
|
|
662
|
+
value={String(FINANCE_KPIS.highValueCount)}
|
|
663
|
+
hint="Clientes acima de R$ 5.000"
|
|
664
|
+
tone="success"
|
|
665
|
+
/>
|
|
666
|
+
<KpiCard
|
|
667
|
+
icon={Banknote}
|
|
668
|
+
title="Saldo médio"
|
|
669
|
+
value={formatBRL(FINANCE_KPIS.averageBalance)}
|
|
670
|
+
hint="Por cliente"
|
|
671
|
+
tone="info"
|
|
672
|
+
/>
|
|
673
|
+
<KpiCard
|
|
674
|
+
icon={ShieldAlert}
|
|
675
|
+
title="Em risco"
|
|
676
|
+
value={String(FINANCE_KPIS.atRiskCount)}
|
|
677
|
+
hint="Em negociação ou bloqueado"
|
|
678
|
+
tone="danger"
|
|
679
|
+
/>
|
|
680
|
+
</section>
|
|
681
|
+
)}
|
|
682
|
+
|
|
683
|
+
<DataTable<FinanceClientRow>
|
|
684
|
+
ref={tableRef}
|
|
685
|
+
rows={rows}
|
|
686
|
+
columns={columns}
|
|
687
|
+
getRowId={(r) => r.id}
|
|
688
|
+
// v4 — colunas + kanban + inline edit reformulados (auditoria 2026-06-10)
|
|
689
|
+
persistId="showcase-finance-crud-v4"
|
|
690
|
+
defaultViews={DEFAULT_VIEWS}
|
|
691
|
+
viewMode={viewMode}
|
|
692
|
+
onViewModeChange={setViewMode}
|
|
693
|
+
kanbanConfig={kanbanConfig}
|
|
694
|
+
// Click na row (no licenciado) abre o painel de detalhe (FloatingPanel).
|
|
695
|
+
onRowClick={(row) => setDetailRowId(row.id)}
|
|
696
|
+
// Extrato da row (chevron na coluna ID).
|
|
697
|
+
renderRowExpansion={({ row }) => <ExtratoExpansion row={row} />}
|
|
698
|
+
// Inline edit da comissão (async, latência simulada).
|
|
699
|
+
onCellEditCommit={handleCellEditCommit}
|
|
700
|
+
// Totalizers no footer (só faz sentido na tabela).
|
|
701
|
+
showTotalizers={isTable}
|
|
702
|
+
toolbar={{
|
|
703
|
+
enableSearch: true,
|
|
704
|
+
enableFilters: true,
|
|
705
|
+
// Cols/density só na tabela (board não tem colunas/densidade).
|
|
706
|
+
enableColumns: isTable,
|
|
707
|
+
enableDensity: isTable,
|
|
708
|
+
enableExport: true,
|
|
709
|
+
}}
|
|
710
|
+
paginationConfig={{
|
|
711
|
+
enabled: isTable,
|
|
712
|
+
initialPageSize: 25,
|
|
713
|
+
pageSizeOptions: [10, 25, 50, 100],
|
|
714
|
+
}}
|
|
715
|
+
selectionConfig={{
|
|
716
|
+
enabled: true,
|
|
717
|
+
enableGlobal: true,
|
|
718
|
+
actions: (selectedIds, clearSelection) => (
|
|
719
|
+
<>
|
|
720
|
+
<BulkActionButton
|
|
721
|
+
icon={<Download />}
|
|
722
|
+
onClick={() => tableRef.current?.exportCsv("selected")}
|
|
723
|
+
>
|
|
724
|
+
Exportar
|
|
725
|
+
</BulkActionButton>
|
|
726
|
+
<BulkActionButton
|
|
727
|
+
icon={<Pause />}
|
|
728
|
+
onClick={() => {
|
|
729
|
+
const ids = new Set(selectedIds.map(String));
|
|
730
|
+
setRows((prev) =>
|
|
731
|
+
prev.map((r) =>
|
|
732
|
+
ids.has(r.id) ? { ...r, autoWithdraw: false } : r,
|
|
733
|
+
),
|
|
734
|
+
);
|
|
735
|
+
clearSelection();
|
|
736
|
+
}}
|
|
737
|
+
>
|
|
738
|
+
Pausar saque auto
|
|
739
|
+
</BulkActionButton>
|
|
740
|
+
<BulkActionButton
|
|
741
|
+
icon={<AlertTriangle />}
|
|
742
|
+
variant="danger"
|
|
743
|
+
onClick={() => {
|
|
744
|
+
const ids = new Set(selectedIds.map(String));
|
|
745
|
+
setRows((prev) =>
|
|
746
|
+
prev.map((r) =>
|
|
747
|
+
ids.has(r.id) ? { ...r, accountStatus: "negociacao" } : r,
|
|
748
|
+
),
|
|
749
|
+
);
|
|
750
|
+
clearSelection();
|
|
751
|
+
}}
|
|
752
|
+
>
|
|
753
|
+
Marcar p/ revisão
|
|
754
|
+
</BulkActionButton>
|
|
755
|
+
</>
|
|
756
|
+
),
|
|
757
|
+
}}
|
|
758
|
+
className="flex-1 min-h-0"
|
|
759
|
+
/>
|
|
760
|
+
|
|
761
|
+
{/* Drawers + Modals */}
|
|
762
|
+
<NovoClienteDrawer
|
|
763
|
+
open={novoClienteOpen}
|
|
764
|
+
onOpenChange={setNovoClienteOpen}
|
|
765
|
+
onSubmit={() => window.alert("Cliente criado com sucesso! (mock)")}
|
|
766
|
+
/>
|
|
767
|
+
<EditarFinanceDrawer
|
|
768
|
+
client={editingClient}
|
|
769
|
+
onOpenChange={(o) => !o && setEditingClient(null)}
|
|
770
|
+
onSave={(id, p) => {
|
|
771
|
+
setRows((prev) =>
|
|
772
|
+
prev.map((r) =>
|
|
773
|
+
r.id === id
|
|
774
|
+
? {
|
|
775
|
+
...r,
|
|
776
|
+
name: p.name,
|
|
777
|
+
email: p.email,
|
|
778
|
+
phone: p.phone,
|
|
779
|
+
companyName: p.companyName,
|
|
780
|
+
cnpj: p.cnpj,
|
|
781
|
+
accountStatus: p.accountStatus,
|
|
782
|
+
commissionRate: p.commissionRate,
|
|
783
|
+
autoWithdraw: p.autoWithdraw,
|
|
784
|
+
paymentMethods: p.paymentMethods,
|
|
785
|
+
agentId: p.agentId as FinanceClientRow["agentId"],
|
|
786
|
+
bankAccount: {
|
|
787
|
+
...r.bankAccount,
|
|
788
|
+
bank: p.bank,
|
|
789
|
+
bankName: BANKS[p.bank].name,
|
|
790
|
+
},
|
|
791
|
+
}
|
|
792
|
+
: r,
|
|
793
|
+
),
|
|
794
|
+
);
|
|
795
|
+
}}
|
|
796
|
+
/>
|
|
797
|
+
<SacarDialog
|
|
798
|
+
open={sacarClient !== null}
|
|
799
|
+
onOpenChange={(o) => !o && setSacarClient(null)}
|
|
800
|
+
client={sacarClient}
|
|
801
|
+
onConfirm={handleSacarConfirm}
|
|
802
|
+
/>
|
|
803
|
+
|
|
804
|
+
{/* Detalhe do licenciado — FloatingPanel (abre no click da row). */}
|
|
805
|
+
<FinanceDetailPanel
|
|
806
|
+
row={detailRow}
|
|
807
|
+
onClose={() => setDetailRowId(null)}
|
|
808
|
+
onEdit={(row) => {
|
|
809
|
+
setDetailRowId(null);
|
|
810
|
+
setEditingClient(row);
|
|
811
|
+
}}
|
|
812
|
+
onSacar={(row) => {
|
|
813
|
+
setDetailRowId(null);
|
|
814
|
+
setSacarClient(row);
|
|
815
|
+
}}
|
|
816
|
+
/>
|
|
817
|
+
</div>
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export default FinanceScreen;
|