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
@@ -10,11 +10,10 @@ import { Eye } from "lucide-react";
10
10
  import type { JSX } from "react";
11
11
  import { useMemo, useState } from "react";
12
12
 
13
- import { formatDateTime, isIsoDateTime } from "#core/_utils/format";
13
+ import { formatDateTime } from "#core/_utils/format";
14
14
  import {
15
15
  Badge,
16
16
  Button,
17
- Checkbox,
18
17
  DataTable,
19
18
  DataTableFeatures,
20
19
  Dialog,
@@ -22,88 +21,21 @@ import {
22
21
  DialogHeader,
23
22
  DialogTitle,
24
23
  RowActions,
25
- Table,
26
- TableBody,
27
- TableCell,
28
- TableHead,
29
- TableHeader,
30
- TableRow,
31
24
  } from "#core/components/ui";
32
25
  import { useI18n } from "#core/contexts";
33
- import { useAuditTrail } from "#core/features/audit/hooks/use-audit-trail";
26
+ import { AuditDiff } from "#core/features/audit/components/audit-diff";
34
27
  import {
35
- AuditAction,
36
- AuditDataChange,
37
- } from "#core/features/audit/services/audit.service";
38
-
39
- /**
40
- * Campos de controle interno: aparecem no diff porque a auditoria versiona a
41
- * linha inteira, mas não são informação de negócio. Ficam escondidos por padrão.
42
- */
43
- const TECHNICAL_FIELDS = new Set([
44
- "failedLoginAttempts",
45
- "createdAt",
46
- "updatedAt",
47
- "deletedAt",
48
- "passwordChangedAt",
49
- "password",
50
- "twoFactorSecret",
51
- "backupCodes",
52
- "mustChangePassword",
53
- ]);
54
-
55
- interface DiffRow {
56
- field: string;
57
- before: string;
58
- after: string;
59
- technical: boolean;
60
- }
61
-
62
- type Translate = (key: string) => string;
63
-
64
- function formatValue(
65
- value: unknown,
66
- locale: string,
67
- t: Translate,
68
- ): string {
69
- if (value === null || value === undefined) {
70
- return "—";
71
- }
72
- if (typeof value === "boolean") {
73
- return value ? t("common.yes") : t("common.no");
74
- }
75
- if (isIsoDateTime(value)) {
76
- return formatDateTime(value, locale);
77
- }
78
- if (typeof value === "object") {
79
- // Indentado: objeto e array em uma linha só ficavam ilegíveis no modal.
80
- return JSON.stringify(value, null, 2);
81
- }
82
- return String(value);
83
- }
84
-
85
- function diffRows(
86
- item: AuditDataChange,
87
- locale: string,
88
- t: Translate,
89
- ): DiffRow[] {
90
- const keys = new Set([
91
- ...Object.keys(item.oldData ?? {}),
92
- ...Object.keys(item.newData ?? {}),
93
- ]);
94
- return Array.from(keys).map((field) => ({
95
- field,
96
- before: formatValue(item.oldData?.[field], locale, t),
97
- after: formatValue(item.newData?.[field], locale, t),
98
- technical: TECHNICAL_FIELDS.has(field),
99
- }));
100
- }
28
+ AuditDataAction,
29
+ AuditDataActionLabelKey,
30
+ } from "#core/features/audit/enums/audit-data-action.enum";
31
+ import { useAuditTrail } from "#core/features/audit/hooks/use-audit-trail";
32
+ import { AuditDataChange } from "#core/features/audit/services/audit.service";
101
33
 
102
34
  function actionVariant(
103
- action: AuditAction,
35
+ action: AuditDataAction,
104
36
  ): "success" | "warning" | "destructive" {
105
- if (action === "CREATE") {return "success";}
106
- if (action === "DELETE") {return "destructive";}
37
+ if (action === AuditDataAction.CREATE) {return "success";}
38
+ if (action === AuditDataAction.DELETE) {return "destructive";}
107
39
  return "warning";
108
40
  }
109
41
 
@@ -111,21 +43,9 @@ export function AuditScreen(): JSX.Element {
111
43
  const { t, locale } = useI18n();
112
44
  const trail = useAuditTrail();
113
45
  const [detail, setDetail] = useState<AuditDataChange | null>(null);
114
- const [showTechnical, setShowTechnical] = useState(false);
115
46
 
116
- /** Rótulo do campo; sem tradução cadastrada, mostra o nome cru da coluna. */
117
- const fieldLabel = (field: string): string => {
118
- const key = `auditFields.${field}`;
119
- const label = t(key);
120
- return label === key ? field : label;
121
- };
122
-
123
- const actionLabel = (action: AuditAction): string =>
124
- action === "CREATE"
125
- ? t("auditTrail.actionCreate")
126
- : action === "DELETE"
127
- ? t("auditTrail.actionDelete")
128
- : t("auditTrail.actionUpdate");
47
+ const actionLabel = (action: AuditDataAction): string =>
48
+ t(AuditDataActionLabelKey[action]);
129
49
 
130
50
  const columns = useMemo<
131
51
  ColumnDef<DataTableFeatures, AuditDataChange, unknown>[]
@@ -193,10 +113,6 @@ export function AuditScreen(): JSX.Element {
193
113
  [t, locale],
194
114
  );
195
115
 
196
- const allRows = detail ? diffRows(detail, locale, t) : [];
197
- const rows = showTechnical ? allRows : allRows.filter((r) => !r.technical);
198
- const hiddenCount = allRows.length - rows.length;
199
-
200
116
  const pagination: PaginationState = {
201
117
  pageIndex: trail.page,
202
118
  pageSize: trail.pageSize,
@@ -248,50 +164,7 @@ export function AuditScreen(): JSX.Element {
248
164
  {detail && actionLabel(detail.action)})
249
165
  </DialogTitle>
250
166
  </DialogHeader>
251
- {detail && (
252
- <div className="max-h-[60vh] overflow-y-auto">
253
- {rows.length === 0 ? (
254
- <p className="text-sm text-muted-foreground">
255
- {t("auditTrail.noFields")}
256
- </p>
257
- ) : (
258
- <Table>
259
- <TableHeader>
260
- <TableRow>
261
- <TableHead>{t("auditTrail.field")}</TableHead>
262
- <TableHead>{t("auditTrail.before")}</TableHead>
263
- <TableHead>{t("auditTrail.after")}</TableHead>
264
- </TableRow>
265
- </TableHeader>
266
- <TableBody>
267
- {rows.map((d) => (
268
- <TableRow key={d.field}>
269
- <TableCell className="font-medium">
270
- {fieldLabel(d.field)}
271
- </TableCell>
272
- <TableCell className="whitespace-pre-wrap text-destructive/90">
273
- {d.before}
274
- </TableCell>
275
- <TableCell className="whitespace-pre-wrap text-success">
276
- {d.after}
277
- </TableCell>
278
- </TableRow>
279
- ))}
280
- </TableBody>
281
- </Table>
282
- )}
283
- </div>
284
- )}
285
- {detail && (hiddenCount > 0 || showTechnical) && (
286
- <label className="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
287
- <Checkbox
288
- checked={showTechnical}
289
- onCheckedChange={(checked) => setShowTechnical(!!checked)}
290
- />
291
- {t("auditTrail.showTechnical")}
292
- {hiddenCount > 0 && ` (${hiddenCount})`}
293
- </label>
294
- )}
167
+ {detail && <AuditDiff change={detail} />}
295
168
  </DialogContent>
296
169
  </Dialog>
297
170
  </div>
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+ import { useMemo, useState } from "react";
5
+
6
+ import { formatDateTime, isIsoDateTime } from "#core/_utils/format";
7
+ import {
8
+ Checkbox,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ } from "#core/components/ui";
16
+ import { useI18n } from "#core/contexts";
17
+ import { AuditDataChange } from "#core/features/audit/services/audit.service";
18
+
19
+ /**
20
+ * Campos de controle interno: aparecem no diff porque a auditoria versiona a
21
+ * linha inteira, mas não são informação de negócio. Ficam escondidos por padrão.
22
+ */
23
+ const TECHNICAL_FIELDS = new Set([
24
+ "failedLoginAttempts",
25
+ "createdAt",
26
+ "updatedAt",
27
+ "deletedAt",
28
+ "passwordChangedAt",
29
+ "password",
30
+ "twoFactorSecret",
31
+ "backupCodes",
32
+ "mustChangePassword",
33
+ ]);
34
+
35
+ interface DiffRow {
36
+ field: string;
37
+ before: string;
38
+ after: string;
39
+ technical: boolean;
40
+ }
41
+
42
+ type Translate = (key: string) => string;
43
+
44
+ function formatValue(value: unknown, locale: string, t: Translate): string {
45
+ if (value === null || value === undefined) {
46
+ return "—";
47
+ }
48
+ if (typeof value === "boolean") {
49
+ return value ? t("common.yes") : t("common.no");
50
+ }
51
+ if (isIsoDateTime(value)) {
52
+ return formatDateTime(value, locale);
53
+ }
54
+ if (typeof value === "object") {
55
+ // Indentado: objeto e array em uma linha só ficavam ilegíveis no modal.
56
+ return JSON.stringify(value, null, 2);
57
+ }
58
+ return String(value);
59
+ }
60
+
61
+ function diffRows(
62
+ item: AuditDataChange,
63
+ locale: string,
64
+ t: Translate,
65
+ ): DiffRow[] {
66
+ const keys = new Set([
67
+ ...Object.keys(item.oldData ?? {}),
68
+ ...Object.keys(item.newData ?? {}),
69
+ ]);
70
+ return Array.from(keys).map((field) => ({
71
+ field,
72
+ before: formatValue(item.oldData?.[field], locale, t),
73
+ after: formatValue(item.newData?.[field], locale, t),
74
+ technical: TECHNICAL_FIELDS.has(field),
75
+ }));
76
+ }
77
+
78
+ export interface AuditDiffProps {
79
+ change: AuditDataChange;
80
+ }
81
+
82
+ /**
83
+ * Diff de uma alteração: campo, antes e depois.
84
+ *
85
+ * Mora aqui, e não na tela de auditoria, porque os dois lugares mostram o mesmo
86
+ * diff — a listagem geral e o histórico de um registro. Duas cópias divergiriam
87
+ * na primeira vez que alguém acrescentasse um campo técnico (§6).
88
+ */
89
+ export function AuditDiff({ change }: AuditDiffProps): JSX.Element {
90
+ const { t, locale } = useI18n();
91
+ const [showTechnical, setShowTechnical] = useState(false);
92
+
93
+ const allRows = useMemo(
94
+ () => diffRows(change, locale, t),
95
+ [change, locale, t],
96
+ );
97
+ const rows = showTechnical
98
+ ? allRows
99
+ : allRows.filter((row) => !row.technical);
100
+ const hiddenCount = allRows.length - rows.length;
101
+
102
+ /** Rótulo do campo; sem tradução cadastrada, mostra o nome cru da coluna. */
103
+ const fieldLabel = (field: string): string => {
104
+ const key = `auditFields.${field}`;
105
+ const label = t(key);
106
+ return label === key ? field : label;
107
+ };
108
+
109
+ return (
110
+ <>
111
+ <div className="max-h-[60vh] overflow-y-auto">
112
+ {rows.length === 0 ? (
113
+ <p className="text-sm text-muted-foreground">
114
+ {t("auditTrail.noFields")}
115
+ </p>
116
+ ) : (
117
+ <Table>
118
+ <TableHeader>
119
+ <TableRow>
120
+ <TableHead>{t("auditTrail.field")}</TableHead>
121
+ <TableHead>{t("auditTrail.before")}</TableHead>
122
+ <TableHead>{t("auditTrail.after")}</TableHead>
123
+ </TableRow>
124
+ </TableHeader>
125
+ <TableBody>
126
+ {rows.map((row) => (
127
+ <TableRow key={row.field}>
128
+ <TableCell className="font-medium">
129
+ {fieldLabel(row.field)}
130
+ </TableCell>
131
+ <TableCell className="whitespace-pre-wrap text-destructive/90">
132
+ {row.before}
133
+ </TableCell>
134
+ <TableCell className="whitespace-pre-wrap text-success">
135
+ {row.after}
136
+ </TableCell>
137
+ </TableRow>
138
+ ))}
139
+ </TableBody>
140
+ </Table>
141
+ )}
142
+ </div>
143
+
144
+ {(hiddenCount > 0 || showTechnical) && (
145
+ <label className="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
146
+ <Checkbox
147
+ checked={showTechnical}
148
+ onCheckedChange={(checked) => setShowTechnical(!!checked)}
149
+ />
150
+ {t("auditTrail.showTechnical")}
151
+ {hiddenCount > 0 && ` (${hiddenCount})`}
152
+ </label>
153
+ )}
154
+ </>
155
+ );
156
+ }
@@ -0,0 +1,3 @@
1
+ // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
+ export * from "./audit-diff";
3
+ export * from "./record-audit-button";
@@ -0,0 +1,176 @@
1
+ "use client";
2
+
3
+ import { Eye, History } from "lucide-react";
4
+ import type { JSX } from "react";
5
+ import { useCallback, useEffect, useState } from "react";
6
+
7
+ import { formatDateTime } from "#core/_utils/format";
8
+ import {
9
+ Badge,
10
+ Button,
11
+ Dialog,
12
+ DialogContent,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ Spinner,
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableHead,
20
+ TableHeader,
21
+ TableRow,
22
+ } from "#core/components/ui";
23
+ import { useAuth, useI18n } from "#core/contexts";
24
+ import { AuditDiff } from "#core/features/audit/components/audit-diff";
25
+ import {
26
+ AuditDataAction,
27
+ AuditDataActionLabelKey,
28
+ } from "#core/features/audit/enums/audit-data-action.enum";
29
+ import {
30
+ AuditDataChange,
31
+ auditService,
32
+ } from "#core/features/audit/services/audit.service";
33
+ import { useRequest } from "#core/hooks/use-request";
34
+
35
+ /** Mesma permissão da tela de auditoria: ver a trilha é ler dado de todos. */
36
+ const TRAIL_PERMISSION = "audit:read-trail:any";
37
+
38
+ function actionVariant(
39
+ action: AuditDataAction,
40
+ ): "success" | "warning" | "destructive" {
41
+ if (action === AuditDataAction.CREATE) {return "success";}
42
+ if (action === AuditDataAction.DELETE) {return "destructive";}
43
+ return "warning";
44
+ }
45
+
46
+ export interface RecordAuditButtonProps {
47
+ /**
48
+ * Nome da **classe** da entidade, como o backend a grava: `"User"`, `"Role"`,
49
+ * `"Group"` — não o nome da tabela.
50
+ */
51
+ entity: string;
52
+ entityId: string;
53
+ /** Texto do botão. Ausente, usa "Histórico". */
54
+ label?: string;
55
+ }
56
+
57
+ /**
58
+ * Histórico de alterações de um registro, onde ele é editado.
59
+ *
60
+ * Consulta a mesma trilha da tela de auditoria, filtrada por `entity` +
61
+ * `entityId`. Some por completo para quem não tem permissão de ler a trilha —
62
+ * botão que só entrega 403 é pior que botão nenhum.
63
+ *
64
+ * ```tsx
65
+ * <RecordAuditButton entity="User" entityId={user.id} />
66
+ * ```
67
+ */
68
+ export function RecordAuditButton({
69
+ entity,
70
+ entityId,
71
+ label,
72
+ }: RecordAuditButtonProps): JSX.Element | null {
73
+ const { t, locale } = useI18n();
74
+ const { hasPermission } = useAuth();
75
+ const { run, loading } = useRequest();
76
+
77
+ const [open, setOpen] = useState(false);
78
+ const [items, setItems] = useState<AuditDataChange[]>([]);
79
+ const [detail, setDetail] = useState<AuditDataChange | null>(null);
80
+
81
+ const fetch = useCallback(async () => {
82
+ const res = await run(() => auditService.listByRecord(entity, entityId));
83
+ if (res) {
84
+ setItems(res.items);
85
+ }
86
+ }, [run, entity, entityId]);
87
+
88
+ // Busca só ao abrir: a tela de edição não paga a consulta de quem nunca
89
+ // clica no histórico.
90
+ useEffect(() => {
91
+ if (open) {
92
+ void fetch();
93
+ }
94
+ }, [open, fetch]);
95
+
96
+ if (!hasPermission(TRAIL_PERMISSION)) {
97
+ return null;
98
+ }
99
+
100
+ return (
101
+ <>
102
+ <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
103
+ <History className="h-4 w-4" />
104
+ {label ?? t("auditTrail.recordHistory")}
105
+ </Button>
106
+
107
+ <Dialog open={open} onOpenChange={setOpen}>
108
+ <DialogContent className="max-w-3xl">
109
+ <DialogHeader>
110
+ <DialogTitle>{t("auditTrail.recordHistory")}</DialogTitle>
111
+ </DialogHeader>
112
+
113
+ {loading ? (
114
+ <div className="flex justify-center py-8">
115
+ <Spinner className="h-6 w-6" />
116
+ </div>
117
+ ) : items.length === 0 ? (
118
+ <p className="py-4 text-sm text-muted-foreground">
119
+ {t("auditTrail.noRecordHistory")}
120
+ </p>
121
+ ) : (
122
+ <div className="max-h-[60vh] overflow-y-auto">
123
+ <Table>
124
+ <TableHeader>
125
+ <TableRow>
126
+ <TableHead>{t("auditTrail.action")}</TableHead>
127
+ <TableHead>{t("auditTrail.user")}</TableHead>
128
+ <TableHead>{t("auditTrail.date")}</TableHead>
129
+ <TableHead />
130
+ </TableRow>
131
+ </TableHeader>
132
+ <TableBody>
133
+ {items.map((item) => (
134
+ <TableRow key={item.id}>
135
+ <TableCell>
136
+ <Badge variant={actionVariant(item.action)}>
137
+ {t(AuditDataActionLabelKey[item.action])}
138
+ </Badge>
139
+ </TableCell>
140
+ <TableCell>
141
+ {item.user?.name ?? t("auditTrail.systemUser")}
142
+ </TableCell>
143
+ <TableCell>{formatDateTime(item.createdAt, locale)}</TableCell>
144
+ <TableCell className="text-right">
145
+ <Button
146
+ variant="ghost"
147
+ size="sm"
148
+ onClick={() => setDetail(item)}
149
+ >
150
+ <Eye className="h-4 w-4" />
151
+ {t("auditTrail.viewChanges")}
152
+ </Button>
153
+ </TableCell>
154
+ </TableRow>
155
+ ))}
156
+ </TableBody>
157
+ </Table>
158
+ </div>
159
+ )}
160
+ </DialogContent>
161
+ </Dialog>
162
+
163
+ <Dialog open={!!detail} onOpenChange={(o) => !o && setDetail(null)}>
164
+ <DialogContent className="max-w-2xl">
165
+ <DialogHeader>
166
+ <DialogTitle>
167
+ {t("auditTrail.changesTitle")} — {entity} (
168
+ {detail && t(AuditDataActionLabelKey[detail.action])})
169
+ </DialogTitle>
170
+ </DialogHeader>
171
+ {detail && <AuditDiff change={detail} />}
172
+ </DialogContent>
173
+ </Dialog>
174
+ </>
175
+ );
176
+ }
@@ -0,0 +1,20 @@
1
+ // O que aconteceu com o registro (espelha o AuditDataAction do backend:
2
+ // audit_data_changes.action).
3
+ export enum AuditDataAction {
4
+ CREATE = 0,
5
+ UPDATE = 1,
6
+ DELETE = 2,
7
+ }
8
+
9
+ /**
10
+ * Chave de i18n de cada valor.
11
+ *
12
+ * Aponta a chave e não o texto porque o core é bilíngue (§6): o rótulo mora em
13
+ * `i18n/messages`, e escrevê-lo aqui criaria uma segunda fonte do mesmo texto —
14
+ * que sairia em português também no inglês.
15
+ */
16
+ export const AuditDataActionLabelKey: Record<AuditDataAction, string> = {
17
+ [AuditDataAction.CREATE]: "auditTrail.actionCreate",
18
+ [AuditDataAction.UPDATE]: "auditTrail.actionUpdate",
19
+ [AuditDataAction.DELETE]: "auditTrail.actionDelete",
20
+ };
@@ -48,9 +48,14 @@ export function useAuditTrail(): UseAuditTrailResult {
48
48
  }
49
49
  }, [run, list.query]);
50
50
 
51
+ // Não busca antes da URL ser lida: a query sairia sem o filtro, e essa
52
+ // resposta pode chegar depois da certa e pintar a tela com tudo.
51
53
  useEffect(() => {
54
+ if (!list.ready) {
55
+ return;
56
+ }
52
57
  void fetch();
53
- }, [fetch]);
58
+ }, [fetch, list.ready]);
54
59
 
55
60
  return {
56
61
  rows,
@@ -58,7 +63,9 @@ export function useAuditTrail(): UseAuditTrailResult {
58
63
  page: list.page,
59
64
  pageSize: list.pageSize,
60
65
  sorting: list.sorting,
61
- loading,
66
+ // Esperar o catálogo também é carregar: sem isso a tabela pisca
67
+ // "Nenhum registro" antes da primeira busca sair.
68
+ loading: loading || !list.ready,
62
69
  setPage: list.setPage,
63
70
  setPageSize: list.setPageSize,
64
71
  setSorting: list.setSorting,
@@ -4,16 +4,44 @@ import { toListParams } from "#core/_services/api/list-query";
4
4
  import type { components } from "#core/_services/api/schema";
5
5
 
6
6
  /** Tipos derivados do `openapi.json` (§5) — não redigitar a forma da resposta. */
7
- export type AuditAction = components["schemas"]["AuditDataChangeResponse"]["action"];
8
7
  export type AuditUserRef = components["schemas"]["AuditUserRefResponse"];
9
8
  export type AuditDataChange = components["schemas"]["AuditDataChangeResponse"];
9
+ export type RequestLog = components["schemas"]["RequestLogResponse"];
10
10
 
11
11
  export interface Paginated<T> {
12
12
  items: T[];
13
13
  meta: components["schemas"]["PaginationMetaResponse"];
14
14
  }
15
15
 
16
+ /**
17
+ * Teto do histórico de um registro.
18
+ *
19
+ * O modal não pagina: quem precisa de mais que isso está fazendo investigação,
20
+ * e aí a tela de auditoria com filtro é o lugar certo.
21
+ */
22
+ const RECORD_HISTORY_LIMIT = 100;
23
+
16
24
  export const auditService = {
25
+ /**
26
+ * Histórico de um registro específico (`entity` + `entityId`).
27
+ *
28
+ * Usa o mesmo endpoint e o mesmo catálogo da listagem geral — a consulta cai
29
+ * no índice `(entity, entity_id, created_at)`, que existe exatamente por isso.
30
+ */
31
+ listByRecord(entity: string, entityId: string) {
32
+ return api
33
+ .get<Paginated<AuditDataChange>>("/audit/data-changes", {
34
+ params: {
35
+ page: 1,
36
+ limit: RECORD_HISTORY_LIMIT,
37
+ sortBy: "createdAt",
38
+ sortDir: "DESC",
39
+ filter: JSON.stringify({ entity, entityId }),
40
+ },
41
+ })
42
+ .then((r) => r.data);
43
+ },
44
+
17
45
  listDataChanges(query: ListQuery) {
18
46
  return api
19
47
  .get<Paginated<AuditDataChange>>("/audit/data-changes", {
@@ -21,4 +49,18 @@ export const auditService = {
21
49
  })
22
50
  .then((r) => r.data);
23
51
  },
52
+
53
+ /**
54
+ * Log de requisição: erro, requisição lenta e ação marcada com `@Auditable`.
55
+ *
56
+ * Uma rota só para o que antes eram `/audit/changes` e `/audit/errors` — a
57
+ * distinção agora é o filtro `outcome`.
58
+ */
59
+ listRequests(query: ListQuery) {
60
+ return api
61
+ .get<Paginated<RequestLog>>("/audit/requests", {
62
+ params: toListParams(query),
63
+ })
64
+ .then((r) => r.data);
65
+ },
24
66
  };