@hed-hog/core 0.0.360 → 0.0.362

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 (153) hide show
  1. package/dist/access-log/access-log.controller.d.ts +1 -1
  2. package/dist/access-log/access-log.service.d.ts +1 -1
  3. package/dist/access-log/access-log.service.d.ts.map +1 -1
  4. package/dist/access-log/access-log.service.js +43 -1
  5. package/dist/access-log/access-log.service.js.map +1 -1
  6. package/dist/ai/ai.service.d.ts +1 -1
  7. package/dist/auth/auth.controller.d.ts +27 -26
  8. package/dist/auth/auth.controller.d.ts.map +1 -1
  9. package/dist/auth/auth.controller.js +6 -2
  10. package/dist/auth/auth.controller.js.map +1 -1
  11. package/dist/auth/auth.service.d.ts +35 -34
  12. package/dist/auth/auth.service.d.ts.map +1 -1
  13. package/dist/auth/auth.service.js +15 -5
  14. package/dist/auth/auth.service.js.map +1 -1
  15. package/dist/auth/guards/auth.guard.d.ts +1 -0
  16. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  17. package/dist/auth/guards/auth.guard.js +11 -4
  18. package/dist/auth/guards/auth.guard.js.map +1 -1
  19. package/dist/challenge/challenge.service.d.ts +12 -12
  20. package/dist/core/system.controller.d.ts +2 -2
  21. package/dist/core/system.service.d.ts +2 -2
  22. package/dist/dashboard/dashboard/dashboard.controller.d.ts +66 -66
  23. package/dist/dashboard/dashboard/dashboard.service.d.ts +66 -66
  24. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +26 -26
  25. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +26 -26
  26. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +34 -34
  27. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +34 -34
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +8 -8
  29. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +8 -8
  30. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +5 -5
  31. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +5 -5
  32. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +25 -25
  33. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +25 -25
  34. package/dist/file/file.controller.d.ts +6 -6
  35. package/dist/file/file.module.d.ts.map +1 -1
  36. package/dist/file/file.module.js +3 -2
  37. package/dist/file/file.module.js.map +1 -1
  38. package/dist/file/file.service.d.ts +121 -25
  39. package/dist/file/file.service.d.ts.map +1 -1
  40. package/dist/file/file.service.js +217 -32
  41. package/dist/file/file.service.js.map +1 -1
  42. package/dist/file/image-optimization.service.d.ts +20 -0
  43. package/dist/file/image-optimization.service.d.ts.map +1 -0
  44. package/dist/file/image-optimization.service.js +126 -0
  45. package/dist/file/image-optimization.service.js.map +1 -0
  46. package/dist/file/provider/local.provider.d.ts +1 -1
  47. package/dist/file/provider/local.provider.d.ts.map +1 -1
  48. package/dist/file/provider/local.provider.js +2 -9
  49. package/dist/file/provider/local.provider.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/integration-profile/integration-profile.controller.d.ts +15 -15
  55. package/dist/integration-profile/integration-profile.service.d.ts +15 -15
  56. package/dist/locale/locale.controller.d.ts +4 -4
  57. package/dist/locale/locale.service.d.ts +4 -4
  58. package/dist/mail/mail.controller.d.ts +7 -7
  59. package/dist/mail/mail.service.d.ts +7 -7
  60. package/dist/mail-sent/mail-sent.controller.d.ts +12 -12
  61. package/dist/mail-sent/mail-sent.service.d.ts +12 -12
  62. package/dist/mcp/mcp-api-key.controller.d.ts +2 -2
  63. package/dist/mcp/mcp-api-key.service.d.ts +2 -2
  64. package/dist/mcp/tools/core-users.mcp-tools.d.ts +1 -1
  65. package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -1
  66. package/dist/mcp/tools/core-users.mcp-tools.js +2 -2
  67. package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -1
  68. package/dist/mcp-chat/mcp-chat.controller.d.ts +8 -8
  69. package/dist/mcp-chat/mcp-chat.service.d.ts +8 -8
  70. package/dist/menu/menu.controller.d.ts +10 -10
  71. package/dist/menu/menu.service.d.ts +10 -10
  72. package/dist/notification/notification.controller.d.ts +36 -36
  73. package/dist/notification/notification.service.d.ts +36 -36
  74. package/dist/profile/profile.controller.d.ts +16 -16
  75. package/dist/profile/profile.service.d.ts +16 -16
  76. package/dist/profile/profile.service.d.ts.map +1 -1
  77. package/dist/profile/profile.service.js +44 -23
  78. package/dist/profile/profile.service.js.map +1 -1
  79. package/dist/role/role.controller.d.ts +6 -6
  80. package/dist/role/role.service.d.ts +6 -6
  81. package/dist/screen/screen.controller.d.ts +3 -3
  82. package/dist/screen/screen.service.d.ts +3 -3
  83. package/dist/session/session.controller.d.ts +2 -2
  84. package/dist/session/session.service.d.ts +6 -6
  85. package/dist/session/session.service.d.ts.map +1 -1
  86. package/dist/session/session.service.js +7 -2
  87. package/dist/session/session.service.js.map +1 -1
  88. package/dist/setting/setting.controller.d.ts +18 -18
  89. package/dist/setting/setting.service.d.ts +18 -18
  90. package/dist/setting/setting.service.d.ts.map +1 -1
  91. package/dist/setting/setting.service.js +0 -1
  92. package/dist/setting/setting.service.js.map +1 -1
  93. package/dist/token/token.service.d.ts +1 -0
  94. package/dist/token/token.service.d.ts.map +1 -1
  95. package/dist/token/token.service.js +23 -6
  96. package/dist/token/token.service.js.map +1 -1
  97. package/dist/user/user.controller.d.ts +30 -30
  98. package/dist/user/user.controller.d.ts.map +1 -1
  99. package/dist/user/user.controller.js +4 -3
  100. package/dist/user/user.controller.js.map +1 -1
  101. package/dist/user/user.service.d.ts +68 -68
  102. package/dist/user/user.service.d.ts.map +1 -1
  103. package/dist/user/user.service.js +6 -6
  104. package/dist/user/user.service.js.map +1 -1
  105. package/dist/webhook-integration/webhook-integration.controller.d.ts +2 -2
  106. package/dist/webhook-integration/webhook-integration.service.d.ts +2 -2
  107. package/hedhog/data/route.yaml +2 -0
  108. package/hedhog/data/setting_group.yaml +117 -26
  109. package/hedhog/data/setting_subgroup.yaml +8 -0
  110. package/hedhog/frontend/app/access-log/page.tsx.ejs +170 -11
  111. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +1 -1
  112. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +1 -1
  113. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +113 -38
  114. package/hedhog/frontend/app/users/page.tsx.ejs +7 -7
  115. package/hedhog/frontend/app/users/permissions.tsx.ejs +137 -83
  116. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +28 -26
  117. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +207 -75
  118. package/hedhog/frontend/messages/en.json +9 -2
  119. package/hedhog/frontend/messages/pt.json +9 -2
  120. package/hedhog/table/setting_runtime_state.yaml +9 -0
  121. package/package.json +12 -6
  122. package/src/access-log/access-log.service.ts +19 -1
  123. package/src/auth/auth.controller.ts +5 -0
  124. package/src/auth/auth.service.ts +14 -4
  125. package/src/auth/guards/auth.guard.ts +10 -3
  126. package/src/file/file.module.ts +3 -2
  127. package/src/file/file.service.ts +448 -46
  128. package/src/file/image-optimization.service.ts +123 -0
  129. package/src/file/provider/local.provider.ts +2 -9
  130. package/src/index.ts +1 -0
  131. package/src/mcp/tools/core-users.mcp-tools.ts +267 -267
  132. package/src/profile/profile.service.ts +40 -22
  133. package/src/session/session.service.ts +10 -2
  134. package/src/setting/setting.service.ts +0 -1
  135. package/src/token/token.service.ts +26 -6
  136. package/src/user/user.controller.ts +5 -2
  137. package/src/user/user.service.ts +11 -8
  138. package/dist/menu/menu.service.spec.d.ts +0 -1
  139. package/dist/menu/menu.service.spec.d.ts.map +0 -1
  140. package/dist/menu/menu.service.spec.js +0 -208
  141. package/dist/menu/menu.service.spec.js.map +0 -1
  142. package/dist/route/route.service.spec.d.ts +0 -1
  143. package/dist/route/route.service.spec.d.ts.map +0 -1
  144. package/dist/route/route.service.spec.js +0 -262
  145. package/dist/route/route.service.spec.js.map +0 -1
  146. package/dist/screen/screen.service.spec.d.ts +0 -1
  147. package/dist/screen/screen.service.spec.d.ts.map +0 -1
  148. package/dist/screen/screen.service.spec.js +0 -259
  149. package/dist/screen/screen.service.spec.js.map +0 -1
  150. package/dist/user/user.service.spec.d.ts +0 -1
  151. package/dist/user/user.service.spec.d.ts.map +0 -1
  152. package/dist/user/user.service.spec.js +0 -261
  153. package/dist/user/user.service.spec.js.map +0 -1
@@ -87,6 +87,7 @@ function getLocalizedIntegrationName(
87
87
  );
88
88
  }
89
89
 
90
+ const MAIL_INTEGRATION_PROFILE_SETTING_SLUG = 'mail-integration-profile-id';
90
91
  const DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG =
91
92
  'operations.daily-report.email-integration-profile-id';
92
93
  const DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG =
@@ -103,14 +104,6 @@ const OAUTH_PROFILE_SLUG_TO_PROVIDER: Record<string, string> = {
103
104
  'oauth-microsoft-profile-id': 'microsoft-oauth',
104
105
  'oauth-microsoft-entra-id-profile-id': 'microsoft-entra-id-oauth',
105
106
  };
106
- const ADMIN_LOGIN_OAUTH_SETTING_SLUG = 'admin-login-page-oauth-providers';
107
- const ADMIN_LOGIN_PROVIDER_ENABLED_SLUG_BY_PROVIDER: Record<string, string> = {
108
- facebook: 'oauth-facebook-enabled',
109
- github: 'oauth-github-enabled',
110
- google: 'oauth-google-enabled',
111
- microsoft: 'oauth-microsoft-enabled',
112
- 'microsoft-entra-id': 'oauth-microsoft-entra-id-enabled',
113
- };
114
107
  const INTEGRATION_PROFILE_PICKER_SLUGS = [
115
108
  'file-storage-profile-id',
116
109
  ...AI_PROFILE_SETTING_SLUGS,
@@ -524,8 +517,6 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
524
517
  const selectedValues: string[] = Array.isArray(parsedValue)
525
518
  ? parsedValue
526
519
  : [];
527
- const isAdminLoginOAuthSetting =
528
- setting.slug === ADMIN_LOGIN_OAUTH_SETTING_SLUG;
529
520
 
530
521
  return (
531
522
  <div
@@ -535,25 +526,12 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
535
526
  )}
536
527
  >
537
528
  {options.map((option) => {
538
- const enabledSettingSlug =
539
- ADMIN_LOGIN_PROVIDER_ENABLED_SLUG_BY_PROVIDER[option.value];
540
- const isProviderGloballyEnabled = enabledSettingSlug
541
- ? getSettingValue(enabledSettingSlug) === true
542
- : true;
543
- const isOptionDisabled =
544
- isAdminLoginOAuthSetting && !isProviderGloballyEnabled;
545
-
546
529
  return (
547
530
  <div key={option.id} className="flex items-center space-x-2">
548
531
  <Checkbox
549
532
  className="bg-background"
550
533
  checked={selectedValues.includes(option.value)}
551
- disabled={isOptionDisabled}
552
534
  onCheckedChange={(checked) => {
553
- if (isOptionDisabled) {
554
- return;
555
- }
556
-
557
535
  let newValues = [...selectedValues];
558
536
  if (checked) {
559
537
  newValues.push(option.value);
@@ -565,19 +543,10 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
565
543
  handleChangeValue(formatValue(newValues), true);
566
544
  }}
567
545
  />
568
- <Label className={cn(isOptionDisabled && 'opacity-60')}>
569
- {option.value}
570
- </Label>
546
+ <Label>{option.value}</Label>
571
547
  </div>
572
548
  );
573
549
  })}
574
- {isAdminLoginOAuthSetting && (
575
- <p className="text-xs text-muted-foreground">
576
- {currentLocaleCode.startsWith('pt')
577
- ? 'Providers desabilitados globalmente (oauth-*-enabled = false) ficam indisponíveis nesta lista.'
578
- : 'Providers disabled globally (oauth-*-enabled = false) are unavailable in this list.'}
579
- </p>
580
- )}
581
550
  </div>
582
551
  );
583
552
  }
@@ -667,7 +636,10 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
667
636
  setting.slug
668
637
  );
669
638
 
670
- if (setting.slug === 'file-storage-profile-id') {
639
+ if (
640
+ setting.slug === 'file-storage-profile-id' ||
641
+ setting.slug === 'lms-bulk-upload-storage-profile-id'
642
+ ) {
671
643
  const loadStorageTypeId = async () => {
672
644
  const typesResponse = await request<IntegrationTypeOption[]>({
673
645
  url: '/integration-profile/types',
@@ -1004,10 +976,113 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
1004
976
  );
1005
977
  }
1006
978
 
1007
- if (
1008
- setting.slug !== 'mail-integration-profile-id' &&
1009
- !dailyReportIntegrationTypeSlug
1010
- ) {
979
+ if (setting.slug === MAIL_INTEGRATION_PROFILE_SETTING_SLUG) {
980
+ return (
981
+ <>
982
+ <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
983
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
984
+ placeholder={t('selectMailProfile')}
985
+ emptyStateDescription={t('mailProfileEmptyState')}
986
+ value={parsedValue ? Number(parsedValue) : null}
987
+ onChange={(id, option) => {
988
+ setStorageProfileSelectedLabel(option?.name ?? '');
989
+ handleChangeValue(id != null ? String(id) : '', true);
990
+ }}
991
+ valueType="number"
992
+ initialSelectedLabel={storageProfileSelectedLabel}
993
+ showCreateButton={false}
994
+ clearable={false}
995
+ loadOptions={async ({ page, pageSize, search }) => {
996
+ const qs = new URLSearchParams({
997
+ page: String(page),
998
+ pageSize: String(pageSize),
999
+ typeSlug: 'email',
1000
+ });
1001
+ if (search) qs.set('search', search);
1002
+ const response = await request<
1003
+ ApiPaginated<
1004
+ IntegrationProfileOption & Record<string, unknown>
1005
+ >
1006
+ >({
1007
+ url: `/integration-profile?${qs}`,
1008
+ method: 'GET',
1009
+ });
1010
+ const paged = response.data;
1011
+ return {
1012
+ items: paged?.data ?? [],
1013
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1014
+ };
1015
+ }}
1016
+ getOptionValue={(opt) => opt.id}
1017
+ getOptionLabel={(opt) => String(opt.name ?? '')}
1018
+ getOptionDescription={(opt) => {
1019
+ const providerSlug = String(
1020
+ opt.integration_provider?.slug ?? ''
1021
+ );
1022
+ const providerName = getLocalizedIntegrationName(
1023
+ opt.integration_provider?.integration_provider_locale,
1024
+ currentLocaleCode
1025
+ );
1026
+ return [providerName || providerSlug, opt.slug]
1027
+ .filter(Boolean)
1028
+ .join(' • ');
1029
+ }}
1030
+ className={cn('flex-1 min-w-0', className)}
1031
+ />
1032
+
1033
+ <Button
1034
+ type="button"
1035
+ variant="outline"
1036
+ size="icon"
1037
+ className="h-10 w-10 shrink-0"
1038
+ onClick={() => {
1039
+ setStorageProfileEditingId(
1040
+ parsedValue ? Number(parsedValue) : null
1041
+ );
1042
+ setIsStorageProfileSheetOpen(true);
1043
+ }}
1044
+ disabled={!parsedValue}
1045
+ aria-label="Editar perfil de email"
1046
+ title="Editar perfil de email"
1047
+ >
1048
+ <Pencil className="h-4 w-4" />
1049
+ </Button>
1050
+
1051
+ <Button
1052
+ type="button"
1053
+ variant="outline"
1054
+ size="icon"
1055
+ className="h-10 w-10 shrink-0"
1056
+ onClick={() => {
1057
+ setStorageProfileEditingId(null);
1058
+ setIsStorageProfileSheetOpen(true);
1059
+ }}
1060
+ aria-label="Criar perfil de email"
1061
+ title="Criar perfil de email"
1062
+ >
1063
+ <Plus className="h-4 w-4" />
1064
+ </Button>
1065
+ </div>
1066
+
1067
+ <IntegrationProfileSheet
1068
+ open={isStorageProfileSheetOpen}
1069
+ onOpenChange={(nextOpen) => {
1070
+ setIsStorageProfileSheetOpen(nextOpen);
1071
+ if (!nextOpen) setStorageProfileEditingId(null);
1072
+ }}
1073
+ profileId={storageProfileEditingId}
1074
+ initialTypeSlug="email"
1075
+ lockedTypeSlug="email"
1076
+ onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
1077
+ setStorageProfileSelectedLabel(profile.name ?? '');
1078
+ handleChangeValue(String(profile.id), true);
1079
+ }}
1080
+ />
1081
+ </>
1082
+ );
1083
+ }
1084
+
1085
+ if (!dailyReportIntegrationTypeSlug) {
1011
1086
  return (
1012
1087
  <Input
1013
1088
  type="text"
@@ -17,6 +17,12 @@ import {
17
17
  CardHeader,
18
18
  CardTitle,
19
19
  } from '@/components/ui/card';
20
+ import {
21
+ DropdownMenu,
22
+ DropdownMenuContent,
23
+ DropdownMenuItem,
24
+ DropdownMenuTrigger,
25
+ } from '@/components/ui/dropdown-menu';
20
26
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
21
27
  import { Skeleton } from '@/components/ui/skeleton';
22
28
  import {
@@ -53,12 +59,6 @@ import {
53
59
  import { useTranslations } from 'next-intl';
54
60
  import { useEffect, useMemo, useState } from 'react';
55
61
  import { UserSheet } from './user-sheet';
56
- import {
57
- DropdownMenu,
58
- DropdownMenuContent,
59
- DropdownMenuItem,
60
- DropdownMenuTrigger,
61
- } from '@/components/ui/dropdown-menu';
62
62
 
63
63
  const USER_VIEW_STORAGE_KEY = 'core-user-view-mode';
64
64
  type UserViewMode = 'table' | 'cards';
@@ -366,7 +366,7 @@ export default function UserPage() {
366
366
  viewMode === 'cards' ? (
367
367
  <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
368
368
  {Array.from({ length: 6 }).map((_, i) => (
369
- <Card key={i} className="overflow-hidden py-0">
369
+ <Card key={i} className="overflow-hidden">
370
370
  <CardContent className="space-y-3 p-4">
371
371
  <div className="flex items-center gap-3">
372
372
  <Skeleton className="h-12 w-12 rounded-full" />
@@ -1,11 +1,23 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
4
5
  import { Button } from '@/components/ui/button';
5
- import { Card, CardContent } from '@/components/ui/card';
6
6
  import { Input } from '@/components/ui/input';
7
- import { Label } from '@/components/ui/label';
8
7
  import { Switch } from '@/components/ui/switch';
8
+ import {
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ } from '@/components/ui/table';
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from '@/components/ui/tooltip';
9
21
  import { Role } from '@hed-hog/api-types';
10
22
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
11
23
  import { CheckCheck, Eye, Loader2, Search, ShieldCheck, X } from 'lucide-react';
@@ -167,36 +179,50 @@ export function PermissionsSection({
167
179
  />
168
180
  </div>
169
181
  {hasUnassigned && (
170
- <Button
171
- variant="outline"
172
- size="sm"
173
- className="shrink-0 gap-1.5"
174
- onClick={handleAssignAll}
175
- disabled={isDisabled}
176
- >
177
- {isBulkLoading ? (
178
- <Loader2 className="h-4 w-4 animate-spin" />
179
- ) : (
180
- <CheckCheck className="h-4 w-4" />
181
- )}
182
- {t('addAllRoles')}
183
- </Button>
182
+ <Tooltip>
183
+ <TooltipTrigger asChild>
184
+ <Button
185
+ variant="outline"
186
+ size="icon"
187
+ className="h-8 w-8 shrink-0"
188
+ onClick={handleAssignAll}
189
+ disabled={isDisabled}
190
+ aria-label={t('addAllRoles')}
191
+ >
192
+ {isBulkLoading ? (
193
+ <Loader2 className="h-4 w-4 animate-spin" />
194
+ ) : (
195
+ <CheckCheck className="h-4 w-4" />
196
+ )}
197
+ </Button>
198
+ </TooltipTrigger>
199
+ <TooltipContent>
200
+ <p>{t('addAllRoles')}</p>
201
+ </TooltipContent>
202
+ </Tooltip>
184
203
  )}
185
204
  {hasAssigned && (
186
- <Button
187
- variant="outline"
188
- size="sm"
189
- className="shrink-0 gap-1.5"
190
- onClick={handleRemoveAll}
191
- disabled={isDisabled}
192
- >
193
- {isBulkLoading ? (
194
- <Loader2 className="h-4 w-4 animate-spin" />
195
- ) : (
196
- <X className="h-4 w-4" />
197
- )}
198
- {t('removeAllRoles')}
199
- </Button>
205
+ <Tooltip>
206
+ <TooltipTrigger asChild>
207
+ <Button
208
+ variant="outline"
209
+ size="icon"
210
+ className="h-8 w-8 shrink-0"
211
+ onClick={handleRemoveAll}
212
+ disabled={isDisabled}
213
+ aria-label={t('removeAllRoles')}
214
+ >
215
+ {isBulkLoading ? (
216
+ <Loader2 className="h-4 w-4 animate-spin" />
217
+ ) : (
218
+ <X className="h-4 w-4" />
219
+ )}
220
+ </Button>
221
+ </TooltipTrigger>
222
+ <TooltipContent>
223
+ <p>{t('removeAllRoles')}</p>
224
+ </TooltipContent>
225
+ </Tooltip>
200
226
  )}
201
227
  <Button
202
228
  variant="outline"
@@ -215,61 +241,89 @@ export function PermissionsSection({
215
241
  <p className="text-sm text-muted-foreground">{t('noRolesMatch')}</p>
216
242
  </div>
217
243
  ) : (
218
- <div className="space-y-2">
219
- {filteredRoles.map((role, index) => {
220
- const isToggling = togglingRoleId === role.id;
244
+ <div className="overflow-hidden rounded-lg border border-border/70 bg-background">
245
+ <Table className="table-fixed">
246
+ <TableHeader className="bg-muted/20">
247
+ <TableRow className="hover:bg-transparent">
248
+ <TableHead className="h-8 px-2 text-[11px] font-medium text-muted-foreground">
249
+ Cargo
250
+ </TableHead>
251
+ <TableHead className="h-8 w-37.5 px-2 text-[11px] font-medium text-muted-foreground">
252
+ Slug
253
+ </TableHead>
254
+ <TableHead className="h-8 w-19.5 px-2 text-right text-[11px] font-medium text-muted-foreground">
255
+ Acesso
256
+ </TableHead>
257
+ </TableRow>
258
+ </TableHeader>
259
+ <TableBody>
260
+ {filteredRoles.map((role, index) => {
261
+ const isToggling = togglingRoleId === role.id;
221
262
 
222
- return (
223
- <Card
224
- key={role.id ?? `role-${index}`}
225
- className={`transition-all py-4 ${
226
- role.isAssigned
227
- ? 'border-primary bg-primary/5'
228
- : 'border-border hover:border-primary/50'
229
- }`}
230
- >
231
- <CardContent className="flex items-center justify-between px-4">
232
- <div className="flex items-center gap-2 flex-1">
233
- <div
234
- className={`rounded-md p-2 ${
235
- role.isAssigned ? 'bg-primary/10' : 'bg-muted'
236
- }`}
237
- >
238
- <ShieldCheck
239
- className={`h-5 w-5 ${
240
- role.isAssigned
241
- ? 'text-primary'
242
- : 'text-muted-foreground'
243
- }`}
244
- />
245
- </div>
246
- <div className="flex-1">
247
- <Label
248
- htmlFor={`role-${role.id}`}
249
- className="text-sm font-medium cursor-pointer"
250
- >
251
- {role.name} ({role.slug})
252
- </Label>
253
- {role.description && (
254
- <p className="text-xs text-muted-foreground">
255
- {role.description}
256
- </p>
257
- )}
258
- </div>
259
- </div>
260
- <Switch
261
- id={`role-${role.id}`}
262
- checked={role.isAssigned}
263
- disabled={isToggling || isDisabled}
264
- onCheckedChange={() =>
265
- handleToggleRole(role.id!, role.isAssigned)
263
+ return (
264
+ <TableRow
265
+ key={role.id ?? `role-${index}`}
266
+ className={
267
+ role.isAssigned
268
+ ? 'bg-primary/5 hover:bg-primary/5'
269
+ : 'hover:bg-muted/20'
266
270
  }
267
- className="ml-4"
268
- />
269
- </CardContent>
270
- </Card>
271
- );
272
- })}
271
+ >
272
+ <TableCell className="px-2 py-1.5 align-middle">
273
+ <div className="flex min-w-0 items-start gap-1.5">
274
+ <div
275
+ className={`mt-0.5 rounded-md p-0.5 ${
276
+ role.isAssigned ? 'bg-primary/10' : 'bg-muted/70'
277
+ }`}
278
+ >
279
+ <ShieldCheck
280
+ className={`h-3.5 w-3.5 ${
281
+ role.isAssigned
282
+ ? 'text-primary'
283
+ : 'text-muted-foreground'
284
+ }`}
285
+ />
286
+ </div>
287
+ <div className="min-w-0 flex-1">
288
+ <div className="truncate text-[13px] font-semibold leading-4 text-foreground">
289
+ {role.name}
290
+ </div>
291
+ {role.description && (
292
+ <p className="truncate text-[11px] leading-3 text-muted-foreground">
293
+ {role.description}
294
+ </p>
295
+ )}
296
+ </div>
297
+ </div>
298
+ </TableCell>
299
+ <TableCell className="px-2 py-1.5 align-middle">
300
+ {role.slug ? (
301
+ <Badge
302
+ variant="outline"
303
+ className="h-4 rounded-sm border-border/70 bg-muted/40 px-1 font-mono text-[9px] font-medium lowercase leading-none tracking-[0.02em] text-muted-foreground"
304
+ >
305
+ {role.slug}
306
+ </Badge>
307
+ ) : null}
308
+ </TableCell>
309
+ <TableCell className="px-2 py-1.5 align-middle">
310
+ <div className="flex justify-end">
311
+ <Switch
312
+ id={`role-${role.id}`}
313
+ checked={role.isAssigned}
314
+ disabled={isToggling || isDisabled}
315
+ onCheckedChange={() =>
316
+ handleToggleRole(role.id!, role.isAssigned)
317
+ }
318
+ className="scale-90 origin-center"
319
+ />
320
+ </div>
321
+ </TableCell>
322
+ </TableRow>
323
+ );
324
+ })}
325
+ </TableBody>
326
+ </Table>
273
327
  </div>
274
328
  )}
275
329
 
@@ -203,32 +203,34 @@ export function UserAccessSheet({
203
203
  <div className="w-full border-t" />
204
204
 
205
205
  <Tabs defaultValue="menu" className="px-6 pt-4">
206
- <TabsList className="w-full mb-4">
207
- <TabsTrigger
208
- value="menu"
209
- className="flex-1 flex items-center gap-2"
210
- >
211
- <Menu className="h-4 w-4" />
212
- {t('accessTabMenu')}
213
- {!loadingMenus && (
214
- <Badge variant="secondary" className="ml-1 text-xs">
215
- {menus.length}
216
- </Badge>
217
- )}
218
- </TabsTrigger>
219
- <TabsTrigger
220
- value="routes"
221
- className="flex-1 flex items-center gap-2"
222
- >
223
- <Globe className="h-4 w-4" />
224
- {t('accessTabRoutes')}
225
- {!loadingRoutes && (
226
- <Badge variant="secondary" className="ml-1 text-xs">
227
- {routes.length}
228
- </Badge>
229
- )}
230
- </TabsTrigger>
231
- </TabsList>
206
+ <div className="sticky top-0 z-20 -mx-6 bg-background/95 px-6 backdrop-blur supports-backdrop-filter:bg-background/80">
207
+ <TabsList className="mb-4 w-full">
208
+ <TabsTrigger
209
+ value="menu"
210
+ className="flex-1 flex items-center gap-2"
211
+ >
212
+ <Menu className="h-4 w-4" />
213
+ {t('accessTabMenu')}
214
+ {!loadingMenus && (
215
+ <Badge variant="secondary" className="ml-1 text-xs">
216
+ {menus.length}
217
+ </Badge>
218
+ )}
219
+ </TabsTrigger>
220
+ <TabsTrigger
221
+ value="routes"
222
+ className="flex-1 flex items-center gap-2"
223
+ >
224
+ <Globe className="h-4 w-4" />
225
+ {t('accessTabRoutes')}
226
+ {!loadingRoutes && (
227
+ <Badge variant="secondary" className="ml-1 text-xs">
228
+ {routes.length}
229
+ </Badge>
230
+ )}
231
+ </TabsTrigger>
232
+ </TabsList>
233
+ </div>
232
234
 
233
235
  <TabsContent value="menu" className="mt-0">
234
236
  {loadingMenus ? (