@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,814 @@
1
+ 'use client';
2
+
3
+ import {
4
+ PageHeader,
5
+ PaginationFooter,
6
+ SearchBar,
7
+ StatsCards,
8
+ } from '@/components/entity-list';
9
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
10
+ import { Button } from '@/components/ui/button';
11
+ import {
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardHeader,
16
+ CardTitle,
17
+ } from '@/components/ui/card';
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ } from '@/components/ui/dialog';
25
+ import {
26
+ Form,
27
+ FormControl,
28
+ FormField,
29
+ FormItem,
30
+ FormLabel,
31
+ FormMessage,
32
+ } from '@/components/ui/form';
33
+ import { Input } from '@/components/ui/input';
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from '@/components/ui/select';
41
+ import {
42
+ Sheet,
43
+ SheetContent,
44
+ SheetDescription,
45
+ SheetHeader,
46
+ SheetTitle,
47
+ } from '@/components/ui/sheet';
48
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
49
+ import { Textarea } from '@/components/ui/textarea';
50
+ import { Role } from '@hed-hog/api-types';
51
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
52
+ import { zodResolver } from '@hookform/resolvers/zod';
53
+ import {
54
+ FileText,
55
+ Menu,
56
+ Route,
57
+ Save,
58
+ ShieldCheck,
59
+ Trash2,
60
+ Users,
61
+ } from 'lucide-react';
62
+ import { useTranslations } from 'next-intl';
63
+ import { useEffect, useState } from 'react';
64
+ import { useForm } from 'react-hook-form';
65
+ import { toast } from 'sonner';
66
+ import { z } from 'zod';
67
+ import { RoleMenusSection } from './menus';
68
+ import { RoleRoutesSection } from './routes';
69
+ import { RoleUsersSection } from './users';
70
+
71
+ type PaginatedResponse<T> = {
72
+ data: T[];
73
+ total: number;
74
+ page: number;
75
+ pageSize: number;
76
+ };
77
+
78
+ type RoleStats = {
79
+ totalRoles: number;
80
+ };
81
+
82
+ type Locale = {
83
+ code: string;
84
+ name: string;
85
+ };
86
+
87
+ type RoleLocale = {
88
+ name: string;
89
+ description?: string;
90
+ };
91
+
92
+ type RoleDetail = {
93
+ id: number;
94
+ slug: string;
95
+ role_locale?: Array<{
96
+ name: string;
97
+ description?: string;
98
+ locale?: {
99
+ code: string;
100
+ };
101
+ }>;
102
+ locale?: Record<string, RoleLocale>;
103
+ };
104
+
105
+ export default function RolePage() {
106
+ const t = useTranslations('core.RolePage');
107
+ const { request, currentLocaleCode, locales } = useApp();
108
+
109
+ const [searchQuery, setSearchQuery] = useState('');
110
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
111
+ const [editingRole, setEditingRole] = useState<RoleDetail | null>(null);
112
+ const [formError, setFormError] = useState<string | null>(null);
113
+ const [editFormError, setEditFormError] = useState<string | null>(null);
114
+ const [selectedLocale, setSelectedLocale] = useState(currentLocaleCode);
115
+
116
+ const [page, setPage] = useState(1);
117
+ const [pageSize, setPageSize] = useState(12);
118
+ const [activeTab, setActiveTab] = useState('basic-info');
119
+ const [openDeleteModal, setOpenDeleteModal] = useState(false);
120
+
121
+ const {
122
+ data: rolesResponse,
123
+ isLoading,
124
+ refetch,
125
+ } = useQuery<PaginatedResponse<Role>>({
126
+ queryKey: ['roles', page, pageSize, searchQuery, currentLocaleCode],
127
+ queryFn: async () => {
128
+ const params = new URLSearchParams();
129
+ params.set('page', String(page));
130
+ params.set('pageSize', String(pageSize));
131
+ if (searchQuery) params.set('search', searchQuery);
132
+
133
+ const response = await request<PaginatedResponse<Role>>({
134
+ url: `/role?${params.toString()}`,
135
+ method: 'GET',
136
+ });
137
+ return response.data;
138
+ },
139
+ });
140
+
141
+ const stats: RoleStats = {
142
+ totalRoles: Number(rolesResponse?.total || 0),
143
+ };
144
+
145
+ const addRoleSchema = z.object({
146
+ slug: z.string().min(2, t('errorSlug')),
147
+ name: z.string().min(2, t('errorName')),
148
+ description: z.string().optional(),
149
+ });
150
+
151
+ const form = useForm<z.infer<typeof addRoleSchema>>({
152
+ resolver: zodResolver(addRoleSchema),
153
+ defaultValues: {
154
+ slug: '',
155
+ name: '',
156
+ description: '',
157
+ },
158
+ });
159
+
160
+ const editRoleSchema = z.object({
161
+ slug: z.string().min(2, t('errorSlug')),
162
+ name: z.string().min(2, t('errorName')),
163
+ description: z.string().optional(),
164
+ });
165
+
166
+ const editForm = useForm<z.infer<typeof editRoleSchema>>({
167
+ resolver: zodResolver(editRoleSchema),
168
+ defaultValues: {
169
+ slug: '',
170
+ name: '',
171
+ description: '',
172
+ },
173
+ });
174
+
175
+ useEffect(() => {
176
+ if (editingRole && editingRole.locale && selectedLocale) {
177
+ const localeData = editingRole.locale[selectedLocale];
178
+ editForm.reset({
179
+ slug: editingRole.slug || '',
180
+ name: localeData?.name || '',
181
+ description: localeData?.description || '',
182
+ });
183
+ }
184
+ }, [editingRole, selectedLocale]);
185
+
186
+ useEffect(() => {
187
+ if (editingRole) {
188
+ setSelectedLocale(currentLocaleCode);
189
+ setActiveTab('basic-info');
190
+ }
191
+ }, [editingRole?.id]);
192
+
193
+ const onSubmit = async (values: z.infer<typeof addRoleSchema>) => {
194
+ try {
195
+ const localeData: Record<string, RoleLocale> = {};
196
+ localeData[currentLocaleCode] = {
197
+ name: values.name,
198
+ description: values.description || '',
199
+ };
200
+
201
+ await request({
202
+ url: '/role',
203
+ method: 'POST',
204
+ data: {
205
+ slug: values.slug,
206
+ locale: localeData,
207
+ },
208
+ });
209
+
210
+ form.reset();
211
+ refetch();
212
+ setIsDialogOpen(false);
213
+ setFormError(null);
214
+ toast.success(t('roleCreatedSuccess'));
215
+ } catch (err: any) {
216
+ const msg =
217
+ err?.response?.data?.message ||
218
+ err?.response?.data?.error ||
219
+ err?.message ||
220
+ t('serverError');
221
+
222
+ setFormError(String(msg));
223
+ }
224
+ };
225
+
226
+ const handleEdit = async (role: Role) => {
227
+ setEditFormError(null);
228
+
229
+ try {
230
+ const response = await request<RoleDetail>({
231
+ url: `/role/${role.role_id}`,
232
+ method: 'GET',
233
+ });
234
+
235
+ const fullRole = response.data;
236
+ const localeData: Record<string, RoleLocale> = {};
237
+ if (fullRole.role_locale && Array.isArray(fullRole.role_locale)) {
238
+ fullRole.role_locale.forEach((rl: any) => {
239
+ const localeCode = rl.locale?.code;
240
+ if (localeCode) {
241
+ localeData[localeCode] = {
242
+ name: rl.name || '',
243
+ description: rl.description || '',
244
+ };
245
+ }
246
+ });
247
+ }
248
+
249
+ locales?.forEach((locale: Locale) => {
250
+ if (!localeData[locale.code]) {
251
+ localeData[locale.code] = {
252
+ name: '',
253
+ description: '',
254
+ };
255
+ }
256
+ });
257
+
258
+ setEditingRole({
259
+ ...fullRole,
260
+ locale: localeData,
261
+ });
262
+ } catch (err) {
263
+ console.error('Error fetching role:', err);
264
+ toast.error(t('serverError'));
265
+ }
266
+ };
267
+
268
+ const onEditSubmit = async (values: z.infer<typeof editRoleSchema>) => {
269
+ if (!editingRole || !editingRole.locale) return;
270
+
271
+ try {
272
+ const updatedLocale = {
273
+ ...editingRole.locale,
274
+ [selectedLocale]: {
275
+ name: values.name,
276
+ description: values.description || '',
277
+ },
278
+ };
279
+
280
+ await request({
281
+ url: `/role/${editingRole.id}`,
282
+ method: 'PATCH',
283
+ data: {
284
+ slug: values.slug,
285
+ locale: updatedLocale,
286
+ },
287
+ });
288
+
289
+ toast.success(t('roleUpdatedSuccess'));
290
+ setEditFormError(null);
291
+ await refetch();
292
+ setEditingRole(null);
293
+ } catch (err: any) {
294
+ const msg =
295
+ err?.response?.data?.message ||
296
+ err?.response?.data?.error ||
297
+ err?.message ||
298
+ t('serverError');
299
+ setEditFormError(String(msg));
300
+ }
301
+ };
302
+
303
+ const onDelete = async () => {
304
+ try {
305
+ await request({
306
+ url: `/role`,
307
+ method: 'DELETE',
308
+ data: {
309
+ ids: [Number(editingRole?.id)],
310
+ },
311
+ });
312
+ refetch();
313
+ setOpenDeleteModal(false);
314
+ setEditingRole(null);
315
+ setEditFormError(null);
316
+ toast.success(t('roleDeletedSuccess'));
317
+ } catch (err: any) {
318
+ const msg =
319
+ err?.response?.data?.message ||
320
+ err?.response?.data?.error ||
321
+ err?.message ||
322
+ t('serverError');
323
+ setEditFormError(String(msg));
324
+ }
325
+ };
326
+
327
+ const handleRefreshEditingRole = async () => {
328
+ if (!editingRole?.id) return;
329
+
330
+ try {
331
+ const response = await request<RoleDetail>({
332
+ url: `/role/${editingRole.id}`,
333
+ method: 'GET',
334
+ });
335
+
336
+ const fullRole = response.data;
337
+ const localeData: Record<string, RoleLocale> = {};
338
+
339
+ if (fullRole.role_locale && Array.isArray(fullRole.role_locale)) {
340
+ fullRole.role_locale.forEach((rl: any) => {
341
+ const localeCode = rl.locale?.code;
342
+ if (localeCode) {
343
+ localeData[localeCode] = {
344
+ name: rl.name || '',
345
+ description: rl.description || '',
346
+ };
347
+ }
348
+ });
349
+ }
350
+
351
+ locales?.forEach((locale: Locale) => {
352
+ if (!localeData[locale.code]) {
353
+ localeData[locale.code] = {
354
+ name: '',
355
+ description: '',
356
+ };
357
+ }
358
+ });
359
+
360
+ setEditingRole({
361
+ ...fullRole,
362
+ locale: localeData,
363
+ });
364
+ } catch (err) {
365
+ console.error('Error refreshing role:', err);
366
+ }
367
+ };
368
+
369
+ return (
370
+ <div className="flex flex-col h-screen px-4">
371
+ <PageHeader
372
+ breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('roles') }]}
373
+ actions={[
374
+ {
375
+ label: t('buttonAddRole'),
376
+ onClick: () => setIsDialogOpen(true),
377
+ variant: 'default',
378
+ },
379
+ ]}
380
+ title={t('title')}
381
+ description={t('description')}
382
+ />
383
+
384
+ <StatsCards
385
+ className="sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-1"
386
+ stats={[
387
+ {
388
+ title: t('totalRoles'),
389
+ value: String(stats.totalRoles),
390
+ icon: <ShieldCheck className="h-5 w-5" />,
391
+ iconBgColor: 'bg-purple-50',
392
+ iconColor: 'text-purple-600',
393
+ },
394
+ ]}
395
+ />
396
+
397
+ <SearchBar
398
+ searchQuery={searchQuery}
399
+ onSearchChange={setSearchQuery}
400
+ onSearch={() => refetch()}
401
+ placeholder={t('searchPlaceholder')}
402
+ className="mt-4"
403
+ />
404
+
405
+ <div className="flex-1 pt-4">
406
+ {isLoading && (
407
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
408
+ {Array.from({ length: 3 }).map((_, i) => (
409
+ <Card
410
+ key={`skeleton-${i}`}
411
+ className="flex flex-col justify-between gap-2 rounded-2xl border border-border/60 bg-card p-4 shadow-sm animate-pulse"
412
+ >
413
+ <CardHeader className="p-0">
414
+ <div className="space-y-2">
415
+ <div className="h-4 w-40 rounded bg-muted" />
416
+ <div className="h-3 w-32 rounded bg-muted" />
417
+ </div>
418
+ </CardHeader>
419
+ </Card>
420
+ ))}
421
+ </div>
422
+ )}
423
+
424
+ {!isLoading &&
425
+ (!rolesResponse?.data || rolesResponse.data.length === 0) ? (
426
+ <p className="text-sm text-muted-foreground">{t('noRolesFound')}</p>
427
+ ) : (
428
+ <div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
429
+ {rolesResponse?.data?.map((role: Role & { role_id: number }) => (
430
+ <Card
431
+ key={String(role.role_id)}
432
+ onDoubleClick={() => handleEdit(role)}
433
+ 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"
434
+ >
435
+ <CardHeader className="flex items-start justify-between gap-4 p-0">
436
+ <div className="flex items-center gap-3 flex-1">
437
+ <div className="h-12 w-12 shrink-0 rounded-full bg-primary/10 flex items-center justify-center">
438
+ <ShieldCheck className="h-6 w-6 text-primary" />
439
+ </div>
440
+ <div className="flex-1">
441
+ <CardTitle className="text-sm font-semibold">
442
+ {role.name}
443
+ </CardTitle>
444
+ <CardDescription className="text-xs text-muted-foreground">
445
+ {role.slug}
446
+ </CardDescription>
447
+ </div>
448
+ </div>
449
+ <Button
450
+ variant="outline"
451
+ size="sm"
452
+ onClick={() => handleEdit(role)}
453
+ >
454
+ {t('buttonEditRole')}
455
+ </Button>
456
+ </CardHeader>
457
+ {role.description && (
458
+ <CardContent className="p-0">
459
+ <p className="text-xs text-muted-foreground line-clamp-2">
460
+ {role.description}
461
+ </p>
462
+ <div className="text-xs line-clamp-2 flex gap-2 py-2">
463
+ <div>
464
+ {(role as any).user_count}{' '}
465
+ {(role as any).user_count === 1
466
+ ? t('user')
467
+ : t('users')}
468
+ </div>
469
+ <div>•</div>
470
+ <div>
471
+ {(role as any).menu_count}{' '}
472
+ {(role as any).menu_count === 1
473
+ ? t('menu')
474
+ : t('menus')}
475
+ </div>
476
+ <div>•</div>
477
+ <div>
478
+ {(role as any).route_count}{' '}
479
+ {(role as any).route_count === 1
480
+ ? t('route')
481
+ : t('routes')}
482
+ </div>
483
+ </div>
484
+ </CardContent>
485
+ )}
486
+ </Card>
487
+ ))}
488
+ </div>
489
+ )}
490
+
491
+ <div className="w-full border-t pt-2 mt-4">
492
+ <PaginationFooter
493
+ currentPage={page}
494
+ pageSize={pageSize}
495
+ totalItems={rolesResponse?.total || 0}
496
+ onPageChange={setPage}
497
+ onPageSizeChange={(size) => {
498
+ setPageSize(size);
499
+ setPage(1);
500
+ }}
501
+ pageSizeOptions={[6, 12, 24, 48]}
502
+ />
503
+ </div>
504
+
505
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
506
+ <DialogContent className="sm:max-w-lg">
507
+ <DialogHeader>
508
+ <DialogTitle>{t('dialogAddRoleTitle')}</DialogTitle>
509
+ <DialogDescription>
510
+ {t('dialogAddRoleDescription')}
511
+ </DialogDescription>
512
+ </DialogHeader>
513
+ <div className="w-full border-t pt-1 mt-1" />
514
+ <Form {...form}>
515
+ <form
516
+ onSubmit={form.handleSubmit(onSubmit)}
517
+ className="space-y-4"
518
+ >
519
+ <FormField
520
+ control={form.control}
521
+ name="slug"
522
+ render={({ field }) => (
523
+ <FormItem>
524
+ <FormLabel>{t('formSlugLabel')}</FormLabel>
525
+ <FormControl>
526
+ <Input
527
+ placeholder={t('formSlugPlaceholder')}
528
+ {...field}
529
+ />
530
+ </FormControl>
531
+ <FormMessage />
532
+ </FormItem>
533
+ )}
534
+ />
535
+ <FormField
536
+ control={form.control}
537
+ name="name"
538
+ render={({ field }) => (
539
+ <FormItem>
540
+ <FormLabel>{t('formNameLabel')}</FormLabel>
541
+ <FormControl>
542
+ <Input
543
+ placeholder={t('formNamePlaceholder')}
544
+ {...field}
545
+ />
546
+ </FormControl>
547
+ <FormMessage />
548
+ </FormItem>
549
+ )}
550
+ />
551
+ <FormField
552
+ control={form.control}
553
+ name="description"
554
+ render={({ field }) => (
555
+ <FormItem>
556
+ <FormLabel>{t('formDescriptionLabel')}</FormLabel>
557
+ <FormControl>
558
+ <Textarea
559
+ placeholder={t('formDescriptionPlaceholder')}
560
+ {...field}
561
+ />
562
+ </FormControl>
563
+ <FormMessage />
564
+ </FormItem>
565
+ )}
566
+ />
567
+
568
+ {formError && (
569
+ <Alert
570
+ variant="destructive"
571
+ className="border-red-300 bg-red-50 rounded-md p-4"
572
+ >
573
+ <AlertTitle className="text-sm">
574
+ {t('verifyYourInput')}
575
+ </AlertTitle>
576
+ <AlertDescription className="text-sm">
577
+ {formError}
578
+ </AlertDescription>
579
+ </Alert>
580
+ )}
581
+
582
+ <Button type="submit" className="w-full">
583
+ {t('buttonAddRole')}
584
+ </Button>
585
+ </form>
586
+ </Form>
587
+ </DialogContent>
588
+ </Dialog>
589
+
590
+ {editingRole && (
591
+ <Sheet open={!!editingRole} onOpenChange={() => setEditingRole(null)}>
592
+ <SheetContent className="w-full sm:max-w-3xl overflow-y-auto gap-0">
593
+ <SheetHeader>
594
+ <SheetTitle>{t('titleEditRole')}</SheetTitle>
595
+ <SheetDescription>
596
+ {editingRole.locale?.[currentLocaleCode]?.name ||
597
+ editingRole.slug}
598
+ </SheetDescription>
599
+ </SheetHeader>
600
+
601
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
602
+ <TabsList className="grid w-full grid-cols-4 bg-muted rounded-md text-muted-foreground text-sm">
603
+ <TabsTrigger
604
+ value="basic-info"
605
+ className="flex items-center justify-center gap-2 px-3 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
606
+ >
607
+ <FileText className="h-4 w-4 min-h-4 min-w-4" />
608
+ <span className="hidden md:inline">
609
+ {t('tabBasicInfo')}
610
+ </span>
611
+ </TabsTrigger>
612
+ <TabsTrigger
613
+ value="users"
614
+ className="flex items-center justify-center gap-2 px-3 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
615
+ >
616
+ <Users className="h-4 w-4 min-h-4 min-w-4" />
617
+ <span className="hidden md:inline">{t('tabUsers')}</span>
618
+ </TabsTrigger>
619
+ <TabsTrigger
620
+ value="menus"
621
+ className="flex items-center justify-center gap-2 px-3 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
622
+ >
623
+ <Menu className="h-4 w-4 min-h-4 min-w-4" />
624
+ <span className="hidden md:inline">{t('tabMenus')}</span>
625
+ </TabsTrigger>
626
+ <TabsTrigger
627
+ value="routes"
628
+ className="flex items-center justify-center gap-2 px-3 data-[state=active]:bg-card data-[state=active]:text-primary data-[state=active]:font-medium"
629
+ >
630
+ <Route className="h-4 w-4 min-h-4 min-w-4" />
631
+ <span className="hidden md:inline">{t('tabRoutes')}</span>
632
+ </TabsTrigger>
633
+ </TabsList>
634
+
635
+ <TabsContent
636
+ value="basic-info"
637
+ className="space-y-4 mt-4 p-4 pt-0"
638
+ >
639
+ <div className="space-y-3">
640
+ <div>
641
+ <h4 className="text-sm font-semibold flex items-center gap-2">
642
+ <Save className="h-4 w-4" />
643
+ {t('basicInfoTitle')}
644
+ </h4>
645
+ <p className="text-xs text-muted-foreground mt-1">
646
+ {t('basicInfoDescription')}
647
+ </p>
648
+ </div>
649
+
650
+ <div className="flex items-center gap-2 p-3 bg-muted/50 rounded-md">
651
+ <span className="text-xs font-medium text-muted-foreground">
652
+ {t('editingInLocale')}:
653
+ </span>
654
+ <Select
655
+ value={selectedLocale}
656
+ onValueChange={(value) => setSelectedLocale(value)}
657
+ >
658
+ <SelectTrigger className="w-[180px] h-8">
659
+ <SelectValue placeholder={t('selectLocale')} />
660
+ </SelectTrigger>
661
+ <SelectContent>
662
+ {locales?.map((locale: any) => (
663
+ <SelectItem key={locale.code} value={locale.code}>
664
+ {locale.name}
665
+ </SelectItem>
666
+ ))}
667
+ </SelectContent>
668
+ </Select>
669
+ </div>
670
+
671
+ <Form {...editForm}>
672
+ <form
673
+ onSubmit={editForm.handleSubmit(onEditSubmit)}
674
+ className="space-y-4"
675
+ >
676
+ <FormField
677
+ control={editForm.control}
678
+ name="slug"
679
+ render={({ field }) => (
680
+ <FormItem>
681
+ <FormLabel>{t('editSlugLabel')}</FormLabel>
682
+ <FormControl>
683
+ <Input {...field} />
684
+ </FormControl>
685
+ <FormMessage />
686
+ </FormItem>
687
+ )}
688
+ />
689
+ <FormField
690
+ control={editForm.control}
691
+ name="name"
692
+ render={({ field }) => (
693
+ <FormItem>
694
+ <FormLabel>{t('editNameLabel')}</FormLabel>
695
+ <FormControl>
696
+ <Input {...field} />
697
+ </FormControl>
698
+ <FormMessage />
699
+ </FormItem>
700
+ )}
701
+ />
702
+ <FormField
703
+ control={editForm.control}
704
+ name="description"
705
+ render={({ field }) => (
706
+ <FormItem>
707
+ <FormLabel>{t('editDescriptionLabel')}</FormLabel>
708
+ <FormControl>
709
+ <Textarea {...field} />
710
+ </FormControl>
711
+ <FormMessage />
712
+ </FormItem>
713
+ )}
714
+ />
715
+
716
+ {editFormError && (
717
+ <Alert
718
+ variant="destructive"
719
+ className="border-red-300 bg-red-50 rounded-md p-4"
720
+ >
721
+ <AlertTitle className="text-sm">
722
+ {t('verifyYourInput')}
723
+ </AlertTitle>
724
+ <AlertDescription className="text-sm">
725
+ {editFormError}
726
+ </AlertDescription>
727
+ </Alert>
728
+ )}
729
+
730
+ <div className="flex flex-col w-full gap-2 pt-2">
731
+ <Button type="submit" className="w-full">
732
+ {t('saveChanges')}
733
+ </Button>
734
+ <Button
735
+ className="w-full"
736
+ type="button"
737
+ variant="outline"
738
+ onClick={() => setEditingRole(null)}
739
+ >
740
+ {t('cancel')}
741
+ </Button>
742
+ </div>
743
+ </form>
744
+ </Form>
745
+
746
+ <div className="border-t pt-4">
747
+ <Button
748
+ className="w-full cursor-pointer"
749
+ variant="destructive"
750
+ onClick={() => setOpenDeleteModal(true)}
751
+ >
752
+ <Trash2 className="w-4 h-4" />
753
+ <span>{t('buttonDeleteRole')}</span>
754
+ </Button>
755
+ </div>
756
+ </div>
757
+ </TabsContent>
758
+
759
+ <TabsContent value="users" className="space-y-4 mt-4 p-4 pt-0">
760
+ <RoleUsersSection
761
+ roleId={editingRole.id!}
762
+ onUserChange={handleRefreshEditingRole}
763
+ />
764
+ </TabsContent>
765
+
766
+ <TabsContent value="menus" className="space-y-4 mt-4 p-4 pt-0">
767
+ <RoleMenusSection
768
+ roleId={editingRole.id!}
769
+ onMenuChange={handleRefreshEditingRole}
770
+ />
771
+ </TabsContent>
772
+
773
+ <TabsContent value="routes" className="space-y-4 mt-4 p-4 pt-0">
774
+ <RoleRoutesSection
775
+ roleId={editingRole.id!}
776
+ onRouteChange={handleRefreshEditingRole}
777
+ />
778
+ </TabsContent>
779
+ </Tabs>
780
+ </SheetContent>
781
+ </Sheet>
782
+ )}
783
+
784
+ <Dialog open={openDeleteModal} onOpenChange={setOpenDeleteModal}>
785
+ <DialogContent className="sm:max-w-lg">
786
+ <DialogHeader>
787
+ <DialogTitle>{t('dialogDeleteRoleTitle')}</DialogTitle>
788
+ <DialogDescription>
789
+ {t('dialogDeleteRoleDescription')}
790
+ </DialogDescription>
791
+ </DialogHeader>
792
+ <hr className="mt-4" />
793
+ <div className="flex justify-end">
794
+ <Button
795
+ type="button"
796
+ className="px-4 w-28 h-12 py-2 bg-gray-300 text-black hover:bg-gray-300 hover:text-black rounded-sm mr-2 text-md"
797
+ onClick={() => setOpenDeleteModal(false)}
798
+ >
799
+ {t('deleteRoleCancel')}
800
+ </Button>
801
+ <Button
802
+ onClick={onDelete}
803
+ variant="destructive"
804
+ className="px-4 w-32 h-12 py-2 text-white hover:text-white rounded-sm text-md cursor-pointer"
805
+ >
806
+ {t('deleteRoleConfirm')}
807
+ </Button>
808
+ </div>
809
+ </DialogContent>
810
+ </Dialog>
811
+ </div>
812
+ </div>
813
+ );
814
+ }