rl-core-front 0.1.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 (112) hide show
  1. package/README.md +129 -0
  2. package/package.json +113 -0
  3. package/src/_services/api/axios.factory.ts +97 -0
  4. package/src/_services/api/list-query.ts +26 -0
  5. package/src/_services/api/schema.d.ts +2527 -0
  6. package/src/_services/auth/index.ts +127 -0
  7. package/src/_utils/advanced-filter.ts +386 -0
  8. package/src/_utils/calendar.ts +103 -0
  9. package/src/_utils/crop-image.ts +65 -0
  10. package/src/_utils/filter.ts +313 -0
  11. package/src/_utils/format.ts +35 -0
  12. package/src/_utils/initials.ts +24 -0
  13. package/src/_utils/password.ts +7 -0
  14. package/src/components/app-shell.tsx +314 -0
  15. package/src/components/breadcrumbs.tsx +55 -0
  16. package/src/components/index.ts +7 -0
  17. package/src/components/language-selector.tsx +51 -0
  18. package/src/components/password-field.tsx +40 -0
  19. package/src/components/theme-toggle.tsx +27 -0
  20. package/src/components/ui/alert.tsx +27 -0
  21. package/src/components/ui/avatar.tsx +50 -0
  22. package/src/components/ui/badge.tsx +36 -0
  23. package/src/components/ui/button.tsx +62 -0
  24. package/src/components/ui/card.tsx +86 -0
  25. package/src/components/ui/checkbox.tsx +30 -0
  26. package/src/components/ui/column-visibility-modal.tsx +110 -0
  27. package/src/components/ui/confirm-dialog.tsx +69 -0
  28. package/src/components/ui/data-table.tsx +401 -0
  29. package/src/components/ui/date-range-picker.tsx +250 -0
  30. package/src/components/ui/dialog.tsx +138 -0
  31. package/src/components/ui/dropdown-menu.tsx +108 -0
  32. package/src/components/ui/field.tsx +20 -0
  33. package/src/components/ui/filter-chips.tsx +88 -0
  34. package/src/components/ui/filter-field.tsx +137 -0
  35. package/src/components/ui/filter-group.tsx +155 -0
  36. package/src/components/ui/filter-rule.tsx +221 -0
  37. package/src/components/ui/filter-sheet.tsx +201 -0
  38. package/src/components/ui/index.ts +30 -0
  39. package/src/components/ui/input.tsx +25 -0
  40. package/src/components/ui/label.tsx +23 -0
  41. package/src/components/ui/row-actions.tsx +32 -0
  42. package/src/components/ui/select.tsx +97 -0
  43. package/src/components/ui/separator.tsx +31 -0
  44. package/src/components/ui/sheet.tsx +119 -0
  45. package/src/components/ui/sonner.tsx +28 -0
  46. package/src/components/ui/spinner.tsx +12 -0
  47. package/src/components/ui/switch.tsx +29 -0
  48. package/src/components/ui/table.tsx +84 -0
  49. package/src/components/ui/tabs.tsx +52 -0
  50. package/src/components/user-avatar.tsx +54 -0
  51. package/src/contexts/auth-context.tsx +90 -0
  52. package/src/contexts/color-mode-context.tsx +105 -0
  53. package/src/contexts/i18n-context.tsx +76 -0
  54. package/src/contexts/index.ts +6 -0
  55. package/src/contexts/socket-context.tsx +153 -0
  56. package/src/contexts/toast-context.tsx +40 -0
  57. package/src/features/audit/audit-screen.tsx +299 -0
  58. package/src/features/audit/hooks/use-audit-trail.ts +68 -0
  59. package/src/features/audit/services/audit.service.ts +24 -0
  60. package/src/features/dashboard/dashboard-screen.tsx +73 -0
  61. package/src/features/login/components/backup-codes-dialog.tsx +66 -0
  62. package/src/features/login/components/index.ts +5 -0
  63. package/src/features/login/components/login-form.tsx +62 -0
  64. package/src/features/login/components/two-factor-setup.tsx +71 -0
  65. package/src/features/login/components/two-factor-verify.tsx +56 -0
  66. package/src/features/login/hooks/use-login-flow.ts +119 -0
  67. package/src/features/login/login-screen.tsx +83 -0
  68. package/src/features/login/validation/schemas.ts +30 -0
  69. package/src/features/logs/hooks/use-error-logs.ts +65 -0
  70. package/src/features/logs/hooks/use-request-logs.ts +65 -0
  71. package/src/features/logs/logs-screen.tsx +31 -0
  72. package/src/features/logs/panels/error-logs-panel.tsx +183 -0
  73. package/src/features/logs/panels/index.ts +3 -0
  74. package/src/features/logs/panels/request-logs-panel.tsx +124 -0
  75. package/src/features/logs/services/logs.service.ts +68 -0
  76. package/src/features/notifications/components/notification-item.tsx +67 -0
  77. package/src/features/notifications/hooks/use-notifications.ts +78 -0
  78. package/src/features/notifications/notifications-center.tsx +119 -0
  79. package/src/features/notifications/services/notifications.service.ts +38 -0
  80. package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
  81. package/src/features/profile/components/index.ts +2 -0
  82. package/src/features/profile/force-password-change.tsx +114 -0
  83. package/src/features/profile/profile-screen.tsx +385 -0
  84. package/src/features/rbac/panels/actions-panel.tsx +20 -0
  85. package/src/features/rbac/panels/catalog-panel.tsx +243 -0
  86. package/src/features/rbac/panels/groups-panel.tsx +269 -0
  87. package/src/features/rbac/panels/index.ts +8 -0
  88. package/src/features/rbac/panels/permissions-panel.tsx +204 -0
  89. package/src/features/rbac/panels/resources-panel.tsx +20 -0
  90. package/src/features/rbac/panels/roles-panel.tsx +224 -0
  91. package/src/features/rbac/panels/scopes-panel.tsx +20 -0
  92. package/src/features/rbac/rbac-screen.tsx +80 -0
  93. package/src/features/rbac/services/rbac.service.ts +110 -0
  94. package/src/features/recovery/forgot-password-screen.tsx +69 -0
  95. package/src/features/recovery/reset-password-screen.tsx +163 -0
  96. package/src/features/users/components/user-form-dialog.tsx +187 -0
  97. package/src/features/users/hooks/use-users.ts +181 -0
  98. package/src/features/users/services/users.service.ts +83 -0
  99. package/src/features/users/users-screen.tsx +331 -0
  100. package/src/hooks/use-column-visibility.ts +54 -0
  101. package/src/hooks/use-confirm.tsx +70 -0
  102. package/src/hooks/use-filter-schema.ts +85 -0
  103. package/src/hooks/use-filters.ts +148 -0
  104. package/src/hooks/use-list-query.ts +69 -0
  105. package/src/hooks/use-request.ts +114 -0
  106. package/src/i18n/messages/en.ts +296 -0
  107. package/src/i18n/messages/pt.ts +305 -0
  108. package/src/index.ts +60 -0
  109. package/src/lib/utils.ts +6 -0
  110. package/src/middleware.ts +42 -0
  111. package/src/styles/core.css +142 -0
  112. package/tailwind-preset.ts +103 -0
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+ import {
5
+ createContext,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import { io, Socket } from "socket.io-client";
14
+
15
+ import { useAuth } from "#core/contexts/auth-context";
16
+
17
+ export type SocketStatus = "connecting" | "connected" | "disconnected";
18
+
19
+ /** Envelope com que o backend embrulha todo evento (ver socketEvent.envelope.ts). */
20
+ export interface SocketEvent<T> {
21
+ v: number;
22
+ event: string;
23
+ emittedAt: string;
24
+ data: T;
25
+ }
26
+
27
+ type Handler = (payload: SocketEvent<unknown>) => void;
28
+
29
+ export interface SocketContextType {
30
+ status: SocketStatus;
31
+ /**
32
+ * Assina um evento e devolve a função de cancelar. Devolver o cancelamento
33
+ * evita o vazamento clássico de assinar no efeito e esquecer de remover.
34
+ */
35
+ on: <T>(event: string, handler: (data: T) => void) => () => void;
36
+ /**
37
+ * Cresce a cada reconexão. Quem manteve estado local usa isso como gatilho
38
+ * para recarregar — enquanto o socket esteve fora, eventos se perderam.
39
+ */
40
+ reconnectCount: number;
41
+ }
42
+
43
+ const SocketContext = createContext<SocketContextType | undefined>(undefined);
44
+
45
+ const SOCKET_URL =
46
+ process.env.NEXT_PUBLIC_SOCKET_URL ?? "http://localhost:3108";
47
+ const NAMESPACE = "/notifications";
48
+
49
+ /**
50
+ * Conexão única de socket da aplicação.
51
+ *
52
+ * Uma só, no provider: cada tela abrir a sua multiplicaria conexões por aba e
53
+ * espalharia a lógica de reconexão.
54
+ *
55
+ * Autentica sozinho — o cookie httpOnly de sessão viaja no handshake por causa
56
+ * do `withCredentials`. Não há token para o JS ler nem repassar.
57
+ */
58
+ export function SocketProvider({
59
+ children,
60
+ }: {
61
+ children: React.ReactNode;
62
+ }): JSX.Element {
63
+ const { user } = useAuth();
64
+ const [status, setStatus] = useState<SocketStatus>("disconnected");
65
+ const [reconnectCount, setReconnectCount] = useState(0);
66
+ const socketRef = useRef<Socket | null>(null);
67
+
68
+ /**
69
+ * Assinaturas ficam aqui, não só no socket.
70
+ *
71
+ * Efeito de filho roda antes do efeito do pai: quem assina no mount o faz
72
+ * quando o socket ainda não existe. Guardar o registro e reaplicá-lo na
73
+ * criação é o que impede essas assinaturas de se perderem — e é o mesmo
74
+ * caminho que as repõe quando o usuário troca de sessão.
75
+ */
76
+ const handlersRef = useRef(new Map<string, Set<Handler>>());
77
+
78
+ // Sem sessão não há o que conectar: o handshake seria recusado e o
79
+ // socket.io entraria em ciclo de retentativa.
80
+ const userId = user?.id;
81
+
82
+ useEffect(() => {
83
+ if (!userId) {
84
+ return;
85
+ }
86
+
87
+ setStatus("connecting");
88
+ const socket = io(`${SOCKET_URL}${NAMESPACE}`, {
89
+ withCredentials: true,
90
+ transports: ["websocket"],
91
+ reconnectionDelay: 2000,
92
+ });
93
+ socketRef.current = socket;
94
+
95
+ handlersRef.current.forEach((handlers, event) => {
96
+ handlers.forEach((handler) => socket.on(event, handler));
97
+ });
98
+
99
+ let hadConnected = false;
100
+ socket.on("connect", () => {
101
+ setStatus("connected");
102
+ // A primeira conexão não é reconexão — só as seguintes indicam janela
103
+ // sem eventos a recuperar.
104
+ if (hadConnected) {
105
+ setReconnectCount((count) => count + 1);
106
+ }
107
+ hadConnected = true;
108
+ });
109
+ socket.on("disconnect", () => setStatus("disconnected"));
110
+ socket.on("connect_error", () => setStatus("disconnected"));
111
+
112
+ return () => {
113
+ socket.removeAllListeners();
114
+ socket.disconnect();
115
+ socketRef.current = null;
116
+ setStatus("disconnected");
117
+ };
118
+ }, [userId]);
119
+
120
+ const on = useCallback(
121
+ <T,>(event: string, handler: (data: T) => void): (() => void) => {
122
+ const wrapped: Handler = (payload) => handler(payload.data as T);
123
+
124
+ const forEvent = handlersRef.current.get(event) ?? new Set<Handler>();
125
+ forEvent.add(wrapped);
126
+ handlersRef.current.set(event, forEvent);
127
+ socketRef.current?.on(event, wrapped);
128
+
129
+ return () => {
130
+ forEvent.delete(wrapped);
131
+ socketRef.current?.off(event, wrapped);
132
+ };
133
+ },
134
+ [],
135
+ );
136
+
137
+ const value = useMemo<SocketContextType>(
138
+ () => ({ status, reconnectCount, on }),
139
+ [status, reconnectCount, on],
140
+ );
141
+
142
+ return (
143
+ <SocketContext.Provider value={value}>{children}</SocketContext.Provider>
144
+ );
145
+ }
146
+
147
+ export function useSocket(): SocketContextType {
148
+ const ctx = useContext(SocketContext);
149
+ if (!ctx) {
150
+ throw new Error("useSocket deve ser usado dentro de SocketProvider");
151
+ }
152
+ return ctx;
153
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+ import { createContext, useCallback, useContext } from "react";
5
+ import { toast } from "sonner";
6
+
7
+ import { Toaster } from "#core/components/ui";
8
+
9
+ export type ToastSeverity = "success" | "info" | "warning" | "error";
10
+
11
+ export interface ToastContextType {
12
+ notify: (message: string, severity?: ToastSeverity) => void;
13
+ }
14
+
15
+ const ToastContext = createContext<ToastContextType | undefined>(undefined);
16
+
17
+ export function ToastProvider({ children }: { children: React.ReactNode }): JSX.Element {
18
+ const notify = useCallback(
19
+ (message: string, severity: ToastSeverity = "info") => {
20
+ if (severity === "success") {toast.success(message);}
21
+ else if (severity === "error") {toast.error(message);}
22
+ else if (severity === "warning") {toast.warning(message);}
23
+ else {toast.info(message);}
24
+ },
25
+ [],
26
+ );
27
+
28
+ return (
29
+ <ToastContext.Provider value={{ notify }}>
30
+ {children}
31
+ <Toaster />
32
+ </ToastContext.Provider>
33
+ );
34
+ }
35
+
36
+ export function useToast(): ToastContextType {
37
+ const ctx = useContext(ToastContext);
38
+ if (!ctx) {throw new Error("useToast deve ser usado dentro de ToastProvider");}
39
+ return ctx;
40
+ }
@@ -0,0 +1,299 @@
1
+ "use client";
2
+
3
+ import type {
4
+ ColumnDef,
5
+ OnChangeFn,
6
+ PaginationState,
7
+ SortingState,
8
+ } from "@tanstack/react-table";
9
+ import { Eye } from "lucide-react";
10
+ import type { JSX } from "react";
11
+ import { useMemo, useState } from "react";
12
+
13
+ import { formatDateTime, isIsoDateTime } from "#core/_utils/format";
14
+ import {
15
+ Badge,
16
+ Button,
17
+ Checkbox,
18
+ DataTable,
19
+ DataTableFeatures,
20
+ Dialog,
21
+ DialogContent,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ RowActions,
25
+ Table,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
31
+ } from "#core/components/ui";
32
+ import { useI18n } from "#core/contexts";
33
+ import { useAuditTrail } from "#core/features/audit/hooks/use-audit-trail";
34
+ 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
+ }
101
+
102
+ function actionVariant(
103
+ action: AuditAction,
104
+ ): "success" | "warning" | "destructive" {
105
+ if (action === "CREATE") {return "success";}
106
+ if (action === "DELETE") {return "destructive";}
107
+ return "warning";
108
+ }
109
+
110
+ export function AuditScreen(): JSX.Element {
111
+ const { t, locale } = useI18n();
112
+ const trail = useAuditTrail();
113
+ const [detail, setDetail] = useState<AuditDataChange | null>(null);
114
+ const [showTechnical, setShowTechnical] = useState(false);
115
+
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");
129
+
130
+ const columns = useMemo<
131
+ ColumnDef<DataTableFeatures, AuditDataChange, unknown>[]
132
+ >(
133
+ () => [
134
+ {
135
+ accessorKey: "createdAt",
136
+ header: t("auditTrail.date"),
137
+ meta: { label: t("auditTrail.date") },
138
+ cell: ({ getValue }) => formatDateTime(String(getValue()), locale),
139
+ },
140
+ {
141
+ accessorKey: "entity",
142
+ header: t("auditTrail.entity"),
143
+ meta: { label: t("auditTrail.entity") },
144
+ },
145
+ {
146
+ accessorKey: "action",
147
+ header: t("auditTrail.action"),
148
+ meta: { label: t("auditTrail.action") },
149
+ cell: ({ row }) => (
150
+ <Badge variant={actionVariant(row.original.action)}>
151
+ {actionLabel(row.original.action)}
152
+ </Badge>
153
+ ),
154
+ },
155
+ {
156
+ id: "user",
157
+ header: t("auditTrail.user"),
158
+ meta: { label: t("auditTrail.user") },
159
+ enableSorting: false,
160
+ cell: ({ row }) =>
161
+ row.original.user?.name ?? t("auditTrail.systemUser"),
162
+ },
163
+ {
164
+ accessorKey: "ipAddress",
165
+ header: t("auditTrail.ip"),
166
+ meta: { label: t("auditTrail.ip") },
167
+ enableSorting: false,
168
+ cell: ({ getValue }) => (
169
+ <span className="font-mono text-xs">{String(getValue() ?? "—")}</span>
170
+ ),
171
+ },
172
+ {
173
+ id: "actions",
174
+ header: () => <div className="text-center">{t("common.actions")}</div>,
175
+ meta: { shrink: true },
176
+ enableHiding: false,
177
+ enableSorting: false,
178
+ cell: ({ row }) => (
179
+ <RowActions>
180
+ <Button
181
+ variant="ghost"
182
+ size="sm"
183
+ onClick={() => setDetail(row.original)}
184
+ >
185
+ <Eye className="h-4 w-4" />
186
+ {t("auditTrail.viewChanges")}
187
+ </Button>
188
+ </RowActions>
189
+ ),
190
+ },
191
+ ],
192
+ // eslint-disable-next-line react-hooks/exhaustive-deps
193
+ [t, locale],
194
+ );
195
+
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
+ const pagination: PaginationState = {
201
+ pageIndex: trail.page,
202
+ pageSize: trail.pageSize,
203
+ };
204
+ const onPaginationChange: OnChangeFn<PaginationState> = (updater) => {
205
+ const next = typeof updater === "function" ? updater(pagination) : updater;
206
+ if (next.pageSize !== trail.pageSize) {
207
+ trail.setPageSize(next.pageSize);
208
+ trail.setPage(0);
209
+ } else if (next.pageIndex !== trail.page) {
210
+ trail.setPage(next.pageIndex);
211
+ }
212
+ };
213
+ const onSortingChange: OnChangeFn<SortingState> = (updater) => {
214
+ const next =
215
+ typeof updater === "function" ? updater(trail.sorting) : updater;
216
+ trail.setSorting(next);
217
+ trail.setPage(0);
218
+ };
219
+
220
+ return (
221
+ <div>
222
+ <h1 className="mb-1 text-3xl font-bold">{t("auditTrail.title")}</h1>
223
+ <p className="mb-4 text-sm text-muted-foreground">
224
+ {t("auditTrail.subtitle")}
225
+ </p>
226
+
227
+ <DataTable
228
+ columns={columns}
229
+ data={trail.rows}
230
+ getRowId={(r) => r.id}
231
+ storageKey="audit-trail"
232
+ loading={trail.loading}
233
+ manualPagination
234
+ rowCount={trail.total}
235
+ pagination={pagination}
236
+ onPaginationChange={onPaginationChange}
237
+ manualSorting
238
+ sorting={trail.sorting}
239
+ onSortingChange={onSortingChange}
240
+ filters={trail.filters}
241
+ />
242
+
243
+ <Dialog open={!!detail} onOpenChange={(o) => !o && setDetail(null)}>
244
+ <DialogContent className="max-w-2xl">
245
+ <DialogHeader>
246
+ <DialogTitle>
247
+ {t("auditTrail.changesTitle")} — {detail?.entity} (
248
+ {detail && actionLabel(detail.action)})
249
+ </DialogTitle>
250
+ </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
+ )}
295
+ </DialogContent>
296
+ </Dialog>
297
+ </div>
298
+ );
299
+ }
@@ -0,0 +1,68 @@
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 {
13
+ AuditDataChange,
14
+ auditService,
15
+ } from "#core/features/audit/services/audit.service";
16
+ import type { FiltersState } from "#core/hooks/use-filters";
17
+ import { useListQuery } from "#core/hooks/use-list-query";
18
+ import { useRequest } from "#core/hooks/use-request";
19
+
20
+ export interface UseAuditTrailResult {
21
+ rows: AuditDataChange[];
22
+ total: number;
23
+ page: number;
24
+ pageSize: number;
25
+ sorting: SortingState;
26
+ loading: boolean;
27
+ setPage: Dispatch<SetStateAction<number>>;
28
+ setPageSize: Dispatch<SetStateAction<number>>;
29
+ setSorting: Dispatch<SetStateAction<SortingState>>;
30
+ /** Filtros dinâmicos — repassar para o `DataTable`. */
31
+ filters: FiltersState;
32
+ refresh: () => Promise<void>;
33
+ }
34
+
35
+ export function useAuditTrail(): UseAuditTrailResult {
36
+ const { run, loading } = useRequest();
37
+
38
+ const list = useListQuery("/audit/data-changes/filter-schema", 20);
39
+
40
+ const [rows, setRows] = useState<AuditDataChange[]>([]);
41
+ const [total, setTotal] = useState(0);
42
+
43
+ const fetch = useCallback(async () => {
44
+ const res = await run(() => auditService.listDataChanges(list.query));
45
+ if (res) {
46
+ setRows(res.items);
47
+ setTotal(res.meta.total);
48
+ }
49
+ }, [run, list.query]);
50
+
51
+ useEffect(() => {
52
+ void fetch();
53
+ }, [fetch]);
54
+
55
+ return {
56
+ rows,
57
+ total,
58
+ page: list.page,
59
+ pageSize: list.pageSize,
60
+ sorting: list.sorting,
61
+ loading,
62
+ setPage: list.setPage,
63
+ setPageSize: list.setPageSize,
64
+ setSorting: list.setSorting,
65
+ filters: list.filters,
66
+ refresh: fetch,
67
+ };
68
+ }
@@ -0,0 +1,24 @@
1
+ import { api } from "#core/_services/api/axios.factory";
2
+ import type { ListQuery } from "#core/_services/api/list-query";
3
+ import { toListParams } from "#core/_services/api/list-query";
4
+ import type { components } from "#core/_services/api/schema";
5
+
6
+ /** Tipos derivados do `openapi.json` (§5) — não redigitar a forma da resposta. */
7
+ export type AuditAction = components["schemas"]["AuditDataChangeResponse"]["action"];
8
+ export type AuditUserRef = components["schemas"]["AuditUserRefResponse"];
9
+ export type AuditDataChange = components["schemas"]["AuditDataChangeResponse"];
10
+
11
+ export interface Paginated<T> {
12
+ items: T[];
13
+ meta: components["schemas"]["PaginationMetaResponse"];
14
+ }
15
+
16
+ export const auditService = {
17
+ listDataChanges(query: ListQuery) {
18
+ return api
19
+ .get<Paginated<AuditDataChange>>("/audit/data-changes", {
20
+ params: toListParams(query),
21
+ })
22
+ .then((r) => r.data);
23
+ },
24
+ };
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { ShieldCheck } from "lucide-react";
4
+ import type { JSX } from "react";
5
+
6
+ import {
7
+ Badge,
8
+ Card,
9
+ CardContent,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "#core/components/ui";
13
+ import { useAuth, useI18n } from "#core/contexts";
14
+
15
+ export function DashboardScreen(): JSX.Element {
16
+ const { user } = useAuth();
17
+ const { t } = useI18n();
18
+
19
+ return (
20
+ <div>
21
+ <h1 className="mb-6 text-3xl font-bold">
22
+ {t("dashboard.welcome", { name: user?.name ?? "" })}
23
+ </h1>
24
+
25
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
26
+ <Card>
27
+ <CardHeader className="pb-2">
28
+ <CardTitle className="text-sm font-medium text-muted-foreground">
29
+ {t("dashboard.roles")}
30
+ </CardTitle>
31
+ </CardHeader>
32
+ <CardContent className="flex flex-wrap gap-2">
33
+ {user?.roles.map((r) => (
34
+ <Badge key={r}>{r}</Badge>
35
+ ))}
36
+ </CardContent>
37
+ </Card>
38
+
39
+ <Card>
40
+ <CardHeader className="pb-2">
41
+ <CardTitle className="text-sm font-medium text-muted-foreground">
42
+ {t("dashboard.permissions")}
43
+ </CardTitle>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <p className="text-4xl font-bold">
47
+ {user?.permissions.length ?? 0}
48
+ </p>
49
+ </CardContent>
50
+ </Card>
51
+
52
+ <Card>
53
+ <CardHeader className="pb-2">
54
+ <CardTitle className="text-sm font-medium text-muted-foreground">
55
+ {t("dashboard.security")}
56
+ </CardTitle>
57
+ </CardHeader>
58
+ <CardContent>
59
+ <Badge
60
+ variant={user?.twoFactorEnabled ? "success" : "warning"}
61
+ className="gap-1"
62
+ >
63
+ <ShieldCheck className="h-3.5 w-3.5" />
64
+ {user?.twoFactorEnabled
65
+ ? t("dashboard.twoFactorActive")
66
+ : t("dashboard.twoFactorPending")}
67
+ </Badge>
68
+ </CardContent>
69
+ </Card>
70
+ </div>
71
+ </div>
72
+ );
73
+ }