@seedgrid/fe-security 2026.7.0 → 2026.7.1

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 (124) hide show
  1. package/dist/client/index.d.ts +4 -0
  2. package/dist/client/index.d.ts.map +1 -0
  3. package/dist/client/index.js +5 -0
  4. package/dist/client/permission-provider.d.ts +18 -0
  5. package/dist/client/permission-provider.d.ts.map +1 -0
  6. package/dist/client/permission-provider.js +57 -0
  7. package/dist/client/pick-list-field.d.ts +15 -0
  8. package/dist/client/pick-list-field.d.ts.map +1 -0
  9. package/dist/client/pick-list-field.js +30 -0
  10. package/dist/client/status-badge.d.ts +6 -0
  11. package/dist/client/status-badge.d.ts.map +1 -0
  12. package/dist/client/status-badge.js +6 -0
  13. package/dist/config.d.ts +34 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +4 -0
  16. package/dist/dtos.d.ts +88 -0
  17. package/dist/dtos.d.ts.map +1 -0
  18. package/dist/dtos.js +3 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -0
  22. package/dist/menu.d.ts +19 -0
  23. package/dist/menu.d.ts.map +1 -0
  24. package/dist/menu.js +41 -0
  25. package/dist/messages.d.ts +1858 -0
  26. package/dist/messages.d.ts.map +1 -0
  27. package/dist/messages.js +990 -0
  28. package/dist/module.d.ts +3 -0
  29. package/dist/module.d.ts.map +1 -0
  30. package/dist/module.js +37 -0
  31. package/dist/module.json +21 -0
  32. package/dist/paths.d.ts +1 -0
  33. package/dist/paths.d.ts.map +1 -1
  34. package/dist/paths.js +3 -0
  35. package/dist/permissions.d.ts +14 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +39 -0
  38. package/dist/server/action.d.ts +3 -0
  39. package/dist/server/action.d.ts.map +1 -0
  40. package/dist/server/action.js +21 -0
  41. package/dist/server/create-security-server.d.ts +4 -0
  42. package/dist/server/create-security-server.d.ts.map +1 -0
  43. package/dist/server/create-security-server.js +166 -0
  44. package/dist/server/errors.d.ts +13 -0
  45. package/dist/server/errors.d.ts.map +1 -0
  46. package/dist/server/errors.js +26 -0
  47. package/dist/server/index.d.ts +8 -0
  48. package/dist/server/index.d.ts.map +1 -0
  49. package/dist/server/index.js +8 -0
  50. package/dist/server/problem-response.d.ts +3 -0
  51. package/dist/server/problem-response.d.ts.map +1 -0
  52. package/dist/server/problem-response.js +28 -0
  53. package/dist/server/route.d.ts +3 -0
  54. package/dist/server/route.d.ts.map +1 -0
  55. package/dist/server/route.js +22 -0
  56. package/dist/server/to-problem.d.ts +3 -0
  57. package/dist/server/to-problem.d.ts.map +1 -0
  58. package/dist/server/to-problem.js +22 -0
  59. package/dist/server-contract.d.ts +23 -0
  60. package/dist/server-contract.d.ts.map +1 -0
  61. package/dist/server-contract.js +4 -0
  62. package/dist/session-contract.d.ts +36 -0
  63. package/dist/session-contract.d.ts.map +1 -0
  64. package/dist/session-contract.js +8 -0
  65. package/dist/session-result.d.ts +22 -0
  66. package/dist/session-result.d.ts.map +1 -0
  67. package/dist/session-result.js +14 -0
  68. package/package.json +38 -3
  69. package/scaffold/README.md +97 -0
  70. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  71. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  73. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  74. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  75. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  76. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  77. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  78. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  79. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  80. package/scaffold/src/app/auth/login/page.tsx +386 -0
  81. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  82. package/scaffold/src/app/login/page.tsx +7 -0
  83. package/scaffold/src/app/page.tsx +12 -0
  84. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  85. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  86. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  87. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  88. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  89. package/scaffold/src/app/security/profile/page.tsx +492 -0
  90. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  91. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  92. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  93. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  94. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  95. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  96. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  97. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  98. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  99. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  100. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  101. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  102. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  103. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  104. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  105. package/scaffold/src/modules/security/config.ts +55 -0
  106. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  107. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  108. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  109. package/scaffold/src/modules/security/host-access.ts +59 -0
  110. package/scaffold/src/modules/security/index.ts +44 -0
  111. package/scaffold/src/modules/security/navigation.ts +41 -0
  112. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  113. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  114. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  115. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  116. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  117. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  118. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  119. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  120. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  121. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  122. package/scaffold/src/modules/security/types.ts +295 -0
  123. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  124. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,754 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ import {
6
+ SgAutocomplete,
7
+ SgButton,
8
+ SgConfirmationDialog,
9
+ SgGrid,
10
+ SgGroupBox,
11
+ SgInputCNPJ,
12
+ SgInputCPF,
13
+ SgInputEmail,
14
+ SgInputPhone,
15
+ SgInputPostalCode,
16
+ SgInputText,
17
+ SgStack,
18
+ SgToggleSwitch,
19
+ toast,
20
+ } from "@seedgrid/fe-components";
21
+ import { useRouter } from "next/navigation";
22
+ import { type FieldErrors, useForm } from "react-hook-form";
23
+
24
+ import { useI18n } from "@/i18n";
25
+ import {
26
+ InlineNotice,
27
+ PageFrame,
28
+ extractApiMessage,
29
+ securityConfig,
30
+ securityService,
31
+ type CompanySummary,
32
+ useCrudPermission,
33
+ } from "@/modules/security";
34
+
35
+ type CompanyEditorProps = {
36
+ publicId?: string;
37
+ };
38
+
39
+ type CompanyFormValues = {
40
+ cnpj: string;
41
+ corporateName: string;
42
+ tradeName: string;
43
+ active: boolean;
44
+ addrStreet: string;
45
+ addrNumber: string;
46
+ addrComplement: string;
47
+ addrPostalCode: string;
48
+ addrCity: string;
49
+ addrState: string;
50
+ addrDistrict: string;
51
+ addrPhone: string;
52
+ repFirstName: string;
53
+ repLastName: string;
54
+ repCpf: string;
55
+ repPhone: string;
56
+ repWhatsapp: string;
57
+ repEmail: string;
58
+ };
59
+
60
+ const EMPTY_FORM: CompanyFormValues = {
61
+ cnpj: "",
62
+ corporateName: "",
63
+ tradeName: "",
64
+ active: true,
65
+ addrStreet: "",
66
+ addrNumber: "",
67
+ addrComplement: "",
68
+ addrPostalCode: "",
69
+ addrCity: "",
70
+ addrState: "",
71
+ addrDistrict: "",
72
+ addrPhone: "",
73
+ repFirstName: "",
74
+ repLastName: "",
75
+ repCpf: "",
76
+ repPhone: "",
77
+ repWhatsapp: "",
78
+ repEmail: "",
79
+ };
80
+
81
+ const BRAZIL_STATES = [
82
+ { id: "AC", label: "AC - Acre", value: "AC" },
83
+ { id: "AL", label: "AL - Alagoas", value: "AL" },
84
+ { id: "AP", label: "AP - Amapa", value: "AP" },
85
+ { id: "AM", label: "AM - Amazonas", value: "AM" },
86
+ { id: "BA", label: "BA - Bahia", value: "BA" },
87
+ { id: "CE", label: "CE - Ceara", value: "CE" },
88
+ { id: "DF", label: "DF - Distrito Federal", value: "DF" },
89
+ { id: "ES", label: "ES - Espirito Santo", value: "ES" },
90
+ { id: "GO", label: "GO - Goias", value: "GO" },
91
+ { id: "MA", label: "MA - Maranhao", value: "MA" },
92
+ { id: "MT", label: "MT - Mato Grosso", value: "MT" },
93
+ { id: "MS", label: "MS - Mato Grosso do Sul", value: "MS" },
94
+ { id: "MG", label: "MG - Minas Gerais", value: "MG" },
95
+ { id: "PA", label: "PA - Para", value: "PA" },
96
+ { id: "PB", label: "PB - Paraiba", value: "PB" },
97
+ { id: "PR", label: "PR - Parana", value: "PR" },
98
+ { id: "PE", label: "PE - Pernambuco", value: "PE" },
99
+ { id: "PI", label: "PI - Piaui", value: "PI" },
100
+ { id: "RJ", label: "RJ - Rio de Janeiro", value: "RJ" },
101
+ { id: "RN", label: "RN - Rio Grande do Norte", value: "RN" },
102
+ { id: "RS", label: "RS - Rio Grande do Sul", value: "RS" },
103
+ { id: "RO", label: "RO - Rondonia", value: "RO" },
104
+ { id: "RR", label: "RR - Roraima", value: "RR" },
105
+ { id: "SC", label: "SC - Santa Catarina", value: "SC" },
106
+ { id: "SP", label: "SP - Sao Paulo", value: "SP" },
107
+ { id: "SE", label: "SE - Sergipe", value: "SE" },
108
+ { id: "TO", label: "TO - Tocantins", value: "TO" },
109
+ ] as const;
110
+
111
+ function readLookupString(value: unknown, ...path: string[]) {
112
+ let current: unknown = value;
113
+
114
+ for (const key of path) {
115
+ if (!current || typeof current !== "object") {
116
+ return null;
117
+ }
118
+
119
+ current = Reflect.get(current, key);
120
+ }
121
+
122
+ return normalizeLookupValue(current);
123
+ }
124
+
125
+ function normalizeLookupValue(value: unknown) {
126
+ return typeof value === "string" && value.trim() ? value.trim() : null;
127
+ }
128
+
129
+ function resolveLookupValue(nextValue: string | null, currentValue: string) {
130
+ return nextValue ?? currentValue;
131
+ }
132
+
133
+ function joinStreetLine(type: string | null, street: string | null) {
134
+ const parts = [type, street].filter(Boolean);
135
+ return parts.length ? parts.join(" ") : null;
136
+ }
137
+
138
+ function formatPostalCode(value: string | null) {
139
+ const digits = (value ?? "").replace(/\D/g, "").slice(0, 8);
140
+
141
+ if (digits.length <= 5) {
142
+ return digits || null;
143
+ }
144
+
145
+ return `${digits.slice(0, 5)}-${digits.slice(5)}`;
146
+ }
147
+
148
+ function formatBrazilPhone(ddd: string | null, phone: string | null) {
149
+ const digits = `${ddd ?? ""}${phone ?? ""}`.replace(/\D/g, "").slice(0, 11);
150
+
151
+ if (digits.length < 10) {
152
+ return null;
153
+ }
154
+
155
+ const areaCode = digits.slice(0, 2);
156
+ const isMobile = digits.length === 11;
157
+ const firstPart = digits.slice(2, isMobile ? 7 : 6);
158
+ const secondPart = digits.slice(isMobile ? 7 : 6);
159
+
160
+ return `(${areaCode}) ${firstPart}-${secondPart}`;
161
+ }
162
+
163
+ function normalizeStateCode(value: string | null | undefined) {
164
+ return typeof value === "string" && value.trim()
165
+ ? value.trim().toUpperCase()
166
+ : null;
167
+ }
168
+
169
+ function searchBrazilStates(query: string) {
170
+ const normalized = query.trim().toLowerCase();
171
+
172
+ if (!normalized) {
173
+ return [...BRAZIL_STATES];
174
+ }
175
+
176
+ return BRAZIL_STATES.filter((state) =>
177
+ `${state.id} ${state.label}`.toLowerCase().includes(normalized)
178
+ );
179
+ }
180
+
181
+ export function CompanyEditor({ publicId }: CompanyEditorProps) {
182
+ const router = useRouter();
183
+ const { t } = useI18n();
184
+ const permissions = useCrudPermission("COMPANY");
185
+ const isEditing = Boolean(publicId);
186
+ const isReadOnly = isEditing
187
+ ? permissions.canRead && !permissions.canUpdate
188
+ : !permissions.canCreate;
189
+ const [loading, setLoading] = useState(true);
190
+ const [saving, setSaving] = useState(false);
191
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
192
+ const [loadError, setLoadError] = useState<string | null>(null);
193
+ const {
194
+ control,
195
+ handleSubmit,
196
+ register,
197
+ reset,
198
+ setValue,
199
+ watch,
200
+ } = useForm<CompanyFormValues>({
201
+ defaultValues: EMPTY_FORM,
202
+ });
203
+ const formValues = watch();
204
+
205
+ useEffect(() => {
206
+ let active = true;
207
+ setLoading(true);
208
+
209
+ void (async () => {
210
+ try {
211
+ if (!publicId) {
212
+ reset(EMPTY_FORM);
213
+ return;
214
+ }
215
+
216
+ const company = await securityService.getCompany(publicId);
217
+
218
+ if (!active) {
219
+ return;
220
+ }
221
+
222
+ reset({
223
+ cnpj: company.cnpj ?? "",
224
+ corporateName: company.corporateName ?? "",
225
+ tradeName: company.tradeName ?? "",
226
+ active: Boolean(company.active),
227
+ addrStreet: company.addrStreet ?? "",
228
+ addrNumber: company.addrNumber ?? "",
229
+ addrComplement: company.addrComplement ?? "",
230
+ addrPostalCode: company.addrPostalCode ?? "",
231
+ addrCity: company.addrCity ?? "",
232
+ addrState: company.addrState ?? "",
233
+ addrDistrict: company.addrDistrict ?? "",
234
+ addrPhone: company.addrPhone ?? "",
235
+ repFirstName: company.repFirstName ?? "",
236
+ repLastName: company.repLastName ?? "",
237
+ repCpf: company.repCpf ?? "",
238
+ repPhone: company.repPhone ?? "",
239
+ repWhatsapp: company.repWhatsapp ?? "",
240
+ repEmail: company.repEmail ?? "",
241
+ });
242
+ } catch (error) {
243
+ if (!active) {
244
+ return;
245
+ }
246
+
247
+ setLoadError(
248
+ extractApiMessage(error) ?? t("security.companies.form.load_error")
249
+ );
250
+ } finally {
251
+ if (active) {
252
+ setLoading(false);
253
+ }
254
+ }
255
+ })();
256
+
257
+ return () => {
258
+ active = false;
259
+ };
260
+ }, [publicId, reset, t]);
261
+
262
+ function handlePublicaCnpjResult(data: unknown | null) {
263
+ setValue(
264
+ "corporateName",
265
+ resolveLookupValue(
266
+ readLookupString(data, "razao_social"),
267
+ formValues.corporateName
268
+ )
269
+ );
270
+ setValue(
271
+ "tradeName",
272
+ resolveLookupValue(
273
+ readLookupString(data, "estabelecimento", "nome_fantasia"),
274
+ formValues.tradeName
275
+ )
276
+ );
277
+ setValue(
278
+ "addrStreet",
279
+ resolveLookupValue(
280
+ joinStreetLine(
281
+ readLookupString(data, "estabelecimento", "tipo_logradouro"),
282
+ readLookupString(data, "estabelecimento", "logradouro")
283
+ ),
284
+ formValues.addrStreet
285
+ )
286
+ );
287
+ setValue(
288
+ "addrNumber",
289
+ resolveLookupValue(
290
+ readLookupString(data, "estabelecimento", "numero"),
291
+ formValues.addrNumber
292
+ )
293
+ );
294
+ setValue(
295
+ "addrComplement",
296
+ resolveLookupValue(
297
+ readLookupString(data, "estabelecimento", "complemento"),
298
+ formValues.addrComplement
299
+ )
300
+ );
301
+ setValue(
302
+ "addrDistrict",
303
+ resolveLookupValue(
304
+ readLookupString(data, "estabelecimento", "bairro"),
305
+ formValues.addrDistrict
306
+ )
307
+ );
308
+ setValue(
309
+ "addrPostalCode",
310
+ resolveLookupValue(
311
+ formatPostalCode(readLookupString(data, "estabelecimento", "cep")),
312
+ formValues.addrPostalCode
313
+ )
314
+ );
315
+ setValue(
316
+ "addrCity",
317
+ resolveLookupValue(
318
+ readLookupString(data, "estabelecimento", "cidade", "nome"),
319
+ formValues.addrCity
320
+ )
321
+ );
322
+ setValue(
323
+ "addrState",
324
+ resolveLookupValue(
325
+ normalizeStateCode(
326
+ readLookupString(data, "estabelecimento", "estado", "sigla")
327
+ ),
328
+ formValues.addrState
329
+ )
330
+ );
331
+ setValue(
332
+ "addrPhone",
333
+ resolveLookupValue(
334
+ formatBrazilPhone(
335
+ readLookupString(data, "estabelecimento", "ddd1"),
336
+ readLookupString(data, "estabelecimento", "telefone1")
337
+ ),
338
+ formValues.addrPhone
339
+ )
340
+ );
341
+ }
342
+
343
+ function handleViaCepResult(data: {
344
+ erro?: boolean;
345
+ logradouro?: string;
346
+ complemento?: string;
347
+ bairro?: string;
348
+ localidade?: string;
349
+ uf?: string;
350
+ }) {
351
+ if (data.erro) {
352
+ return;
353
+ }
354
+
355
+ setValue(
356
+ "addrStreet",
357
+ resolveLookupValue(
358
+ normalizeLookupValue(data.logradouro),
359
+ formValues.addrStreet
360
+ )
361
+ );
362
+ setValue(
363
+ "addrComplement",
364
+ resolveLookupValue(
365
+ normalizeLookupValue(data.complemento),
366
+ formValues.addrComplement
367
+ )
368
+ );
369
+ setValue(
370
+ "addrDistrict",
371
+ resolveLookupValue(
372
+ normalizeLookupValue(data.bairro),
373
+ formValues.addrDistrict
374
+ )
375
+ );
376
+ setValue(
377
+ "addrState",
378
+ resolveLookupValue(normalizeStateCode(data.uf), formValues.addrState)
379
+ );
380
+ setValue(
381
+ "addrCity",
382
+ resolveLookupValue(
383
+ normalizeLookupValue(data.localidade),
384
+ formValues.addrCity
385
+ )
386
+ );
387
+ }
388
+
389
+ async function handleValidSubmit(values: CompanyFormValues) {
390
+ if (isReadOnly) {
391
+ return;
392
+ }
393
+
394
+ setSaving(true);
395
+
396
+ try {
397
+ const payload = {
398
+ cnpj: values.cnpj.trim() || undefined,
399
+ corporateName: values.corporateName.trim(),
400
+ tradeName: values.tradeName.trim(),
401
+ active: values.active,
402
+ addrStreet: values.addrStreet.trim() || undefined,
403
+ addrNumber: values.addrNumber.trim() || undefined,
404
+ addrComplement: values.addrComplement.trim() || undefined,
405
+ addrPostalCode: values.addrPostalCode.trim() || undefined,
406
+ addrCity: values.addrCity.trim() || undefined,
407
+ addrState: values.addrState.trim() || undefined,
408
+ addrDistrict: values.addrDistrict.trim() || undefined,
409
+ addrPhone: values.addrPhone.trim() || undefined,
410
+ repFirstName: values.repFirstName.trim() || undefined,
411
+ repLastName: values.repLastName.trim() || undefined,
412
+ repCpf: values.repCpf.trim() || undefined,
413
+ repPhone: values.repPhone.trim() || undefined,
414
+ repWhatsapp: values.repWhatsapp.trim() || undefined,
415
+ repEmail: values.repEmail.trim() || undefined,
416
+ };
417
+
418
+ const savedCompany = publicId
419
+ ? await securityService.updateCompany(publicId, payload)
420
+ : await securityService.createCompany(payload);
421
+
422
+ toast.success(t("security.companies.form.save_success"));
423
+ router.push(`${securityConfig.routes.companies}/${savedCompany.publicId}`);
424
+ } catch (error) {
425
+ toast.error(
426
+ extractApiMessage(error) ?? t("security.companies.form.save_error")
427
+ );
428
+ } finally {
429
+ setSaving(false);
430
+ }
431
+ }
432
+
433
+ function handleInvalidSubmit(fieldErrors: FieldErrors<CompanyFormValues>) {
434
+ const firstMessage =
435
+ fieldErrors.cnpj?.message ??
436
+ fieldErrors.corporateName?.message ??
437
+ fieldErrors.tradeName?.message ??
438
+ fieldErrors.addrPostalCode?.message ??
439
+ fieldErrors.addrState?.message ??
440
+ fieldErrors.repCpf?.message ??
441
+ fieldErrors.repEmail?.message;
442
+
443
+ if (firstMessage) {
444
+ toast.error(String(firstMessage));
445
+ }
446
+ }
447
+
448
+ async function handleDelete() {
449
+ if (!publicId) {
450
+ return;
451
+ }
452
+
453
+ try {
454
+ await securityService.deleteCompany(publicId);
455
+ toast.success(t("security.companies.form.delete_success"));
456
+ router.push(securityConfig.routes.companies);
457
+ } catch (error) {
458
+ toast.error(
459
+ extractApiMessage(error) ?? t("security.companies.list.delete_error")
460
+ );
461
+ } finally {
462
+ setDeleteDialogOpen(false);
463
+ }
464
+ }
465
+
466
+ return (
467
+ <PageFrame
468
+ title={
469
+ isEditing
470
+ ? t("security.companies.form.title_edit")
471
+ : t("security.companies.form.title_new")
472
+ }
473
+ description={
474
+ isEditing
475
+ ? t("security.companies.form.description_edit")
476
+ : t("security.companies.form.description_new")
477
+ }
478
+ readonlyLabel={isReadOnly ? t("security.common.readonly") : null}
479
+ actions={
480
+ <SgStack direction="row" wrap gap={8}>
481
+ <SgButton
482
+ appearance="outline"
483
+ severity="primary"
484
+ shape="rounded"
485
+ label={t("security.common.back")}
486
+ onClick={() => {
487
+ router.push(securityConfig.routes.companies);
488
+ }}
489
+ />
490
+ {isEditing && permissions.canDelete ? (
491
+ <SgButton
492
+ appearance="outline"
493
+ severity="danger"
494
+ shape="rounded"
495
+ label={t("security.common.delete")}
496
+ onClick={() => {
497
+ setDeleteDialogOpen(true);
498
+ }}
499
+ />
500
+ ) : null}
501
+ </SgStack>
502
+ }
503
+ >
504
+ {loadError ? <InlineNotice tone="danger">{loadError}</InlineNotice> : null}
505
+
506
+ {loading ? (
507
+ <InlineNotice>{t("security.common.loading")}</InlineNotice>
508
+ ) : (
509
+ <form
510
+ noValidate
511
+ onSubmit={handleSubmit(handleValidSubmit, handleInvalidSubmit)}
512
+ >
513
+ <SgStack gap={24}>
514
+ <SgGrid minItemWidth={280} gap={16}>
515
+ <SgInputCNPJ
516
+ id="security-company-cnpj"
517
+ name="cnpj"
518
+ label={t("security.companies.form.cnpj")}
519
+ register={register}
520
+ hintText={t("security.companies.form.cnpj")}
521
+ required
522
+ rules={ {
523
+ required: t("security.common.field_required"),
524
+ }}
525
+ validateOnBlur
526
+ validateWithPublicaCnpj
527
+ publicaCnpjErrorMessage={t(
528
+ "security.companies.form.cnpj_lookup_error"
529
+ )}
530
+ onPublicaCnpjResult={handlePublicaCnpjResult}
531
+ readOnly={isReadOnly}
532
+ />
533
+ <SgInputText
534
+ id="security-company-corporate-name"
535
+ name="corporateName"
536
+ label={t("security.companies.form.corporate_name")}
537
+ register={register}
538
+ hintText={t("security.companies.form.corporate_name")}
539
+ required
540
+ rules={ {
541
+ required: t("security.common.field_required"),
542
+ }}
543
+ readOnly={isReadOnly}
544
+ />
545
+ <SgInputText
546
+ id="security-company-trade-name"
547
+ name="tradeName"
548
+ label={t("security.companies.form.trade_name")}
549
+ register={register}
550
+ hintText={t("security.companies.form.trade_name")}
551
+ required
552
+ rules={ {
553
+ required: t("security.common.field_required"),
554
+ }}
555
+ readOnly={isReadOnly}
556
+ />
557
+ <SgToggleSwitch
558
+ id="security-company-active"
559
+ name="active"
560
+ label={t("security.companies.form.active")}
561
+ control={control}
562
+ readOnly={isReadOnly}
563
+ />
564
+ </SgGrid>
565
+
566
+ <SgGroupBox title={t("security.companies.form.address_section")}>
567
+ <SgGrid minItemWidth={220} gap={16}>
568
+ <SgInputPostalCode
569
+ id="security-company-addr-postal-code"
570
+ name="addrPostalCode"
571
+ label={t("security.companies.form.addr_postal_code")}
572
+ register={register}
573
+ hintText={t("security.companies.form.addr_postal_code")}
574
+ validateOnBlur
575
+ validateWithViaCep
576
+ viaCepErrorMessage={t(
577
+ "security.companies.form.postal_code_lookup_error"
578
+ )}
579
+ onViaCepResult={handleViaCepResult}
580
+ readOnly={isReadOnly}
581
+ />
582
+ <SgInputText
583
+ id="security-company-addr-street"
584
+ name="addrStreet"
585
+ label={t("security.companies.form.addr_street")}
586
+ register={register}
587
+ hintText={t("security.companies.form.addr_street")}
588
+ readOnly={isReadOnly}
589
+ />
590
+ <SgInputText
591
+ id="security-company-addr-number"
592
+ name="addrNumber"
593
+ label={t("security.companies.form.addr_number")}
594
+ register={register}
595
+ hintText={t("security.companies.form.addr_number")}
596
+ readOnly={isReadOnly}
597
+ />
598
+ <SgAutocomplete
599
+ id="security-company-addr-state"
600
+ name="addrState"
601
+ label={t("security.companies.form.addr_state")}
602
+ control={control}
603
+ hintText={t("security.companies.form.addr_state")}
604
+ source={searchBrazilStates}
605
+ minLengthForSearch={0}
606
+ openOnFocus
607
+ showDropDownButton
608
+ readOnly={isReadOnly}
609
+ required
610
+ rules={ {
611
+ required: t("security.common.field_required"),
612
+ }}
613
+ onSelect={(item) => {
614
+ setValue("addrState", String(item.value ?? item.id), {
615
+ shouldDirty: true,
616
+ shouldTouch: true,
617
+ shouldValidate: true,
618
+ });
619
+ setValue("addrCity", "", {
620
+ shouldDirty: true,
621
+ shouldTouch: true,
622
+ });
623
+ }}
624
+ formatSelection={(item) => String(item.value ?? item.id)}
625
+ renderItem={(item) => (
626
+ <SgStack direction="row" justify="between" align="center" gap={12}>
627
+ <span>{item.label}</span>
628
+ <span className="text-xs text-slate-400">
629
+ {String(item.value ?? item.id)}
630
+ </span>
631
+ </SgStack>
632
+ )}
633
+ />
634
+ <SgInputText
635
+ id="security-company-addr-city"
636
+ name="addrCity"
637
+ label={t("security.companies.form.addr_city")}
638
+ register={register}
639
+ hintText={t("security.companies.form.addr_city")}
640
+ readOnly={isReadOnly}
641
+ />
642
+ <SgInputText
643
+ id="security-company-addr-district"
644
+ name="addrDistrict"
645
+ label={t("security.companies.form.addr_district")}
646
+ register={register}
647
+ hintText={t("security.companies.form.addr_district")}
648
+ readOnly={isReadOnly}
649
+ />
650
+ <SgInputText
651
+ id="security-company-addr-complement"
652
+ name="addrComplement"
653
+ label={t("security.companies.form.addr_complement")}
654
+ register={register}
655
+ hintText={t("security.companies.form.addr_complement")}
656
+ readOnly={isReadOnly}
657
+ />
658
+ <SgInputPhone
659
+ id="security-company-addr-phone"
660
+ name="addrPhone"
661
+ label={t("security.companies.form.addr_phone")}
662
+ register={register}
663
+ hintText="(11) 99999-0000"
664
+ readOnly={isReadOnly}
665
+ />
666
+ </SgGrid>
667
+ </SgGroupBox>
668
+
669
+ <SgGroupBox
670
+ title={t("security.companies.form.legal_representative_section")}
671
+ >
672
+ <SgGrid minItemWidth={220} gap={16}>
673
+ <SgInputCPF
674
+ id="security-company-rep-cpf"
675
+ name="repCpf"
676
+ label={t("security.companies.form.rep_cpf")}
677
+ register={register}
678
+ hintText={t("security.companies.form.rep_cpf")}
679
+ readOnly={isReadOnly}
680
+ />
681
+ <SgInputText
682
+ id="security-company-rep-first-name"
683
+ name="repFirstName"
684
+ label={t("security.companies.form.rep_first_name")}
685
+ register={register}
686
+ hintText={t("security.companies.form.rep_first_name")}
687
+ readOnly={isReadOnly}
688
+ />
689
+ <SgInputText
690
+ id="security-company-rep-last-name"
691
+ name="repLastName"
692
+ label={t("security.companies.form.rep_last_name")}
693
+ register={register}
694
+ hintText={t("security.companies.form.rep_last_name")}
695
+ readOnly={isReadOnly}
696
+ />
697
+ <SgInputPhone
698
+ id="security-company-rep-phone"
699
+ name="repPhone"
700
+ label={t("security.companies.form.rep_phone")}
701
+ register={register}
702
+ hintText="(11) 99999-0000"
703
+ readOnly={isReadOnly}
704
+ />
705
+ <SgInputPhone
706
+ id="security-company-rep-whatsapp"
707
+ name="repWhatsapp"
708
+ label={t("security.companies.form.rep_whatsapp")}
709
+ register={register}
710
+ hintText="(11) 99999-0000"
711
+ readOnly={isReadOnly}
712
+ />
713
+ <SgInputEmail
714
+ id="security-company-rep-email"
715
+ name="repEmail"
716
+ label={t("security.companies.form.rep_email")}
717
+ register={register}
718
+ hintText={t("security.companies.form.rep_email")}
719
+ readOnly={isReadOnly}
720
+ />
721
+ </SgGrid>
722
+ </SgGroupBox>
723
+
724
+ {!isReadOnly ? (
725
+ <SgStack direction="row" justify="end">
726
+ <SgButton
727
+ appearance="solid"
728
+ severity="primary"
729
+ shape="rounded"
730
+ label={t("security.common.save")}
731
+ loading={saving}
732
+ disabled={saving}
733
+ type="submit"
734
+ />
735
+ </SgStack>
736
+ ) : null}
737
+ </SgStack>
738
+ </form>
739
+ )}
740
+
741
+ <SgConfirmationDialog
742
+ open={deleteDialogOpen}
743
+ severity="danger"
744
+ title={t("security.companies.list.delete_title")}
745
+ message={t("security.companies.list.delete_message")}
746
+ onCancel={() => {
747
+ setDeleteDialogOpen(false);
748
+ }}
749
+ onConfirm={handleDelete}
750
+ />
751
+ </PageFrame>
752
+ );
753
+ }
754
+