@seedgrid/fe-security 2026.7.0 → 2026.7.2

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 (125) hide show
  1. package/README.md +113 -0
  2. package/dist/client/index.d.ts +4 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +5 -0
  5. package/dist/client/permission-provider.d.ts +18 -0
  6. package/dist/client/permission-provider.d.ts.map +1 -0
  7. package/dist/client/permission-provider.js +57 -0
  8. package/dist/client/pick-list-field.d.ts +15 -0
  9. package/dist/client/pick-list-field.d.ts.map +1 -0
  10. package/dist/client/pick-list-field.js +30 -0
  11. package/dist/client/status-badge.d.ts +6 -0
  12. package/dist/client/status-badge.d.ts.map +1 -0
  13. package/dist/client/status-badge.js +6 -0
  14. package/dist/config.d.ts +34 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +4 -0
  17. package/dist/dtos.d.ts +88 -0
  18. package/dist/dtos.d.ts.map +1 -0
  19. package/dist/dtos.js +3 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -0
  23. package/dist/menu.d.ts +19 -0
  24. package/dist/menu.d.ts.map +1 -0
  25. package/dist/menu.js +41 -0
  26. package/dist/messages.d.ts +1858 -0
  27. package/dist/messages.d.ts.map +1 -0
  28. package/dist/messages.js +990 -0
  29. package/dist/module.d.ts +3 -0
  30. package/dist/module.d.ts.map +1 -0
  31. package/dist/module.js +37 -0
  32. package/dist/module.json +21 -0
  33. package/dist/paths.d.ts +1 -0
  34. package/dist/paths.d.ts.map +1 -1
  35. package/dist/paths.js +3 -0
  36. package/dist/permissions.d.ts +14 -0
  37. package/dist/permissions.d.ts.map +1 -0
  38. package/dist/permissions.js +39 -0
  39. package/dist/server/action.d.ts +3 -0
  40. package/dist/server/action.d.ts.map +1 -0
  41. package/dist/server/action.js +21 -0
  42. package/dist/server/create-security-server.d.ts +4 -0
  43. package/dist/server/create-security-server.d.ts.map +1 -0
  44. package/dist/server/create-security-server.js +166 -0
  45. package/dist/server/errors.d.ts +13 -0
  46. package/dist/server/errors.d.ts.map +1 -0
  47. package/dist/server/errors.js +26 -0
  48. package/dist/server/index.d.ts +8 -0
  49. package/dist/server/index.d.ts.map +1 -0
  50. package/dist/server/index.js +8 -0
  51. package/dist/server/problem-response.d.ts +3 -0
  52. package/dist/server/problem-response.d.ts.map +1 -0
  53. package/dist/server/problem-response.js +28 -0
  54. package/dist/server/route.d.ts +3 -0
  55. package/dist/server/route.d.ts.map +1 -0
  56. package/dist/server/route.js +22 -0
  57. package/dist/server/to-problem.d.ts +3 -0
  58. package/dist/server/to-problem.d.ts.map +1 -0
  59. package/dist/server/to-problem.js +22 -0
  60. package/dist/server-contract.d.ts +23 -0
  61. package/dist/server-contract.d.ts.map +1 -0
  62. package/dist/server-contract.js +4 -0
  63. package/dist/session-contract.d.ts +36 -0
  64. package/dist/session-contract.d.ts.map +1 -0
  65. package/dist/session-contract.js +8 -0
  66. package/dist/session-result.d.ts +22 -0
  67. package/dist/session-result.d.ts.map +1 -0
  68. package/dist/session-result.js +14 -0
  69. package/package.json +38 -3
  70. package/scaffold/README.md +97 -0
  71. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  73. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  74. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  75. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  76. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  77. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  78. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  79. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  80. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  81. package/scaffold/src/app/auth/login/page.tsx +386 -0
  82. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  83. package/scaffold/src/app/login/page.tsx +7 -0
  84. package/scaffold/src/app/page.tsx +12 -0
  85. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  86. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  87. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  88. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  89. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  90. package/scaffold/src/app/security/profile/page.tsx +492 -0
  91. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  92. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  93. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  94. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  95. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  96. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  97. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  98. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  99. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  100. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  101. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  102. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  103. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  104. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  105. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  106. package/scaffold/src/modules/security/config.ts +55 -0
  107. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  108. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  109. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  110. package/scaffold/src/modules/security/host-access.ts +59 -0
  111. package/scaffold/src/modules/security/index.ts +44 -0
  112. package/scaffold/src/modules/security/navigation.ts +41 -0
  113. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  114. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  115. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  116. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  117. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  118. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  119. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  120. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  121. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  122. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  123. package/scaffold/src/modules/security/types.ts +295 -0
  124. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  125. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,370 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useForm } from "react-hook-form";
5
+
6
+ import {
7
+ SgButton,
8
+ SgCombobox,
9
+ SgGrid,
10
+ SgInputPassword,
11
+ SgInputText,
12
+ SgPanel,
13
+ SgStack,
14
+ sgWhistle,
15
+ } from "@seedgrid/fe-components";
16
+
17
+ import { useI18n } from "@/i18n";
18
+ import {
19
+ InlineNotice,
20
+ PageFrame,
21
+ RouteGuard,
22
+ StatusBadge,
23
+ extractApiMessage,
24
+ securityService,
25
+ type TwoFactorEnableResponse,
26
+ type TwoFactorStatus,
27
+ } from "@/modules/security";
28
+
29
+ export default function TwoFactorSettingsPage() {
30
+ return (
31
+ <RouteGuard>
32
+ <TwoFactorSettingsScreen />
33
+ </RouteGuard>
34
+ );
35
+ }
36
+
37
+ type TwoFactorSettingsFormValues = {
38
+ totpCode: string;
39
+ disableTwoFactorCode: string;
40
+ disablePassword: string;
41
+ backupCode: string;
42
+ };
43
+
44
+ function TwoFactorSettingsScreen() {
45
+ const { t } = useI18n();
46
+ const [status, setStatus] = useState<TwoFactorStatus | null>(null);
47
+ const [enableResponse, setEnableResponse] =
48
+ useState<TwoFactorEnableResponse | null>(null);
49
+ const [loading, setLoading] = useState(true);
50
+ const [pending, setPending] = useState(false);
51
+ const [selectedMethod, setSelectedMethod] = useState("EMAIL");
52
+ const { getValues, register, resetField } = useForm<TwoFactorSettingsFormValues>({
53
+ defaultValues: {
54
+ totpCode: "",
55
+ disableTwoFactorCode: "",
56
+ disablePassword: "",
57
+ backupCode: "",
58
+ },
59
+ });
60
+ const disablePasswordInputProps = {
61
+ autoComplete: "current-password",
62
+ };
63
+
64
+ useEffect(() => {
65
+ void loadStatus();
66
+ }, []);
67
+
68
+ async function loadStatus() {
69
+ setLoading(true);
70
+
71
+ try {
72
+ const nextStatus = await securityService.getTwoFactorStatus();
73
+ setStatus(nextStatus);
74
+ setSelectedMethod(nextStatus.availableMethods[0] ?? "EMAIL");
75
+ } catch (error) {
76
+ sgWhistle.error({
77
+ message: extractApiMessage(error) ?? t("security.two_factor.error"),
78
+ });
79
+ } finally {
80
+ setLoading(false);
81
+ }
82
+ }
83
+
84
+ async function handleEnable() {
85
+ setPending(true);
86
+
87
+ try {
88
+ const response = await securityService.enableTwoFactor(selectedMethod, true);
89
+ setEnableResponse(response);
90
+
91
+ if (response.backupCodes?.length && selectedMethod !== "TOTP") {
92
+ sgWhistle.success({ message: t("security.two_factor.success_enable") });
93
+ await loadStatus();
94
+ }
95
+ } catch (error) {
96
+ sgWhistle.error({
97
+ message: extractApiMessage(error) ?? t("security.two_factor.error"),
98
+ });
99
+ } finally {
100
+ setPending(false);
101
+ }
102
+ }
103
+
104
+ async function handleConfirmTotp() {
105
+ setPending(true);
106
+
107
+ try {
108
+ const response = await securityService.confirmTotp(getValues("totpCode").trim());
109
+ setEnableResponse(response);
110
+ sgWhistle.success({ message: t("security.two_factor.success_confirm") });
111
+ resetField("totpCode");
112
+ await loadStatus();
113
+ } catch (error) {
114
+ sgWhistle.error({
115
+ message: extractApiMessage(error) ?? t("security.two_factor.error"),
116
+ });
117
+ } finally {
118
+ setPending(false);
119
+ }
120
+ }
121
+
122
+ async function handleDisable() {
123
+ const requiresTotpCode = status?.primaryMethod === "TOTP";
124
+ const disablePassword = getValues("disablePassword");
125
+ const backupCode = getValues("backupCode");
126
+ const disableTwoFactorCode = getValues("disableTwoFactorCode");
127
+
128
+ if (
129
+ !disablePassword.trim() &&
130
+ !backupCode.trim() &&
131
+ (!requiresTotpCode || !disableTwoFactorCode.trim())
132
+ ) {
133
+ sgWhistle.error({
134
+ message: t(
135
+ requiresTotpCode
136
+ ? "security.two_factor.disable_validation_totp"
137
+ : "security.two_factor.disable_validation"
138
+ ),
139
+ });
140
+ return;
141
+ }
142
+
143
+ setPending(true);
144
+
145
+ try {
146
+ await securityService.disableTwoFactor({
147
+ twoFactorCode: disableTwoFactorCode.trim() || undefined,
148
+ password: disablePassword.trim() || undefined,
149
+ backupCode: backupCode.trim() || undefined,
150
+ });
151
+ sgWhistle.success({ message: t("security.two_factor.success_disable") });
152
+ resetField("disableTwoFactorCode");
153
+ resetField("disablePassword");
154
+ resetField("backupCode");
155
+ setEnableResponse(null);
156
+ await loadStatus();
157
+ } catch (error) {
158
+ sgWhistle.error({
159
+ message: extractApiMessage(error) ?? t("security.two_factor.error"),
160
+ });
161
+ } finally {
162
+ setPending(false);
163
+ }
164
+ }
165
+
166
+ function handleDownloadBackupCodesCsv() {
167
+ const backupCodes = enableResponse?.backupCodes ?? [];
168
+
169
+ if (!backupCodes.length || typeof window === "undefined") {
170
+ return;
171
+ }
172
+
173
+ const csvContent = ["code", ...backupCodes].join("\n");
174
+ const csvBlob = new Blob([csvContent], {
175
+ type: "text/csv;charset=utf-8;",
176
+ });
177
+ const downloadUrl = window.URL.createObjectURL(csvBlob);
178
+ const link = document.createElement("a");
179
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
180
+
181
+ link.href = downloadUrl;
182
+ link.download = `seedgrid-2fa-backup-codes-${timestamp}.csv`;
183
+ document.body.appendChild(link);
184
+ link.click();
185
+ document.body.removeChild(link);
186
+ window.URL.revokeObjectURL(downloadUrl);
187
+ }
188
+
189
+ return (
190
+ <PageFrame
191
+ title={t("security.two_factor.title")}
192
+ description={t("security.two_factor.description")}
193
+ >
194
+ {loading ? (
195
+ <InlineNotice>{t("security.common.loading")}</InlineNotice>
196
+ ) : status ? (
197
+ <SgStack gap={16}>
198
+ <SgGrid minItemWidth={320} gap={16}>
199
+ <InlineNotice>
200
+ <SgStack gap={12}>
201
+ <div className="font-medium">{t("security.two_factor.status")}</div>
202
+ <StatusBadge
203
+ active={status.enabled}
204
+ activeLabel={t("security.common.enabled")}
205
+ inactiveLabel={t("security.common.disabled")}
206
+ />
207
+ <div>
208
+ <div className="text-xs uppercase tracking-wide text-slate-500">
209
+ {t("security.two_factor.primary_method")}
210
+ </div>
211
+ <div>{status.primaryMethod ?? "-"}</div>
212
+ </div>
213
+ <div>
214
+ <div className="text-xs uppercase tracking-wide text-slate-500">
215
+ {t("security.two_factor.available_methods")}
216
+ </div>
217
+ <div>{status.availableMethods.join(", ") || "-"}</div>
218
+ </div>
219
+ <div>
220
+ <div className="text-xs uppercase tracking-wide text-slate-500">
221
+ {t("security.two_factor.backup_codes")}
222
+ </div>
223
+ <div>{status.unusedBackupCodesCount ?? 0}</div>
224
+ </div>
225
+ </SgStack>
226
+ </InlineNotice>
227
+
228
+ <SgStack gap={16}>
229
+ {!status.enabled ? (
230
+ <SgGrid minItemWidth={220} gap={16} align="end">
231
+ <SgCombobox
232
+ id="security-2fa-method"
233
+ label={t("security.two_factor.method")}
234
+ source={status.availableMethods}
235
+ value={selectedMethod}
236
+ onValueChange={(value) => setSelectedMethod(String(value))}
237
+ />
238
+ <SgButton
239
+ appearance="solid"
240
+ severity="primary"
241
+ shape="rounded"
242
+ label={t("security.two_factor.enable")}
243
+ loading={pending}
244
+ disabled={pending}
245
+ onClick={handleEnable}
246
+ />
247
+ </SgGrid>
248
+ ) : (
249
+ <SgGrid minItemWidth={280} gap={16}>
250
+ {status.primaryMethod === "TOTP" ? (
251
+ <SgInputText
252
+ id="security-2fa-disable-code"
253
+ name="disableTwoFactorCode"
254
+ label={t("security.two_factor.code")}
255
+ register={register}
256
+ />
257
+ ) : null}
258
+ <SgInputPassword
259
+ id="security-2fa-disable-password"
260
+ name="disablePassword"
261
+ label={t("security.two_factor.password")}
262
+ register={register}
263
+ minSize={0}
264
+ showStrengthBar={false}
265
+ commonPasswordCheck={false}
266
+ upperRequired={false}
267
+ lowerRequired={false}
268
+ numberRequired={false}
269
+ specialCharacterRequired={false}
270
+ prohibitsRepeatedCharactersInSequence={false}
271
+ prohibitsSequentialAscCharacters={false}
272
+ prohibitsSequentialDescCharacters={false}
273
+ inputProps={disablePasswordInputProps}
274
+ />
275
+ <SgInputText
276
+ id="security-2fa-backup-code"
277
+ name="backupCode"
278
+ label={t("security.two_factor.backup_code")}
279
+ register={register}
280
+ />
281
+ <SgPanel borderStyle="none" className="min-h-0 min-w-0">
282
+ <InlineNotice>
283
+ {t(
284
+ status.primaryMethod === "TOTP"
285
+ ? "security.two_factor.disable_hint_totp"
286
+ : "security.two_factor.disable_hint"
287
+ )}
288
+ </InlineNotice>
289
+ </SgPanel>
290
+ <SgStack direction="row" justify="end">
291
+ <SgButton
292
+ appearance="outline"
293
+ severity="danger"
294
+ shape="rounded"
295
+ label={t("security.two_factor.disable")}
296
+ loading={pending}
297
+ disabled={pending}
298
+ onClick={handleDisable}
299
+ />
300
+ </SgStack>
301
+ </SgGrid>
302
+ )}
303
+
304
+ {enableResponse?.qrCode?.base64 ? (
305
+ <SgGrid minItemWidth={280} gap={24}>
306
+ <SgPanel className="rounded-3xl border border-slate-200 bg-slate-50 p-4">
307
+ <img
308
+ alt={t("security.two_factor.qr_code")}
309
+ className="h-48 w-48 rounded-2xl object-contain"
310
+ src={`data:image/png;base64,${enableResponse.qrCode.base64}`}
311
+ />
312
+ </SgPanel>
313
+ <SgStack gap={16}>
314
+ <SgInputText
315
+ id="security-2fa-totp-code"
316
+ name="totpCode"
317
+ label={t("security.two_factor.code")}
318
+ register={register}
319
+ />
320
+ <SgStack direction="row" justify="end">
321
+ <SgButton
322
+ appearance="solid"
323
+ severity="primary"
324
+ shape="rounded"
325
+ label={t("security.two_factor.confirm_totp")}
326
+ loading={pending}
327
+ disabled={pending}
328
+ onClick={handleConfirmTotp}
329
+ />
330
+ </SgStack>
331
+ </SgStack>
332
+ </SgGrid>
333
+ ) : null}
334
+
335
+ {enableResponse?.backupCodes?.length ? (
336
+ <InlineNotice tone="success">
337
+ <SgStack gap={12}>
338
+ <SgStack direction="row" wrap justify="between" align="center" gap={12}>
339
+ <div className="font-medium">
340
+ {t("security.two_factor.backup_codes")}
341
+ </div>
342
+ <SgButton
343
+ appearance="solid"
344
+ severity="primary"
345
+ shape="rounded"
346
+ label={t("security.two_factor.download_backup_codes_csv")}
347
+ onClick={handleDownloadBackupCodesCsv}
348
+ />
349
+ </SgStack>
350
+ <div>{t("security.two_factor.backup_codes_hint")}</div>
351
+ <SgGrid columns={2} gap={8}>
352
+ {enableResponse.backupCodes.map((code) => (
353
+ <div
354
+ key={code}
355
+ className="rounded-xl border border-emerald-200 bg-white px-3 py-2 font-mono text-sm"
356
+ >
357
+ {code}
358
+ </div>
359
+ ))}
360
+ </SgGrid>
361
+ </SgStack>
362
+ </InlineNotice>
363
+ ) : null}
364
+ </SgStack>
365
+ </SgGrid>
366
+ </SgStack>
367
+ ) : null}
368
+ </PageFrame>
369
+ );
370
+ }
@@ -0,0 +1,12 @@
1
+ import { CompanyManagementGuard, RouteGuard } from "@/modules/security";
2
+ import { CompanyEditor } from "@/app/security/companies/editors/CompanyEditor";
3
+
4
+ export default function NewCompanyPage() {
5
+ return (
6
+ <CompanyManagementGuard>
7
+ <RouteGuard permissions={["COMPANY_CREATE"]}>
8
+ <CompanyEditor />
9
+ </RouteGuard>
10
+ </CompanyManagementGuard>
11
+ );
12
+ }
@@ -0,0 +1,18 @@
1
+ import { CompanyManagementGuard, RouteGuard } from "@/modules/security";
2
+ import { CompanyEditor } from "@/app/security/companies/editors/CompanyEditor";
3
+
4
+ export default async function EditCompanyPage({
5
+ params,
6
+ }: {
7
+ params: Promise<{ publicId: string }>;
8
+ }) {
9
+ const resolvedParams = await params;
10
+
11
+ return (
12
+ <CompanyManagementGuard>
13
+ <RouteGuard permissions={["COMPANY_READ"]}>
14
+ <CompanyEditor publicId={resolvedParams.publicId} />
15
+ </RouteGuard>
16
+ </CompanyManagementGuard>
17
+ );
18
+ }
@@ -0,0 +1,298 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+
5
+ import {
6
+ SgButton,
7
+ SgConfirmationDialog,
8
+ SgDatatable,
9
+ SgInputText,
10
+ SgStack,
11
+ toast,
12
+ } from "@seedgrid/fe-components";
13
+ import { Pencil, Trash2 } from "lucide-react";
14
+ import { useRouter } from "next/navigation";
15
+
16
+ import { useI18n } from "@/i18n";
17
+ import {
18
+ CompanyManagementGuard,
19
+ InlineNotice,
20
+ PageFrame,
21
+ RouteGuard,
22
+ StatusBadge,
23
+ extractApiMessage,
24
+ securityConfig,
25
+ securityService,
26
+ useCrudPermission,
27
+ useDebouncedSearch,
28
+ type CompanySummary,
29
+ type SortState,
30
+ } from "@/modules/security";
31
+ import { isSearchReady } from "@/modules/security/utils";
32
+
33
+ export default function CompaniesListPage() {
34
+ return (
35
+ <CompanyManagementGuard>
36
+ <RouteGuard permissions={["COMPANY_READ"]}>
37
+ <CompaniesListScreen />
38
+ </RouteGuard>
39
+ </CompanyManagementGuard>
40
+ );
41
+ }
42
+
43
+ function CompaniesListScreen() {
44
+ const router = useRouter();
45
+ const { t } = useI18n();
46
+ const permissions = useCrudPermission("COMPANY");
47
+ const [companies, setCompanies] = useState<CompanySummary[]>([]);
48
+ const [page, setPage] = useState(0);
49
+ // <number> explicito: defaultPageSize e um literal (ex.: 10), entao sem a
50
+ // anotacao o useState inferiria o tipo `10` e setRows(event.rows ?? ...)
51
+ // recusaria um number qualquer (TS2345 SetStateAction<10>).
52
+ const [rows, setRows] = useState<number>(securityConfig.pagination.defaultPageSize);
53
+ const [totalRecords, setTotalRecords] = useState(0);
54
+ const [loading, setLoading] = useState(true);
55
+ const [searchValue, setSearchValue] = useState("");
56
+ const [sort, setSort] = useState<SortState>({
57
+ sortField: "tradeName",
58
+ sortOrder: 1,
59
+ });
60
+ const [selectedCompany, setSelectedCompany] = useState<CompanySummary | null>(
61
+ null
62
+ );
63
+ const debouncedSearch = useDebouncedSearch(searchValue);
64
+ const effectiveSearch = isSearchReady(debouncedSearch) ? debouncedSearch : "";
65
+
66
+ const searchInputProps = {
67
+ value: searchValue,
68
+ onChange: (event: { target: { value: string } }) => {
69
+ setSearchValue(event.target.value);
70
+ setPage(0);
71
+ },
72
+ };
73
+
74
+ useEffect(() => {
75
+ let active = true;
76
+ setLoading(true);
77
+
78
+ void (async () => {
79
+ try {
80
+ const response = await securityService.listCompaniesPage({
81
+ page,
82
+ size: rows,
83
+ search: effectiveSearch,
84
+ sort,
85
+ });
86
+
87
+ if (!active) {
88
+ return;
89
+ }
90
+
91
+ setCompanies(response.items);
92
+ setTotalRecords(response.total);
93
+ } catch (error) {
94
+ if (!active) {
95
+ return;
96
+ }
97
+
98
+ toast.error(
99
+ extractApiMessage(error) ??
100
+ t("security.companies.list.delete_error")
101
+ );
102
+ } finally {
103
+ if (active) {
104
+ setLoading(false);
105
+ }
106
+ }
107
+ })();
108
+
109
+ return () => {
110
+ active = false;
111
+ };
112
+ }, [effectiveSearch, page, rows, sort, t]);
113
+
114
+ const columns = useMemo(
115
+ () => [
116
+ {
117
+ field: "tradeName",
118
+ header: t("security.companies.list.column.trade_name"),
119
+ sortable: true,
120
+ },
121
+ {
122
+ field: "corporateName",
123
+ header: t("security.companies.list.column.corporate_name"),
124
+ sortable: true,
125
+ },
126
+ {
127
+ field: "cnpj",
128
+ header: t("security.companies.list.column.cnpj"),
129
+ },
130
+ {
131
+ field: "addrCity",
132
+ header: t("security.companies.list.column.location"),
133
+ body: (company: CompanySummary) =>
134
+ [company.addrCity, company.addrState].filter(Boolean).join(" / ") ||
135
+ "-",
136
+ },
137
+ {
138
+ field: "active",
139
+ header: t("security.companies.list.column.status"),
140
+ body: (company: CompanySummary) => (
141
+ <StatusBadge
142
+ active={Boolean(company.active)}
143
+ activeLabel={t("security.common.enabled")}
144
+ inactiveLabel={t("security.common.disabled")}
145
+ />
146
+ ),
147
+ },
148
+ {
149
+ header: t("security.common.actions"),
150
+ body: (company: CompanySummary) => (
151
+ <SgStack direction="row" wrap gap={8} data-sg-stop-row-select="true">
152
+ <SgButton
153
+ appearance="ghost"
154
+ severity="info"
155
+ iconOnly
156
+ leftIcon={<Pencil size={16} />}
157
+ title={
158
+ permissions.canUpdate
159
+ ? t("security.companies.list.action.edit")
160
+ : t("security.companies.list.action.view")
161
+ }
162
+ aria-label={
163
+ permissions.canUpdate
164
+ ? t("security.companies.list.action.edit")
165
+ : t("security.companies.list.action.view")
166
+ }
167
+ onClick={() => {
168
+ router.push(
169
+ `${securityConfig.routes.companies}/${company.publicId}`
170
+ );
171
+ }}
172
+ className="border border-slate-200/80 bg-slate-50/55 text-slate-700 hover:border-sky-200 hover:bg-slate-50 hover:text-sky-700"
173
+ />
174
+ {permissions.canDelete ? (
175
+ <SgButton
176
+ appearance="ghost"
177
+ severity="danger"
178
+ iconOnly
179
+ leftIcon={<Trash2 size={16} />}
180
+ title={t("security.companies.list.action.delete")}
181
+ aria-label={t("security.companies.list.action.delete")}
182
+ onClick={() => {
183
+ setSelectedCompany(company);
184
+ }}
185
+ className="border border-rose-200/80 bg-rose-50/45 text-rose-600 hover:border-rose-200 hover:bg-rose-50/70 hover:text-rose-700"
186
+ />
187
+ ) : null}
188
+ </SgStack>
189
+ ),
190
+ },
191
+ ],
192
+ [permissions.canDelete, permissions.canUpdate, router, t]
193
+ );
194
+
195
+ async function handleDelete() {
196
+ if (!selectedCompany) {
197
+ return;
198
+ }
199
+
200
+ try {
201
+ await securityService.deleteCompany(selectedCompany.publicId);
202
+ toast.success(t("security.companies.list.delete_success"));
203
+ setSelectedCompany(null);
204
+ setLoading(true);
205
+ const response = await securityService.listCompaniesPage({
206
+ page,
207
+ size: rows,
208
+ search: effectiveSearch,
209
+ sort,
210
+ });
211
+ setCompanies(response.items);
212
+ setTotalRecords(response.total);
213
+ } catch (error) {
214
+ toast.error(
215
+ extractApiMessage(error) ?? t("security.companies.list.delete_error")
216
+ );
217
+ } finally {
218
+ setLoading(false);
219
+ }
220
+ }
221
+
222
+ return (
223
+ <PageFrame
224
+ title={t("security.companies.list.title")}
225
+ description={t("security.companies.list.description")}
226
+ readonlyLabel={
227
+ permissions.isReadOnly ? t("security.common.readonly") : null
228
+ }
229
+ actions={
230
+ permissions.canCreate ? (
231
+ <SgButton
232
+ appearance="solid"
233
+ severity="primary"
234
+ shape="rounded"
235
+ label={t("security.companies.list.new")}
236
+ onClick={() => {
237
+ router.push(`${securityConfig.routes.companies}/new`);
238
+ }}
239
+ />
240
+ ) : undefined
241
+ }
242
+ >
243
+ <SgStack gap={16}>
244
+ <SgInputText
245
+ id="security-companies-search"
246
+ label={t("security.common.search")}
247
+ inputProps={searchInputProps}
248
+ placeholder={t("security.companies.list.search_placeholder")}
249
+ />
250
+
251
+ {!isSearchReady(searchValue) && searchValue.trim().length > 0 ? (
252
+ <InlineNotice>{t("security.common.search_hint")}</InlineNotice>
253
+ ) : null}
254
+
255
+ <SgDatatable<CompanySummary>
256
+ title=" "
257
+ value={companies}
258
+ columns={columns}
259
+ dataKey="publicId"
260
+ lazy
261
+ paginator
262
+ rows={rows}
263
+ first={page * rows}
264
+ totalRecords={totalRecords}
265
+ sortField={sort.sortField}
266
+ sortOrder={sort.sortOrder}
267
+ loading={loading}
268
+ emptyMessage={t("security.companies.list.empty")}
269
+ onPage={(event) => {
270
+ setRows(event.rows ?? securityConfig.pagination.defaultPageSize);
271
+ setPage(event.page ?? 0);
272
+ }}
273
+ onSort={(event) => {
274
+ setPage(0);
275
+ setSort({
276
+ sortField: event.sortField ?? "tradeName",
277
+ sortOrder:
278
+ event.sortOrder === 1 || event.sortOrder === -1
279
+ ? event.sortOrder
280
+ : 1,
281
+ });
282
+ }}
283
+ />
284
+ </SgStack>
285
+
286
+ <SgConfirmationDialog
287
+ open={Boolean(selectedCompany)}
288
+ severity="danger"
289
+ title={t("security.companies.list.delete_title")}
290
+ message={t("security.companies.list.delete_message")}
291
+ onCancel={() => {
292
+ setSelectedCompany(null);
293
+ }}
294
+ onConfirm={handleDelete}
295
+ />
296
+ </PageFrame>
297
+ );
298
+ }