@hed-hog/core 0.0.297 → 0.0.299

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 (183) hide show
  1. package/dist/auth/auth.controller.d.ts +10 -10
  2. package/dist/auth/auth.service.d.ts +10 -10
  3. package/dist/dashboard/dashboard/dashboard.controller.d.ts +3 -0
  4. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard/dashboard.service.d.ts +3 -0
  6. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +12 -0
  8. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
  9. package/dist/dashboard/dashboard-component/dashboard-component.controller.js +22 -0
  10. package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
  11. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +15 -0
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
  13. package/dist/dashboard/dashboard-component/dashboard-component.service.js +110 -3
  14. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  15. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts +1 -0
  16. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts.map +1 -1
  17. package/dist/dashboard/dashboard-component/dto/create.dto.js +5 -0
  18. package/dist/dashboard/dashboard-component/dto/create.dto.js.map +1 -1
  19. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts +1 -0
  20. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts.map +1 -1
  21. package/dist/dashboard/dashboard-component/dto/update.dto.js +5 -0
  22. package/dist/dashboard/dashboard-component/dto/update.dto.js.map +1 -1
  23. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -0
  24. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -0
  26. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +21 -1
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  29. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  30. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  31. package/dist/dashboard/dashboard-core/dashboard-core.module.d.ts.map +1 -1
  32. package/dist/dashboard/dashboard-core/dashboard-core.module.js +6 -1
  33. package/dist/dashboard/dashboard-core/dashboard-core.module.js.map +1 -1
  34. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +180 -2
  35. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  36. package/dist/dashboard/dashboard-core/dashboard-core.service.js +619 -9
  37. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  38. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +1 -0
  39. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  40. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +1 -0
  41. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  42. package/dist/file/file.controller.d.ts.map +1 -1
  43. package/dist/file/file.controller.js +16 -0
  44. package/dist/file/file.controller.js.map +1 -1
  45. package/dist/file/file.service.d.ts +7 -1
  46. package/dist/file/file.service.d.ts.map +1 -1
  47. package/dist/file/file.service.js +38 -1
  48. package/dist/file/file.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts +1 -0
  50. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  51. package/dist/file/provider/s3.provider.js +38 -29
  52. package/dist/file/provider/s3.provider.js.map +1 -1
  53. package/dist/oauth/oauth.service.d.ts.map +1 -1
  54. package/dist/oauth/oauth.service.js +2 -1
  55. package/dist/oauth/oauth.service.js.map +1 -1
  56. package/dist/user/constants/user.constants.d.ts +1 -0
  57. package/dist/user/constants/user.constants.d.ts.map +1 -1
  58. package/dist/user/constants/user.constants.js +2 -1
  59. package/dist/user/constants/user.constants.js.map +1 -1
  60. package/dist/user/user.controller.d.ts +10 -10
  61. package/dist/user/user.service.d.ts +30 -30
  62. package/dist/user/user.service.d.ts.map +1 -1
  63. package/dist/user/user.service.js +2 -1
  64. package/dist/user/user.service.js.map +1 -1
  65. package/hedhog/data/dashboard_item.yaml +10 -10
  66. package/hedhog/data/route.yaml +20 -0
  67. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +212 -34
  68. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +3 -0
  69. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +136 -23
  70. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +266 -85
  71. package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +11 -0
  72. package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +192 -0
  73. package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +226 -0
  74. package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +168 -0
  75. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +199 -0
  76. package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +175 -0
  77. package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +186 -0
  78. package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +196 -0
  79. package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +213 -0
  80. package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +132 -0
  81. package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +236 -0
  82. package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +108 -0
  83. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +66 -0
  84. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +122 -0
  85. package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +63 -0
  86. package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +73 -0
  87. package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +73 -0
  88. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +123 -0
  89. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +118 -0
  90. package/hedhog/frontend/messages/en.json +93 -0
  91. package/hedhog/frontend/messages/pt.json +93 -0
  92. package/hedhog/frontend/public/dashboard-previews/.gitkeep +12 -0
  93. package/hedhog/frontend/public/dashboard-previews/account-security.png +0 -0
  94. package/hedhog/frontend/public/dashboard-previews/active-users-card.png +0 -0
  95. package/hedhog/frontend/public/dashboard-previews/activity-timeline.png +0 -0
  96. package/hedhog/frontend/public/dashboard-previews/cash-balance-kpi.png +0 -0
  97. package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
  98. package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
  99. package/hedhog/frontend/public/dashboard-previews/email-notifications.png +0 -0
  100. package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
  101. package/hedhog/frontend/public/dashboard-previews/login-history-chart.png +0 -0
  102. package/hedhog/frontend/public/dashboard-previews/mail-sent-card.png +0 -0
  103. package/hedhog/frontend/public/dashboard-previews/mail-sent-chart.png +0 -0
  104. package/hedhog/frontend/public/dashboard-previews/menus-card.png +0 -0
  105. package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
  106. package/hedhog/frontend/public/dashboard-previews/permissions-card.png +0 -0
  107. package/hedhog/frontend/public/dashboard-previews/permissions-chart.png +0 -0
  108. package/hedhog/frontend/public/dashboard-previews/profile-card.png +0 -0
  109. package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
  110. package/hedhog/frontend/public/dashboard-previews/routes-card.png +0 -0
  111. package/hedhog/frontend/public/dashboard-previews/session-activity-chart.png +0 -0
  112. package/hedhog/frontend/public/dashboard-previews/sessions-today-card.png +0 -0
  113. package/hedhog/frontend/public/dashboard-previews/stat-access-level.png +0 -0
  114. package/hedhog/frontend/public/dashboard-previews/stat-actions-today.png +0 -0
  115. package/hedhog/frontend/public/dashboard-previews/stat-consecutive-days.png +0 -0
  116. package/hedhog/frontend/public/dashboard-previews/stat-online-time.png +0 -0
  117. package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
  118. package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
  119. package/hedhog/frontend/public/dashboard-previews/user-growth-chart.png +0 -0
  120. package/hedhog/frontend/public/dashboard-previews/user-roles.png +0 -0
  121. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/account-security.tsx.ejs +33 -29
  122. package/hedhog/frontend/widgets/active-users-card.tsx.ejs +58 -0
  123. package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +223 -0
  124. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/email-notifications.tsx.ejs +85 -61
  125. package/hedhog/frontend/widgets/locale-config.tsx.ejs +168 -0
  126. package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +115 -0
  127. package/hedhog/frontend/widgets/mail-config.tsx.ejs +199 -0
  128. package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +58 -0
  129. package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +149 -0
  130. package/hedhog/frontend/widgets/menus-card.tsx.ejs +58 -0
  131. package/hedhog/frontend/widgets/oauth-config.tsx.ejs +175 -0
  132. package/hedhog/frontend/widgets/permissions-card.tsx.ejs +61 -0
  133. package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +156 -0
  134. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/profile-card.tsx.ejs +3 -3
  135. package/hedhog/frontend/widgets/routes-card.tsx.ejs +58 -0
  136. package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +183 -0
  137. package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +62 -0
  138. package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +57 -0
  139. package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +57 -0
  140. package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +57 -0
  141. package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +57 -0
  142. package/hedhog/frontend/widgets/storage-config.tsx.ejs +196 -0
  143. package/hedhog/frontend/widgets/theme-config.tsx.ejs +213 -0
  144. package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +210 -0
  145. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-roles.tsx.ejs +12 -14
  146. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-sessions.tsx.ejs +1 -1
  147. package/hedhog/table/dashboard_component.yaml +7 -0
  148. package/hedhog/table/mail_sent_user.yaml +75 -0
  149. package/package.json +4 -4
  150. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +36 -12
  151. package/src/dashboard/dashboard-component/dashboard-component.service.ts +150 -3
  152. package/src/dashboard/dashboard-component/dto/create.dto.ts +4 -0
  153. package/src/dashboard/dashboard-component/dto/update.dto.ts +4 -0
  154. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  155. package/src/dashboard/dashboard-core/dashboard-core.module.ts +6 -1
  156. package/src/dashboard/dashboard-core/dashboard-core.service.ts +874 -8
  157. package/src/file/file.controller.ts +37 -13
  158. package/src/file/file.service.ts +47 -5
  159. package/src/file/provider/s3.provider.ts +39 -29
  160. package/src/oauth/oauth.service.ts +8 -7
  161. package/src/user/constants/user.constants.ts +1 -0
  162. package/src/user/user.service.ts +2 -1
  163. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +0 -309
  164. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +0 -445
  165. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +0 -296
  166. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +0 -340
  167. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +0 -275
  168. /package/hedhog/frontend/app/dashboard/components/widgets/{active-users-card.tsx.ejs → core.active-users-card.tsx.ejs} +0 -0
  169. /package/hedhog/frontend/app/dashboard/components/widgets/{activity-timeline.tsx.ejs → core.activity-timeline.tsx.ejs} +0 -0
  170. /package/hedhog/frontend/app/dashboard/components/widgets/{login-history-chart.tsx.ejs → core.login-history-chart.tsx.ejs} +0 -0
  171. /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-card.tsx.ejs → core.mail-sent-card.tsx.ejs} +0 -0
  172. /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-chart.tsx.ejs → core.mail-sent-chart.tsx.ejs} +0 -0
  173. /package/hedhog/frontend/app/dashboard/components/widgets/{menus-card.tsx.ejs → core.menus-card.tsx.ejs} +0 -0
  174. /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-card.tsx.ejs → core.permissions-card.tsx.ejs} +0 -0
  175. /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-chart.tsx.ejs → core.permissions-chart.tsx.ejs} +0 -0
  176. /package/hedhog/frontend/app/dashboard/components/widgets/{routes-card.tsx.ejs → core.routes-card.tsx.ejs} +0 -0
  177. /package/hedhog/frontend/app/dashboard/components/widgets/{session-activity-chart.tsx.ejs → core.session-activity-chart.tsx.ejs} +0 -0
  178. /package/hedhog/frontend/app/dashboard/components/widgets/{sessions-today-card.tsx.ejs → core.sessions-today-card.tsx.ejs} +0 -0
  179. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-access-level.tsx.ejs → core.stat-access-level.tsx.ejs} +0 -0
  180. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-actions-today.tsx.ejs → core.stat-actions-today.tsx.ejs} +0 -0
  181. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-consecutive-days.tsx.ejs → core.stat-consecutive-days.tsx.ejs} +0 -0
  182. /package/hedhog/frontend/app/dashboard/components/widgets/{stat-online-time.tsx.ejs → core.stat-online-time.tsx.ejs} +0 -0
  183. /package/hedhog/frontend/app/dashboard/components/widgets/{user-growth-chart.tsx.ejs → core.user-growth-chart.tsx.ejs} +0 -0
@@ -1,10 +1,646 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PrismaService } from '@hed-hog/api-prisma';
3
- import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
3
+ import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
4
+ import { SettingService } from '../../setting/setting.service';
5
+
6
+ type DashboardCoreConfigStatus = {
7
+ isConfigured: boolean;
8
+ };
9
+
10
+ type DashboardCoreLocaleConfigOverview = {
11
+ status: DashboardCoreConfigStatus & {
12
+ enabledLocaleCount: number;
13
+ disabledLocaleCount: number;
14
+ };
15
+ settings: {
16
+ dateFormat: string | null;
17
+ timeFormat: string | null;
18
+ timezone: string | null;
19
+ };
20
+ locales: Array<{
21
+ id: number;
22
+ code: string;
23
+ region: string;
24
+ name: string;
25
+ enabled: boolean;
26
+ }>;
27
+ };
28
+
29
+ type DashboardCoreMailProviderOverview = {
30
+ id: string;
31
+ label: string;
32
+ selected: boolean;
33
+ configured: boolean;
34
+ missingKeys: string[];
35
+ };
36
+
37
+ type DashboardCoreMailConfigOverview = {
38
+ status: DashboardCoreConfigStatus & {
39
+ selectedProvider: string | null;
40
+ configuredProvider: string | null;
41
+ };
42
+ sender: {
43
+ from: string | null;
44
+ };
45
+ metrics: {
46
+ templateCount: number;
47
+ sentCount: number;
48
+ sentLast30Days: number;
49
+ };
50
+ providers: DashboardCoreMailProviderOverview[];
51
+ };
52
+
53
+ type DashboardCoreOAuthProviderOverview = {
54
+ id: string;
55
+ label: string;
56
+ enabled: boolean;
57
+ configured: boolean;
58
+ missingKeys: string[];
59
+ scopesCount: number;
60
+ connectedUsers: number;
61
+ };
62
+
63
+ type DashboardCoreOAuthConfigOverview = {
64
+ status: DashboardCoreConfigStatus & {
65
+ enabledProviderCount: number;
66
+ configuredProviderCount: number;
67
+ connectedAccountCount: number;
68
+ };
69
+ providers: DashboardCoreOAuthProviderOverview[];
70
+ };
71
+
72
+ type DashboardCoreStorageConfigOverview = {
73
+ status: DashboardCoreConfigStatus & {
74
+ totalProfiles: number;
75
+ activeProfiles: number;
76
+ defaultProfileId: number | null;
77
+ };
78
+ providers: Array<{
79
+ providerType: string;
80
+ total: number;
81
+ active: number;
82
+ defaults: number;
83
+ }>;
84
+ profiles: Array<{
85
+ id: number;
86
+ name: string;
87
+ providerType: string;
88
+ bucketName: string;
89
+ region: string | null;
90
+ endpointUrl: string | null;
91
+ basePath: string | null;
92
+ pathTemplate: string | null;
93
+ forcePathStyle: boolean;
94
+ isDefault: boolean;
95
+ isActive: boolean;
96
+ testStatus: string;
97
+ lastTestedAt: Date | null;
98
+ updatedAt: Date;
99
+ }>;
100
+ };
101
+
102
+ type DashboardCoreThemePaletteMode = {
103
+ primary: string | null;
104
+ primaryForeground: string | null;
105
+ secondary: string | null;
106
+ secondaryForeground: string | null;
107
+ accent: string | null;
108
+ accentForeground: string | null;
109
+ muted: string | null;
110
+ mutedForeground: string | null;
111
+ background: string | null;
112
+ backgroundForeground: string | null;
113
+ card: string | null;
114
+ cardForeground: string | null;
115
+ };
116
+
117
+ type DashboardCoreThemeConfigOverview = {
118
+ status: DashboardCoreConfigStatus & {
119
+ configuredTokenCount: number;
120
+ };
121
+ branding: {
122
+ systemName: string | null;
123
+ systemSlogan: string | null;
124
+ iconUrl: string | null;
125
+ imageUrl: string | null;
126
+ };
127
+ presentation: {
128
+ mode: string | null;
129
+ font: string | null;
130
+ textSize: string | null;
131
+ radius: string | null;
132
+ };
133
+ palette: {
134
+ light: DashboardCoreThemePaletteMode;
135
+ dark: DashboardCoreThemePaletteMode;
136
+ };
137
+ };
138
+
139
+ export type DashboardCoreConfigOverview = {
140
+ localeConfig: DashboardCoreLocaleConfigOverview;
141
+ mailConfig: DashboardCoreMailConfigOverview;
142
+ oauthConfig: DashboardCoreOAuthConfigOverview;
143
+ storageConfig: DashboardCoreStorageConfigOverview;
144
+ themeConfig: DashboardCoreThemeConfigOverview;
145
+ };
146
+
147
+ const MAIL_PROVIDER_REQUIREMENTS: Record<string, string[]> = {
148
+ SMTP: ['mail-from', 'mail-smtp-host', 'mail-smtp-port', 'mail-client-secret'],
149
+ GMAIL: [
150
+ 'mail-from',
151
+ 'mail-gmail-client-id',
152
+ 'mail-gmail-client-secret',
153
+ 'mail-gmail-refresh-token',
154
+ ],
155
+ SES: [
156
+ 'mail-from',
157
+ 'mail-aws-access-key-id',
158
+ 'mail-aws-secret-access-key',
159
+ 'mail-aws-region',
160
+ ],
161
+ };
162
+
163
+ const MAIL_PROVIDER_LABELS: Record<string, string> = {
164
+ SMTP: 'SMTP',
165
+ GMAIL: 'Gmail',
166
+ SES: 'Amazon SES',
167
+ };
168
+
169
+ const OAUTH_PROVIDER_DEFINITIONS = [
170
+ {
171
+ id: 'google',
172
+ label: 'Google',
173
+ requiredKeys: ['google_client_id', 'google_client_secret', 'url'],
174
+ scopeKey: 'google_scopes',
175
+ },
176
+ {
177
+ id: 'facebook',
178
+ label: 'Facebook',
179
+ requiredKeys: ['facebook_client_id', 'facebook_client_secret', 'url'],
180
+ scopeKey: 'facebook_scopes',
181
+ },
182
+ {
183
+ id: 'github',
184
+ label: 'GitHub',
185
+ requiredKeys: ['github_client_id', 'github_client_secret', 'api-url'],
186
+ scopeKey: 'github_scopes',
187
+ },
188
+ {
189
+ id: 'microsoft',
190
+ label: 'Microsoft',
191
+ requiredKeys: ['microsoft_client_id', 'microsoft_client_secret', 'url'],
192
+ scopeKey: 'microsoft_scopes',
193
+ },
194
+ {
195
+ id: 'microsoft_entra_id',
196
+ label: 'Microsoft Entra ID',
197
+ requiredKeys: [
198
+ 'microsoft_entra_id_client_id',
199
+ 'microsoft_entra_id_client_secret',
200
+ 'microsoft_entra_id_tenant_id',
201
+ 'url',
202
+ ],
203
+ scopeKey: 'microsoft_entra_id_scopes',
204
+ },
205
+ ] as const;
206
+
207
+ const LOCALE_SETTING_KEYS = ['date-format', 'time-format', 'timezone'] as const;
208
+
209
+ const MAIL_SETTING_KEYS = [
210
+ 'mail-provider',
211
+ 'mail-from',
212
+ 'mail-gmail-client-id',
213
+ 'mail-gmail-client-secret',
214
+ 'mail-gmail-refresh-token',
215
+ 'mail-smtp-host',
216
+ 'mail-smtp-port',
217
+ 'mail-smtp-secure',
218
+ 'mail-client-secret',
219
+ 'mail-aws-access-key-id',
220
+ 'mail-aws-secret-access-key',
221
+ 'mail-aws-region',
222
+ ] as const;
223
+
224
+ const OAUTH_SETTING_KEYS = [
225
+ 'providers',
226
+ 'url',
227
+ 'api-url',
228
+ 'google_client_id',
229
+ 'google_client_secret',
230
+ 'google_scopes',
231
+ 'facebook_client_id',
232
+ 'facebook_client_secret',
233
+ 'facebook_scopes',
234
+ 'github_client_id',
235
+ 'github_client_secret',
236
+ 'github_scopes',
237
+ 'microsoft_client_id',
238
+ 'microsoft_client_secret',
239
+ 'microsoft_scopes',
240
+ 'microsoft_entra_id_client_id',
241
+ 'microsoft_entra_id_client_secret',
242
+ 'microsoft_entra_id_tenant_id',
243
+ 'microsoft_entra_id_scopes',
244
+ ] as const;
245
+
246
+ const THEME_SETTING_KEYS = [
247
+ 'system-name',
248
+ 'system-slogan',
249
+ 'icon-url',
250
+ 'image-url',
251
+ 'theme-mode',
252
+ 'theme-font',
253
+ 'theme-text-size',
254
+ 'theme-radius',
255
+ 'theme-primary-light',
256
+ 'theme-primary-foreground-light',
257
+ 'theme-secondary-light',
258
+ 'theme-secondary-foreground-light',
259
+ 'theme-accent-light',
260
+ 'theme-accent-foreground-light',
261
+ 'theme-muted-light',
262
+ 'theme-muted-foreground-light',
263
+ 'theme-background-light',
264
+ 'theme-background-foreground-light',
265
+ 'theme-card-light',
266
+ 'theme-card-foreground-light',
267
+ 'theme-primary-dark',
268
+ 'theme-primary-foreground-dark',
269
+ 'theme-secondary-dark',
270
+ 'theme-secondary-foreground-dark',
271
+ 'theme-accent-dark',
272
+ 'theme-accent-foreground-dark',
273
+ 'theme-muted-dark',
274
+ 'theme-muted-foreground-dark',
275
+ 'theme-background-dark',
276
+ 'theme-background-foreground-dark',
277
+ 'theme-card-dark',
278
+ 'theme-card-foreground-dark',
279
+ ] as const;
4
280
 
5
281
  @Injectable()
6
282
  export class DashboardCoreService {
7
- constructor(private readonly prismaService: PrismaService) {}
283
+ private readonly logger = new Logger(DashboardCoreService.name);
284
+
285
+ constructor(
286
+ private readonly prismaService: PrismaService,
287
+ private readonly settingService: SettingService,
288
+ ) {}
289
+
290
+ async getConfigOverview(): Promise<DashboardCoreConfigOverview> {
291
+ const [localeConfig, mailConfig, oauthConfig, storageConfig, themeConfig] =
292
+ await Promise.all([
293
+ this.getLocaleConfigOverview(),
294
+ this.getMailConfigOverview(),
295
+ this.getOAuthConfigOverview(),
296
+ this.getStorageConfigOverview(),
297
+ this.getThemeConfigOverview(),
298
+ ]);
299
+
300
+ return {
301
+ localeConfig,
302
+ mailConfig,
303
+ oauthConfig,
304
+ storageConfig,
305
+ themeConfig,
306
+ };
307
+ }
308
+
309
+ private async getLocaleConfigOverview(): Promise<DashboardCoreLocaleConfigOverview> {
310
+ const [settings, locales] = await Promise.all([
311
+ this.settingService.getSettingValues([...LOCALE_SETTING_KEYS]),
312
+ this.prismaService.locale.findMany({
313
+ orderBy: [{ enabled: 'desc' }, { code: 'asc' }],
314
+ select: {
315
+ id: true,
316
+ code: true,
317
+ region: true,
318
+ name: true,
319
+ enabled: true,
320
+ },
321
+ }),
322
+ ]);
323
+
324
+ const enabledLocaleCount = locales.filter((locale) => locale.enabled).length;
325
+ const disabledLocaleCount = locales.length - enabledLocaleCount;
326
+
327
+ return {
328
+ status: {
329
+ isConfigured:
330
+ enabledLocaleCount > 0 &&
331
+ this.hasConfigValue(settings['date-format']) &&
332
+ this.hasConfigValue(settings['time-format']) &&
333
+ this.hasConfigValue(settings['timezone']),
334
+ enabledLocaleCount,
335
+ disabledLocaleCount,
336
+ },
337
+ settings: {
338
+ dateFormat: this.toNullableString(settings['date-format']),
339
+ timeFormat: this.toNullableString(settings['time-format']),
340
+ timezone: this.toNullableString(settings['timezone']),
341
+ },
342
+ locales,
343
+ };
344
+ }
345
+
346
+ private async getMailConfigOverview(): Promise<DashboardCoreMailConfigOverview> {
347
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
348
+
349
+ const [settings, templateCount, sentCount, sentLast30Days] = await Promise.all([
350
+ this.settingService.getSettingValues([...MAIL_SETTING_KEYS]),
351
+ this.prismaService.mail.count(),
352
+ this.prismaService.mail_sent.count(),
353
+ this.prismaService.mail_sent.count({
354
+ where: {
355
+ created_at: {
356
+ gte: thirtyDaysAgo,
357
+ },
358
+ },
359
+ }),
360
+ ]);
361
+
362
+ const selectedProvider = this.toNullableUppercaseString(settings['mail-provider']);
363
+
364
+ const providers = Object.entries(MAIL_PROVIDER_REQUIREMENTS).map(
365
+ ([providerId, requiredKeys]) => {
366
+ const missingKeys = this.getMissingSettingKeys(settings, requiredKeys);
367
+
368
+ return {
369
+ id: providerId,
370
+ label: MAIL_PROVIDER_LABELS[providerId] ?? providerId,
371
+ selected: providerId === selectedProvider,
372
+ configured: missingKeys.length === 0,
373
+ missingKeys,
374
+ };
375
+ },
376
+ );
377
+
378
+ const configuredProvider = providers.find(
379
+ (provider) => provider.id === selectedProvider && provider.configured,
380
+ );
381
+
382
+ return {
383
+ status: {
384
+ isConfigured: configuredProvider !== undefined,
385
+ selectedProvider,
386
+ configuredProvider: configuredProvider?.id ?? null,
387
+ },
388
+ sender: {
389
+ from: this.toNullableString(settings['mail-from']),
390
+ },
391
+ metrics: {
392
+ templateCount,
393
+ sentCount,
394
+ sentLast30Days,
395
+ },
396
+ providers,
397
+ };
398
+ }
399
+
400
+ private async getOAuthConfigOverview(): Promise<DashboardCoreOAuthConfigOverview> {
401
+ const [settings, connectedAccounts] = await Promise.all([
402
+ this.settingService.getSettingValues([...OAUTH_SETTING_KEYS]),
403
+ this.prismaService.user_account.groupBy({
404
+ by: ['provider'],
405
+ _count: {
406
+ _all: true,
407
+ },
408
+ }),
409
+ ]);
410
+
411
+ const enabledProviders = this.normalizeProviderList(settings['providers']);
412
+ const connectedAccountsByProvider = new Map(
413
+ connectedAccounts.map((entry) => [String(entry.provider), entry._count._all]),
414
+ );
415
+
416
+ const providers = OAUTH_PROVIDER_DEFINITIONS.map((provider) => {
417
+ const missingKeys = this.getMissingSettingKeys(settings, provider.requiredKeys);
418
+ const scopes = Array.isArray(settings[provider.scopeKey])
419
+ ? (settings[provider.scopeKey] as unknown[])
420
+ : [];
421
+
422
+ return {
423
+ id: provider.id,
424
+ label: provider.label,
425
+ enabled: enabledProviders.includes(provider.id),
426
+ configured: missingKeys.length === 0,
427
+ missingKeys,
428
+ scopesCount: scopes.length,
429
+ connectedUsers: connectedAccountsByProvider.get(provider.id) ?? 0,
430
+ };
431
+ });
432
+
433
+ return {
434
+ status: {
435
+ isConfigured: providers.some((provider) => provider.enabled && provider.configured),
436
+ enabledProviderCount: providers.filter((provider) => provider.enabled).length,
437
+ configuredProviderCount: providers.filter((provider) => provider.configured).length,
438
+ connectedAccountCount: providers.reduce(
439
+ (total, provider) => total + provider.connectedUsers,
440
+ 0,
441
+ ),
442
+ },
443
+ providers,
444
+ };
445
+ }
446
+
447
+ private async getStorageConfigOverview(): Promise<DashboardCoreStorageConfigOverview> {
448
+ const profiles = await this.prismaService.storage_profile.findMany({
449
+ where: {
450
+ deleted_at: null,
451
+ },
452
+ orderBy: [{ is_default: 'desc' }, { is_active: 'desc' }, { name: 'asc' }],
453
+ select: {
454
+ id: true,
455
+ name: true,
456
+ provider_type: true,
457
+ bucket_name: true,
458
+ region: true,
459
+ endpoint_url: true,
460
+ base_path: true,
461
+ path_template: true,
462
+ force_path_style: true,
463
+ is_default: true,
464
+ is_active: true,
465
+ test_status: true,
466
+ last_tested_at: true,
467
+ updated_at: true,
468
+ },
469
+ });
470
+
471
+ const providerMap = new Map<
472
+ string,
473
+ { providerType: string; total: number; active: number; defaults: number }
474
+ >();
475
+
476
+ profiles.forEach((profile) => {
477
+ const providerType = String(profile.provider_type);
478
+ const current = providerMap.get(providerType) ?? {
479
+ providerType,
480
+ total: 0,
481
+ active: 0,
482
+ defaults: 0,
483
+ };
484
+
485
+ current.total += 1;
486
+ current.active += profile.is_active ? 1 : 0;
487
+ current.defaults += profile.is_default ? 1 : 0;
488
+ providerMap.set(providerType, current);
489
+ });
490
+
491
+ const activeProfiles = profiles.filter((profile) => profile.is_active).length;
492
+ const defaultProfile = profiles.find((profile) => profile.is_default) ?? null;
493
+
494
+ return {
495
+ status: {
496
+ isConfigured: activeProfiles > 0,
497
+ totalProfiles: profiles.length,
498
+ activeProfiles,
499
+ defaultProfileId: defaultProfile?.id ?? null,
500
+ },
501
+ providers: Array.from(providerMap.values()),
502
+ profiles: profiles.map((profile) => ({
503
+ id: profile.id,
504
+ name: profile.name,
505
+ providerType: String(profile.provider_type),
506
+ bucketName: profile.bucket_name,
507
+ region: profile.region,
508
+ endpointUrl: profile.endpoint_url,
509
+ basePath: profile.base_path,
510
+ pathTemplate: profile.path_template,
511
+ forcePathStyle: profile.force_path_style,
512
+ isDefault: profile.is_default,
513
+ isActive: profile.is_active,
514
+ testStatus: String(profile.test_status),
515
+ lastTestedAt: profile.last_tested_at,
516
+ updatedAt: profile.updated_at,
517
+ })),
518
+ };
519
+ }
520
+
521
+ private async getThemeConfigOverview(): Promise<DashboardCoreThemeConfigOverview> {
522
+ const settings = await this.settingService.getSettingValues([...THEME_SETTING_KEYS]);
523
+ const configuredTokenCount = THEME_SETTING_KEYS.filter((key) =>
524
+ this.hasConfigValue(settings[key]),
525
+ ).length;
526
+
527
+ return {
528
+ status: {
529
+ isConfigured:
530
+ this.hasConfigValue(settings['system-name']) ||
531
+ this.hasConfigValue(settings['theme-primary-light']) ||
532
+ this.hasConfigValue(settings['theme-primary-dark']),
533
+ configuredTokenCount,
534
+ },
535
+ branding: {
536
+ systemName: this.toNullableString(settings['system-name']),
537
+ systemSlogan: this.toNullableString(settings['system-slogan']),
538
+ iconUrl: this.toNullableString(settings['icon-url']),
539
+ imageUrl: this.toNullableString(settings['image-url']),
540
+ },
541
+ presentation: {
542
+ mode: this.toNullableString(settings['theme-mode']),
543
+ font: this.toNullableString(settings['theme-font']),
544
+ textSize: this.toNullableString(settings['theme-text-size']),
545
+ radius: this.toNullableString(settings['theme-radius']),
546
+ },
547
+ palette: {
548
+ light: {
549
+ primary: this.toNullableString(settings['theme-primary-light']),
550
+ primaryForeground: this.toNullableString(
551
+ settings['theme-primary-foreground-light'],
552
+ ),
553
+ secondary: this.toNullableString(settings['theme-secondary-light']),
554
+ secondaryForeground: this.toNullableString(
555
+ settings['theme-secondary-foreground-light'],
556
+ ),
557
+ accent: this.toNullableString(settings['theme-accent-light']),
558
+ accentForeground: this.toNullableString(
559
+ settings['theme-accent-foreground-light'],
560
+ ),
561
+ muted: this.toNullableString(settings['theme-muted-light']),
562
+ mutedForeground: this.toNullableString(
563
+ settings['theme-muted-foreground-light'],
564
+ ),
565
+ background: this.toNullableString(settings['theme-background-light']),
566
+ backgroundForeground: this.toNullableString(
567
+ settings['theme-background-foreground-light'],
568
+ ),
569
+ card: this.toNullableString(settings['theme-card-light']),
570
+ cardForeground: this.toNullableString(
571
+ settings['theme-card-foreground-light'],
572
+ ),
573
+ },
574
+ dark: {
575
+ primary: this.toNullableString(settings['theme-primary-dark']),
576
+ primaryForeground: this.toNullableString(
577
+ settings['theme-primary-foreground-dark'],
578
+ ),
579
+ secondary: this.toNullableString(settings['theme-secondary-dark']),
580
+ secondaryForeground: this.toNullableString(
581
+ settings['theme-secondary-foreground-dark'],
582
+ ),
583
+ accent: this.toNullableString(settings['theme-accent-dark']),
584
+ accentForeground: this.toNullableString(
585
+ settings['theme-accent-foreground-dark'],
586
+ ),
587
+ muted: this.toNullableString(settings['theme-muted-dark']),
588
+ mutedForeground: this.toNullableString(
589
+ settings['theme-muted-foreground-dark'],
590
+ ),
591
+ background: this.toNullableString(settings['theme-background-dark']),
592
+ backgroundForeground: this.toNullableString(
593
+ settings['theme-background-foreground-dark'],
594
+ ),
595
+ card: this.toNullableString(settings['theme-card-dark']),
596
+ cardForeground: this.toNullableString(
597
+ settings['theme-card-foreground-dark'],
598
+ ),
599
+ },
600
+ },
601
+ };
602
+ }
603
+
604
+ private getMissingSettingKeys(
605
+ settings: Record<string, unknown>,
606
+ requiredKeys: readonly string[],
607
+ ): string[] {
608
+ return requiredKeys.filter((key) => !this.hasConfigValue(settings[key]));
609
+ }
610
+
611
+ private hasConfigValue(value: unknown): boolean {
612
+ if (Array.isArray(value)) {
613
+ return value.length > 0;
614
+ }
615
+
616
+ if (typeof value === 'string') {
617
+ return value.trim().length > 0;
618
+ }
619
+
620
+ return value !== null && value !== undefined;
621
+ }
622
+
623
+ private toNullableString(value: unknown): string | null {
624
+ if (typeof value !== 'string') {
625
+ return value === null || value === undefined ? null : String(value);
626
+ }
627
+
628
+ const normalized = value.trim();
629
+ return normalized.length > 0 ? normalized : null;
630
+ }
631
+
632
+ private toNullableUppercaseString(value: unknown): string | null {
633
+ const normalized = this.toNullableString(value);
634
+ return normalized ? normalized.toUpperCase() : null;
635
+ }
636
+
637
+ private normalizeProviderList(value: unknown): string[] {
638
+ if (!Array.isArray(value)) {
639
+ return [];
640
+ }
641
+
642
+ return value.map((provider) => String(provider).toLowerCase());
643
+ }
8
644
 
9
645
  async getHome(userId: number, locale: string) {
10
646
  const user = await this.prismaService.user.findUnique({
@@ -378,6 +1014,7 @@ export class DashboardCoreService {
378
1014
  i: `widget-${item.id}`,
379
1015
  component_id: item.component_id,
380
1016
  slug: component.slug,
1017
+ library_slug: component.library_slug,
381
1018
  name: locale?.name || component.slug,
382
1019
  description: locale?.description || '',
383
1020
  x: item.x_axis,
@@ -477,8 +1114,50 @@ export class DashboardCoreService {
477
1114
  throw new ForbiddenException('Access denied to this dashboard');
478
1115
  }
479
1116
 
1117
+ const userRoles = await this.prismaService.role_user.findMany({
1118
+ where: { user_id: userId },
1119
+ select: { role_id: true },
1120
+ });
1121
+
1122
+ const userRoleIds = userRoles.map((item) => item.role_id);
1123
+
1124
+ if (userRoleIds.length === 0) {
1125
+ throw new ForbiddenException('Access denied to this component');
1126
+ }
1127
+
1128
+ const slugParts = componentSlug.split('.').filter(Boolean);
1129
+ const requestedSlug =
1130
+ slugParts.length > 0 ? slugParts[slugParts.length - 1]! : componentSlug;
1131
+ const requestedLibrarySlug =
1132
+ slugParts.length > 1 ? slugParts[0] : undefined;
1133
+
480
1134
  const component = await this.prismaService.dashboard_component.findFirst({
481
- where: { slug: componentSlug },
1135
+ where: {
1136
+ AND: [
1137
+ requestedLibrarySlug
1138
+ ? {
1139
+ OR: [
1140
+ { slug: componentSlug },
1141
+ {
1142
+ slug: requestedSlug,
1143
+ library_slug: requestedLibrarySlug,
1144
+ },
1145
+ ],
1146
+ }
1147
+ : {
1148
+ OR: [{ slug: componentSlug }, { slug: requestedSlug }],
1149
+ },
1150
+ {
1151
+ dashboard_component_role: {
1152
+ some: {
1153
+ role_id: {
1154
+ in: userRoleIds,
1155
+ },
1156
+ },
1157
+ },
1158
+ },
1159
+ ],
1160
+ },
482
1161
  include: {
483
1162
  dashboard_component_locale: {
484
1163
  where: {
@@ -491,7 +1170,7 @@ export class DashboardCoreService {
491
1170
  });
492
1171
 
493
1172
  if (!component) {
494
- throw new Error(`Component with slug '${componentSlug}' not found`);
1173
+ throw new ForbiddenException('Access denied to this component');
495
1174
  }
496
1175
 
497
1176
  let dashboardItem = await this.prismaService.dashboard_item.findFirst({
@@ -502,6 +1181,21 @@ export class DashboardCoreService {
502
1181
  });
503
1182
 
504
1183
  if (!dashboardItem) {
1184
+ const dashboardItems = await this.prismaService.dashboard_item.findMany({
1185
+ where: {
1186
+ dashboard_id: dashboard.id,
1187
+ },
1188
+ select: {
1189
+ y_axis: true,
1190
+ height: true,
1191
+ },
1192
+ });
1193
+
1194
+ const nextAvailableY = dashboardItems.reduce(
1195
+ (maxY, item) => Math.max(maxY, item.y_axis + item.height),
1196
+ 0,
1197
+ );
1198
+
505
1199
  dashboardItem = await this.prismaService.dashboard_item.create({
506
1200
  data: {
507
1201
  dashboard_id: dashboard.id,
@@ -509,7 +1203,7 @@ export class DashboardCoreService {
509
1203
  width: component.width,
510
1204
  height: component.height,
511
1205
  x_axis: 0,
512
- y_axis: 0,
1206
+ y_axis: nextAvailableY,
513
1207
  },
514
1208
  });
515
1209
  }
@@ -520,6 +1214,7 @@ export class DashboardCoreService {
520
1214
  i: `widget-${dashboardItem.id}`,
521
1215
  component_id: component.id,
522
1216
  slug: component.slug,
1217
+ library_slug: component.library_slug,
523
1218
  name: locale?.name || component.slug,
524
1219
  description: locale?.description || '',
525
1220
  x: dashboardItem.x_axis,
@@ -539,8 +1234,52 @@ export class DashboardCoreService {
539
1234
  slug: string,
540
1235
  widgetId: string,
541
1236
  ) {
542
-
543
- throw new Error('Not implemented yet');
1237
+ const dashboard = await this.prismaService.dashboard.findFirst({
1238
+ where: { slug },
1239
+ });
1240
+
1241
+ if (!dashboard) {
1242
+ throw new Error(`Dashboard with slug '${slug}' not found`);
1243
+ }
1244
+
1245
+ const canAccess = await this.prismaService.dashboard_user.findFirst({
1246
+ where: {
1247
+ dashboard_id: dashboard.id,
1248
+ user_id: userId,
1249
+ },
1250
+ select: { id: true },
1251
+ });
1252
+
1253
+ if (!canAccess) {
1254
+ throw new ForbiddenException('Access denied to this dashboard');
1255
+ }
1256
+
1257
+ const parsedWidgetId = Number(widgetId.replace(/^widget-/, ''));
1258
+
1259
+ if (!Number.isInteger(parsedWidgetId) || parsedWidgetId <= 0) {
1260
+ throw new BadRequestException('Invalid widget id');
1261
+ }
1262
+
1263
+ const dashboardItem = await this.prismaService.dashboard_item.findFirst({
1264
+ where: {
1265
+ id: parsedWidgetId,
1266
+ dashboard_id: dashboard.id,
1267
+ },
1268
+ select: { id: true },
1269
+ });
1270
+
1271
+ if (!dashboardItem) {
1272
+ throw new BadRequestException('Widget not found in this dashboard');
1273
+ }
1274
+
1275
+ await this.prismaService.dashboard_item.delete({
1276
+ where: { id: dashboardItem.id },
1277
+ });
1278
+
1279
+ return {
1280
+ success: true,
1281
+ removedWidgetId: `widget-${dashboardItem.id}`,
1282
+ };
544
1283
  }
545
1284
 
546
1285
  async checkDashboardAccess(userId: number, slug: string, locale: string) {
@@ -864,11 +1603,137 @@ export class DashboardCoreService {
864
1603
  }));
865
1604
  }
866
1605
 
1606
+ async getEmailNotificationStats(userId: number) {
1607
+ const now = new Date();
1608
+ const periodStart = new Date(now);
1609
+ periodStart.setHours(0, 0, 0, 0);
1610
+ periodStart.setDate(periodStart.getDate() - 13);
1611
+
1612
+ const periodEnd = new Date(now);
1613
+ periodEnd.setHours(23, 59, 59, 999);
1614
+
1615
+ const toDateKey = (date: Date) => {
1616
+ const year = date.getFullYear();
1617
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1618
+ const day = String(date.getDate()).padStart(2, '0');
1619
+ return `${year}-${month}-${day}`;
1620
+ };
1621
+
1622
+ try {
1623
+ const [cardsRows, chartRows] = await Promise.all([
1624
+ this.prismaService.$queryRaw<
1625
+ Array<{ received: bigint; read: bigint; unread: bigint; error: bigint }>
1626
+ >`
1627
+ SELECT
1628
+ COUNT(*) FILTER (WHERE "status" IN ('received', 'read'))::bigint as received,
1629
+ COUNT(*) FILTER (WHERE "status" = 'read')::bigint as read,
1630
+ COUNT(*) FILTER (WHERE "status" = 'received' AND "read_at" IS NULL)::bigint as unread,
1631
+ COUNT(*) FILTER (WHERE "status" = 'error')::bigint as error
1632
+ FROM "mail_sent_user"
1633
+ WHERE "user_id" = ${userId}
1634
+ AND "created_at" >= ${periodStart}
1635
+ AND "created_at" <= ${periodEnd}
1636
+ `,
1637
+ this.prismaService.$queryRaw<Array<{ date: Date; received: bigint; read: bigint }>>`
1638
+ SELECT
1639
+ DATE("created_at") as date,
1640
+ COUNT(*) FILTER (WHERE "status" IN ('received', 'read'))::bigint as received,
1641
+ COUNT(*) FILTER (WHERE "status" = 'read')::bigint as read
1642
+ FROM "mail_sent_user"
1643
+ WHERE "user_id" = ${userId}
1644
+ AND "created_at" >= ${periodStart}
1645
+ AND "created_at" <= ${periodEnd}
1646
+ GROUP BY DATE("created_at")
1647
+ ORDER BY date ASC
1648
+ `,
1649
+ ]);
1650
+
1651
+ const cards = cardsRows[0] ?? {
1652
+ received: BigInt(0),
1653
+ read: BigInt(0),
1654
+ unread: BigInt(0),
1655
+ error: BigInt(0),
1656
+ };
1657
+
1658
+ const chartMap = new Map<string, { received: number; read: number }>();
1659
+ for (const row of chartRows) {
1660
+ const rowDate = new Date(row.date);
1661
+ rowDate.setHours(0, 0, 0, 0);
1662
+ const key = toDateKey(rowDate);
1663
+
1664
+ chartMap.set(key, {
1665
+ received: Number(row.received),
1666
+ read: Number(row.read),
1667
+ });
1668
+ }
1669
+
1670
+ const dateFormatter = new Intl.DateTimeFormat('pt-BR', {
1671
+ day: '2-digit',
1672
+ month: '2-digit',
1673
+ });
1674
+
1675
+ const chart: Array<{ date: string; received: number; read: number }> = [];
1676
+ for (let i = 13; i >= 0; i--) {
1677
+ const day = new Date(now);
1678
+ day.setDate(day.getDate() - i);
1679
+ day.setHours(0, 0, 0, 0);
1680
+
1681
+ const key = toDateKey(day);
1682
+ const values = chartMap.get(key) ?? { received: 0, read: 0 };
1683
+
1684
+ chart.push({
1685
+ date: dateFormatter.format(day),
1686
+ received: values.received,
1687
+ read: values.read,
1688
+ });
1689
+ }
1690
+
1691
+ return {
1692
+ cards: {
1693
+ received: Number(cards.received),
1694
+ read: Number(cards.read),
1695
+ unread: Number(cards.unread),
1696
+ error: Number(cards.error),
1697
+ },
1698
+ chart,
1699
+ };
1700
+ } catch (error) {
1701
+ this.logger.error('Error loading email notification stats:', error);
1702
+
1703
+ const dateFormatter = new Intl.DateTimeFormat('pt-BR', {
1704
+ day: '2-digit',
1705
+ month: '2-digit',
1706
+ });
1707
+
1708
+ const chart: Array<{ date: string; received: number; read: number }> = [];
1709
+ for (let i = 13; i >= 0; i--) {
1710
+ const day = new Date(now);
1711
+ day.setDate(day.getDate() - i);
1712
+ chart.push({
1713
+ date: dateFormatter.format(day),
1714
+ received: 0,
1715
+ read: 0,
1716
+ });
1717
+ }
1718
+
1719
+ return {
1720
+ cards: {
1721
+ received: 0,
1722
+ read: 0,
1723
+ unread: 0,
1724
+ error: 0,
1725
+ },
1726
+ chart,
1727
+ };
1728
+ }
1729
+ }
1730
+
867
1731
  async getWidgetsData(userId: number, locale: string) {
868
- const [accountSecurity, activityTimeline, loginHistory, profile, quickStats, userRoles, userSessions] =
1732
+ const [accountSecurity, activityTimeline, emailNotifications, loginHistory, profile, quickStats, userRoles, userSessions] =
869
1733
  await Promise.all([
870
1734
  this.getAccountSecurity(userId),
871
1735
  this.getActivityTimeline(userId),
1736
+ this.getEmailNotificationStats(userId),
872
1737
  this.getLoginHistory(userId),
873
1738
  this.getProfile(userId),
874
1739
  this.getQuickStats(userId),
@@ -879,6 +1744,7 @@ export class DashboardCoreService {
879
1744
  return {
880
1745
  accountSecurity,
881
1746
  activityTimeline,
1747
+ emailNotifications,
882
1748
  loginHistory,
883
1749
  profile,
884
1750
  quickStats,