rl-core-front 0.3.0 → 0.4.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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/_services/api/schema.d.ts +43 -132
  3. package/src/_utils/filter.ts +15 -34
  4. package/src/components/app-shell.tsx +51 -1
  5. package/src/components/brand-header.tsx +23 -8
  6. package/src/components/brand-panel.tsx +41 -0
  7. package/src/components/index.ts +1 -0
  8. package/src/components/ui/button.tsx +5 -0
  9. package/src/components/ui/confirm-dialog.tsx +3 -1
  10. package/src/components/ui/data-table.tsx +13 -4
  11. package/src/components/ui/dialog.tsx +120 -21
  12. package/src/components/ui/filter-sheet.tsx +47 -25
  13. package/src/components/ui/index.ts +1 -1
  14. package/src/components/ui/segmented-control.tsx +74 -0
  15. package/src/contexts/brand-context.tsx +15 -0
  16. package/src/contexts/i18n-context.tsx +69 -4
  17. package/src/features/audit/audit-screen.tsx +13 -140
  18. package/src/features/audit/components/audit-diff.tsx +156 -0
  19. package/src/features/audit/components/index.ts +3 -0
  20. package/src/features/audit/components/record-audit-button.tsx +176 -0
  21. package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
  22. package/src/features/audit/hooks/use-audit-trail.ts +9 -2
  23. package/src/features/audit/services/audit.service.ts +43 -1
  24. package/src/features/login/login-screen.tsx +86 -40
  25. package/src/features/logs/enums/error-type.enum.ts +21 -0
  26. package/src/features/logs/enums/request-outcome.enum.ts +14 -0
  27. package/src/features/logs/hooks/use-request-logs.ts +26 -3
  28. package/src/features/logs/logs-screen.tsx +2 -14
  29. package/src/features/logs/panels/index.ts +0 -1
  30. package/src/features/logs/panels/request-logs-panel.tsx +177 -25
  31. package/src/features/logs/services/logs.service.ts +13 -51
  32. package/src/features/notifications/components/notification-item.tsx +10 -8
  33. package/src/features/notifications/enums/notification-type.enum.ts +8 -0
  34. package/src/features/profile/profile-screen.tsx +2 -1
  35. package/src/features/rbac/panels/groups-panel.tsx +20 -7
  36. package/src/features/users/components/user-form-dialog.tsx +16 -7
  37. package/src/features/users/hooks/use-users.ts +9 -2
  38. package/src/hooks/use-filters.ts +25 -2
  39. package/src/hooks/use-list-query.ts +14 -1
  40. package/src/i18n/messages/en.ts +17 -5
  41. package/src/i18n/messages/pt.ts +18 -5
  42. package/src/index.ts +12 -1
  43. package/src/components/ui/filter-chips.tsx +0 -88
  44. package/src/features/logs/hooks/use-error-logs.ts +0 -65
  45. package/src/features/logs/panels/error-logs-panel.tsx +0 -183
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-front",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Telas e componentes Next.js do core: login com 2FA, usu\u00e1rios, RBAC, auditoria, logs e listagens com filtro din\u00e2mico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",
@@ -608,14 +608,14 @@ export interface paths {
608
608
  patch?: never;
609
609
  trace?: never;
610
610
  };
611
- "/audit/changes": {
611
+ "/audit/requests": {
612
612
  parameters: {
613
613
  query?: never;
614
614
  header?: never;
615
615
  path?: never;
616
616
  cookie?: never;
617
617
  };
618
- get: operations["AuditController_changes"];
618
+ get: operations["AuditController_requests"];
619
619
  put?: never;
620
620
  post?: never;
621
621
  delete?: never;
@@ -624,46 +624,14 @@ export interface paths {
624
624
  patch?: never;
625
625
  trace?: never;
626
626
  };
627
- "/audit/changes/filter-schema": {
627
+ "/audit/requests/filter-schema": {
628
628
  parameters: {
629
629
  query?: never;
630
630
  header?: never;
631
631
  path?: never;
632
632
  cookie?: never;
633
633
  };
634
- get: operations["AuditController_changesFilterSchema"];
635
- put?: never;
636
- post?: never;
637
- delete?: never;
638
- options?: never;
639
- head?: never;
640
- patch?: never;
641
- trace?: never;
642
- };
643
- "/audit/errors": {
644
- parameters: {
645
- query?: never;
646
- header?: never;
647
- path?: never;
648
- cookie?: never;
649
- };
650
- get: operations["AuditController_errors"];
651
- put?: never;
652
- post?: never;
653
- delete?: never;
654
- options?: never;
655
- head?: never;
656
- patch?: never;
657
- trace?: never;
658
- };
659
- "/audit/errors/filter-schema": {
660
- parameters: {
661
- query?: never;
662
- header?: never;
663
- path?: never;
664
- cookie?: never;
665
- };
666
- get: operations["AuditController_errorsFilterSchema"];
634
+ get: operations["AuditController_requestsFilterSchema"];
667
635
  put?: never;
668
636
  post?: never;
669
637
  delete?: never;
@@ -1122,6 +1090,11 @@ export interface components {
1122
1090
  permissionIds?: string[];
1123
1091
  isActive?: boolean;
1124
1092
  };
1093
+ /**
1094
+ * @description 0 = sucesso, 1 = erro, 2 = abortada
1095
+ * @enum {number}
1096
+ */
1097
+ RequestOutcome: 0 | 1 | 2;
1125
1098
  AuditUserRefResponse: {
1126
1099
  /** Format: uuid */
1127
1100
  id: string;
@@ -1129,58 +1102,43 @@ export interface components {
1129
1102
  /** @example admin@admin.com */
1130
1103
  email: string;
1131
1104
  };
1105
+ /**
1106
+ * @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno
1107
+ * @enum {number}
1108
+ */
1109
+ ErrorType: 0 | 1 | 2 | 3 | 4 | 5 | 6;
1132
1110
  RequestLogResponse: {
1133
1111
  /** Format: uuid */
1134
1112
  id: string;
1135
- /** @example GET /api/v1/users */
1136
- action: string;
1137
- /** @example User */
1138
- entity?: string | null;
1139
- entityId?: string | null;
1140
- /** @example GET */
1113
+ /**
1114
+ * @description 0 = sucesso, 1 = erro, 2 = abortada
1115
+ * @example 0
1116
+ */
1117
+ outcome: components["schemas"]["RequestOutcome"];
1118
+ /** @example Login */
1119
+ action?: string | null;
1120
+ /** @example POST */
1141
1121
  method?: string | null;
1122
+ /** @example /api/v1/users/9f3c… */
1142
1123
  path?: string | null;
1124
+ /** @example /users/:id */
1125
+ route?: string | null;
1143
1126
  /** @example 200 */
1144
1127
  statusCode?: number | null;
1145
1128
  /** @example 42 */
1146
1129
  durationMs?: number | null;
1147
- /** @example 10.0.0.1 */
1148
- ipAddress?: string | null;
1149
- /** Format: uuid */
1150
- requestId?: string | null;
1151
- metadata?: Record<string, never> | null;
1152
1130
  userId?: string | null;
1153
1131
  user?: components["schemas"]["AuditUserRefResponse"] | null;
1154
- /** Format: date-time */
1155
- createdAt: string;
1156
- /** Format: date-time */
1157
- updatedAt: string;
1158
- /** Format: date-time */
1159
- deletedAt?: string | null;
1160
- };
1161
- PaginatedRequestLogsResponse: {
1162
- items: components["schemas"]["RequestLogResponse"][];
1163
- meta: components["schemas"]["PaginationMetaResponse"];
1164
- };
1165
- /** @enum {string} */
1166
- ErrorType: "VALIDATION" | "AUTH" | "NOT_FOUND" | "RATE_LIMIT" | "DATABASE" | "INTEGRATION" | "INTERNAL";
1167
- ErrorLogResponse: {
1168
- /** Format: uuid */
1169
- id: string;
1170
- type: components["schemas"]["ErrorType"];
1171
- /** @example 500 */
1172
- statusCode?: number | null;
1173
- message: string;
1174
- stack?: string | null;
1175
- /** @example POST */
1176
- method?: string | null;
1177
- path?: string | null;
1178
- /** @example 10.0.0.1 */
1179
- ipAddress?: string | null;
1132
+ /** @description HMAC do IP — não é reversível ao endereço original */
1133
+ ipHash?: string | null;
1134
+ userAgent?: string | null;
1180
1135
  /** Format: uuid */
1181
1136
  requestId?: string | null;
1182
- userId?: string | null;
1183
- user?: components["schemas"]["AuditUserRefResponse"] | null;
1137
+ /** @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno */
1138
+ errorType?: components["schemas"]["ErrorType"] | null;
1139
+ errorCode?: string | null;
1140
+ errorMessage?: string | null;
1141
+ errorStack?: string | null;
1184
1142
  /** Format: date-time */
1185
1143
  createdAt: string;
1186
1144
  /** Format: date-time */
@@ -1188,12 +1146,12 @@ export interface components {
1188
1146
  /** Format: date-time */
1189
1147
  deletedAt?: string | null;
1190
1148
  };
1191
- PaginatedErrorLogsResponse: {
1192
- items: components["schemas"]["ErrorLogResponse"][];
1149
+ PaginatedRequestLogsResponse: {
1150
+ items: components["schemas"]["RequestLogResponse"][];
1193
1151
  meta: components["schemas"]["PaginationMetaResponse"];
1194
1152
  };
1195
- /** @enum {string} */
1196
- AuditDataAction: "CREATE" | "UPDATE" | "DELETE";
1153
+ /** @enum {number} */
1154
+ AuditDataAction: 0 | 1 | 2;
1197
1155
  AuditDataChangeResponse: {
1198
1156
  /** Format: uuid */
1199
1157
  id: string;
@@ -1218,8 +1176,8 @@ export interface components {
1218
1176
  items: components["schemas"]["AuditDataChangeResponse"][];
1219
1177
  meta: components["schemas"]["PaginationMetaResponse"];
1220
1178
  };
1221
- /** @enum {string} */
1222
- NotificationType: "INFO" | "SUCCESS" | "WARNING" | "ERROR";
1179
+ /** @enum {number} */
1180
+ NotificationType: 0 | 1 | 2 | 3;
1223
1181
  UserNotificationResponse: {
1224
1182
  /** Format: uuid */
1225
1183
  id: string;
@@ -1245,8 +1203,8 @@ export interface components {
1245
1203
  /** @example 3 */
1246
1204
  unread: number;
1247
1205
  };
1248
- /** @enum {string} */
1249
- ScopeType: "USER" | "ROLE" | "ALL";
1206
+ /** @enum {number} */
1207
+ ScopeType: 0 | 1 | 2;
1250
1208
  NotificationTargetCommand: {
1251
1209
  type: components["schemas"]["ScopeType"];
1252
1210
  /** @description Id do usuário quando type=USER, nome do papel quando type=ROLE. Ignorado em ALL. */
@@ -2487,7 +2445,7 @@ export interface operations {
2487
2445
  };
2488
2446
  };
2489
2447
  };
2490
- AuditController_changes: {
2448
+ AuditController_requests: {
2491
2449
  parameters: {
2492
2450
  query?: {
2493
2451
  page?: number;
@@ -2515,54 +2473,7 @@ export interface operations {
2515
2473
  };
2516
2474
  };
2517
2475
  };
2518
- AuditController_changesFilterSchema: {
2519
- parameters: {
2520
- query?: never;
2521
- header?: never;
2522
- path?: never;
2523
- cookie?: never;
2524
- };
2525
- requestBody?: never;
2526
- responses: {
2527
- 200: {
2528
- headers: {
2529
- [name: string]: unknown;
2530
- };
2531
- content: {
2532
- "application/json": components["schemas"]["FilterFieldResponse"][];
2533
- };
2534
- };
2535
- };
2536
- };
2537
- AuditController_errors: {
2538
- parameters: {
2539
- query?: {
2540
- page?: number;
2541
- limit?: number;
2542
- search?: string;
2543
- /** @description Coluna para ordenar (whitelist por endpoint) */
2544
- sortBy?: string;
2545
- sortDir?: "ASC" | "DESC";
2546
- /** @description JSON, ex.: {"name":{"$SUBSTRING":"ana"}} */
2547
- filter?: string;
2548
- };
2549
- header?: never;
2550
- path?: never;
2551
- cookie?: never;
2552
- };
2553
- requestBody?: never;
2554
- responses: {
2555
- 200: {
2556
- headers: {
2557
- [name: string]: unknown;
2558
- };
2559
- content: {
2560
- "application/json": components["schemas"]["PaginatedErrorLogsResponse"];
2561
- };
2562
- };
2563
- };
2564
- };
2565
- AuditController_errorsFilterSchema: {
2476
+ AuditController_requestsFilterSchema: {
2566
2477
  parameters: {
2567
2478
  query?: never;
2568
2479
  header?: never;
@@ -1,5 +1,5 @@
1
1
  import type { components } from "#core/_services/api/schema";
2
- import { formatDay, fromIsoDay, toIsoDay } from "#core/_utils/calendar";
2
+ import { fromIsoDay, toIsoDay } from "#core/_utils/calendar";
3
3
 
4
4
  /**
5
5
  * Tipos derivados do `openapi.json` (§5): o catálogo é servido pelo backend,
@@ -63,8 +63,20 @@ export const hasValue = (draft: FilterDraftValue | undefined): boolean => {
63
63
  );
64
64
  };
65
65
 
66
- export const activeFilterCount = (values: FilterValues): number =>
67
- Object.values(values).filter(hasValue).length;
66
+ /**
67
+ * Quantos filtros o botão "Filtros" anuncia.
68
+ *
69
+ * `ignore` são os campos que a barra da listagem controla: eles filtram de
70
+ * verdade, mas o contador não pode cobrá-los, senão a tela abre marcando 1 num
71
+ * painel onde não há nada marcado.
72
+ */
73
+ export const activeFilterCount = (
74
+ values: FilterValues,
75
+ ignore: string[] = [],
76
+ ): number =>
77
+ Object.entries(values).filter(
78
+ ([field, draft]) => hasValue(draft) && !ignore.includes(field),
79
+ ).length;
68
80
 
69
81
  /**
70
82
  * Ponta de um intervalo de datas em ISO **com o fuso de quem filtrou**.
@@ -280,34 +292,3 @@ const draftFrom = (
280
292
  }
281
293
  }
282
294
  };
283
-
284
- /** Resumo de um filtro ativo, para o chip. */
285
- export const describeValue = (
286
- field: FilterField,
287
- draft: FilterDraftValue,
288
- labels: { from: string; to: string; yes: string; no: string; locale: string },
289
- ): string => {
290
- const show = (value: string): string =>
291
- field.type === "DATE" || field.type === "DATETIME"
292
- ? formatDay(value, labels.locale)
293
- : value;
294
-
295
- if (field.type === "BOOLEAN") {
296
- return draft.value === "true" ? labels.yes : labels.no;
297
- }
298
- if (draft.values?.length) {
299
- const optionLabel = (value: string): string =>
300
- field.options?.find((option) => option.value === value)?.label ?? value;
301
- return draft.values.map(optionLabel).join(", ");
302
- }
303
- if (draft.from && draft.to) {
304
- return `${show(draft.from)} – ${show(draft.to)}`;
305
- }
306
- if (draft.from) {
307
- return `${labels.from} ${show(draft.from)}`;
308
- }
309
- if (draft.to) {
310
- return `${labels.to} ${show(draft.to)}`;
311
- }
312
- return draft.value ?? "";
313
- };
@@ -46,6 +46,30 @@ interface NavItem {
46
46
  perm: string | null;
47
47
  }
48
48
 
49
+ /**
50
+ * Item de menu declarado pelo projeto.
51
+ *
52
+ * O core não conhece as telas de quem o instala (§14), então elas entram por
53
+ * aqui em vez de o projeto reescrever o shell.
54
+ *
55
+ * ```tsx
56
+ * <AppShell navItems={[{ label: "nav.tasks", href: "/tarefas", icon: ListTodo }]}>
57
+ * ```
58
+ */
59
+ export interface AppNavItem {
60
+ /**
61
+ * Chave de i18n (`nav.tasks`) ou o texto pronto (`"Tarefas"`).
62
+ *
63
+ * Chave sem tradução cadastrada volta como veio, então as duas formas
64
+ * funcionam — o projeto só precisa do dicionário quando for bilíngue.
65
+ */
66
+ label: string;
67
+ href: string;
68
+ icon: React.ComponentType<{ className?: string }>;
69
+ /** Permissão exigida para o item aparecer. Ausente, aparece para todos. */
70
+ permission?: string | null;
71
+ }
72
+
49
73
  const DASHBOARD: NavItem = {
50
74
  key: "nav.dashboard",
51
75
  href: "/dashboard",
@@ -71,7 +95,13 @@ const ADMIN_CHILDREN: NavItem[] = [
71
95
  },
72
96
  ];
73
97
 
74
- export function AppShell({ children }: { children: React.ReactNode }): JSX.Element {
98
+ export interface AppShellProps {
99
+ children: React.ReactNode;
100
+ /** Telas do projeto no menu, entre o dashboard e o grupo de administração. */
101
+ navItems?: AppNavItem[];
102
+ }
103
+
104
+ export function AppShell({ children, navItems }: AppShellProps): JSX.Element {
75
105
  const { user, loading, logout, hasPermission } = useAuth();
76
106
  const { t } = useI18n();
77
107
  const brand = useBrand();
@@ -89,6 +119,22 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
89
119
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
120
  [user],
91
121
  );
122
+
123
+ // Os itens do projeto passam pelo mesmo filtro de permissão dos do core: um
124
+ // menu que mostra o que o usuário não pode abrir só entrega 403.
125
+ const projectItems = useMemo<NavItem[]>(
126
+ () =>
127
+ (navItems ?? [])
128
+ .filter((item) => !item.permission || hasPermission(item.permission))
129
+ .map((item) => ({
130
+ key: item.label,
131
+ href: item.href,
132
+ icon: item.icon,
133
+ perm: item.permission ?? null,
134
+ })),
135
+ // eslint-disable-next-line react-hooks/exhaustive-deps
136
+ [user, navItems],
137
+ );
92
138
  const adminActive = adminChildren.some((c) => pathname === c.href);
93
139
  const [adminOpen, setAdminOpen] = useState(adminActive);
94
140
 
@@ -158,6 +204,10 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
158
204
  <div className="flex flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden p-3">
159
205
  <NavLink item={DASHBOARD} mini={mini} />
160
206
 
207
+ {projectItems.map((item) => (
208
+ <NavLink key={item.href} item={item} mini={mini} />
209
+ ))}
210
+
161
211
  {adminChildren.length > 0 &&
162
212
  (mini ? (
163
213
  <>
@@ -7,6 +7,12 @@ import { useBrand } from "#core/contexts";
7
7
  export interface BrandHeaderProps {
8
8
  /** Título do passo ou da tela, exibido sob a marca. */
9
9
  title: string;
10
+ /**
11
+ * Mostra logo e nome acima do título. Desligue quando a marca já estiver na
12
+ * tela por outro caminho — é o caso do `BrandPanel` ao lado do formulário,
13
+ * onde repeti-la no cartão seria dizer a mesma coisa duas vezes.
14
+ */
15
+ showBrand?: boolean;
10
16
  }
11
17
 
12
18
  /**
@@ -17,19 +23,28 @@ export interface BrandHeaderProps {
17
23
  * ela está. O conteúdo vem do `BrandProvider` do projeto; sem ele, do padrão do
18
24
  * core.
19
25
  */
20
- export function BrandHeader({ title }: BrandHeaderProps): JSX.Element {
26
+ export function BrandHeader({
27
+ title,
28
+ showBrand = true,
29
+ }: BrandHeaderProps): JSX.Element {
21
30
  const brand = useBrand();
22
31
 
23
32
  return (
24
33
  <div className="mb-6 flex flex-col items-center gap-1">
25
- <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
26
- {brand.logo}
27
- </div>
28
- <span className="text-lg font-extrabold">{brand.name}</span>
29
- {brand.tagline && (
30
- <p className="text-sm text-muted-foreground">{brand.tagline}</p>
34
+ {showBrand && (
35
+ <>
36
+ <div className="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
37
+ {brand.logo}
38
+ </div>
39
+ <span className="text-lg font-extrabold">{brand.name}</span>
40
+ {brand.tagline && (
41
+ <p className="text-sm text-muted-foreground">{brand.tagline}</p>
42
+ )}
43
+ </>
31
44
  )}
32
- <h1 className="mt-2 text-xl font-semibold">{title}</h1>
45
+ <h1 className={showBrand ? "mt-2 text-xl font-semibold" : "text-xl font-semibold"}>
46
+ {title}
47
+ </h1>
33
48
  </div>
34
49
  );
35
50
  }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+
5
+ import { useBrand } from "#core/contexts";
6
+ import { cn } from "#core/lib/utils";
7
+
8
+ export interface BrandPanelProps {
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * A marca ocupando metade da tela de login, ao lado do formulário.
14
+ *
15
+ * É o irmão grande do `BrandHeader`: mesma fonte (o `BrandProvider` do
16
+ * projeto), espaço diferente. Serve a quem tem logo grande — no cabeçalho do
17
+ * cartão ela sairia espremida.
18
+ *
19
+ * O core não dimensiona a logo: ela é um `ReactNode` do projeto, que sabe se é
20
+ * um SVG, um `next/image` ou um ícone. Aqui ela só ganha o espaço.
21
+ */
22
+ export function BrandPanel({ className }: BrandPanelProps): JSX.Element {
23
+ const brand = useBrand();
24
+
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "flex flex-1 flex-col items-center justify-center gap-4 bg-muted/30 p-8 text-center",
29
+ className,
30
+ )}
31
+ >
32
+ {brand.logo}
33
+ <span className="text-3xl font-extrabold">{brand.name}</span>
34
+ {brand.tagline && (
35
+ <p className="max-w-sm text-sm text-muted-foreground">
36
+ {brand.tagline}
37
+ </p>
38
+ )}
39
+ </div>
40
+ );
41
+ }
@@ -1,6 +1,7 @@
1
1
  // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
2
  export * from "./app-shell";
3
3
  export * from "./brand-header";
4
+ export * from "./brand-panel";
4
5
  export * from "./breadcrumbs";
5
6
  export * from "./language-selector";
6
7
  export * from "./password-field";
@@ -52,6 +52,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
52
52
  <Comp
53
53
  className={cn(buttonVariants({ variant, size, className }))}
54
54
  ref={ref}
55
+ // `<button>` sem `type` nasce `submit`: dentro de um form, um botão que
56
+ // só abre um modal acabava salvando o registro junto. Quem submete
57
+ // declara `type="submit"` — o spread abaixo deixa isso sobrescrever.
58
+ // Com `asChild` o filho pode não ser um `<button>`, então não força.
59
+ {...(asChild ? {} : { type: "button" as const })}
55
60
  {...props}
56
61
  />
57
62
  );
@@ -43,7 +43,9 @@ export function ConfirmDialog({
43
43
  }: ConfirmDialogProps): React.JSX.Element {
44
44
  return (
45
45
  <Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
46
- <DialogContent className="max-w-sm">
46
+ {/* Sem ampliar: são duas linhas e dois botões, e o tamanho pequeno é
47
+ parte do recado — esticado só ganharia vazio. */}
48
+ <DialogContent className="max-w-sm" expandable={false}>
47
49
  <DialogHeader>
48
50
  <DialogTitle>{title}</DialogTitle>
49
51
  {description && <DialogDescription>{description}</DialogDescription>}
@@ -39,7 +39,6 @@ import {
39
39
  ColumnVisibilityModal,
40
40
  ColumnVisibilityOption,
41
41
  } from "#core/components/ui/column-visibility-modal";
42
- import { FilterChips } from "#core/components/ui/filter-chips";
43
42
  import { FilterSheet } from "#core/components/ui/filter-sheet";
44
43
  import { Spinner } from "#core/components/ui/spinner";
45
44
  import {
@@ -94,6 +93,16 @@ interface DataTableProps<T extends RowData> {
94
93
  * não aparece — listagem sem catálogo continua funcionando igual.
95
94
  */
96
95
  filters?: FiltersState;
96
+ /**
97
+ * Campos que a `toolbar` já controla: somem do painel de filtros e não são
98
+ * contados no botão "Filtros", para a mesma escolha não existir em dois
99
+ * lugares.
100
+ *
101
+ * Some da interface, não do catálogo — é o catálogo que serializa o filtro,
102
+ * então o atalho da barra continua funcionando normalmente. O painel também
103
+ * não os desmarca ao aplicar nem ao limpar: não são dele.
104
+ */
105
+ hiddenFilterFields?: string[];
97
106
  }
98
107
 
99
108
  /** Extras que uma coluna pode declarar em `meta`. */
@@ -144,6 +153,7 @@ export function DataTable<T extends RowData>({
144
153
  onSortingChange,
145
154
  enableSorting = true,
146
155
  filters,
156
+ hiddenFilterFields,
147
157
  }: DataTableProps<T>): JSX.Element {
148
158
  const { t } = useI18n();
149
159
  const [colsOpen, setColsOpen] = useState(false);
@@ -220,7 +230,7 @@ export function DataTable<T extends RowData>({
220
230
  ? filters.tree && filters.filter
221
231
  ? countConditions(filters.tree)
222
232
  : 0
223
- : activeFilterCount(filters.values);
233
+ : activeFilterCount(filters.values, hiddenFilterFields);
224
234
 
225
235
  return (
226
236
  <div>
@@ -257,8 +267,6 @@ export function DataTable<T extends RowData>({
257
267
  </div>
258
268
  </div>
259
269
 
260
- {filters && <FilterChips filters={filters} />}
261
-
262
270
  <Card>
263
271
  <Table>
264
272
  <TableHeader>
@@ -394,6 +402,7 @@ export function DataTable<T extends RowData>({
394
402
  open={filtersOpen}
395
403
  onOpenChange={setFiltersOpen}
396
404
  filters={filters}
405
+ hiddenFields={hiddenFilterFields}
397
406
  />
398
407
  )}
399
408
  </div>