@hed-hog/core 0.0.304 → 0.0.305

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 +84 -36
  2. package/dist/auth/auth.controller.d.ts +0 -3
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +4 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +6 -3
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  17. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
  18. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
  19. package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
  20. package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
  21. package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
  22. package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
  23. package/dist/mail-sent/mail-sent.controller.js +5 -3
  24. package/dist/mail-sent/mail-sent.controller.js.map +1 -1
  25. package/dist/mail-sent/mail-sent.service.d.ts +28 -3
  26. package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
  27. package/dist/mail-sent/mail-sent.service.js +108 -3
  28. package/dist/mail-sent/mail-sent.service.js.map +1 -1
  29. package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
  30. package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
  31. package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
  32. package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
  33. package/dist/oauth/oauth.controller.d.ts +4 -2
  34. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  35. package/dist/oauth/oauth.controller.js +39 -11
  36. package/dist/oauth/oauth.controller.js.map +1 -1
  37. package/dist/oauth/oauth.errors.d.ts +16 -0
  38. package/dist/oauth/oauth.errors.d.ts.map +1 -0
  39. package/dist/oauth/oauth.errors.js +30 -0
  40. package/dist/oauth/oauth.errors.js.map +1 -0
  41. package/dist/oauth/oauth.module.d.ts.map +1 -1
  42. package/dist/oauth/oauth.module.js +10 -1
  43. package/dist/oauth/oauth.module.js.map +1 -1
  44. package/dist/oauth/oauth.service.d.ts +9 -1
  45. package/dist/oauth/oauth.service.d.ts.map +1 -1
  46. package/dist/oauth/oauth.service.js +97 -24
  47. package/dist/oauth/oauth.service.js.map +1 -1
  48. package/dist/oauth/providers/abstract.provider.d.ts +6 -0
  49. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  50. package/dist/oauth/providers/abstract.provider.js +34 -0
  51. package/dist/oauth/providers/abstract.provider.js.map +1 -1
  52. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
  53. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  54. package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
  55. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  56. package/dist/security/security.service.d.ts +1 -0
  57. package/dist/security/security.service.d.ts.map +1 -1
  58. package/dist/security/security.service.js +9 -10
  59. package/dist/security/security.service.js.map +1 -1
  60. package/dist/session/session.service.d.ts +5 -1
  61. package/dist/session/session.service.d.ts.map +1 -1
  62. package/dist/session/session.service.js +76 -10
  63. package/dist/session/session.service.js.map +1 -1
  64. package/dist/setting/setting.service.js +4 -4
  65. package/dist/setting/setting.service.js.map +1 -1
  66. package/dist/token/token.service.d.ts +1 -0
  67. package/dist/token/token.service.d.ts.map +1 -1
  68. package/dist/token/token.service.js +23 -12
  69. package/dist/token/token.service.js.map +1 -1
  70. package/dist/user/user.service.d.ts +1 -1
  71. package/dist/user/user.service.d.ts.map +1 -1
  72. package/dist/user/user.service.js +3 -0
  73. package/dist/user/user.service.js.map +1 -1
  74. package/hedhog/data/dashboard.yaml +2 -2
  75. package/hedhog/data/route.yaml +8 -0
  76. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
  77. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
  78. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
  79. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  80. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
  81. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
  82. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
  83. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
  84. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
  85. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
  86. package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
  87. package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
  88. package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
  89. package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
  90. package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
  91. package/hedhog/frontend/messages/en.json +45 -6
  92. package/hedhog/frontend/messages/pt.json +45 -6
  93. package/package.json +5 -5
  94. package/src/auth/auth.controller.ts +15 -19
  95. package/src/auth/auth.service.ts +13 -8
  96. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
  97. package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
  98. package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
  99. package/src/mail-sent/mail-sent.controller.ts +14 -2
  100. package/src/mail-sent/mail-sent.service.ts +159 -4
  101. package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
  102. package/src/oauth/oauth.controller.ts +58 -1
  103. package/src/oauth/oauth.errors.ts +32 -0
  104. package/src/oauth/oauth.module.ts +10 -1
  105. package/src/oauth/oauth.service.ts +178 -28
  106. package/src/oauth/providers/abstract.provider.ts +64 -0
  107. package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
  108. package/src/security/security.service.ts +12 -10
  109. package/src/session/session.service.ts +84 -9
  110. package/src/setting/setting.service.ts +10 -10
  111. package/src/token/token.service.ts +29 -11
  112. package/src/user/user.service.ts +10 -6
@@ -52,9 +52,13 @@ import {
52
52
  } from '@/components/ui/sheet';
53
53
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
54
54
  import { Textarea } from '@/components/ui/textarea';
55
+ import { useFormDraft } from '@/hooks/use-form-draft';
56
+ import { formatDateTime } from '@/lib/format-date';
55
57
  import { Role } from '@hed-hog/api-types';
56
58
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
57
59
  import { zodResolver } from '@hookform/resolvers/zod';
60
+ import { formatDistanceToNow } from 'date-fns';
61
+ import { enUS, ptBR } from 'date-fns/locale';
58
62
  import {
59
63
  FileText,
60
64
  Menu,
@@ -65,8 +69,8 @@ import {
65
69
  Users,
66
70
  } from 'lucide-react';
67
71
  import { useTranslations } from 'next-intl';
68
- import { useEffect, useState } from 'react';
69
- import { useForm } from 'react-hook-form';
72
+ import { useEffect, useMemo, useState } from 'react';
73
+ import { useForm, useWatch } from 'react-hook-form';
70
74
  import { toast } from 'sonner';
71
75
  import { z } from 'zod';
72
76
  import { RoleMenusSection } from './menus';
@@ -107,9 +111,53 @@ type RoleDetail = {
107
111
  locale?: Record<string, RoleLocale>;
108
112
  };
109
113
 
114
+ type RoleListItem = Role & {
115
+ role_id: number;
116
+ user_count?: number;
117
+ menu_count?: number;
118
+ route_count?: number;
119
+ };
120
+
121
+ type RoleDraftPayload = {
122
+ mode: 'create' | 'edit';
123
+ roleId: number | null;
124
+ localeCode: string;
125
+ values: {
126
+ slug: string;
127
+ name: string;
128
+ description: string;
129
+ };
130
+ };
131
+
132
+ const ROLE_CREATE_DRAFT_STORAGE_KEY = 'core-role-create-draft';
133
+ const ROLE_EDIT_DRAFT_STORAGE_KEY = 'core-role-edit-draft';
134
+
135
+ function getErrorMessage(error: unknown, fallbackMessage: string) {
136
+ if (error && typeof error === 'object') {
137
+ const record = error as {
138
+ message?: string;
139
+ response?: {
140
+ data?: {
141
+ message?: string;
142
+ error?: string;
143
+ };
144
+ };
145
+ };
146
+
147
+ return (
148
+ record.response?.data?.message ||
149
+ record.response?.data?.error ||
150
+ record.message ||
151
+ fallbackMessage
152
+ );
153
+ }
154
+
155
+ return fallbackMessage;
156
+ }
157
+
110
158
  export default function RolePage() {
111
159
  const t = useTranslations('core.RolePage');
112
- const { request, currentLocaleCode, locales } = useApp();
160
+ const { request, currentLocaleCode, locales, getSettingValue } = useApp();
113
161
 
114
162
  const [searchQuery, setSearchQuery] = useState('');
115
163
  const [isDialogOpen, setIsDialogOpen] = useState(false);
@@ -127,7 +175,7 @@ export default function RolePage() {
127
175
  data: rolesResponse,
128
176
  isLoading,
129
177
  refetch,
130
- } = useQuery<PaginatedResponse<Role>>({
178
+ } = useQuery<PaginatedResponse<RoleListItem>>({
131
179
  queryKey: ['roles', page, pageSize, searchQuery, currentLocaleCode],
132
180
  queryFn: async () => {
133
181
  const params = new URLSearchParams();
@@ -135,7 +183,7 @@ export default function RolePage() {
135
183
  params.set('pageSize', String(pageSize));
136
184
  if (searchQuery) params.set('search', searchQuery);
137
185
 
138
- const response = await request<PaginatedResponse<Role>>({
186
+ const response = await request<PaginatedResponse<RoleListItem>>({
139
187
  url: `/role?${params.toString()}`,
140
188
  method: 'GET',
141
189
  });
@@ -177,23 +225,151 @@ export default function RolePage() {
177
225
  },
178
226
  });
179
227
 
228
+ const watchedCreateValues = useWatch({
229
+ control: form.control,
230
+ });
231
+ const watchedEditValues = useWatch({
232
+ control: editForm.control,
233
+ });
234
+
235
+ const {
236
+ clearDraft: clearCreateDraft,
237
+ loadDraft: loadCreateDraft,
238
+ hasDraft: hasCreateDraft,
239
+ savedAt: createDraftSavedAt,
240
+ } = useFormDraft<RoleDraftPayload>({
241
+ storageKey: ROLE_CREATE_DRAFT_STORAGE_KEY,
242
+ value: {
243
+ mode: 'create',
244
+ roleId: null,
245
+ localeCode: currentLocaleCode,
246
+ values: {
247
+ slug: watchedCreateValues.slug ?? '',
248
+ name: watchedCreateValues.name ?? '',
249
+ description: watchedCreateValues.description ?? '',
250
+ },
251
+ },
252
+ hasData: Boolean(
253
+ (watchedCreateValues.slug ?? '').trim() ||
254
+ (watchedCreateValues.name ?? '').trim() ||
255
+ (watchedCreateValues.description ?? '').trim()
256
+ ),
257
+ enabled: isDialogOpen,
258
+ });
259
+
260
+ const {
261
+ clearDraft: clearEditDraft,
262
+ loadDraft: loadEditDraft,
263
+ hasDraft: hasEditDraft,
264
+ savedAt: editDraftSavedAt,
265
+ } = useFormDraft<RoleDraftPayload>({
266
+ storageKey: ROLE_EDIT_DRAFT_STORAGE_KEY,
267
+ value: {
268
+ mode: 'edit',
269
+ roleId: editingRole?.id ?? null,
270
+ localeCode: selectedLocale,
271
+ values: {
272
+ slug: watchedEditValues.slug ?? '',
273
+ name: watchedEditValues.name ?? '',
274
+ description: watchedEditValues.description ?? '',
275
+ },
276
+ },
277
+ hasData: Boolean(
278
+ (watchedEditValues.slug ?? '').trim() ||
279
+ (watchedEditValues.name ?? '').trim() ||
280
+ (watchedEditValues.description ?? '').trim()
281
+ ),
282
+ enabled: Boolean(editingRole),
283
+ });
284
+
285
+ const createDraftStatusContent = useMemo(() => {
286
+ if (!hasCreateDraft || !createDraftSavedAt) {
287
+ return null;
288
+ }
289
+
290
+ const savedDate = new Date(createDraftSavedAt);
291
+ if (Number.isNaN(savedDate.getTime())) {
292
+ return null;
293
+ }
294
+
295
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
296
+ const relativeLabel = formatDistanceToNow(savedDate, {
297
+ addSuffix: true,
298
+ locale,
299
+ });
300
+ const absoluteLabel = formatDateTime(
301
+ savedDate,
302
+ getSettingValue,
303
+ currentLocaleCode
304
+ );
305
+
306
+ return currentLocaleCode.startsWith('pt')
307
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
308
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
309
+ }, [createDraftSavedAt, currentLocaleCode, getSettingValue, hasCreateDraft]);
310
+
311
+ const editDraftStatusContent = useMemo(() => {
312
+ if (!hasEditDraft || !editDraftSavedAt) {
313
+ return null;
314
+ }
315
+
316
+ const savedDate = new Date(editDraftSavedAt);
317
+ if (Number.isNaN(savedDate.getTime())) {
318
+ return null;
319
+ }
320
+
321
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
322
+ const relativeLabel = formatDistanceToNow(savedDate, {
323
+ addSuffix: true,
324
+ locale,
325
+ });
326
+ const absoluteLabel = formatDateTime(
327
+ savedDate,
328
+ getSettingValue,
329
+ currentLocaleCode
330
+ );
331
+
332
+ return currentLocaleCode.startsWith('pt')
333
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
334
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
335
+ }, [editDraftSavedAt, currentLocaleCode, getSettingValue, hasEditDraft]);
336
+
337
+ const openCreateDialog = () => {
338
+ const storedDraft = loadCreateDraft();
339
+
340
+ form.reset(
341
+ storedDraft?.payload.mode === 'create'
342
+ ? storedDraft.payload.values
343
+ : {
344
+ slug: '',
345
+ name: '',
346
+ description: '',
347
+ }
348
+ );
349
+ setFormError(null);
350
+ setIsDialogOpen(true);
351
+ };
352
+
180
353
  useEffect(() => {
181
354
  if (editingRole && editingRole.locale && selectedLocale) {
355
+ const storedDraft = loadEditDraft();
356
+ const shouldRestoreDraft =
357
+ storedDraft?.payload.mode === 'edit' &&
358
+ storedDraft.payload.roleId === editingRole.id &&
359
+ storedDraft.payload.localeCode === selectedLocale;
182
360
  const localeData = editingRole.locale[selectedLocale];
183
- editForm.reset({
184
- slug: editingRole.slug || '',
185
- name: localeData?.name || '',
186
- description: localeData?.description || '',
187
- });
188
- }
189
- }, [editingRole, selectedLocale]);
190
361
 
191
- useEffect(() => {
192
- if (editingRole) {
193
- setSelectedLocale(currentLocaleCode);
194
- setActiveTab('basic-info');
362
+ editForm.reset(
363
+ shouldRestoreDraft
364
+ ? storedDraft.payload.values
365
+ : {
366
+ slug: editingRole.slug || '',
367
+ name: localeData?.name || '',
368
+ description: localeData?.description || '',
369
+ }
370
+ );
195
371
  }
196
- }, [editingRole?.id]);
372
+ }, [editForm, editingRole, loadEditDraft, selectedLocale]);
197
373
 
198
374
  const onSubmit = async (values: z.infer<typeof addRoleSchema>) => {
199
375
  try {
@@ -212,23 +388,18 @@ export default function RolePage() {
212
388
  },
213
389
  });
214
390
 
391
+ clearCreateDraft();
215
392
  form.reset();
216
393
  refetch();
217
394
  setIsDialogOpen(false);
218
395
  setFormError(null);
219
396
  toast.success(t('roleCreatedSuccess'));
220
- } catch (err: any) {
221
- const msg =
222
- err?.response?.data?.message ||
223
- err?.response?.data?.error ||
224
- err?.message ||
225
- t('serverError');
226
-
227
- setFormError(String(msg));
397
+ } catch (err: unknown) {
398
+ setFormError(String(getErrorMessage(err, t('serverError'))));
228
399
  }
229
400
  };
230
401
 
231
- const handleEdit = async (role: Role & { role_id: number }) => {
402
+ const handleEdit = async (role: RoleListItem) => {
232
403
  setEditFormError(null);
233
404
 
234
405
  try {
@@ -240,7 +411,7 @@ export default function RolePage() {
240
411
  const fullRole = response.data;
241
412
  const localeData: Record<string, RoleLocale> = {};
242
413
  if (fullRole.role_locale && Array.isArray(fullRole.role_locale)) {
243
- fullRole.role_locale.forEach((rl: any) => {
414
+ fullRole.role_locale.forEach((rl) => {
244
415
  const localeCode = rl.locale?.code;
245
416
  if (localeCode) {
246
417
  localeData[localeCode] = {
@@ -260,6 +431,15 @@ export default function RolePage() {
260
431
  }
261
432
  });
262
433
 
434
+ const storedDraft = loadEditDraft();
435
+ const nextLocale =
436
+ storedDraft?.payload.mode === 'edit' &&
437
+ storedDraft.payload.roleId === fullRole.id
438
+ ? storedDraft.payload.localeCode
439
+ : currentLocaleCode;
440
+
441
+ setSelectedLocale(nextLocale);
442
+ setActiveTab('basic-info');
263
443
  setEditingRole({
264
444
  ...fullRole,
265
445
  locale: localeData,
@@ -291,17 +471,13 @@ export default function RolePage() {
291
471
  },
292
472
  });
293
473
 
474
+ clearEditDraft();
294
475
  toast.success(t('roleUpdatedSuccess'));
295
476
  setEditFormError(null);
296
477
  await refetch();
297
478
  setEditingRole(null);
298
- } catch (err: any) {
299
- const msg =
300
- err?.response?.data?.message ||
301
- err?.response?.data?.error ||
302
- err?.message ||
303
- t('serverError');
304
- setEditFormError(String(msg));
479
+ } catch (err: unknown) {
480
+ setEditFormError(String(getErrorMessage(err, t('serverError'))));
305
481
  }
306
482
  };
307
483
 
@@ -319,13 +495,8 @@ export default function RolePage() {
319
495
  setEditingRole(null);
320
496
  setEditFormError(null);
321
497
  toast.success(t('roleDeletedSuccess'));
322
- } catch (err: any) {
323
- const msg =
324
- err?.response?.data?.message ||
325
- err?.response?.data?.error ||
326
- err?.message ||
327
- t('serverError');
328
- setEditFormError(String(msg));
498
+ } catch (err: unknown) {
499
+ setEditFormError(String(getErrorMessage(err, t('serverError'))));
329
500
  }
330
501
  };
331
502
 
@@ -342,7 +513,7 @@ export default function RolePage() {
342
513
  const localeData: Record<string, RoleLocale> = {};
343
514
 
344
515
  if (fullRole.role_locale && Array.isArray(fullRole.role_locale)) {
345
- fullRole.role_locale.forEach((rl: any) => {
516
+ fullRole.role_locale.forEach((rl) => {
346
517
  const localeCode = rl.locale?.code;
347
518
  if (localeCode) {
348
519
  localeData[localeCode] = {
@@ -378,7 +549,7 @@ export default function RolePage() {
378
549
  actions={[
379
550
  {
380
551
  label: t('buttonAddRole'),
381
- onClick: () => setIsDialogOpen(true),
552
+ onClick: openCreateDialog,
382
553
  variant: 'default',
383
554
  },
384
555
  ]}
@@ -433,71 +604,65 @@ export default function RolePage() {
433
604
  title={t('noRolesFound')}
434
605
  description={t('description')}
435
606
  actionLabel={t('buttonAddRole')}
436
- onAction={() => setIsDialogOpen(true)}
607
+ onAction={openCreateDialog}
437
608
  />
438
609
  ) : (
439
610
  <div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
440
- {(rolesResponse?.data as (Role & { role_id: number })[])?.map(
441
- (role: Role & { role_id: number }) => (
442
- <Card
443
- key={String(role.role_id)}
444
- onDoubleClick={() => handleEdit(role)}
445
- className="cursor-pointer rounded-md flex flex-col justify-between gap-2 border border-border/60 bg-card p-4 shadow-sm transition hover:border-primary"
446
- >
447
- <CardHeader className="flex items-start justify-between gap-4 p-0">
448
- <div className="flex items-center gap-3 flex-1">
449
- <div className="h-12 w-12 shrink-0 rounded-full bg-primary/10 flex items-center justify-center">
450
- <ShieldCheck className="h-6 w-6 text-primary" />
611
+ {(rolesResponse?.data as RoleListItem[])?.map((role) => (
612
+ <Card
613
+ key={String(role.role_id)}
614
+ onDoubleClick={() => handleEdit(role)}
615
+ className="cursor-pointer rounded-md flex flex-col justify-between gap-2 border border-border/60 bg-card p-4 shadow-sm transition hover:border-primary"
616
+ >
617
+ <CardHeader className="flex items-start justify-between gap-4 p-0">
618
+ <div className="flex items-center gap-3 flex-1">
619
+ <div className="h-12 w-12 shrink-0 rounded-full bg-primary/10 flex items-center justify-center">
620
+ <ShieldCheck className="h-6 w-6 text-primary" />
621
+ </div>
622
+ <div className="flex-1">
623
+ <CardTitle className="text-sm font-semibold">
624
+ {role.name}
625
+ </CardTitle>
626
+ <CardDescription className="text-xs text-muted-foreground">
627
+ {role.slug}
628
+ </CardDescription>
629
+ </div>
630
+ </div>
631
+ <Button
632
+ variant="outline"
633
+ size="sm"
634
+ onClick={() => handleEdit(role)}
635
+ >
636
+ {t('buttonEditRole')}
637
+ </Button>
638
+ </CardHeader>
639
+ {role.description && (
640
+ <CardContent className="p-0">
641
+ <p className="text-xs text-muted-foreground line-clamp-2">
642
+ {role.description}
643
+ </p>
644
+ <div className="text-xs line-clamp-2 flex gap-2 py-2">
645
+ <div>
646
+ {role.user_count ?? 0}{' '}
647
+ {(role.user_count ?? 0) === 1 ? t('user') : t('users')}
451
648
  </div>
452
- <div className="flex-1">
453
- <CardTitle className="text-sm font-semibold">
454
- {role.name}
455
- </CardTitle>
456
- <CardDescription className="text-xs text-muted-foreground">
457
- {role.slug}
458
- </CardDescription>
649
+ <div>•</div>
650
+ <div>
651
+ {role.menu_count ?? 0}{' '}
652
+ {(role.menu_count ?? 0) === 1 ? t('menu') : t('menus')}
459
653
  </div>
460
- </div>
461
- <Button
462
- variant="outline"
463
- size="sm"
464
- onClick={() => handleEdit(role)}
465
- >
466
- {t('buttonEditRole')}
467
- </Button>
468
- </CardHeader>
469
- {role.description && (
470
- <CardContent className="p-0">
471
- <p className="text-xs text-muted-foreground line-clamp-2">
472
- {role.description}
473
- </p>
474
- <div className="text-xs line-clamp-2 flex gap-2 py-2">
475
- <div>
476
- {(role as any).user_count}{' '}
477
- {(role as any).user_count === 1
478
- ? t('user')
479
- : t('users')}
480
- </div>
481
- <div>•</div>
482
- <div>
483
- {(role as any).menu_count}{' '}
484
- {(role as any).menu_count === 1
485
- ? t('menu')
486
- : t('menus')}
487
- </div>
488
- <div>•</div>
489
- <div>
490
- {(role as any).route_count}{' '}
491
- {(role as any).route_count === 1
492
- ? t('route')
493
- : t('routes')}
494
- </div>
654
+ <div>•</div>
655
+ <div>
656
+ {role.route_count ?? 0}{' '}
657
+ {(role.route_count ?? 0) === 1
658
+ ? t('route')
659
+ : t('routes')}
495
660
  </div>
496
- </CardContent>
497
- )}
498
- </Card>
499
- )
500
- )}
661
+ </div>
662
+ </CardContent>
663
+ )}
664
+ </Card>
665
+ ))}
501
666
  </div>
502
667
  )}
503
668
 
@@ -515,7 +680,15 @@ export default function RolePage() {
515
680
  />
516
681
  </div>
517
682
 
518
- <Sheet open={isDialogOpen} onOpenChange={setIsDialogOpen}>
683
+ <Sheet
684
+ open={isDialogOpen}
685
+ onOpenChange={(open) => {
686
+ setIsDialogOpen(open);
687
+ if (!open) {
688
+ setFormError(null);
689
+ }
690
+ }}
691
+ >
519
692
  <SheetContent className="w-full sm:max-w-lg overflow-y-auto gap-0">
520
693
  <SheetHeader>
521
694
  <SheetTitle>{t('dialogAddRoleTitle')}</SheetTitle>
@@ -591,6 +764,12 @@ export default function RolePage() {
591
764
  </Alert>
592
765
  )}
593
766
 
767
+ {createDraftStatusContent ? (
768
+ <p className="text-xs text-muted-foreground">
769
+ {createDraftStatusContent}
770
+ </p>
771
+ ) : null}
772
+
594
773
  <Button type="submit" className="w-full">
595
774
  {t('buttonAddRole')}
596
775
  </Button>
@@ -671,7 +850,7 @@ export default function RolePage() {
671
850
  <SelectValue placeholder={t('selectLocale')} />
672
851
  </SelectTrigger>
673
852
  <SelectContent>
674
- {locales?.map((locale: any) => (
853
+ {locales?.map((locale: Locale) => (
675
854
  <SelectItem key={locale.code} value={locale.code}>
676
855
  {locale.name}
677
856
  </SelectItem>
@@ -740,6 +919,11 @@ export default function RolePage() {
740
919
  )}
741
920
 
742
921
  <div className="flex flex-col w-full gap-2 pt-2">
922
+ {editDraftStatusContent ? (
923
+ <p className="text-xs text-muted-foreground">
924
+ {editDraftStatusContent}
925
+ </p>
926
+ ) : null}
743
927
  <Button type="submit" className="w-full">
744
928
  {t('saveChanges')}
745
929
  </Button>