@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
@@ -642,6 +642,153 @@ export class DashboardCoreService {
642
642
  return value.map((provider) => String(provider).toLowerCase());
643
643
  }
644
644
 
645
+ private slugifyDashboardName(value: string): string {
646
+ const normalized = value
647
+ .normalize('NFD')
648
+ .replace(/[\u0300-\u036f]/g, '')
649
+ .toLowerCase()
650
+ .replace(/[^a-z0-9]+/g, '-')
651
+ .replace(/^-+|-+$/g, '');
652
+
653
+ return normalized || `dashboard-${Date.now()}`;
654
+ }
655
+
656
+ private getDashboardDisplayName(
657
+ dashboard: {
658
+ slug: string;
659
+ dashboard_locale?: Array<{ name?: string | null }>;
660
+ },
661
+ ): string {
662
+ return dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
663
+ }
664
+
665
+ private async buildUniqueDashboardSlug(baseSlug: string): Promise<string> {
666
+ let slug = baseSlug;
667
+ let suffix = 2;
668
+
669
+ while (
670
+ await this.prismaService.dashboard.findFirst({
671
+ where: { slug },
672
+ select: { id: true },
673
+ })
674
+ ) {
675
+ slug = `${baseSlug}-${suffix}`;
676
+ suffix += 1;
677
+ }
678
+
679
+ return slug;
680
+ }
681
+
682
+ private async getDashboardUserOrThrow(
683
+ userId: number,
684
+ slug: string,
685
+ locale: string,
686
+ ) {
687
+ const dashboardUser = await this.prismaService.dashboard_user.findFirst({
688
+ where: {
689
+ user_id: userId,
690
+ dashboard: { slug },
691
+ },
692
+ include: {
693
+ dashboard: {
694
+ include: {
695
+ dashboard_locale: {
696
+ where: {
697
+ locale: {
698
+ code: locale,
699
+ },
700
+ },
701
+ },
702
+ },
703
+ },
704
+ },
705
+ });
706
+
707
+ if (!dashboardUser) {
708
+ throw new ForbiddenException(
709
+ getLocaleText('dashboardNotFound', locale, 'Dashboard not found.'),
710
+ );
711
+ }
712
+
713
+ return dashboardUser;
714
+ }
715
+
716
+ private async getUserRoleIds(userId: number) {
717
+ const roleUsers = await this.prismaService.role_user.findMany({
718
+ where: { user_id: userId },
719
+ select: { role_id: true },
720
+ });
721
+
722
+ return roleUsers.map((roleUser) => roleUser.role_id);
723
+ }
724
+
725
+ private async getAccessibleTemplateOrThrow(
726
+ userId: number,
727
+ templateSlug: string,
728
+ locale: string,
729
+ ) {
730
+ const userRoleIds = await this.getUserRoleIds(userId);
731
+ const templateAccessFilter =
732
+ userRoleIds.length > 0
733
+ ? {
734
+ OR: [
735
+ {
736
+ dashboard_role: {
737
+ some: {
738
+ role_id: {
739
+ in: userRoleIds,
740
+ },
741
+ },
742
+ },
743
+ },
744
+ {
745
+ dashboard_role: {
746
+ none: {},
747
+ },
748
+ },
749
+ ],
750
+ }
751
+ : {
752
+ dashboard_role: {
753
+ none: {},
754
+ },
755
+ };
756
+
757
+ const template = await this.prismaService.dashboard.findFirst({
758
+ where: {
759
+ slug: templateSlug,
760
+ is_template: true,
761
+ ...templateAccessFilter,
762
+ },
763
+ include: {
764
+ dashboard_locale: {
765
+ where: {
766
+ locale: {
767
+ code: locale,
768
+ },
769
+ },
770
+ },
771
+ dashboard_item: {
772
+ select: {
773
+ component_id: true,
774
+ width: true,
775
+ height: true,
776
+ x_axis: true,
777
+ y_axis: true,
778
+ },
779
+ },
780
+ },
781
+ });
782
+
783
+ if (!template) {
784
+ throw new ForbiddenException(
785
+ getLocaleText('dashboardNotFound', locale, 'Dashboard template not found.'),
786
+ );
787
+ }
788
+
789
+ return template;
790
+ }
791
+
645
792
  async getHome(userId: number, locale: string) {
646
793
  const user = await this.prismaService.user.findUnique({
647
794
  where: { id: userId },
@@ -1014,6 +1161,7 @@ export class DashboardCoreService {
1014
1161
  i: `widget-${item.id}`,
1015
1162
  component_id: item.component_id,
1016
1163
  slug: component.slug,
1164
+ library_slug: component.library_slug,
1017
1165
  name: locale?.name || component.slug,
1018
1166
  description: locale?.description || '',
1019
1167
  x: item.x_axis,
@@ -1060,28 +1208,34 @@ export class DashboardCoreService {
1060
1208
  throw new ForbiddenException('Access denied to this dashboard');
1061
1209
  }
1062
1210
 
1063
- for (const item of layout) {
1064
- const itemId = parseInt(item.i.replace('widget-', ''));
1065
- const dashboardItem = await this.prismaService.dashboard_item.findFirst({
1066
- where: {
1067
- id: itemId,
1068
- dashboard_id: dashboard.id,
1069
- },
1070
- });
1211
+ const layoutUpdates = layout.flatMap((item) => {
1212
+ const itemId = Number.parseInt(item.i.replace('widget-', ''), 10);
1071
1213
 
1072
- if (!dashboardItem) {
1073
- continue;
1214
+ if (Number.isNaN(itemId)) {
1215
+ this.logger.warn(
1216
+ `Skipping dashboard layout item with invalid id: ${item.i}`,
1217
+ );
1218
+ return [];
1074
1219
  }
1075
1220
 
1076
- await this.prismaService.dashboard_item.update({
1077
- where: { id: dashboardItem.id },
1078
- data: {
1079
- x_axis: item.x,
1080
- y_axis: item.y,
1081
- width: item.w,
1082
- height: item.h,
1083
- },
1084
- });
1221
+ return [
1222
+ this.prismaService.dashboard_item.updateMany({
1223
+ where: {
1224
+ id: itemId,
1225
+ dashboard_id: dashboard.id,
1226
+ },
1227
+ data: {
1228
+ x_axis: item.x,
1229
+ y_axis: item.y,
1230
+ width: item.w,
1231
+ height: item.h,
1232
+ },
1233
+ }),
1234
+ ];
1235
+ });
1236
+
1237
+ if (layoutUpdates.length > 0) {
1238
+ await this.prismaService.$transaction(layoutUpdates);
1085
1239
  }
1086
1240
 
1087
1241
  return { success: true };
@@ -1113,8 +1267,50 @@ export class DashboardCoreService {
1113
1267
  throw new ForbiddenException('Access denied to this dashboard');
1114
1268
  }
1115
1269
 
1270
+ const userRoles = await this.prismaService.role_user.findMany({
1271
+ where: { user_id: userId },
1272
+ select: { role_id: true },
1273
+ });
1274
+
1275
+ const userRoleIds = userRoles.map((item) => item.role_id);
1276
+
1277
+ if (userRoleIds.length === 0) {
1278
+ throw new ForbiddenException('Access denied to this component');
1279
+ }
1280
+
1281
+ const slugParts = componentSlug.split('.').filter(Boolean);
1282
+ const requestedSlug =
1283
+ slugParts.length > 0 ? slugParts[slugParts.length - 1]! : componentSlug;
1284
+ const requestedLibrarySlug =
1285
+ slugParts.length > 1 ? slugParts[0] : undefined;
1286
+
1116
1287
  const component = await this.prismaService.dashboard_component.findFirst({
1117
- where: { slug: componentSlug },
1288
+ where: {
1289
+ AND: [
1290
+ requestedLibrarySlug
1291
+ ? {
1292
+ OR: [
1293
+ { slug: componentSlug },
1294
+ {
1295
+ slug: requestedSlug,
1296
+ library_slug: requestedLibrarySlug,
1297
+ },
1298
+ ],
1299
+ }
1300
+ : {
1301
+ OR: [{ slug: componentSlug }, { slug: requestedSlug }],
1302
+ },
1303
+ {
1304
+ dashboard_component_role: {
1305
+ some: {
1306
+ role_id: {
1307
+ in: userRoleIds,
1308
+ },
1309
+ },
1310
+ },
1311
+ },
1312
+ ],
1313
+ },
1118
1314
  include: {
1119
1315
  dashboard_component_locale: {
1120
1316
  where: {
@@ -1127,7 +1323,7 @@ export class DashboardCoreService {
1127
1323
  });
1128
1324
 
1129
1325
  if (!component) {
1130
- throw new Error(`Component with slug '${componentSlug}' not found`);
1326
+ throw new ForbiddenException('Access denied to this component');
1131
1327
  }
1132
1328
 
1133
1329
  let dashboardItem = await this.prismaService.dashboard_item.findFirst({
@@ -1138,6 +1334,21 @@ export class DashboardCoreService {
1138
1334
  });
1139
1335
 
1140
1336
  if (!dashboardItem) {
1337
+ const dashboardItems = await this.prismaService.dashboard_item.findMany({
1338
+ where: {
1339
+ dashboard_id: dashboard.id,
1340
+ },
1341
+ select: {
1342
+ y_axis: true,
1343
+ height: true,
1344
+ },
1345
+ });
1346
+
1347
+ const nextAvailableY = dashboardItems.reduce(
1348
+ (maxY, item) => Math.max(maxY, item.y_axis + item.height),
1349
+ 0,
1350
+ );
1351
+
1141
1352
  dashboardItem = await this.prismaService.dashboard_item.create({
1142
1353
  data: {
1143
1354
  dashboard_id: dashboard.id,
@@ -1145,7 +1356,7 @@ export class DashboardCoreService {
1145
1356
  width: component.width,
1146
1357
  height: component.height,
1147
1358
  x_axis: 0,
1148
- y_axis: 0,
1359
+ y_axis: nextAvailableY,
1149
1360
  },
1150
1361
  });
1151
1362
  }
@@ -1156,6 +1367,7 @@ export class DashboardCoreService {
1156
1367
  i: `widget-${dashboardItem.id}`,
1157
1368
  component_id: component.id,
1158
1369
  slug: component.slug,
1370
+ library_slug: component.library_slug,
1159
1371
  name: locale?.name || component.slug,
1160
1372
  description: locale?.description || '',
1161
1373
  x: dashboardItem.x_axis,
@@ -1175,8 +1387,52 @@ export class DashboardCoreService {
1175
1387
  slug: string,
1176
1388
  widgetId: string,
1177
1389
  ) {
1178
-
1179
- throw new Error('Not implemented yet');
1390
+ const dashboard = await this.prismaService.dashboard.findFirst({
1391
+ where: { slug },
1392
+ });
1393
+
1394
+ if (!dashboard) {
1395
+ throw new Error(`Dashboard with slug '${slug}' not found`);
1396
+ }
1397
+
1398
+ const canAccess = await this.prismaService.dashboard_user.findFirst({
1399
+ where: {
1400
+ dashboard_id: dashboard.id,
1401
+ user_id: userId,
1402
+ },
1403
+ select: { id: true },
1404
+ });
1405
+
1406
+ if (!canAccess) {
1407
+ throw new ForbiddenException('Access denied to this dashboard');
1408
+ }
1409
+
1410
+ const parsedWidgetId = Number(widgetId.replace(/^widget-/, ''));
1411
+
1412
+ if (!Number.isInteger(parsedWidgetId) || parsedWidgetId <= 0) {
1413
+ throw new BadRequestException('Invalid widget id');
1414
+ }
1415
+
1416
+ const dashboardItem = await this.prismaService.dashboard_item.findFirst({
1417
+ where: {
1418
+ id: parsedWidgetId,
1419
+ dashboard_id: dashboard.id,
1420
+ },
1421
+ select: { id: true },
1422
+ });
1423
+
1424
+ if (!dashboardItem) {
1425
+ throw new BadRequestException('Widget not found in this dashboard');
1426
+ }
1427
+
1428
+ await this.prismaService.dashboard_item.delete({
1429
+ where: { id: dashboardItem.id },
1430
+ });
1431
+
1432
+ return {
1433
+ success: true,
1434
+ removedWidgetId: `widget-${dashboardItem.id}`,
1435
+ };
1180
1436
  }
1181
1437
 
1182
1438
  async checkDashboardAccess(userId: number, slug: string, locale: string) {
@@ -1222,6 +1478,8 @@ export class DashboardCoreService {
1222
1478
  }
1223
1479
 
1224
1480
  async getUserDashboards(userId: number, locale: string) {
1481
+ await this.getHome(userId, locale);
1482
+
1225
1483
  const dashboardUsers = await this.prismaService.dashboard_user.findMany({
1226
1484
  where: { user_id: userId },
1227
1485
  include: {
@@ -1237,6 +1495,7 @@ export class DashboardCoreService {
1237
1495
  },
1238
1496
  },
1239
1497
  },
1498
+ orderBy: [{ is_home: 'desc' }, { id: 'asc' }],
1240
1499
  });
1241
1500
 
1242
1501
  const uniqueByDashboardId = new Map<number, (typeof dashboardUsers)[number]>();
@@ -1249,10 +1508,509 @@ export class DashboardCoreService {
1249
1508
  return Array.from(uniqueByDashboardId.values()).map((dashboardUser) => ({
1250
1509
  id: dashboardUser.dashboard.id,
1251
1510
  slug: dashboardUser.dashboard.slug,
1511
+ name: this.getDashboardDisplayName(dashboardUser.dashboard),
1512
+ icon: dashboardUser.dashboard.icon,
1513
+ is_home: dashboardUser.is_home,
1252
1514
  dashboard_locale: dashboardUser.dashboard.dashboard_locale,
1253
1515
  }));
1254
1516
  }
1255
1517
 
1518
+ async getAvailableTemplates(userId: number, locale: string) {
1519
+ const userRoleIds = await this.getUserRoleIds(userId);
1520
+ const templateAccessFilter =
1521
+ userRoleIds.length > 0
1522
+ ? {
1523
+ OR: [
1524
+ {
1525
+ dashboard_role: {
1526
+ some: {
1527
+ role_id: {
1528
+ in: userRoleIds,
1529
+ },
1530
+ },
1531
+ },
1532
+ },
1533
+ {
1534
+ dashboard_role: {
1535
+ none: {},
1536
+ },
1537
+ },
1538
+ ],
1539
+ }
1540
+ : {
1541
+ dashboard_role: {
1542
+ none: {},
1543
+ },
1544
+ };
1545
+
1546
+ const templates = await this.prismaService.dashboard.findMany({
1547
+ where: {
1548
+ is_template: true,
1549
+ ...templateAccessFilter,
1550
+ },
1551
+ include: {
1552
+ dashboard_locale: {
1553
+ where: {
1554
+ locale: {
1555
+ code: locale,
1556
+ },
1557
+ },
1558
+ },
1559
+ dashboard_item: {
1560
+ select: { id: true },
1561
+ },
1562
+ },
1563
+ orderBy: [{ id: 'asc' }],
1564
+ });
1565
+
1566
+ return templates.map((template) => ({
1567
+ id: template.id,
1568
+ slug: template.slug,
1569
+ name: this.getDashboardDisplayName(template),
1570
+ icon: template.icon,
1571
+ itemCount: template.dashboard_item.length,
1572
+ }));
1573
+ }
1574
+
1575
+ async createUserDashboard(
1576
+ userId: number,
1577
+ data: { name?: string; slug?: string; icon?: string | null; templateSlug?: string },
1578
+ locale: string,
1579
+ ) {
1580
+ const templateSlug = this.toNullableString(data?.templateSlug);
1581
+ const template = templateSlug
1582
+ ? await this.getAccessibleTemplateOrThrow(userId, templateSlug, locale)
1583
+ : null;
1584
+
1585
+ const templateName = template ? this.getDashboardDisplayName(template) : null;
1586
+ const name = this.toNullableString(data?.name) || templateName;
1587
+
1588
+ if (!name) {
1589
+ throw new BadRequestException(
1590
+ getLocaleText('validation.fieldRequired', locale, 'Dashboard name is required.'),
1591
+ );
1592
+ }
1593
+
1594
+ const requestedSlug = this.toNullableString(data?.slug);
1595
+ const baseSlug = this.slugifyDashboardName(requestedSlug || name);
1596
+ const slug = await this.buildUniqueDashboardSlug(baseSlug);
1597
+ const icon =
1598
+ data?.icon === undefined
1599
+ ? template?.icon ?? null
1600
+ : this.toNullableString(data?.icon) ?? template?.icon ?? null;
1601
+
1602
+ const [localeRecord, existingCount] = await Promise.all([
1603
+ this.prismaService.locale.findFirst({
1604
+ where: { code: locale },
1605
+ select: { id: true },
1606
+ }),
1607
+ this.prismaService.dashboard_user.count({
1608
+ where: { user_id: userId },
1609
+ }),
1610
+ ]);
1611
+
1612
+ const dashboard = await this.prismaService.dashboard.create({
1613
+ data: {
1614
+ slug,
1615
+ icon,
1616
+ },
1617
+ });
1618
+
1619
+ if (localeRecord) {
1620
+ await this.prismaService.dashboard_locale.create({
1621
+ data: {
1622
+ dashboard_id: dashboard.id,
1623
+ locale_id: localeRecord.id,
1624
+ name,
1625
+ },
1626
+ });
1627
+ }
1628
+
1629
+ await this.prismaService.dashboard_user.create({
1630
+ data: {
1631
+ dashboard_id: dashboard.id,
1632
+ user_id: userId,
1633
+ is_home: existingCount === 0,
1634
+ },
1635
+ });
1636
+
1637
+ if (template?.dashboard_item?.length) {
1638
+ await this.prismaService.dashboard_item.createMany({
1639
+ data: template.dashboard_item.map((item) => ({
1640
+ dashboard_id: dashboard.id,
1641
+ component_id: item.component_id,
1642
+ width: item.width,
1643
+ height: item.height,
1644
+ x_axis: item.x_axis,
1645
+ y_axis: item.y_axis,
1646
+ })),
1647
+ });
1648
+ }
1649
+
1650
+ return {
1651
+ id: dashboard.id,
1652
+ slug,
1653
+ name,
1654
+ icon,
1655
+ is_home: existingCount === 0,
1656
+ };
1657
+ }
1658
+
1659
+ async renameUserDashboard(
1660
+ userId: number,
1661
+ slug: string,
1662
+ data: { name?: string; icon?: string | null },
1663
+ locale: string,
1664
+ ) {
1665
+ const dashboardUser = await this.getDashboardUserOrThrow(
1666
+ userId,
1667
+ slug,
1668
+ locale,
1669
+ );
1670
+ const name = this.toNullableString(data?.name);
1671
+ const normalizedIcon =
1672
+ data?.icon === undefined ? undefined : this.toNullableString(data?.icon);
1673
+
1674
+ if (!name && data?.icon === undefined) {
1675
+ throw new BadRequestException(
1676
+ getLocaleText(
1677
+ 'validation.fieldRequired',
1678
+ locale,
1679
+ 'Dashboard name or icon is required.',
1680
+ ),
1681
+ );
1682
+ }
1683
+
1684
+ if (data?.icon !== undefined) {
1685
+ await this.prismaService.dashboard.update({
1686
+ where: { id: dashboardUser.dashboard_id },
1687
+ data: {
1688
+ icon: normalizedIcon ?? null,
1689
+ },
1690
+ });
1691
+ }
1692
+
1693
+ if (name) {
1694
+ const localeRecord = await this.prismaService.locale.findFirst({
1695
+ where: { code: locale },
1696
+ select: { id: true },
1697
+ });
1698
+
1699
+ if (!localeRecord) {
1700
+ throw new BadRequestException(
1701
+ getLocaleText('localeNotFound', locale, 'Locale not found.'),
1702
+ );
1703
+ }
1704
+
1705
+ const existingLocale = await this.prismaService.dashboard_locale.findFirst({
1706
+ where: {
1707
+ dashboard_id: dashboardUser.dashboard_id,
1708
+ locale_id: localeRecord.id,
1709
+ },
1710
+ select: { id: true },
1711
+ });
1712
+
1713
+ if (existingLocale) {
1714
+ await this.prismaService.dashboard_locale.update({
1715
+ where: { id: existingLocale.id },
1716
+ data: { name },
1717
+ });
1718
+ } else {
1719
+ await this.prismaService.dashboard_locale.create({
1720
+ data: {
1721
+ dashboard_id: dashboardUser.dashboard_id,
1722
+ locale_id: localeRecord.id,
1723
+ name,
1724
+ },
1725
+ });
1726
+ }
1727
+ }
1728
+
1729
+ const updatedDashboard = await this.prismaService.dashboard.findUnique({
1730
+ where: { id: dashboardUser.dashboard_id },
1731
+ include: {
1732
+ dashboard_locale: {
1733
+ where: {
1734
+ locale: {
1735
+ code: locale,
1736
+ },
1737
+ },
1738
+ },
1739
+ },
1740
+ });
1741
+
1742
+ return {
1743
+ success: true,
1744
+ slug,
1745
+ name:
1746
+ name ||
1747
+ (updatedDashboard
1748
+ ? this.getDashboardDisplayName(updatedDashboard)
1749
+ : slug),
1750
+ icon: updatedDashboard?.icon ?? normalizedIcon ?? null,
1751
+ };
1752
+ }
1753
+
1754
+ async setHomeDashboard(userId: number, slug: string, locale: string) {
1755
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1756
+
1757
+ await this.prismaService.dashboard_user.updateMany({
1758
+ where: { user_id: userId },
1759
+ data: { is_home: false },
1760
+ });
1761
+
1762
+ await this.prismaService.dashboard_user.update({
1763
+ where: { id: dashboardUser.id },
1764
+ data: { is_home: true },
1765
+ });
1766
+
1767
+ return {
1768
+ success: true,
1769
+ slug,
1770
+ };
1771
+ }
1772
+
1773
+ async getDashboardShares(userId: number, slug: string, locale: string) {
1774
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1775
+
1776
+ const sharedUsers = await this.prismaService.dashboard_user.findMany({
1777
+ where: {
1778
+ dashboard_id: dashboardUser.dashboard_id,
1779
+ },
1780
+ include: {
1781
+ user: {
1782
+ select: {
1783
+ id: true,
1784
+ name: true,
1785
+ user_identifier: {
1786
+ where: {
1787
+ type: 'email',
1788
+ },
1789
+ select: {
1790
+ value: true,
1791
+ },
1792
+ take: 1,
1793
+ },
1794
+ },
1795
+ },
1796
+ },
1797
+ orderBy: {
1798
+ id: 'asc',
1799
+ },
1800
+ });
1801
+
1802
+ return sharedUsers.map((sharedDashboardUser) => ({
1803
+ id: sharedDashboardUser.user.id,
1804
+ name: sharedDashboardUser.user.name,
1805
+ email: sharedDashboardUser.user.user_identifier[0]?.value ?? null,
1806
+ isCurrentUser: sharedDashboardUser.user.id === userId,
1807
+ isHome: sharedDashboardUser.is_home,
1808
+ }));
1809
+ }
1810
+
1811
+ async getShareableUsers(
1812
+ userId: number,
1813
+ slug: string,
1814
+ search: string | undefined,
1815
+ locale: string,
1816
+ ) {
1817
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1818
+ const normalizedSearch = this.toNullableString(search);
1819
+
1820
+ const existingUsers = await this.prismaService.dashboard_user.findMany({
1821
+ where: {
1822
+ dashboard_id: dashboardUser.dashboard_id,
1823
+ },
1824
+ select: {
1825
+ user_id: true,
1826
+ },
1827
+ });
1828
+
1829
+ const users = await this.prismaService.user.findMany({
1830
+ where: {
1831
+ id: {
1832
+ notIn: existingUsers.map((item) => item.user_id),
1833
+ },
1834
+ ...(normalizedSearch
1835
+ ? {
1836
+ OR: [
1837
+ {
1838
+ name: {
1839
+ contains: normalizedSearch,
1840
+ mode: 'insensitive',
1841
+ },
1842
+ },
1843
+ {
1844
+ user_identifier: {
1845
+ some: {
1846
+ type: 'email',
1847
+ value: {
1848
+ contains: normalizedSearch,
1849
+ mode: 'insensitive',
1850
+ },
1851
+ },
1852
+ },
1853
+ },
1854
+ ],
1855
+ }
1856
+ : {}),
1857
+ },
1858
+ select: {
1859
+ id: true,
1860
+ name: true,
1861
+ user_identifier: {
1862
+ where: {
1863
+ type: 'email',
1864
+ },
1865
+ select: {
1866
+ value: true,
1867
+ },
1868
+ take: 1,
1869
+ },
1870
+ },
1871
+ take: 20,
1872
+ orderBy: {
1873
+ id: 'desc',
1874
+ },
1875
+ });
1876
+
1877
+ return users.map((candidateUser) => ({
1878
+ id: candidateUser.id,
1879
+ name: candidateUser.name,
1880
+ email: candidateUser.user_identifier[0]?.value ?? null,
1881
+ }));
1882
+ }
1883
+
1884
+ async shareDashboard(
1885
+ userId: number,
1886
+ slug: string,
1887
+ sharedUserId: number | undefined,
1888
+ locale: string,
1889
+ ) {
1890
+ if (!sharedUserId || Number.isNaN(Number(sharedUserId))) {
1891
+ throw new BadRequestException(
1892
+ getLocaleText('validation.fieldRequired', locale, 'User is required.'),
1893
+ );
1894
+ }
1895
+
1896
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1897
+
1898
+ if (sharedUserId === userId) {
1899
+ throw new BadRequestException(
1900
+ getLocaleText(
1901
+ 'validation.invalidValue',
1902
+ locale,
1903
+ 'You already have access to this dashboard.',
1904
+ ),
1905
+ );
1906
+ }
1907
+
1908
+ const targetUser = await this.prismaService.user.findUnique({
1909
+ where: { id: sharedUserId },
1910
+ select: { id: true },
1911
+ });
1912
+
1913
+ if (!targetUser) {
1914
+ throw new BadRequestException(getLocaleText('userNotFound', locale, 'User not found.'));
1915
+ }
1916
+
1917
+ const existingShare = await this.prismaService.dashboard_user.findFirst({
1918
+ where: {
1919
+ dashboard_id: dashboardUser.dashboard_id,
1920
+ user_id: sharedUserId,
1921
+ },
1922
+ select: { id: true },
1923
+ });
1924
+
1925
+ if (existingShare) {
1926
+ return {
1927
+ success: true,
1928
+ alreadyShared: true,
1929
+ };
1930
+ }
1931
+
1932
+ await this.prismaService.dashboard_user.create({
1933
+ data: {
1934
+ dashboard_id: dashboardUser.dashboard_id,
1935
+ user_id: sharedUserId,
1936
+ is_home: false,
1937
+ },
1938
+ });
1939
+
1940
+ return {
1941
+ success: true,
1942
+ };
1943
+ }
1944
+
1945
+ async revokeDashboardShare(
1946
+ userId: number,
1947
+ slug: string,
1948
+ sharedUserId: number,
1949
+ locale: string,
1950
+ ) {
1951
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1952
+
1953
+ if (sharedUserId === userId) {
1954
+ throw new BadRequestException(
1955
+ getLocaleText(
1956
+ 'validation.invalidValue',
1957
+ locale,
1958
+ 'Use the remove dashboard action to leave this tab.',
1959
+ ),
1960
+ );
1961
+ }
1962
+
1963
+ await this.prismaService.dashboard_user.deleteMany({
1964
+ where: {
1965
+ dashboard_id: dashboardUser.dashboard_id,
1966
+ user_id: sharedUserId,
1967
+ },
1968
+ });
1969
+
1970
+ return {
1971
+ success: true,
1972
+ };
1973
+ }
1974
+
1975
+ async removeUserDashboard(userId: number, slug: string, locale: string) {
1976
+ const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
1977
+
1978
+ await this.prismaService.dashboard_user.delete({
1979
+ where: { id: dashboardUser.id },
1980
+ });
1981
+
1982
+ if (dashboardUser.is_home) {
1983
+ const nextDashboard = await this.prismaService.dashboard_user.findFirst({
1984
+ where: { user_id: userId },
1985
+ orderBy: { id: 'asc' },
1986
+ });
1987
+
1988
+ if (nextDashboard) {
1989
+ await this.prismaService.dashboard_user.update({
1990
+ where: { id: nextDashboard.id },
1991
+ data: { is_home: true },
1992
+ });
1993
+ }
1994
+ }
1995
+
1996
+ const remainingShares = await this.prismaService.dashboard_user.count({
1997
+ where: {
1998
+ dashboard_id: dashboardUser.dashboard_id,
1999
+ },
2000
+ });
2001
+
2002
+ if (remainingShares === 0) {
2003
+ await this.prismaService.dashboard.delete({
2004
+ where: { id: dashboardUser.dashboard_id },
2005
+ });
2006
+ }
2007
+
2008
+ return {
2009
+ success: true,
2010
+ removedSlug: slug,
2011
+ };
2012
+ }
2013
+
1256
2014
  async getAccountSecurity(userId: number) {
1257
2015
  const now = new Date();
1258
2016
  const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);