rl-core-front 0.2.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 +238 -117
  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
@@ -1,68 +1,30 @@
1
1
  import { api } from "#core/_services/api/axios.factory";
2
2
  import type { ListQuery } from "#core/_services/api/list-query";
3
3
  import { toListParams } from "#core/_services/api/list-query";
4
+ import type { components } from "#core/_services/api/schema";
4
5
 
5
- export type ErrorType =
6
- | "VALIDATION"
7
- | "AUTH"
8
- | "NOT_FOUND"
9
- | "RATE_LIMIT"
10
- | "DATABASE"
11
- | "INTEGRATION"
12
- | "INTERNAL";
13
-
14
- export interface LogUserRef {
15
- id: string;
16
- name: string;
17
- email: string;
18
- }
19
-
20
- export interface RequestLog {
21
- id: string;
22
- createdAt: string;
23
- userId?: string | null;
24
- action: string;
25
- method?: string | null;
26
- path?: string | null;
27
- ipAddress?: string | null;
28
- statusCode?: number | null;
29
- durationMs?: number | null;
30
- requestId?: string | null;
31
- user?: LogUserRef | null;
32
- }
33
-
34
- export interface ErrorLog {
35
- id: string;
36
- createdAt: string;
37
- statusCode?: number | null;
38
- type: ErrorType;
39
- message: string;
40
- stack?: string | null;
41
- method?: string | null;
42
- path?: string | null;
43
- userId?: string | null;
44
- ipAddress?: string | null;
45
- requestId?: string | null;
46
- user?: LogUserRef | null;
47
- }
6
+ /** Tipos derivados do `openapi.json` (§5) — não redigitar a forma da resposta. */
7
+ export type RequestLog = components["schemas"]["RequestLogResponse"];
8
+ export type LogUserRef = components["schemas"]["AuditUserRefResponse"];
48
9
 
49
10
  export interface Paginated<T> {
50
11
  items: T[];
51
- meta: { total: number; page: number; limit: number; totalPages: number };
12
+ meta: components["schemas"]["PaginationMetaResponse"];
52
13
  }
53
14
 
54
15
  export const logsService = {
16
+ /**
17
+ * Log de requisição — uma rota só.
18
+ *
19
+ * Antes eram `/audit/changes` e `/audit/errors`, duas tabelas com nove
20
+ * colunas iguais. Agora é uma linha por requisição, e a distinção entre
21
+ * sucesso e erro é o filtro `outcome`.
22
+ */
55
23
  listRequests(query: ListQuery) {
56
24
  return api
57
- .get<Paginated<RequestLog>>("/audit/changes", {
25
+ .get<Paginated<RequestLog>>("/audit/requests", {
58
26
  params: toListParams(query),
59
27
  })
60
28
  .then((r) => r.data);
61
29
  },
62
-
63
- listErrors(query: ListQuery) {
64
- return api
65
- .get<Paginated<ErrorLog>>("/audit/errors", { params: toListParams(query) })
66
- .then((r) => r.data);
67
- },
68
30
  };
@@ -5,17 +5,19 @@ import type { JSX } from "react";
5
5
 
6
6
  import { formatDateTime } from "#core/_utils/format";
7
7
  import { useI18n } from "#core/contexts";
8
- import type {
9
- NotificationType,
10
- UserNotification,
11
- } from "#core/features/notifications/services/notifications.service";
8
+ import { NotificationType } from "#core/features/notifications/enums/notification-type.enum";
9
+ import type { UserNotification } from "#core/features/notifications/services/notifications.service";
12
10
  import { cn } from "#core/lib/utils";
13
11
 
14
12
  const ICONS: Record<NotificationType, JSX.Element> = {
15
- INFO: <Info className="h-4 w-4 text-sky-500" />,
16
- SUCCESS: <CheckCircle2 className="h-4 w-4 text-emerald-500" />,
17
- WARNING: <AlertTriangle className="h-4 w-4 text-amber-500" />,
18
- ERROR: <XCircle className="h-4 w-4 text-destructive" />,
13
+ [NotificationType.INFO]: <Info className="h-4 w-4 text-sky-500" />,
14
+ [NotificationType.SUCCESS]: (
15
+ <CheckCircle2 className="h-4 w-4 text-emerald-500" />
16
+ ),
17
+ [NotificationType.WARNING]: (
18
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
19
+ ),
20
+ [NotificationType.ERROR]: <XCircle className="h-4 w-4 text-destructive" />,
19
21
  };
20
22
 
21
23
  export interface NotificationItemProps {
@@ -0,0 +1,8 @@
1
+ // Severidade da notificação (espelha o NotificationType do backend:
2
+ // notifications.type) — define ícone e cor.
3
+ export enum NotificationType {
4
+ INFO = 0,
5
+ SUCCESS = 1,
6
+ WARNING = 2,
7
+ ERROR = 3,
8
+ }
@@ -339,7 +339,8 @@ export function ProfileScreen(): JSX.Element {
339
339
 
340
340
  {/* diálogo: pede código TOTP para regenerar backup codes */}
341
341
  <Dialog open={regenOpen} onOpenChange={(o) => !o && setRegenOpen(false)}>
342
- <DialogContent className="max-w-sm">
342
+ {/* Confirmação: um campo de seis dígitos não tem o que ampliar. */}
343
+ <DialogContent className="max-w-sm" expandable={false}>
343
344
  <DialogHeader>
344
345
  <DialogTitle>{t("auth.backupTitle")}</DialogTitle>
345
346
  </DialogHeader>
@@ -27,6 +27,7 @@ import {
27
27
  SelectValue,
28
28
  } from "#core/components/ui";
29
29
  import { useAuth, useI18n } from "#core/contexts";
30
+ import { RecordAuditButton } from "#core/features/audit/components";
30
31
  import {
31
32
  Group,
32
33
  rbacService,
@@ -251,13 +252,25 @@ export function GroupsPanel(): JSX.Element {
251
252
  ))}
252
253
  </div>
253
254
  </div>
254
- <DialogFooter className="gap-2">
255
- <Button type="button" variant="outline" onClick={() => setOpen(false)}>
256
- {t("common.cancel")}
257
- </Button>
258
- <Button type="submit" disabled={loading}>
259
- {t("common.save")}
260
- </Button>
255
+ <DialogFooter className="gap-2 sm:justify-between">
256
+ {/* na edição: registro que ainda não existe não tem histórico. */}
257
+ <div>
258
+ {editing && (
259
+ <RecordAuditButton entity="Group" entityId={editing.id} />
260
+ )}
261
+ </div>
262
+ <div className="flex gap-2">
263
+ <Button
264
+ type="button"
265
+ variant="outline"
266
+ onClick={() => setOpen(false)}
267
+ >
268
+ {t("common.cancel")}
269
+ </Button>
270
+ <Button type="submit" disabled={loading}>
271
+ {t("common.save")}
272
+ </Button>
273
+ </div>
261
274
  </DialogFooter>
262
275
  </DialogForm>
263
276
  </DialogContent>
@@ -22,6 +22,7 @@ import {
22
22
  Label,
23
23
  } from "#core/components/ui";
24
24
  import { useI18n } from "#core/contexts";
25
+ import { RecordAuditButton } from "#core/features/audit/components";
25
26
  import {
26
27
  CreateUserPayload,
27
28
  Role,
@@ -172,13 +173,21 @@ export function UserFormDialog({
172
173
  />
173
174
  </div>
174
175
 
175
- <DialogFooter className="gap-2">
176
- <Button type="button" variant="outline" onClick={onClose}>
177
- {t("common.cancel")}
178
- </Button>
179
- <Button type="submit" disabled={loading}>
180
- {isEdit ? t("common.save") : t("common.create")}
181
- </Button>
176
+ <DialogFooter className="gap-2 sm:justify-between">
177
+ {/* na edição: registro que ainda não existe não tem histórico. */}
178
+ <div>
179
+ {editing && (
180
+ <RecordAuditButton entity="User" entityId={editing.id} />
181
+ )}
182
+ </div>
183
+ <div className="flex gap-2">
184
+ <Button type="button" variant="outline" onClick={onClose}>
185
+ {t("common.cancel")}
186
+ </Button>
187
+ <Button type="submit" disabled={loading}>
188
+ {isEdit ? t("common.save") : t("common.create")}
189
+ </Button>
190
+ </div>
182
191
  </DialogFooter>
183
192
  </DialogForm>
184
193
  </DialogContent>
@@ -68,9 +68,14 @@ export function useUsers(): UseUsersResult {
68
68
  }
69
69
  }, [run, list.query]);
70
70
 
71
+ // Não busca antes da URL ser lida: a query sairia sem o filtro, e essa
72
+ // resposta pode chegar depois da certa e pintar a tela com tudo.
71
73
  useEffect(() => {
74
+ if (!list.ready) {
75
+ return;
76
+ }
72
77
  void fetch();
73
- }, [fetch]);
78
+ }, [fetch, list.ready]);
74
79
 
75
80
  useEffect(() => {
76
81
  void run(() => usersService.listRoles()).then((r) => r && setRoles(r));
@@ -163,7 +168,9 @@ export function useUsers(): UseUsersResult {
163
168
  page: list.page,
164
169
  pageSize: list.pageSize,
165
170
  sorting: list.sorting,
166
- loading,
171
+ // Esperar o catálogo também é carregar: sem isso a tabela pisca
172
+ // "Nenhum registro" antes da primeira busca sair.
173
+ loading: loading || !list.ready,
167
174
  acting,
168
175
  setPage: list.setPage,
169
176
  setPageSize: list.setPageSize,
@@ -32,6 +32,15 @@ export interface FiltersState {
32
32
  validation: AdvancedValidation;
33
33
  /** JSON pronto para a query — `undefined` quando não há filtro. */
34
34
  filter: string | undefined;
35
+ /**
36
+ * A URL já foi lida e o `filter` abaixo é o definitivo.
37
+ *
38
+ * Antes disso `filter` é `undefined` porque ninguém leu a querystring ainda —
39
+ * e não porque a listagem esteja sem filtro. Quem busca precisa esperar, ou
40
+ * dispara uma consulta sem filtro cuja resposta pode chegar depois da certa e
41
+ * pintar a tela com tudo.
42
+ */
43
+ hydrated: boolean;
35
44
  }
36
45
 
37
46
  const QUERY_PARAM = "filter";
@@ -57,8 +66,16 @@ const writeToUrl = (encoded: string | undefined): void => {
57
66
  * Espelhar na URL faz o filtro sobreviver ao F5 e virar link compartilhável.
58
67
  * O espelho é `replaceState` e não `router.replace`: o filtro não é navegação,
59
68
  * e trocá-lo não deve empilhar histórico nem refazer o render da rota.
69
+ *
70
+ * `defaultValues` é o filtro de quem chega sem `?filter=` na URL — a listagem
71
+ * abre já filtrada. Vale só na hidratação: link com filtro manda, e limpar os
72
+ * filtros continua limpando (o padrão volta no F5 seguinte, que é o que
73
+ * "padrão" quer dizer).
60
74
  */
61
- export function useFilters(schemaUrl: string): FiltersState {
75
+ export function useFilters(
76
+ schemaUrl: string,
77
+ defaultValues?: FilterValues,
78
+ ): FiltersState {
62
79
  const { schema, loading } = useFilterSchema(schemaUrl);
63
80
 
64
81
  const [mode, setModeState] = useState<FilterMode>("simple");
@@ -82,9 +99,14 @@ export function useFilters(schemaUrl: string): FiltersState {
82
99
  }
83
100
  } else if (raw) {
84
101
  setValuesState(parseFilterValues(raw, schema));
102
+ } else if (defaultValues) {
103
+ // Espelha na URL como qualquer outro filtro: sem isso a tela mostraria
104
+ // um estado que o link não carrega.
105
+ setValuesState(defaultValues);
106
+ writeToUrl(buildFilter(defaultValues, schema));
85
107
  }
86
108
  setHydrated(true);
87
- }, [hydrated, loading, schema]);
109
+ }, [hydrated, loading, schema, defaultValues]);
88
110
 
89
111
  const setValues = useCallback(
90
112
  (next: FilterValues): void => {
@@ -144,5 +166,6 @@ export function useFilters(schemaUrl: string): FiltersState {
144
166
  setTree,
145
167
  validation,
146
168
  filter,
169
+ hydrated,
147
170
  };
148
171
  }
@@ -5,12 +5,22 @@ import type { Dispatch, SetStateAction } from "react";
5
5
  import { useEffect, useMemo, useState } from "react";
6
6
 
7
7
  import type { ListQuery } from "#core/_services/api/list-query";
8
+ import type { FilterValues } from "#core/_utils/filter";
8
9
  import type { FiltersState } from "#core/hooks/use-filters";
9
10
  import { useFilters } from "#core/hooks/use-filters";
10
11
 
11
12
  export interface UseListQueryResult {
12
13
  /** Pronta para o `.service.ts` da feature. */
13
14
  query: ListQuery;
15
+ /**
16
+ * A `query` já vale — **o hook da feature só pode buscar depois disso**.
17
+ *
18
+ * Enquanto é `false`, o filtro da URL ainda não foi lido e a `query` sairia
19
+ * sem ele. Buscar assim mesmo faz duas requisições por carregamento, e a
20
+ * errada pode chegar por último: a tela abre mostrando tudo com o filtro
21
+ * marcado na barra.
22
+ */
23
+ ready: boolean;
14
24
  page: number;
15
25
  pageSize: number;
16
26
  sorting: SortingState;
@@ -30,8 +40,10 @@ export interface UseListQueryResult {
30
40
  export function useListQuery(
31
41
  schemaUrl: string,
32
42
  initialPageSize = 10,
43
+ /** Filtro de quem chega sem `?filter=` — a listagem abre já filtrada. */
44
+ defaultFilterValues?: FilterValues,
33
45
  ): UseListQueryResult {
34
- const filters = useFilters(schemaUrl);
46
+ const filters = useFilters(schemaUrl, defaultFilterValues);
35
47
  const { filter } = filters;
36
48
 
37
49
  const [page, setPage] = useState(0);
@@ -58,6 +70,7 @@ export function useListQuery(
58
70
 
59
71
  return {
60
72
  query,
73
+ ready: filters.hydrated,
61
74
  page,
62
75
  pageSize,
63
76
  sorting,
@@ -13,11 +13,20 @@ export const en: Messages = {
13
13
  audit: "Audit",
14
14
  },
15
15
  logs: {
16
+ outcomeSuccess: "Success",
17
+ outcomeError: "Error",
18
+ outcomeAborted: "Aborted",
19
+ errorValidation: "Validation",
20
+ errorAuth: "Authentication",
21
+ errorNotFound: "Not found",
22
+ errorRateLimit: "Rate limit",
23
+ errorDatabase: "Database",
24
+ errorIntegration: "Integration",
25
+ errorInternal: "Internal",
26
+ outcome: "Outcome",
16
27
  title: "Logs",
17
28
  subtitle:
18
- "Every API request success and error, route, status and duration.",
19
- tabRequests: "Requests",
20
- tabErrors: "Errors",
29
+ "Errors, slow requests and auditable actions route, status and duration.",
21
30
  date: "Date/time",
22
31
  route: "Route",
23
32
  status: "Status",
@@ -29,10 +38,14 @@ export const en: Messages = {
29
38
  type: "Type",
30
39
  message: "Message",
31
40
  viewDetails: "View details",
32
- detailsTitle: "Error details",
41
+ detailsTitle: "Request details",
42
+ errorCode: "Error code",
43
+ userAgent: "User agent",
33
44
  stackTrace: "Stack trace",
34
45
  },
35
46
  auditTrail: {
47
+ recordHistory: "Change history",
48
+ noRecordHistory: "This record has no changes yet.",
36
49
  title: "Audit Trail",
37
50
  subtitle: "Data change history — who changed what, and when.",
38
51
  entity: "Entity",
@@ -87,7 +100,6 @@ export const en: Messages = {
87
100
  hint: "Fill in only the fields you want to use. All of them are combined with AND.",
88
101
  apply: "Apply",
89
102
  clear: "Clear",
90
- clearAll: "Clear all",
91
103
  empty: "This listing has no filters yet.",
92
104
  noOptions: "No options available",
93
105
  all: "All",
@@ -11,10 +11,20 @@ export const pt = {
11
11
  audit: "Auditoria",
12
12
  },
13
13
  logs: {
14
+ outcomeSuccess: "Sucesso",
15
+ outcomeError: "Erro",
16
+ outcomeAborted: "Abortada",
17
+ errorValidation: "Validação",
18
+ errorAuth: "Autenticação",
19
+ errorNotFound: "Não encontrado",
20
+ errorRateLimit: "Limite de requisições",
21
+ errorDatabase: "Banco de dados",
22
+ errorIntegration: "Integração",
23
+ errorInternal: "Interno",
24
+ outcome: "Desfecho",
14
25
  title: "Logs",
15
- subtitle: "Toda requisição à API — sucesso e erro, rota, status e duração.",
16
- tabRequests: "Requisições",
17
- tabErrors: "Erros",
26
+ subtitle:
27
+ "Erros, requisições lentas e ações auditáveis — rota, status e duração.",
18
28
  date: "Data/hora",
19
29
  route: "Rota",
20
30
  status: "Status",
@@ -26,10 +36,14 @@ export const pt = {
26
36
  type: "Tipo",
27
37
  message: "Mensagem",
28
38
  viewDetails: "Ver detalhes",
29
- detailsTitle: "Detalhes do erro",
39
+ detailsTitle: "Detalhes da requisição",
40
+ errorCode: "Código do erro",
41
+ userAgent: "User agent",
30
42
  stackTrace: "Stack trace",
31
43
  },
32
44
  auditTrail: {
45
+ recordHistory: "Histórico de alterações",
46
+ noRecordHistory: "Este registro ainda não sofreu alterações.",
33
47
  title: "Trilha de Auditoria",
34
48
  subtitle: "Histórico de alterações de dados — quem mudou o quê, e quando.",
35
49
  entity: "Entidade",
@@ -88,7 +102,6 @@ export const pt = {
88
102
  hint: "Preencha só os campos que quiser usar. Tudo é combinado com E.",
89
103
  apply: "Aplicar",
90
104
  clear: "Limpar",
91
- clearAll: "Limpar tudo",
92
105
  empty: "Esta listagem ainda não tem filtros.",
93
106
  noOptions: "Nenhuma opção disponível",
94
107
  all: "Todos",
package/src/index.ts CHANGED
@@ -26,6 +26,15 @@ export { UsersScreen } from "#core/features/users/users-screen";
26
26
  // ---------------------------------------------------------------------------
27
27
  export * from "#core/components";
28
28
  export * from "#core/contexts";
29
+ // Histórico de alterações de um registro, para a tela de edição do projeto.
30
+ export type {
31
+ AuditDiffProps,
32
+ RecordAuditButtonProps,
33
+ } from "#core/features/audit/components";
34
+ export {
35
+ AuditDiff,
36
+ RecordAuditButton,
37
+ } from "#core/features/audit/components";
29
38
 
30
39
  // ---------------------------------------------------------------------------
31
40
  // Primitivos de UI — agnósticos de domínio
@@ -47,7 +56,9 @@ export { useFilters } from "#core/hooks/use-filters";
47
56
  export type { UseListQueryResult } from "#core/hooks/use-list-query";
48
57
  export { useListQuery } from "#core/hooks/use-list-query";
49
58
  export type { RunOptions, UseRequestResult } from "#core/hooks/use-request";
50
- export { useRequest } from "#core/hooks/use-request";
59
+ // RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue
60
+ // pedir o toast padrão de criado/alterado/removido que o core já traduz.
61
+ export { RequestOperation, useRequest } from "#core/hooks/use-request";
51
62
 
52
63
  // ---------------------------------------------------------------------------
53
64
  // HTTP e traduções
@@ -1,88 +0,0 @@
1
- "use client";
2
-
3
- import { SlidersHorizontal, X } from "lucide-react";
4
- import type { JSX } from "react";
5
-
6
- import { countConditions } from "#core/_utils/advanced-filter";
7
- import { describeValue, hasValue } from "#core/_utils/filter";
8
- import { Button } from "#core/components/ui/button";
9
- import { useFieldLabel } from "#core/components/ui/filter-sheet";
10
- import { useI18n } from "#core/contexts";
11
- import type { FiltersState } from "#core/hooks/use-filters";
12
-
13
- export interface FilterChipsProps {
14
- filters: FiltersState;
15
- }
16
-
17
- /**
18
- * Filtros ativos em forma de chip, cada um removível.
19
- *
20
- * Existe para o usuário não precisar abrir o painel só para lembrar o que está
21
- * filtrando — e para não olhar uma tabela vazia sem saber por quê. No modo
22
- * avançado a expressão não cabe num chip por campo, então vira um resumo com a
23
- * contagem de condições.
24
- */
25
- export function FilterChips({ filters }: FilterChipsProps): JSX.Element | null {
26
- const { t, locale } = useI18n();
27
- const labelFor = useFieldLabel();
28
- const { schema, values, mode, tree } = filters;
29
-
30
- if (mode === "advanced") {
31
- const total = tree ? countConditions(tree) : 0;
32
- if (total === 0 || !filters.filter) {
33
- return null;
34
- }
35
- return (
36
- <div className="mb-3 flex flex-wrap items-center gap-2">
37
- <span className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs">
38
- <SlidersHorizontal className="h-3 w-3" />
39
- {t("filters.advancedSummary", { count: total })}
40
- </span>
41
- <Button variant="ghost" size="sm" onClick={() => filters.setMode("simple")}>
42
- {t("filters.clearAll")}
43
- </Button>
44
- </div>
45
- );
46
- }
47
-
48
- const active = schema.filter((field) => hasValue(values[field.field]));
49
- if (active.length === 0) {
50
- return null;
51
- }
52
-
53
- const remove = (field: string): void => {
54
- const next = { ...values };
55
- delete next[field];
56
- filters.setValues(next);
57
- };
58
-
59
- const labels = {
60
- from: t("filters.fromChip"),
61
- to: t("filters.toChip"),
62
- yes: t("common.yes"),
63
- no: t("common.no"),
64
- locale,
65
- };
66
-
67
- return (
68
- <div className="mb-3 flex flex-wrap items-center gap-2">
69
- {active.map((field) => (
70
- <button
71
- key={field.field}
72
- type="button"
73
- onClick={() => remove(field.field)}
74
- className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs transition-colors hover:bg-muted"
75
- >
76
- <span className="text-muted-foreground">{labelFor(field)}:</span>
77
- <span className="font-medium">
78
- {describeValue(field, values[field.field], labels)}
79
- </span>
80
- <X className="h-3 w-3" />
81
- </button>
82
- ))}
83
- <Button variant="ghost" size="sm" onClick={() => filters.setValues({})}>
84
- {t("filters.clearAll")}
85
- </Button>
86
- </div>
87
- );
88
- }
@@ -1,65 +0,0 @@
1
- "use client";
2
-
3
- import type { SortingState } from "@tanstack/react-table";
4
- import {
5
- Dispatch,
6
- SetStateAction,
7
- useCallback,
8
- useEffect,
9
- useState,
10
- } from "react";
11
-
12
- import { ErrorLog, logsService } from "#core/features/logs/services/logs.service";
13
- import type { FiltersState } from "#core/hooks/use-filters";
14
- import { useListQuery } from "#core/hooks/use-list-query";
15
- import { useRequest } from "#core/hooks/use-request";
16
-
17
- export interface UseErrorLogsResult {
18
- rows: ErrorLog[];
19
- total: number;
20
- page: number;
21
- pageSize: number;
22
- sorting: SortingState;
23
- loading: boolean;
24
- setPage: Dispatch<SetStateAction<number>>;
25
- setPageSize: Dispatch<SetStateAction<number>>;
26
- setSorting: Dispatch<SetStateAction<SortingState>>;
27
- /** Filtros dinâmicos — repassar para o `DataTable`. */
28
- filters: FiltersState;
29
- refresh: () => Promise<void>;
30
- }
31
-
32
- export function useErrorLogs(): UseErrorLogsResult {
33
- const { run, loading } = useRequest();
34
-
35
- const list = useListQuery("/audit/errors/filter-schema", 20);
36
-
37
- const [rows, setRows] = useState<ErrorLog[]>([]);
38
- const [total, setTotal] = useState(0);
39
-
40
- const fetch = useCallback(async () => {
41
- const res = await run(() => logsService.listErrors(list.query));
42
- if (res) {
43
- setRows(res.items);
44
- setTotal(res.meta.total);
45
- }
46
- }, [run, list.query]);
47
-
48
- useEffect(() => {
49
- void fetch();
50
- }, [fetch]);
51
-
52
- return {
53
- rows,
54
- total,
55
- page: list.page,
56
- pageSize: list.pageSize,
57
- sorting: list.sorting,
58
- loading,
59
- setPage: list.setPage,
60
- setPageSize: list.setPageSize,
61
- setSorting: list.setSorting,
62
- filters: list.filters,
63
- refresh: fetch,
64
- };
65
- }