@hed-hog/core 0.0.299 → 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 (112) hide show
  1. package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -0
  2. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  3. package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -0
  4. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +2 -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 +6 -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 +7 -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 +76 -33
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +65 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +111 -0
  16. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +69 -0
  18. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  19. package/dist/dashboard/dashboard-core/dashboard-core.service.js +526 -19
  20. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  21. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -0
  22. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  23. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -0
  24. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
  26. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
  28. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
  29. package/hedhog/data/dashboard.yaml +12 -6
  30. package/hedhog/data/dashboard_component_role.yaml +66 -0
  31. package/hedhog/data/dashboard_role.yaml +2 -8
  32. package/hedhog/data/route.yaml +72 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +333 -128
  34. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +277 -53
  35. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +179 -231
  36. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
  37. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1389 -0
  38. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
  39. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
  40. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
  41. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
  42. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
  43. package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
  44. package/hedhog/frontend/messages/en.json +112 -2
  45. package/hedhog/frontend/messages/pt.json +111 -1
  46. package/hedhog/frontend/widgets/account-security.tsx.ejs +1 -1
  47. package/hedhog/frontend/widgets/active-users-card.tsx.ejs +2 -2
  48. package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +1 -1
  49. package/hedhog/frontend/widgets/email-notifications.tsx.ejs +1 -1
  50. package/hedhog/frontend/widgets/locale-config.tsx.ejs +1 -1
  51. package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +1 -1
  52. package/hedhog/frontend/widgets/mail-config.tsx.ejs +1 -1
  53. package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +2 -2
  54. package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +1 -1
  55. package/hedhog/frontend/widgets/menus-card.tsx.ejs +2 -2
  56. package/hedhog/frontend/widgets/oauth-config.tsx.ejs +1 -1
  57. package/hedhog/frontend/widgets/permissions-card.tsx.ejs +2 -2
  58. package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +1 -1
  59. package/hedhog/frontend/widgets/profile-card.tsx.ejs +1 -1
  60. package/hedhog/frontend/widgets/routes-card.tsx.ejs +2 -2
  61. package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +1 -1
  62. package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +2 -2
  63. package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +1 -1
  64. package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +1 -1
  65. package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +1 -1
  66. package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +1 -1
  67. package/hedhog/frontend/widgets/storage-config.tsx.ejs +1 -1
  68. package/hedhog/frontend/widgets/theme-config.tsx.ejs +1 -1
  69. package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +1 -1
  70. package/hedhog/frontend/widgets/user-roles.tsx.ejs +1 -1
  71. package/hedhog/frontend/widgets/user-sessions.tsx.ejs +1 -1
  72. package/hedhog/table/dashboard.yaml +6 -0
  73. package/package.json +5 -5
  74. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +15 -2
  75. package/src/dashboard/dashboard-component/dashboard-component.service.ts +107 -43
  76. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +112 -1
  77. package/src/dashboard/dashboard-core/dashboard-core.service.ts +674 -19
  78. package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +0 -11
  79. package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +0 -192
  80. package/hedhog/frontend/app/dashboard/components/widgets/core.active-users-card.tsx.ejs +0 -58
  81. package/hedhog/frontend/app/dashboard/components/widgets/core.activity-timeline.tsx.ejs +0 -223
  82. package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +0 -226
  83. package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +0 -168
  84. package/hedhog/frontend/app/dashboard/components/widgets/core.login-history-chart.tsx.ejs +0 -115
  85. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +0 -199
  86. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-card.tsx.ejs +0 -58
  87. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-chart.tsx.ejs +0 -149
  88. package/hedhog/frontend/app/dashboard/components/widgets/core.menus-card.tsx.ejs +0 -58
  89. package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +0 -175
  90. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-card.tsx.ejs +0 -61
  91. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-chart.tsx.ejs +0 -156
  92. package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +0 -186
  93. package/hedhog/frontend/app/dashboard/components/widgets/core.routes-card.tsx.ejs +0 -58
  94. package/hedhog/frontend/app/dashboard/components/widgets/core.session-activity-chart.tsx.ejs +0 -183
  95. package/hedhog/frontend/app/dashboard/components/widgets/core.sessions-today-card.tsx.ejs +0 -62
  96. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-access-level.tsx.ejs +0 -57
  97. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-actions-today.tsx.ejs +0 -57
  98. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-consecutive-days.tsx.ejs +0 -57
  99. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-online-time.tsx.ejs +0 -57
  100. package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +0 -196
  101. package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +0 -213
  102. package/hedhog/frontend/app/dashboard/components/widgets/core.user-growth-chart.tsx.ejs +0 -210
  103. package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +0 -132
  104. package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +0 -236
  105. package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +0 -108
  106. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +0 -66
  107. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +0 -122
  108. package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +0 -63
  109. package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +0 -73
  110. package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +0 -73
  111. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +0 -123
  112. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +0 -118
@@ -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 },
@@ -1061,28 +1208,34 @@ export class DashboardCoreService {
1061
1208
  throw new ForbiddenException('Access denied to this dashboard');
1062
1209
  }
1063
1210
 
1064
- for (const item of layout) {
1065
- const itemId = parseInt(item.i.replace('widget-', ''));
1066
- const dashboardItem = await this.prismaService.dashboard_item.findFirst({
1067
- where: {
1068
- id: itemId,
1069
- dashboard_id: dashboard.id,
1070
- },
1071
- });
1211
+ const layoutUpdates = layout.flatMap((item) => {
1212
+ const itemId = Number.parseInt(item.i.replace('widget-', ''), 10);
1072
1213
 
1073
- if (!dashboardItem) {
1074
- continue;
1214
+ if (Number.isNaN(itemId)) {
1215
+ this.logger.warn(
1216
+ `Skipping dashboard layout item with invalid id: ${item.i}`,
1217
+ );
1218
+ return [];
1075
1219
  }
1076
1220
 
1077
- await this.prismaService.dashboard_item.update({
1078
- where: { id: dashboardItem.id },
1079
- data: {
1080
- x_axis: item.x,
1081
- y_axis: item.y,
1082
- width: item.w,
1083
- height: item.h,
1084
- },
1085
- });
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);
1086
1239
  }
1087
1240
 
1088
1241
  return { success: true };
@@ -1325,6 +1478,8 @@ export class DashboardCoreService {
1325
1478
  }
1326
1479
 
1327
1480
  async getUserDashboards(userId: number, locale: string) {
1481
+ await this.getHome(userId, locale);
1482
+
1328
1483
  const dashboardUsers = await this.prismaService.dashboard_user.findMany({
1329
1484
  where: { user_id: userId },
1330
1485
  include: {
@@ -1340,6 +1495,7 @@ export class DashboardCoreService {
1340
1495
  },
1341
1496
  },
1342
1497
  },
1498
+ orderBy: [{ is_home: 'desc' }, { id: 'asc' }],
1343
1499
  });
1344
1500
 
1345
1501
  const uniqueByDashboardId = new Map<number, (typeof dashboardUsers)[number]>();
@@ -1352,10 +1508,509 @@ export class DashboardCoreService {
1352
1508
  return Array.from(uniqueByDashboardId.values()).map((dashboardUser) => ({
1353
1509
  id: dashboardUser.dashboard.id,
1354
1510
  slug: dashboardUser.dashboard.slug,
1511
+ name: this.getDashboardDisplayName(dashboardUser.dashboard),
1512
+ icon: dashboardUser.dashboard.icon,
1513
+ is_home: dashboardUser.is_home,
1355
1514
  dashboard_locale: dashboardUser.dashboard.dashboard_locale,
1356
1515
  }));
1357
1516
  }
1358
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
+
1359
2014
  async getAccountSecurity(userId: number) {
1360
2015
  const now = new Date();
1361
2016
  const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);