@hed-hog/core 0.0.298 → 0.0.300

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 (123) hide show
  1. package/dist/dashboard/dashboard/dashboard.controller.d.ts +9 -0
  2. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  3. package/dist/dashboard/dashboard/dashboard.service.d.ts +9 -0
  4. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +14 -1
  6. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-component/dashboard-component.controller.js +28 -3
  8. package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +22 -1
  10. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-component/dashboard-component.service.js +185 -35
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  13. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts +1 -0
  14. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-component/dto/create.dto.js +5 -0
  16. package/dist/dashboard/dashboard-component/dto/create.dto.js.map +1 -1
  17. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts +1 -0
  18. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts.map +1 -1
  19. package/dist/dashboard/dashboard-component/dto/update.dto.js +5 -0
  20. package/dist/dashboard/dashboard-component/dto/update.dto.js.map +1 -1
  21. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -0
  22. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts.map +1 -1
  23. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -0
  24. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +72 -1
  26. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +111 -0
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  29. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +76 -1
  30. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  31. package/dist/dashboard/dashboard-core/dashboard-core.service.js +614 -23
  32. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  33. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +3 -0
  34. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  35. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +3 -0
  36. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  37. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
  38. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
  40. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
  41. package/hedhog/data/dashboard.yaml +12 -6
  42. package/hedhog/data/dashboard_component_role.yaml +66 -0
  43. package/hedhog/data/dashboard_item.yaml +1 -1
  44. package/hedhog/data/dashboard_role.yaml +2 -8
  45. package/hedhog/data/route.yaml +84 -0
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +457 -135
  47. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +3 -0
  48. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +365 -28
  49. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +376 -247
  50. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
  51. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1389 -0
  52. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
  53. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
  54. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
  55. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
  56. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
  57. package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
  58. package/hedhog/frontend/messages/en.json +115 -2
  59. package/hedhog/frontend/messages/pt.json +114 -1
  60. package/hedhog/frontend/public/dashboard-previews/.gitkeep +12 -0
  61. package/hedhog/frontend/public/dashboard-previews/account-security.png +0 -0
  62. package/hedhog/frontend/public/dashboard-previews/active-users-card.png +0 -0
  63. package/hedhog/frontend/public/dashboard-previews/activity-timeline.png +0 -0
  64. package/hedhog/frontend/public/dashboard-previews/cash-balance-kpi.png +0 -0
  65. package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
  66. package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
  67. package/hedhog/frontend/public/dashboard-previews/email-notifications.png +0 -0
  68. package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
  69. package/hedhog/frontend/public/dashboard-previews/login-history-chart.png +0 -0
  70. package/hedhog/frontend/public/dashboard-previews/mail-sent-card.png +0 -0
  71. package/hedhog/frontend/public/dashboard-previews/mail-sent-chart.png +0 -0
  72. package/hedhog/frontend/public/dashboard-previews/menus-card.png +0 -0
  73. package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
  74. package/hedhog/frontend/public/dashboard-previews/permissions-card.png +0 -0
  75. package/hedhog/frontend/public/dashboard-previews/permissions-chart.png +0 -0
  76. package/hedhog/frontend/public/dashboard-previews/profile-card.png +0 -0
  77. package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
  78. package/hedhog/frontend/public/dashboard-previews/routes-card.png +0 -0
  79. package/hedhog/frontend/public/dashboard-previews/session-activity-chart.png +0 -0
  80. package/hedhog/frontend/public/dashboard-previews/sessions-today-card.png +0 -0
  81. package/hedhog/frontend/public/dashboard-previews/stat-access-level.png +0 -0
  82. package/hedhog/frontend/public/dashboard-previews/stat-actions-today.png +0 -0
  83. package/hedhog/frontend/public/dashboard-previews/stat-consecutive-days.png +0 -0
  84. package/hedhog/frontend/public/dashboard-previews/stat-online-time.png +0 -0
  85. package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
  86. package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
  87. package/hedhog/frontend/public/dashboard-previews/user-growth-chart.png +0 -0
  88. package/hedhog/frontend/public/dashboard-previews/user-roles.png +0 -0
  89. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/account-security.tsx.ejs +34 -30
  90. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/active-users-card.tsx.ejs +2 -2
  91. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/activity-timeline.tsx.ejs +1 -1
  92. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/email-notifications.tsx.ejs +1 -1
  93. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/locale-config.tsx.ejs +1 -1
  94. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/login-history-chart.tsx.ejs +1 -1
  95. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-config.tsx.ejs +1 -1
  96. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-sent-card.tsx.ejs +2 -2
  97. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-sent-chart.tsx.ejs +1 -1
  98. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/menus-card.tsx.ejs +2 -2
  99. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/oauth-config.tsx.ejs +1 -1
  100. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/permissions-card.tsx.ejs +2 -2
  101. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/permissions-chart.tsx.ejs +1 -1
  102. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/profile-card.tsx.ejs +1 -1
  103. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/routes-card.tsx.ejs +2 -2
  104. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/session-activity-chart.tsx.ejs +1 -1
  105. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/sessions-today-card.tsx.ejs +2 -2
  106. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-access-level.tsx.ejs +1 -1
  107. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-actions-today.tsx.ejs +1 -1
  108. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-consecutive-days.tsx.ejs +1 -1
  109. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-online-time.tsx.ejs +1 -1
  110. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/storage-config.tsx.ejs +1 -1
  111. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/theme-config.tsx.ejs +1 -1
  112. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-growth-chart.tsx.ejs +1 -1
  113. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-roles.tsx.ejs +1 -1
  114. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-sessions.tsx.ejs +2 -2
  115. package/hedhog/table/dashboard.yaml +6 -0
  116. package/hedhog/table/dashboard_component.yaml +7 -0
  117. package/package.json +5 -5
  118. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +51 -14
  119. package/src/dashboard/dashboard-component/dashboard-component.service.ts +254 -43
  120. package/src/dashboard/dashboard-component/dto/create.dto.ts +4 -0
  121. package/src/dashboard/dashboard-component/dto/update.dto.ts +4 -0
  122. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +112 -1
  123. package/src/dashboard/dashboard-core/dashboard-core.service.ts +782 -24
@@ -1,42 +1,37 @@
1
1
  'use client';
2
2
 
3
+ import { PaginationFooter } from '@/components/entity-list/pagination-footer';
4
+ import { Badge } from '@/components/ui/badge';
3
5
  import { Button } from '@/components/ui/button';
6
+ import { Card, CardDescription, CardTitle } from '@/components/ui/card';
7
+ import { Checkbox } from '@/components/ui/checkbox';
8
+ import { Input } from '@/components/ui/input';
9
+ import { ScrollArea } from '@/components/ui/scroll-area';
4
10
  import {
5
- Card,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
9
- } from '@/components/ui/card';
10
- import {
11
- Dialog,
12
- DialogContent,
13
- DialogDescription,
14
- DialogFooter,
15
- DialogHeader,
16
- DialogTitle,
17
- } from '@/components/ui/dialog';
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '@/components/ui/select';
18
17
  import {
19
- DropdownMenu,
20
- DropdownMenuContent,
21
- DropdownMenuItem,
22
- DropdownMenuTrigger,
23
- } from '@/components/ui/dropdown-menu';
24
- import { ScrollArea } from '@/components/ui/scroll-area';
18
+ Sheet,
19
+ SheetContent,
20
+ SheetDescription,
21
+ SheetFooter,
22
+ SheetHeader,
23
+ SheetTitle,
24
+ } from '@/components/ui/sheet';
25
25
  import { Skeleton } from '@/components/ui/skeleton';
26
26
  import { cn } from '@/lib/utils';
27
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
28
- import {
29
- IconArrowsRightLeft,
30
- IconPlus,
31
- IconSettings,
32
- } from '@tabler/icons-react';
27
+ import { IconLayoutDashboard, IconPlus } from '@tabler/icons-react';
33
28
  import { useTranslations } from 'next-intl';
34
- import { useRouter } from 'next/navigation';
35
- import { useState } from 'react';
29
+ import { useEffect, useRef, useState } from 'react';
36
30
 
37
31
  interface DashboardComponent {
38
32
  id: number;
39
33
  slug: string;
34
+ library_slug?: string;
40
35
  path: string;
41
36
  min_width: number;
42
37
  max_width?: number;
@@ -53,260 +48,394 @@ interface DashboardComponent {
53
48
  }>;
54
49
  }
55
50
 
56
- interface Dashboard {
57
- id: number;
58
- slug: string;
59
- dashboard_locale?: Array<{
60
- name: string;
61
- locale: {
62
- code: string;
63
- };
64
- }>;
65
- }
66
-
67
51
  interface AddWidgetSelectorDialogProps {
68
52
  availableComponents: DashboardComponent[];
53
+ totalItems: number;
54
+ currentPage: number;
55
+ pageSize: number;
69
56
  isLoading: boolean;
70
- onAdd: (slug: string) => void;
71
- currentSlug?: string;
57
+ searchQuery: string;
58
+ moduleFilter: string;
59
+ modules: string[];
60
+ onSearchQueryChange: (value: string) => void;
61
+ onModuleFilterChange: (value: string) => void;
62
+ onPageChange: (page: number) => void;
63
+ onPageSizeChange: (pageSize: number) => void;
64
+ onAdd: (slugs: string[]) => Promise<void> | void;
65
+ buttonLabel?: string;
66
+ buttonVariant?: 'default' | 'outline';
67
+ openSignal?: number;
68
+ onOpenSignalHandled?: () => void;
69
+ }
70
+
71
+ const getWidgetLookupKey = (slug: string, librarySlug?: string): string => {
72
+ const parts = slug.split('.');
73
+ const baseSlug = parts[parts.length - 1] || slug;
74
+
75
+ if (librarySlug) {
76
+ return `${librarySlug}.${baseSlug}`;
77
+ }
78
+
79
+ return slug;
80
+ };
81
+
82
+ const getWidgetPreviewSlug = (slug: string): string => {
83
+ const parts = slug.split('.');
84
+ return parts[parts.length - 1] || slug;
85
+ };
86
+
87
+ const formatModuleLabel = (moduleSlug?: string): string => {
88
+ const normalized = (moduleSlug || 'core').replace(/[._-]+/g, ' ').trim();
89
+
90
+ return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
91
+ };
92
+
93
+ function WidgetPreview({
94
+ name,
95
+ slug,
96
+ width,
97
+ height,
98
+ isResizable,
99
+ resizableLabel,
100
+ fixedLabel,
101
+ }: {
102
+ name: string;
103
+ slug: string;
104
+ width: number;
105
+ height: number;
106
+ isResizable: boolean;
107
+ resizableLabel: string;
108
+ fixedLabel: string;
109
+ }) {
110
+ const previewSlug = getWidgetPreviewSlug(slug);
111
+ const previewUrl = `/libraries/core/dashboard-previews/${previewSlug}.png`;
112
+
113
+ return (
114
+ <div className="overflow-hidden rounded-lg border bg-background/80 p-2">
115
+ <div className="flex items-center gap-2">
116
+ <img
117
+ src={previewUrl}
118
+ alt={name}
119
+ className="h-16 w-24 shrink-0 rounded-md border object-cover"
120
+ onError={(event) => {
121
+ event.currentTarget.style.display = 'none';
122
+ }}
123
+ />
124
+ <div className="min-w-0 flex-1">
125
+ <div className="mb-1.5 flex flex-wrap items-center gap-1.5">
126
+ <Badge variant="secondary" className="px-1.5 py-0 text-[10px]">
127
+ {width}x{height}
128
+ </Badge>
129
+ <Badge variant="outline" className="px-1.5 py-0 text-[10px]">
130
+ {isResizable ? resizableLabel : fixedLabel}
131
+ </Badge>
132
+ </div>
133
+ <p className="truncate text-[10px] text-muted-foreground">{name}</p>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
72
138
  }
73
139
 
74
140
  export function AddWidgetSelectorDialog({
75
141
  availableComponents,
142
+ totalItems,
143
+ currentPage,
144
+ pageSize,
76
145
  isLoading,
146
+ searchQuery,
147
+ moduleFilter,
148
+ modules,
149
+ onSearchQueryChange,
150
+ onModuleFilterChange,
151
+ onPageChange,
152
+ onPageSizeChange,
77
153
  onAdd,
78
- currentSlug = 'default',
154
+ buttonLabel,
155
+ buttonVariant = 'outline',
156
+ openSignal,
157
+ onOpenSignalHandled,
79
158
  }: AddWidgetSelectorDialogProps) {
80
159
  const tWidget = useTranslations('core.AddWidgetDialog');
81
160
  const tMenu = useTranslations('core.DashboardMenu');
82
- const { request } = useApp();
83
- const router = useRouter();
84
161
 
85
162
  const [openWidgets, setOpenWidgets] = useState(false);
86
- const [openDashboards, setOpenDashboards] = useState(false);
87
- const [selectedWidget, setSelectedWidget] = useState<string | null>(null);
88
- const [selectedDashboard, setSelectedDashboard] = useState<string | null>(
89
- null
163
+ const [selectedWidgets, setSelectedWidgets] = useState<Set<string>>(
164
+ new Set()
90
165
  );
166
+ const [isAdding, setIsAdding] = useState(false);
167
+ const lastHandledOpenSignalRef = useRef<number | null>(null);
91
168
 
92
- // Buscar dashboards disponíveis para o usuário
93
- const { data: userDashboards, isLoading: isLoadingDashboards } = useQuery<
94
- Dashboard[]
95
- >({
96
- queryKey: ['user-dashboards'],
97
- queryFn: async () => {
98
- const { data } = await request<Dashboard[]>({
99
- url: '/dashboard-core/user-dashboards',
100
- method: 'GET',
101
- });
102
- return data;
103
- },
104
- });
169
+ const availableModuleOptions = Array.from(new Set(modules.filter(Boolean)));
105
170
 
106
- const handleAdd = () => {
107
- if (selectedWidget) {
108
- onAdd(selectedWidget);
109
- setSelectedWidget(null);
110
- setOpenWidgets(false);
171
+ useEffect(() => {
172
+ if (!openSignal) {
173
+ lastHandledOpenSignalRef.current = 0;
174
+ return;
175
+ }
176
+
177
+ if (openSignal !== lastHandledOpenSignalRef.current) {
178
+ lastHandledOpenSignalRef.current = openSignal;
179
+ setOpenWidgets(true);
180
+ onOpenSignalHandled?.();
111
181
  }
182
+ }, [onOpenSignalHandled, openSignal]);
183
+
184
+ const toggleSelectedWidget = (slug: string) => {
185
+ setSelectedWidgets((prev) => {
186
+ const next = new Set(prev);
187
+
188
+ if (next.has(slug)) {
189
+ next.delete(slug);
190
+ } else {
191
+ next.add(slug);
192
+ }
193
+
194
+ return next;
195
+ });
112
196
  };
113
197
 
114
- const handleSwitchDashboard = () => {
115
- if (selectedDashboard && selectedDashboard !== currentSlug) {
116
- router.push(`/core/dashboard/${selectedDashboard}`);
117
- setOpenDashboards(false);
198
+ const handleAdd = async () => {
199
+ if (selectedWidgets.size === 0 || isAdding) return;
200
+
201
+ setIsAdding(true);
202
+ try {
203
+ await onAdd(Array.from(selectedWidgets));
204
+ setSelectedWidgets(new Set());
205
+ setOpenWidgets(false);
206
+ } finally {
207
+ setIsAdding(false);
118
208
  }
119
209
  };
120
210
 
121
211
  return (
122
212
  <>
123
- <DropdownMenu>
124
- <DropdownMenuTrigger asChild>
125
- <Button size="sm" className="gap-2" variant="outline">
126
- <IconSettings className="size-4" />
127
- </Button>
128
- </DropdownMenuTrigger>
129
- <DropdownMenuContent align="end">
130
- <DropdownMenuItem onClick={() => setOpenWidgets(true)}>
131
- <IconPlus className="mr-2 size-4" />
132
- {tMenu('addWidgets')}
133
- </DropdownMenuItem>
134
- <DropdownMenuItem onClick={() => setOpenDashboards(true)}>
135
- <IconArrowsRightLeft className="mr-2 size-4" />
136
- {tMenu('switchDashboard')}
137
- </DropdownMenuItem>
138
- </DropdownMenuContent>
139
- </DropdownMenu>
213
+ <Button
214
+ size="sm"
215
+ variant={buttonVariant}
216
+ className="cursor-pointer gap-2"
217
+ onClick={() => setOpenWidgets(true)}
218
+ >
219
+ <IconPlus className="size-4" />
220
+ {buttonLabel || tMenu('addWidgets')}
221
+ </Button>
140
222
 
141
- {/* Diálogo de Adicionar Widgets */}
142
- <Dialog open={openWidgets} onOpenChange={setOpenWidgets}>
143
- <DialogContent className="sm:max-w-[600px]">
144
- <DialogHeader>
145
- <DialogTitle>{tWidget('title')}</DialogTitle>
146
- <DialogDescription>{tWidget('description')}</DialogDescription>
147
- </DialogHeader>
148
- <ScrollArea className="max-h-[400px] pr-4">
149
- {isLoading ? (
150
- <div className="grid gap-3">
151
- <Skeleton className="h-24 w-full" />
152
- <Skeleton className="h-24 w-full" />
153
- <Skeleton className="h-24 w-full" />
154
- </div>
155
- ) : (
156
- <div className="grid gap-3">
157
- {availableComponents.length === 0 ? (
158
- <div className="flex min-h-[200px] flex-col items-center justify-center text-center">
159
- <p className="text-muted-foreground text-sm">
160
- {tWidget('noComponentsAvailable')}
161
- </p>
162
- </div>
163
- ) : (
164
- availableComponents.map((component) => {
165
- const name =
166
- component.dashboard_component_locale?.[0]?.name ||
167
- component.slug;
168
- return (
169
- <Card
170
- key={component.slug}
171
- className={cn(
172
- 'cursor-pointer py-4 transition-colors hover:bg-accent',
173
- selectedWidget === component.slug &&
174
- 'border-primary bg-accent'
175
- )}
176
- onClick={() => setSelectedWidget(component.slug)}
177
- >
178
- <CardHeader>
179
- <div className="flex items-start gap-3">
180
- <div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
181
- <IconPlus className="text-primary size-5" />
182
- </div>
183
- <div className="flex-1">
184
- <CardTitle className="text-base">
185
- {name}
186
- </CardTitle>
187
- <CardDescription className="text-sm">
188
- {tWidget('dimensions')}: {component.width}x
189
- {component.height} |
190
- {component.is_resizable
191
- ? ` ${tWidget('resizable')}`
192
- : ` ${tWidget('fixedSize')}`}
193
- </CardDescription>
194
- </div>
195
- </div>
196
- </CardHeader>
197
- </Card>
198
- );
199
- })
200
- )}
223
+ <Sheet
224
+ open={openWidgets}
225
+ onOpenChange={(open) => {
226
+ setOpenWidgets(open);
227
+ if (!open) {
228
+ setSelectedWidgets(new Set());
229
+ }
230
+ }}
231
+ >
232
+ <SheetContent side="right" className="w-full p-0 sm:max-w-4xl">
233
+ <div className="flex h-full flex-col">
234
+ <SheetHeader className="border-b px-6 py-5 text-left">
235
+ <SheetTitle>{tWidget('title')}</SheetTitle>
236
+ <SheetDescription>
237
+ {tWidget('selectedOfTotal', {
238
+ selected: selectedWidgets.size,
239
+ total: totalItems,
240
+ })}
241
+ </SheetDescription>
242
+ </SheetHeader>
243
+
244
+ <div className="grid gap-3 border-b px-4 py-4 md:grid-cols-[minmax(0,1fr)_220px]">
245
+ <div className="space-y-2">
246
+ <label
247
+ htmlFor="dashboard-widget-search"
248
+ className="text-sm font-medium"
249
+ >
250
+ {tWidget('search')}
251
+ </label>
252
+ <Input
253
+ id="dashboard-widget-search"
254
+ value={searchQuery}
255
+ onChange={(event) => onSearchQueryChange(event.target.value)}
256
+ placeholder={tWidget('searchPlaceholder')}
257
+ />
201
258
  </div>
202
- )}
203
- </ScrollArea>
204
- <DialogFooter>
205
- <Button
206
- type="button"
207
- variant="outline"
208
- onClick={() => setOpenWidgets(false)}
209
- >
210
- {tWidget('cancel')}
211
- </Button>
212
- <Button
213
- type="button"
214
- onClick={handleAdd}
215
- disabled={!selectedWidget || isLoading}
216
- >
217
- {tWidget('add')}
218
- </Button>
219
- </DialogFooter>
220
- </DialogContent>
221
- </Dialog>
222
259
 
223
- {/* Diálogo de Trocar Dashboard */}
224
- <Dialog open={openDashboards} onOpenChange={setOpenDashboards}>
225
- <DialogContent className="sm:max-w-[600px]">
226
- <DialogHeader>
227
- <DialogTitle>{tMenu('selectDashboardTitle')}</DialogTitle>
228
- <DialogDescription>
229
- {tMenu('selectDashboardDescription')}
230
- </DialogDescription>
231
- </DialogHeader>
232
- <ScrollArea className="max-h-[400px] pr-4">
233
- {isLoadingDashboards ? (
234
- <div className="grid gap-3">
235
- <Skeleton className="h-24 w-full" />
236
- <Skeleton className="h-24 w-full" />
237
- <Skeleton className="h-24 w-full" />
260
+ <div className="space-y-2">
261
+ <label
262
+ htmlFor="dashboard-widget-module-filter"
263
+ className="text-sm font-medium"
264
+ >
265
+ {tWidget('moduleFilterLabel')}
266
+ </label>
267
+ <Select
268
+ value={moduleFilter}
269
+ onValueChange={onModuleFilterChange}
270
+ >
271
+ <SelectTrigger
272
+ id="dashboard-widget-module-filter"
273
+ className="w-full"
274
+ >
275
+ <SelectValue placeholder={tWidget('allModules')} />
276
+ </SelectTrigger>
277
+ <SelectContent>
278
+ <SelectItem value="all">{tWidget('allModules')}</SelectItem>
279
+ {availableModuleOptions.map((module) => (
280
+ <SelectItem key={module} value={module}>
281
+ {formatModuleLabel(module)}
282
+ </SelectItem>
283
+ ))}
284
+ </SelectContent>
285
+ </Select>
238
286
  </div>
239
- ) : (
240
- <div className="grid gap-3">
241
- {!userDashboards || userDashboards.length === 0 ? (
242
- <div className="flex min-h-[200px] flex-col items-center justify-center text-center">
243
- <p className="text-muted-foreground text-sm">
244
- {tMenu('noDashboardsAvailable')}
245
- </p>
287
+ </div>
288
+
289
+ <div className="grid min-h-0 flex-1 gap-4 p-4">
290
+ <ScrollArea className="min-h-0 pr-4">
291
+ {isLoading ? (
292
+ <div className="grid gap-2.5">
293
+ <Skeleton className="h-24 w-full" />
294
+ <Skeleton className="h-24 w-full" />
295
+ <Skeleton className="h-24 w-full" />
246
296
  </div>
247
297
  ) : (
248
- userDashboards.map((dashboard) => {
249
- const name =
250
- dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
251
- const isCurrent = dashboard.slug === currentSlug;
252
- return (
253
- <Card
254
- key={dashboard.slug}
255
- className={cn(
256
- 'cursor-pointer transition-colors py-4 hover:bg-accent',
257
- selectedDashboard === dashboard.slug &&
258
- 'border-primary bg-accent',
259
- isCurrent && 'opacity-50'
260
- )}
261
- onClick={() =>
262
- !isCurrent && setSelectedDashboard(dashboard.slug)
263
- }
264
- >
265
- <CardHeader>
266
- <div className="flex items-start gap-3">
267
- <div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
268
- <IconArrowsRightLeft className="text-primary size-5" />
269
- </div>
270
- <div className="flex-1">
271
- <CardTitle className="text-base">
272
- {name}
273
- {isCurrent && ' (atual)'}
274
- </CardTitle>
275
- <CardDescription className="text-sm">
276
- {dashboard.slug}
277
- </CardDescription>
298
+ <div className="grid gap-2.5">
299
+ {availableComponents.length === 0 ? (
300
+ <div className="flex min-h-55 flex-col items-center justify-center rounded-xl border border-dashed text-center">
301
+ <p className="text-sm text-muted-foreground">
302
+ {tWidget('noComponentsAvailable')}
303
+ </p>
304
+ </div>
305
+ ) : (
306
+ availableComponents.map((component) => {
307
+ const name =
308
+ component.dashboard_component_locale?.[0]?.name ||
309
+ component.slug;
310
+ const selectionKey = getWidgetLookupKey(
311
+ component.slug,
312
+ component.library_slug
313
+ );
314
+ const isSelected = selectedWidgets.has(selectionKey);
315
+ const moduleLabel = formatModuleLabel(
316
+ component.library_slug
317
+ );
318
+
319
+ return (
320
+ <Card
321
+ key={selectionKey}
322
+ role="checkbox"
323
+ tabIndex={0}
324
+ aria-checked={isSelected}
325
+ className={cn(
326
+ 'cursor-pointer border px-3 py-2.5 transition-all hover:border-primary/40 hover:bg-accent/30',
327
+ isSelected &&
328
+ 'border-primary bg-primary/10 shadow-sm ring-1 ring-primary/25'
329
+ )}
330
+ onClick={() => toggleSelectedWidget(selectionKey)}
331
+ onKeyDown={(event) => {
332
+ if (event.key === 'Enter' || event.key === ' ') {
333
+ event.preventDefault();
334
+ toggleSelectedWidget(selectionKey);
335
+ }
336
+ }}
337
+ >
338
+ <div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_180px] lg:items-center">
339
+ <div className="flex items-start gap-3">
340
+ <div
341
+ className={cn(
342
+ 'flex size-9 items-center justify-center rounded-lg',
343
+ isSelected
344
+ ? 'bg-primary text-primary-foreground'
345
+ : 'bg-primary/10 text-primary'
346
+ )}
347
+ >
348
+ <IconLayoutDashboard className="size-4" />
349
+ </div>
350
+ <div className="min-w-0 flex-1">
351
+ <div className="flex items-start justify-between gap-3">
352
+ <div className="min-w-0">
353
+ <CardTitle className="truncate text-sm">
354
+ {name}
355
+ </CardTitle>
356
+ <CardDescription className="mt-0.5 truncate text-[11px]">
357
+ {selectionKey}
358
+ </CardDescription>
359
+ </div>
360
+ <Checkbox
361
+ checked={isSelected}
362
+ aria-label={name}
363
+ className="pointer-events-none mt-0.5"
364
+ />
365
+ </div>
366
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
367
+ <Badge
368
+ variant="outline"
369
+ className="px-1.5 py-0 text-[10px]"
370
+ >
371
+ {tWidget('module')}: {moduleLabel}
372
+ </Badge>
373
+ <Badge
374
+ variant="secondary"
375
+ className="px-1.5 py-0 text-[10px]"
376
+ >
377
+ {tWidget('dimensions')}: {component.width}
378
+ x{component.height}
379
+ </Badge>
380
+ <Badge
381
+ variant="outline"
382
+ className="px-1.5 py-0 text-[10px]"
383
+ >
384
+ {component.is_resizable
385
+ ? tWidget('resizable')
386
+ : tWidget('fixedSize')}
387
+ </Badge>
388
+ </div>
389
+ </div>
390
+ </div>
391
+ <div className="lg:block">
392
+ <WidgetPreview
393
+ name={name}
394
+ slug={component.slug}
395
+ width={component.width}
396
+ height={component.height}
397
+ isResizable={component.is_resizable}
398
+ resizableLabel={tWidget('resizable')}
399
+ fixedLabel={tWidget('fixedSize')}
400
+ />
401
+ </div>
278
402
  </div>
279
- </div>
280
- </CardHeader>
281
- </Card>
282
- );
283
- })
403
+ </Card>
404
+ );
405
+ })
406
+ )}
407
+ </div>
284
408
  )}
409
+ </ScrollArea>
410
+ </div>
411
+
412
+ <SheetFooter className="border-t px-6 py-4">
413
+ <div className="flex w-full flex-col gap-4">
414
+ <PaginationFooter
415
+ currentPage={currentPage}
416
+ pageSize={pageSize}
417
+ totalItems={totalItems}
418
+ selectedCount={selectedWidgets.size}
419
+ onPageChange={onPageChange}
420
+ onPageSizeChange={onPageSizeChange}
421
+ />
422
+ <div className="flex justify-end">
423
+ <Button
424
+ type="button"
425
+ className="cursor-pointer"
426
+ onClick={handleAdd}
427
+ disabled={
428
+ selectedWidgets.size === 0 || isLoading || isAdding
429
+ }
430
+ >
431
+ {tWidget('add')}
432
+ </Button>
433
+ </div>
285
434
  </div>
286
- )}
287
- </ScrollArea>
288
- <DialogFooter>
289
- <Button
290
- type="button"
291
- variant="outline"
292
- onClick={() => setOpenDashboards(false)}
293
- >
294
- {tWidget('cancel')}
295
- </Button>
296
- <Button
297
- type="button"
298
- onClick={handleSwitchDashboard}
299
- disabled={
300
- !selectedDashboard ||
301
- isLoadingDashboards ||
302
- selectedDashboard === currentSlug
303
- }
304
- >
305
- {tMenu('switch')}
306
- </Button>
307
- </DialogFooter>
308
- </DialogContent>
309
- </Dialog>
435
+ </SheetFooter>
436
+ </div>
437
+ </SheetContent>
438
+ </Sheet>
310
439
  </>
311
440
  );
312
441
  }