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,69 @@
1
+ "use client";
2
+
3
+ import type { SortingState } from "@tanstack/react-table";
4
+ import type { Dispatch, SetStateAction } from "react";
5
+ import { useEffect, useMemo, useState } from "react";
6
+
7
+ import type { ListQuery } from "#core/_services/api/list-query";
8
+ import type { FiltersState } from "#core/hooks/use-filters";
9
+ import { useFilters } from "#core/hooks/use-filters";
10
+
11
+ export interface UseListQueryResult {
12
+ /** Pronta para o `.service.ts` da feature. */
13
+ query: ListQuery;
14
+ page: number;
15
+ pageSize: number;
16
+ sorting: SortingState;
17
+ setPage: Dispatch<SetStateAction<number>>;
18
+ setPageSize: Dispatch<SetStateAction<number>>;
19
+ setSorting: Dispatch<SetStateAction<SortingState>>;
20
+ /** Repassar direto para o `DataTable`. */
21
+ filters: FiltersState;
22
+ }
23
+
24
+ /**
25
+ * Paginação, ordenação e filtros de uma listagem, num lugar só.
26
+ *
27
+ * O hook da feature continua dono do fetch e das ações; o que era igual nos
28
+ * quatro (page/pageSize/sorting, e agora o filtro) mora aqui.
29
+ */
30
+ export function useListQuery(
31
+ schemaUrl: string,
32
+ initialPageSize = 10,
33
+ ): UseListQueryResult {
34
+ const filters = useFilters(schemaUrl);
35
+ const { filter } = filters;
36
+
37
+ const [page, setPage] = useState(0);
38
+ const [pageSize, setPageSize] = useState(initialPageSize);
39
+ const [sorting, setSorting] = useState<SortingState>([]);
40
+
41
+ // Trocar o filtro volta para a primeira página: a página 3 de um resultado
42
+ // que agora tem uma página só voltaria vazia.
43
+ useEffect(() => {
44
+ setPage(0);
45
+ }, [filter]);
46
+
47
+ const sort = sorting[0];
48
+ const query = useMemo<ListQuery>(
49
+ () => ({
50
+ page,
51
+ limit: pageSize,
52
+ sortBy: sort?.id,
53
+ sortDir: sort ? (sort.desc ? "DESC" : "ASC") : undefined,
54
+ filter,
55
+ }),
56
+ [page, pageSize, sort, filter],
57
+ );
58
+
59
+ return {
60
+ query,
61
+ page,
62
+ pageSize,
63
+ sorting,
64
+ setPage,
65
+ setPageSize,
66
+ setSorting,
67
+ filters,
68
+ };
69
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { AxiosError } from "axios";
4
+ import { Dispatch, SetStateAction, useCallback, useState } from "react";
5
+
6
+ import { useI18n, useToast } from "#core/contexts";
7
+
8
+ /** Corpo de erro padronizado pelo AllExceptionsFilter do backend. */
9
+ interface ApiErrorBody {
10
+ message?: string | string[];
11
+ errorCode?: string | null;
12
+ }
13
+
14
+ /** Operação concluída — define o texto padrão do toast de sucesso. */
15
+ export enum RequestOperation {
16
+ Create = "create",
17
+ Update = "update",
18
+ Delete = "delete",
19
+ }
20
+
21
+ const SUCCESS_KEY: Record<RequestOperation, string> = {
22
+ [RequestOperation.Create]: "common.createdSuccess",
23
+ [RequestOperation.Update]: "common.updatedSuccess",
24
+ [RequestOperation.Delete]: "common.deletedSuccess",
25
+ };
26
+
27
+ const isOperation = (value: string): value is RequestOperation =>
28
+ (Object.values(RequestOperation) as string[]).includes(value);
29
+
30
+ export interface RunOptions {
31
+ /**
32
+ * Toast de sucesso. Uma operação usa o texto padrão do `common.*`; uma string
33
+ * é exibida como veio, para quando a tela tem algo melhor a dizer que
34
+ * "Registro criado" (ex.: "Link de primeiro acesso enviado").
35
+ */
36
+ success?: RequestOperation | string;
37
+ }
38
+
39
+ export interface UseRequestResult {
40
+ run: <T>(fn: () => Promise<T>, options?: RunOptions) => Promise<T | null>;
41
+ loading: boolean;
42
+ error: string | null;
43
+ setError: Dispatch<SetStateAction<string | null>>;
44
+ }
45
+
46
+ /**
47
+ * Hook genérico para chamadas assíncronas com estados de loading/erro.
48
+ * Ex.: const { run, loading, error } = useRequest();
49
+ * await run(() => authService.login(email, senha));
50
+ */
51
+ export function useRequest(): UseRequestResult {
52
+ const { t } = useI18n();
53
+ const { notify } = useToast();
54
+ const [loading, setLoading] = useState(false);
55
+ const [error, setError] = useState<string | null>(null);
56
+
57
+ /**
58
+ * Ordem de preferência da mensagem:
59
+ * 1. tradução do `errorCode` — texto do produto, no idioma do usuário;
60
+ * 2. `message` do servidor — usada quando o código não tem tradução, porque
61
+ * costuma carregar dado dinâmico (ex.: "Restam 3 tentativas");
62
+ * 3. genérico.
63
+ *
64
+ * O `t()` devolve a própria chave quando ela não existe: é assim que se sabe
65
+ * que aquele código não tem texto próprio no cliente.
66
+ */
67
+ const messageFor = useCallback(
68
+ (body: ApiErrorBody | undefined): string => {
69
+ if (body?.errorCode) {
70
+ const key = `errors.codes.${body.errorCode}`;
71
+ const translated = t(key);
72
+ if (translated !== key) {
73
+ return translated;
74
+ }
75
+ }
76
+ const msg = body?.message;
77
+ if (Array.isArray(msg)) {
78
+ return msg.join(", ");
79
+ }
80
+ return msg ?? t("errors.unexpected");
81
+ },
82
+ [t],
83
+ );
84
+
85
+ /** Operação cai no texto padrão; string livre é a mensagem já pronta. */
86
+ const successMessage = useCallback(
87
+ (success: RequestOperation | string): string =>
88
+ isOperation(success) ? t(SUCCESS_KEY[success]) : success,
89
+ [t],
90
+ );
91
+
92
+ const run = useCallback(
93
+ async <T>(fn: () => Promise<T>, options?: RunOptions): Promise<T | null> => {
94
+ setLoading(true);
95
+ setError(null);
96
+ try {
97
+ const result = await fn();
98
+ if (options?.success) {
99
+ notify(successMessage(options.success), "success");
100
+ }
101
+ return result;
102
+ } catch (e) {
103
+ const err = e as AxiosError<ApiErrorBody>;
104
+ setError(messageFor(err.response?.data));
105
+ return null;
106
+ } finally {
107
+ setLoading(false);
108
+ }
109
+ },
110
+ [messageFor, notify, successMessage],
111
+ );
112
+
113
+ return { run, loading, error, setError };
114
+ }
@@ -0,0 +1,296 @@
1
+ import type { Messages } from "./pt";
2
+
3
+ export const en: Messages = {
4
+ app: { name: "Core App" },
5
+ nav: {
6
+ home: "Home",
7
+ admin: "Administration",
8
+ dashboard: "Dashboard",
9
+ users: "Users",
10
+ rbac: "Roles & Permissions",
11
+ profile: "My profile",
12
+ logs: "Logs",
13
+ audit: "Audit",
14
+ },
15
+ logs: {
16
+ title: "Logs",
17
+ subtitle:
18
+ "Every API request — success and error, route, status and duration.",
19
+ tabRequests: "Requests",
20
+ tabErrors: "Errors",
21
+ date: "Date/time",
22
+ route: "Route",
23
+ status: "Status",
24
+ duration: "Duration",
25
+ user: "User",
26
+ systemUser: "System",
27
+ ip: "IP",
28
+ requestId: "Request ID",
29
+ type: "Type",
30
+ message: "Message",
31
+ viewDetails: "View details",
32
+ detailsTitle: "Error details",
33
+ stackTrace: "Stack trace",
34
+ },
35
+ auditTrail: {
36
+ title: "Audit Trail",
37
+ subtitle: "Data change history — who changed what, and when.",
38
+ entity: "Entity",
39
+ action: "Action",
40
+ actionCreate: "Create",
41
+ actionUpdate: "Update",
42
+ actionDelete: "Delete",
43
+ user: "User",
44
+ systemUser: "System",
45
+ date: "Date/time",
46
+ ip: "IP",
47
+ viewChanges: "View changes",
48
+ changesTitle: "Changes",
49
+ field: "Field",
50
+ before: "Before",
51
+ after: "After",
52
+ noFields: "No fields recorded for this change.",
53
+ showTechnical: "Show technical fields",
54
+ },
55
+ auditFields: {
56
+ name: "Name",
57
+ lastName: "Last name",
58
+ email: "Email",
59
+ phone: "Phone",
60
+ isActive: "Active",
61
+ avatar: "Avatar",
62
+ twoFactorEnabled: "2FA enabled",
63
+ failedLoginAttempts: "Failed login attempts",
64
+ mustChangePassword: "Must change password",
65
+ passwordChangedAt: "Password changed at",
66
+ createdAt: "Created at",
67
+ updatedAt: "Updated at",
68
+ deletedAt: "Deleted at",
69
+ description: "Description",
70
+ code: "Code",
71
+ permissions: "Permissions",
72
+ roles: "Roles",
73
+ },
74
+ table: {
75
+ columns: "Columns",
76
+ columnsButton: "Columns",
77
+ columnsTitle: "Configure column visibility",
78
+ columnsHint: "Select the columns you want to see in the table.",
79
+ atLeastOne: "At least one column must remain visible",
80
+ results: "results",
81
+ perPage: "per page",
82
+ empty: "No records",
83
+ },
84
+ filters: {
85
+ button: "Filters",
86
+ title: "Filters",
87
+ hint: "Fill in only the fields you want to use. All of them are combined with AND.",
88
+ apply: "Apply",
89
+ clear: "Clear",
90
+ clearAll: "Clear all",
91
+ empty: "This listing has no filters yet.",
92
+ noOptions: "No options available",
93
+ all: "All",
94
+ from: "From",
95
+ to: "To",
96
+ fromChip: "from",
97
+ toChip: "until",
98
+ selectPeriod: "Select a period",
99
+ lastDays: "{days} days",
100
+ previousMonth: "Previous month",
101
+ nextMonth: "Next month",
102
+ simple: "Simple",
103
+ advanced: "Advanced",
104
+ advancedHint:
105
+ "Build the expression: each group combines its conditions with AND or OR.",
106
+ addCondition: "Condition",
107
+ addGroup: "Group",
108
+ and: "AND",
109
+ or: "OR",
110
+ emptyGroup: "Group with no conditions.",
111
+ removeRule: "Remove condition",
112
+ removeGroup: "Remove group",
113
+ advancedSummary: "Advanced filter · {count} conditions",
114
+ field: "Field",
115
+ operator: "Operator",
116
+ value: "Value",
117
+ null: "Empty (null)",
118
+ listHint: "Separate with commas",
119
+ tooDeep: "Groups nested too deep — the limit is 3 levels.",
120
+ tooManyConditions: "Too many conditions — the limit is 50.",
121
+ tooManyItems: "Too many items in a list — the limit is 100.",
122
+ operators: {
123
+ EQ: "equals",
124
+ NE: "not equal to",
125
+ IS: "is",
126
+ NOT: "is not",
127
+ GT: "greater than",
128
+ GTE: "greater or equal to",
129
+ LT: "less than",
130
+ LTE: "less or equal to",
131
+ BETWEEN: "between",
132
+ NOTBETWEEN: "not between",
133
+ IN: "in",
134
+ NOTIN: "not in",
135
+ LIKE: "matches",
136
+ NOTLIKE: "does not match",
137
+ STARTSWITH: "starts with",
138
+ ENDSWITH: "ends with",
139
+ SUBSTRING: "contains",
140
+ },
141
+ },
142
+ notifications: {
143
+ title: "Notifications",
144
+ all: "All",
145
+ unread: "Unread",
146
+ markAllRead: "Mark all as read",
147
+ empty: "No notifications",
148
+ },
149
+ common: {
150
+ save: "Save",
151
+ cancel: "Cancel",
152
+ edit: "Edit",
153
+ delete: "Delete",
154
+ create: "Create",
155
+ confirm: "Confirm",
156
+ send: "Send",
157
+ yes: "Yes",
158
+ no: "No",
159
+ actions: "Actions",
160
+ loading: "Loading...",
161
+ language: "Language",
162
+ theme: "Theme",
163
+ light: "Light",
164
+ dark: "Dark",
165
+ system: "System",
166
+ // Mensagens de sucesso genéricas — só para tela reaproveitada, que não sabe
167
+ // qual entidade está editando. Tela de entidade fixa usa texto próprio.
168
+ createdSuccess: "Record created successfully",
169
+ updatedSuccess: "Record updated successfully",
170
+ deletedSuccess: "Record deleted successfully",
171
+ },
172
+ auth: {
173
+ loginTitle: "Sign in",
174
+ email: "Email",
175
+ password: "Password",
176
+ enter: "Sign in",
177
+ entering: "Signing in...",
178
+ forgotPassword: "Forgot my password",
179
+ twoFactorSetupTitle: "Set up 2FA",
180
+ twoFactorVerifyTitle: "Two-factor verification",
181
+ backupTitle: "Backup codes",
182
+ setupHint:
183
+ "Set up two-factor authentication (required). Scan the QR code with a free authenticator app (Google Authenticator, Microsoft Authenticator, Authy...) and enter the code.",
184
+ manualKey: "Can't scan? Use the manual key:",
185
+ codeApp: "App code (6 digits)",
186
+ confirmActivate: "Confirm and enable 2FA",
187
+ confirming: "Confirming...",
188
+ verifyHint:
189
+ "Enter the code from your authenticator app. No access to the app? Use a backup code or receive a code by email.",
190
+ codeAny: "Code (TOTP, email or backup)",
191
+ verify: "Validate code",
192
+ verifying: "Validating...",
193
+ receiveByEmail: "Receive code by email",
194
+ sendingEmail: "Sending...",
195
+ backupWarning:
196
+ "These codes are shown only now. Each can be used once if you lose access to the app.",
197
+ copy: "Copy",
198
+ codesCopied: "Backup codes copied",
199
+ savedContinue: "Saved, continue",
200
+ loginSuccess: "Signed in successfully",
201
+ twoFactorConfigured: "2FA configured successfully",
202
+ codeSent: "Code sent to your email",
203
+ },
204
+ recovery: {
205
+ forgotTitle: "Recover password",
206
+ forgotHint:
207
+ "Enter your email and we'll send a link to reset your password.",
208
+ sendLink: "Send link",
209
+ sending: "Sending...",
210
+ forgotSuccess: "If the email exists, we sent the reset instructions.",
211
+ backToLogin: "Back to sign in",
212
+ resetTitle: "Reset password",
213
+ newPassword: "New password",
214
+ confirmPassword: "Confirm new password",
215
+ resetCta: "Reset password",
216
+ resetSuccess: "Password reset successfully",
217
+ checking: "Checking the link...",
218
+ firstAccessTitle: "Set your first access password",
219
+ firstAccessHint:
220
+ "Create your password to access the system. On your first sign in you will also set up 2FA.",
221
+ linkExpired: "This link has expired. Request a new one to set your password.",
222
+ linkUsed: "This link has already been used. Request a new one.",
223
+ linkInvalid: "Invalid link. Request a new one to set your password.",
224
+ requestNewLink: "Request a new link",
225
+ },
226
+ user: { menuProfile: "My profile", logout: "Sign out" },
227
+ dashboard: {
228
+ welcome: "Welcome, {name}",
229
+ roles: "Roles",
230
+ permissions: "Permissions",
231
+ security: "Security",
232
+ twoFactorActive: "2FA active",
233
+ twoFactorPending: "2FA pending",
234
+ },
235
+ users: {
236
+ title: "Users",
237
+ newUser: "New user",
238
+ name: "Name",
239
+ lastName: "Last name",
240
+ phone: "Phone",
241
+ email: "Email",
242
+ roles: "Roles",
243
+ status: "Status",
244
+ active: "Active",
245
+ inactive: "Inactive",
246
+ locked: "Locked",
247
+ passwordByEmailHint:
248
+ "The user will receive a link by email to set their own password. No password is set here.",
249
+ sendFirstAccessLink: "Send first access link",
250
+ sendPasswordResetLink: "Send password reset link",
251
+ passwordResetLinkSent:
252
+ "Reset link sent — the user's active sessions were terminated",
253
+ firstAccessLinkSent: "First access link sent to the user's email",
254
+ resetTwoFactor: "Reset 2FA",
255
+ twoFactorReset: "2FA reset — user reconfigures on next login",
256
+ },
257
+ profile: {
258
+ title: "My profile",
259
+ data: "Data",
260
+ changePassword: "Change password",
261
+ currentPassword: "Current password",
262
+ newPassword: "New password",
263
+ confirmPassword: "Confirm new password",
264
+ changed: "Password changed successfully",
265
+ forceProvisional:
266
+ "You are using a temporary password. Set a new one to continue.",
267
+ forceExpired: "Your password has expired. Set a new one to continue.",
268
+ changeAvatar: "Change photo",
269
+ cropTitle: "Crop photo",
270
+ cropHint: "Drag to reposition and use the zoom to adjust.",
271
+ cropZoom: "Zoom",
272
+ saved: "Profile updated",
273
+ avatarUpdated: "Profile photo updated",
274
+ avatarRemoved: "Profile photo removed",
275
+ },
276
+ rbac: { title: "Roles & Permissions" },
277
+ validation: {
278
+ required: "Required field",
279
+ email: "Invalid email",
280
+ passwordStrong: "Min. 8 chars with uppercase, lowercase, number and symbol",
281
+ passwordsNotMatch: "Passwords do not match",
282
+ codeShort: "Code too short",
283
+ },
284
+ errors: {
285
+ unexpected: "Unexpected error",
286
+ codes: {
287
+ ACCOUNT_INACTIVE:
288
+ "Account deactivated. Please contact the system administrator.",
289
+ ACCOUNT_LOCKED:
290
+ "Account locked after too many attempts. Ask an administrator to send you a new link to set your password.",
291
+ PASSWORD_REUSED:
292
+ "This password has already been used. Choose one you have never used in this system.",
293
+ TOO_MANY_REQUESTS: "Too many attempts. Wait a moment and try again.",
294
+ },
295
+ },
296
+ };