@hed-hog/core 0.0.185 → 0.0.190

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 (55) hide show
  1. package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
  2. package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
  3. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
  4. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
  5. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
  6. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
  7. package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
  8. package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
  9. package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
  10. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
  11. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
  12. package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
  13. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
  14. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
  15. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
  16. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
  17. package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
  19. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
  20. package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
  21. package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
  22. package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
  23. package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
  24. package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
  25. package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
  26. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
  27. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
  28. package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
  29. package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
  30. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
  31. package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
  34. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
  35. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
  36. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
  37. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
  38. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
  39. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
  40. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
  41. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
  42. package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
  43. package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
  44. package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
  45. package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
  46. package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
  47. package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
  48. package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
  49. package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
  50. package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
  53. package/hedhog/frontend/messages/en.json +1080 -0
  54. package/hedhog/frontend/messages/pt.json +1135 -0
  55. package/package.json +4 -4
@@ -0,0 +1,504 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ } from '@/components/ui/accordion';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Label } from '@/components/ui/label';
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '@/components/ui/select';
18
+ import { Switch } from '@/components/ui/switch';
19
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
20
+ import * as TablerIcons from '@tabler/icons-react';
21
+ import {
22
+ ChevronLeft,
23
+ ChevronRight,
24
+ ChevronsLeft,
25
+ ChevronsRight,
26
+ Loader2,
27
+ Menu as MenuIcon,
28
+ } from 'lucide-react';
29
+ import { useTranslations } from 'next-intl';
30
+ import { JSX, useEffect, useState } from 'react';
31
+ import { toast } from 'sonner';
32
+
33
+ type Menu = {
34
+ id: number;
35
+ slug: string;
36
+ url: string;
37
+ icon?: string;
38
+ menu_id?: number | null;
39
+ menu_locale?: Array<{
40
+ name: string;
41
+ }>;
42
+ role_menu?: Array<{
43
+ menu_id: number;
44
+ role_id: number;
45
+ }>;
46
+ children?: Menu[];
47
+ };
48
+
49
+ type RoleMenusSectionProps = {
50
+ roleId: number;
51
+ onMenuChange?: () => void;
52
+ };
53
+
54
+ export function RoleMenusSection({
55
+ roleId,
56
+ onMenuChange,
57
+ }: RoleMenusSectionProps) {
58
+ const t = useTranslations('core.RolePage');
59
+ const { request, currentLocaleCode } = useApp();
60
+ const [togglingMenuId, setTogglingMenuId] = useState<number | null>(null);
61
+ const [page, setPage] = useState(1);
62
+ const [pageSize, setPageSize] = useState(10);
63
+ const [expandedMenus, setExpandedMenus] = useState<string[]>([]);
64
+
65
+ const {
66
+ data: assignedMenusData,
67
+ isLoading: isLoadingAssigned,
68
+ refetch: refetchAssignedMenus,
69
+ } = useQuery<{ data: Menu[] }>({
70
+ queryKey: ['role-menus-assigned', roleId, currentLocaleCode],
71
+ queryFn: async () => {
72
+ const response = await request<{ data: Menu[] }>({
73
+ url: `/role/${roleId}/menu?pageSize=10000`,
74
+ method: 'GET',
75
+ });
76
+ return response.data;
77
+ },
78
+ enabled: !!roleId,
79
+ });
80
+
81
+ const {
82
+ data: menusData,
83
+ isLoading: isLoadingMenus,
84
+ refetch: refetchMenus,
85
+ } = useQuery<{ data: Menu[]; total: number; lastPage: number }>({
86
+ queryKey: [
87
+ 'role-menus-paginated',
88
+ roleId,
89
+ currentLocaleCode,
90
+ page,
91
+ pageSize,
92
+ ],
93
+ queryFn: async () => {
94
+ const response = await request<{
95
+ data: Menu[];
96
+ total: number;
97
+ lastPage: number;
98
+ }>({
99
+ url: `/role/${roleId}/menu?page=${page}&pageSize=${pageSize}`,
100
+ method: 'GET',
101
+ });
102
+ return response.data;
103
+ },
104
+ enabled: !!roleId,
105
+ });
106
+
107
+ const menus = menusData?.data || [];
108
+ const totalPages = menusData?.lastPage || 1;
109
+ const totalMenus = menusData?.total || 0;
110
+
111
+ const handleToggleMenu = async (
112
+ menuId: number,
113
+ isAssigned: boolean,
114
+ includeChildren: boolean = true
115
+ ) => {
116
+ setTogglingMenuId(menuId);
117
+ try {
118
+ const currentMenuIds =
119
+ assignedMenusData?.data
120
+ ?.filter((m: Menu) => m.role_menu && m.role_menu.length > 0)
121
+ .map((m: Menu) => m.id) || [];
122
+
123
+ const menuIdsToToggle = [menuId];
124
+ const findMenuWithChildren = (
125
+ menusList: Menu[],
126
+ targetId: number
127
+ ): Menu | undefined => {
128
+ for (const menu of menusList) {
129
+ if (menu.id === targetId) return menu;
130
+ if (menu.children && menu.children.length > 0) {
131
+ const found = findMenuWithChildren(menu.children, targetId);
132
+ if (found) return found;
133
+ }
134
+ }
135
+ return undefined;
136
+ };
137
+
138
+ const findParentMenu = (targetMenuId: number): Menu | undefined => {
139
+ const menu = menus.find((m) => m.id === targetMenuId);
140
+ if (menu?.menu_id) {
141
+ return menus.find((m) => m.id === menu.menu_id);
142
+ }
143
+ return undefined;
144
+ };
145
+
146
+ if (includeChildren) {
147
+ const menu = findMenuWithChildren(hierarchicalMenus, menuId);
148
+ if (menu?.children && menu.children.length > 0) {
149
+ const getAllChildrenIds = (children: Menu[]): number[] => {
150
+ return children.flatMap((child) => [
151
+ child.id,
152
+ ...(child.children ? getAllChildrenIds(child.children) : []),
153
+ ]);
154
+ };
155
+ menuIdsToToggle.push(...getAllChildrenIds(menu.children));
156
+ }
157
+ }
158
+
159
+ let newMenuIds: number[];
160
+ if (isAssigned) {
161
+ newMenuIds = currentMenuIds.filter(
162
+ (id: number) => !menuIdsToToggle.includes(id)
163
+ );
164
+ } else {
165
+ newMenuIds = [...new Set([...currentMenuIds, ...menuIdsToToggle])];
166
+ const getAllParentIds = (childMenuId: number): number[] => {
167
+ const parentIds: number[] = [];
168
+ const parent = findParentMenu(childMenuId);
169
+ if (parent) {
170
+ parentIds.push(parent.id);
171
+ parentIds.push(...getAllParentIds(parent.id));
172
+ }
173
+ return parentIds;
174
+ };
175
+
176
+ const parentIds = getAllParentIds(menuId);
177
+ if (parentIds.length > 0) {
178
+ newMenuIds = [...new Set([...newMenuIds, ...parentIds])];
179
+ }
180
+ }
181
+
182
+ await request({
183
+ url: `/role/${roleId}/menu`,
184
+ method: 'PATCH',
185
+ data: { ids: newMenuIds },
186
+ });
187
+
188
+ toast.success(isAssigned ? t('menuRemoved') : t('menuAssigned'));
189
+ await refetchAssignedMenus();
190
+ await refetchMenus();
191
+ onMenuChange?.();
192
+ } catch (error) {
193
+ toast.error(
194
+ isAssigned ? t('errorRemovingMenu') : t('errorAssigningMenu')
195
+ );
196
+ } finally {
197
+ setTogglingMenuId(null);
198
+ }
199
+ };
200
+
201
+ const isMenuAssigned = (menu: Menu) => {
202
+ const assignedMenu = assignedMenusData?.data?.find(
203
+ (m: Menu) => m.id === menu.id
204
+ );
205
+ return !!(assignedMenu?.role_menu && assignedMenu.role_menu.length > 0);
206
+ };
207
+
208
+ const getMenuName = (menu: Menu) => {
209
+ return menu.menu_locale?.[0]?.name || menu.slug;
210
+ };
211
+
212
+ const renderIcon = (iconName?: string) => {
213
+ if (!iconName) {
214
+ return <MenuIcon className="h-5 w-5" />;
215
+ }
216
+
217
+ const toPascalCase = (str: string) =>
218
+ str.replace(/(^\w|-\w)/g, (match) =>
219
+ match.replace('-', '').toUpperCase()
220
+ );
221
+
222
+ const pascalName = toPascalCase(String(iconName));
223
+ let IconComponent = (TablerIcons as any)[`Icon${pascalName}`];
224
+ if (!IconComponent) {
225
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Filled`];
226
+ }
227
+ if (!IconComponent) {
228
+ IconComponent = (TablerIcons as any)[`Icon${pascalName}Circle`];
229
+ }
230
+ if (IconComponent) {
231
+ return <IconComponent className="h-5 w-5" />;
232
+ }
233
+
234
+ return <MenuIcon className="h-5 w-5" />;
235
+ };
236
+
237
+ const organizeMenuHierarchy = (menusList: Menu[]): Menu[] => {
238
+ const menuMap = new Map<number, Menu>();
239
+ const rootMenus: Menu[] = [];
240
+
241
+ menusList.forEach((menu) => {
242
+ menuMap.set(menu.id, { ...menu, children: [] });
243
+ });
244
+
245
+ menuMap.forEach((menu) => {
246
+ if (menu.menu_id && menuMap.has(menu.menu_id)) {
247
+ const parent = menuMap.get(menu.menu_id)!;
248
+ if (!parent.children) parent.children = [];
249
+ parent.children.push(menu);
250
+ } else {
251
+ rootMenus.push(menu);
252
+ }
253
+ });
254
+
255
+ return rootMenus;
256
+ };
257
+
258
+ const hierarchicalMenus = organizeMenuHierarchy(menus);
259
+ useEffect(() => {
260
+ if (expandedMenus.length === 0 && hierarchicalMenus.length > 0) {
261
+ const allParentIds = hierarchicalMenus
262
+ .filter((m) => m.children && m.children.length > 0)
263
+ .map((m) => `menu-${m.id}`);
264
+ setExpandedMenus(allParentIds);
265
+ }
266
+ }, [hierarchicalMenus.length]);
267
+
268
+ const renderChildMenu = (menu: Menu): JSX.Element => {
269
+ const isAssigned = isMenuAssigned(menu);
270
+ const isToggling = togglingMenuId === menu.id;
271
+
272
+ return (
273
+ <div
274
+ key={menu.id}
275
+ className={`flex items-center justify-between p-3 rounded-md border transition-all ${
276
+ isAssigned
277
+ ? 'border-primary/50 bg-primary/5'
278
+ : 'border-border hover:border-primary/30'
279
+ }`}
280
+ >
281
+ <div className="flex items-center gap-3 flex-1">
282
+ <div
283
+ className={`rounded-md p-2 ${
284
+ isAssigned ? 'bg-primary/10' : 'bg-muted'
285
+ }`}
286
+ >
287
+ <div
288
+ className={`${
289
+ isAssigned ? 'text-primary' : 'text-muted-foreground'
290
+ }`}
291
+ >
292
+ {renderIcon(menu.icon)}
293
+ </div>
294
+ </div>
295
+ <div className="flex-1">
296
+ <Label
297
+ htmlFor={`menu-${menu.id}`}
298
+ className="text-sm font-medium cursor-pointer"
299
+ >
300
+ {getMenuName(menu)}
301
+ </Label>
302
+ <p className="text-xs text-muted-foreground mt-0.5">{menu.url}</p>
303
+ </div>
304
+ </div>
305
+ <Switch
306
+ id={`menu-${menu.id}`}
307
+ checked={isAssigned}
308
+ disabled={isToggling}
309
+ onCheckedChange={() => handleToggleMenu(menu.id, isAssigned, false)}
310
+ className="ml-4"
311
+ />
312
+ </div>
313
+ );
314
+ };
315
+
316
+ if (isLoadingMenus || isLoadingAssigned) {
317
+ return (
318
+ <div className="flex items-center justify-center py-8">
319
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
320
+ <span className="ml-2 text-sm text-muted-foreground">
321
+ {t('loadingMenus')}
322
+ </span>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ if (!menus || menus.length === 0) {
328
+ return (
329
+ <div className="border border-dashed rounded-lg p-8 flex flex-col items-center justify-center">
330
+ <MenuIcon className="h-12 w-12 text-muted-foreground mb-3" />
331
+ <p className="text-sm font-medium text-center">
332
+ {t('noMenusAvailable')}
333
+ </p>
334
+ </div>
335
+ );
336
+ }
337
+
338
+ return (
339
+ <div className="space-y-3">
340
+ <div>
341
+ <h4 className="text-sm font-semibold flex items-center gap-2">
342
+ <MenuIcon className="h-4 w-4" />
343
+ {t('menusTitle')}
344
+ </h4>
345
+ <p className="text-xs text-muted-foreground mt-1">
346
+ {t('menusDescription')}
347
+ </p>
348
+ </div>
349
+
350
+ <Accordion
351
+ type="multiple"
352
+ value={expandedMenus}
353
+ onValueChange={setExpandedMenus}
354
+ className="space-y-2"
355
+ >
356
+ {hierarchicalMenus.map((menu) => {
357
+ const isAssigned = isMenuAssigned(menu);
358
+ const isToggling = togglingMenuId === menu.id;
359
+ const hasChildren = menu.children && menu.children.length > 0;
360
+
361
+ if (!hasChildren) {
362
+ return renderChildMenu(menu);
363
+ }
364
+
365
+ return (
366
+ <AccordionItem
367
+ key={menu.id}
368
+ value={`menu-${menu.id}`}
369
+ className={`border rounded-lg transition-all ${
370
+ isAssigned ? 'border-primary/50 bg-primary/5' : ''
371
+ }`}
372
+ >
373
+ <div className="flex items-center justify-between p-4">
374
+ <div className="flex items-center gap-3 flex-1">
375
+ <AccordionTrigger className="hover:no-underline p-0" />
376
+ <div
377
+ className={`rounded-md p-2 ${
378
+ isAssigned ? 'bg-primary/10' : 'bg-muted'
379
+ }`}
380
+ >
381
+ <div
382
+ className={`${
383
+ isAssigned ? 'text-primary' : 'text-muted-foreground'
384
+ }`}
385
+ >
386
+ {renderIcon(menu.icon)}
387
+ </div>
388
+ </div>
389
+ <div className="flex-1">
390
+ <Label
391
+ htmlFor={`menu-${menu.id}`}
392
+ className="text-sm font-medium cursor-pointer"
393
+ >
394
+ {getMenuName(menu)}
395
+ </Label>
396
+ <p className="text-xs text-muted-foreground mt-0.5">
397
+ {menu.url}
398
+ </p>
399
+ </div>
400
+ </div>
401
+ <Switch
402
+ id={`menu-${menu.id}`}
403
+ checked={isAssigned}
404
+ disabled={isToggling}
405
+ onCheckedChange={() =>
406
+ handleToggleMenu(menu.id, isAssigned, true)
407
+ }
408
+ className="ml-4"
409
+ />
410
+ </div>
411
+ <AccordionContent className="px-4 pb-4 pt-2 space-y-2 ml-8">
412
+ {menu.children?.map((child) => renderChildMenu(child))}
413
+ </AccordionContent>
414
+ </AccordionItem>
415
+ );
416
+ })}
417
+ </Accordion>
418
+
419
+ {/* Paginação */}
420
+ <div className="flex items-center justify-between px-2 pt-4">
421
+ <div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
422
+ {t('showing')} {menus.length} {t('of')} {totalMenus} {t('menus')}
423
+ </div>
424
+ <div className="flex w-full items-center gap-8 lg:w-fit">
425
+ <div className="hidden items-center gap-2 lg:flex">
426
+ <Label
427
+ htmlFor="rows-per-page-menus"
428
+ className="text-sm font-medium"
429
+ >
430
+ {t('rowsPerPage')}
431
+ </Label>
432
+ <Select
433
+ value={`${pageSize}`}
434
+ onValueChange={(value) => {
435
+ setPageSize(Number(value));
436
+ setPage(1);
437
+ }}
438
+ >
439
+ <SelectTrigger
440
+ size="sm"
441
+ className="w-20"
442
+ id="rows-per-page-menus"
443
+ >
444
+ <SelectValue placeholder={pageSize} />
445
+ </SelectTrigger>
446
+ <SelectContent side="top">
447
+ {[10, 20, 30, 40, 50].map((size) => (
448
+ <SelectItem key={size} value={`${size}`}>
449
+ {size}
450
+ </SelectItem>
451
+ ))}
452
+ </SelectContent>
453
+ </Select>
454
+ </div>
455
+ <div className="flex w-fit items-center justify-center text-sm font-medium">
456
+ {t('page')} {page} {t('of')} {totalPages}
457
+ </div>
458
+ <div className="ml-auto flex items-center gap-2 lg:ml-0">
459
+ <Button
460
+ variant="outline"
461
+ className="hidden size-8 lg:flex"
462
+ size="icon"
463
+ onClick={() => setPage(1)}
464
+ disabled={page === 1}
465
+ >
466
+ <span className="sr-only">{t('goToFirstPage')}</span>
467
+ <ChevronsLeft className="size-4" />
468
+ </Button>
469
+ <Button
470
+ variant="outline"
471
+ className="size-8"
472
+ size="icon"
473
+ onClick={() => setPage(page - 1)}
474
+ disabled={page === 1}
475
+ >
476
+ <span className="sr-only">{t('goToPreviousPage')}</span>
477
+ <ChevronLeft className="size-4" />
478
+ </Button>
479
+ <Button
480
+ variant="outline"
481
+ className="size-8"
482
+ size="icon"
483
+ onClick={() => setPage(page + 1)}
484
+ disabled={page >= totalPages}
485
+ >
486
+ <span className="sr-only">{t('goToNextPage')}</span>
487
+ <ChevronRight className="size-4" />
488
+ </Button>
489
+ <Button
490
+ variant="outline"
491
+ className="hidden size-8 lg:flex"
492
+ size="icon"
493
+ onClick={() => setPage(totalPages)}
494
+ disabled={page >= totalPages}
495
+ >
496
+ <span className="sr-only">{t('goToLastPage')}</span>
497
+ <ChevronsRight className="size-4" />
498
+ </Button>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ </div>
503
+ );
504
+ }