@primocaredentgroup/elettromedicali 0.1.0

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 (197) hide show
  1. package/dist/client/index.d.ts +72 -0
  2. package/dist/client/index.d.ts.map +1 -0
  3. package/dist/client/index.js +233 -0
  4. package/dist/client/index.js.map +1 -0
  5. package/dist/component/_generated/api.d.ts +94 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -0
  7. package/dist/component/_generated/api.js +31 -0
  8. package/dist/component/_generated/api.js.map +1 -0
  9. package/dist/component/_generated/component.d.ts +1444 -0
  10. package/dist/component/_generated/component.d.ts.map +1 -0
  11. package/dist/component/_generated/component.js +11 -0
  12. package/dist/component/_generated/component.js.map +1 -0
  13. package/dist/component/_generated/dataModel.d.ts +46 -0
  14. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  15. package/dist/component/_generated/dataModel.js +11 -0
  16. package/dist/component/_generated/dataModel.js.map +1 -0
  17. package/dist/component/_generated/server.d.ts +121 -0
  18. package/dist/component/_generated/server.d.ts.map +1 -0
  19. package/dist/component/_generated/server.js +78 -0
  20. package/dist/component/_generated/server.js.map +1 -0
  21. package/dist/component/apiKeys.d.ts +69 -0
  22. package/dist/component/apiKeys.d.ts.map +1 -0
  23. package/dist/component/apiKeys.js +207 -0
  24. package/dist/component/apiKeys.js.map +1 -0
  25. package/dist/component/clinics.d.ts +103 -0
  26. package/dist/component/clinics.d.ts.map +1 -0
  27. package/dist/component/clinics.js +126 -0
  28. package/dist/component/clinics.js.map +1 -0
  29. package/dist/component/contracts.d.ts +85 -0
  30. package/dist/component/contracts.d.ts.map +1 -0
  31. package/dist/component/contracts.js +115 -0
  32. package/dist/component/contracts.js.map +1 -0
  33. package/dist/component/convex.config.d.ts +3 -0
  34. package/dist/component/convex.config.d.ts.map +1 -0
  35. package/dist/component/convex.config.js +3 -0
  36. package/dist/component/convex.config.js.map +1 -0
  37. package/dist/component/crons.d.ts +3 -0
  38. package/dist/component/crons.d.ts.map +1 -0
  39. package/dist/component/crons.js +7 -0
  40. package/dist/component/crons.js.map +1 -0
  41. package/dist/component/dashboardStats.d.ts +14 -0
  42. package/dist/component/dashboardStats.d.ts.map +1 -0
  43. package/dist/component/dashboardStats.js +136 -0
  44. package/dist/component/dashboardStats.js.map +1 -0
  45. package/dist/component/dashboardStatsCache.d.ts +32 -0
  46. package/dist/component/dashboardStatsCache.d.ts.map +1 -0
  47. package/dist/component/dashboardStatsCache.js +129 -0
  48. package/dist/component/dashboardStatsCache.js.map +1 -0
  49. package/dist/component/deviceCategories.d.ts +108 -0
  50. package/dist/component/deviceCategories.d.ts.map +1 -0
  51. package/dist/component/deviceCategories.js +254 -0
  52. package/dist/component/deviceCategories.js.map +1 -0
  53. package/dist/component/deviceQuestions.d.ts +129 -0
  54. package/dist/component/deviceQuestions.d.ts.map +1 -0
  55. package/dist/component/deviceQuestions.js +175 -0
  56. package/dist/component/deviceQuestions.js.map +1 -0
  57. package/dist/component/deviceRepairHistory.d.ts +30 -0
  58. package/dist/component/deviceRepairHistory.d.ts.map +1 -0
  59. package/dist/component/deviceRepairHistory.js +84 -0
  60. package/dist/component/deviceRepairHistory.js.map +1 -0
  61. package/dist/component/deviceStatus.d.ts +63 -0
  62. package/dist/component/deviceStatus.d.ts.map +1 -0
  63. package/dist/component/deviceStatus.js +58 -0
  64. package/dist/component/deviceStatus.js.map +1 -0
  65. package/dist/component/devices.d.ts +299 -0
  66. package/dist/component/devices.d.ts.map +1 -0
  67. package/dist/component/devices.js +587 -0
  68. package/dist/component/devices.js.map +1 -0
  69. package/dist/component/emailHelpers.d.ts +17 -0
  70. package/dist/component/emailHelpers.d.ts.map +1 -0
  71. package/dist/component/emailHelpers.js +39 -0
  72. package/dist/component/emailHelpers.js.map +1 -0
  73. package/dist/component/emails.d.ts +56 -0
  74. package/dist/component/emails.d.ts.map +1 -0
  75. package/dist/component/emails.js +58 -0
  76. package/dist/component/emails.js.map +1 -0
  77. package/dist/component/http.d.ts +3 -0
  78. package/dist/component/http.d.ts.map +1 -0
  79. package/dist/component/http.js +229 -0
  80. package/dist/component/http.js.map +1 -0
  81. package/dist/component/maintenanceTasks.d.ts +733 -0
  82. package/dist/component/maintenanceTasks.d.ts.map +1 -0
  83. package/dist/component/maintenanceTasks.js +937 -0
  84. package/dist/component/maintenanceTasks.js.map +1 -0
  85. package/dist/component/roles.d.ts +75 -0
  86. package/dist/component/roles.d.ts.map +1 -0
  87. package/dist/component/roles.js +98 -0
  88. package/dist/component/roles.js.map +1 -0
  89. package/dist/component/schema.d.ts +1295 -0
  90. package/dist/component/schema.d.ts.map +1 -0
  91. package/dist/component/schema.js +724 -0
  92. package/dist/component/schema.js.map +1 -0
  93. package/dist/component/slaMonitoring.d.ts +32 -0
  94. package/dist/component/slaMonitoring.d.ts.map +1 -0
  95. package/dist/component/slaMonitoring.js +111 -0
  96. package/dist/component/slaMonitoring.js.map +1 -0
  97. package/dist/component/slaRules.d.ts +72 -0
  98. package/dist/component/slaRules.d.ts.map +1 -0
  99. package/dist/component/slaRules.js +193 -0
  100. package/dist/component/slaRules.js.map +1 -0
  101. package/dist/component/sparePartOrders.d.ts +177 -0
  102. package/dist/component/sparePartOrders.d.ts.map +1 -0
  103. package/dist/component/sparePartOrders.js +243 -0
  104. package/dist/component/sparePartOrders.js.map +1 -0
  105. package/dist/component/spareParts.d.ts +472 -0
  106. package/dist/component/spareParts.d.ts.map +1 -0
  107. package/dist/component/spareParts.js +319 -0
  108. package/dist/component/spareParts.js.map +1 -0
  109. package/dist/component/supplierCategories.d.ts +22 -0
  110. package/dist/component/supplierCategories.d.ts.map +1 -0
  111. package/dist/component/supplierCategories.js +64 -0
  112. package/dist/component/supplierCategories.js.map +1 -0
  113. package/dist/component/suppliers.d.ts +94 -0
  114. package/dist/component/suppliers.d.ts.map +1 -0
  115. package/dist/component/suppliers.js +195 -0
  116. package/dist/component/suppliers.js.map +1 -0
  117. package/dist/component/ticketComments.d.ts +89 -0
  118. package/dist/component/ticketComments.d.ts.map +1 -0
  119. package/dist/component/ticketComments.js +246 -0
  120. package/dist/component/ticketComments.js.map +1 -0
  121. package/dist/component/ticketCustomFields.d.ts +149 -0
  122. package/dist/component/ticketCustomFields.d.ts.map +1 -0
  123. package/dist/component/ticketCustomFields.js +215 -0
  124. package/dist/component/ticketCustomFields.js.map +1 -0
  125. package/dist/component/ticketExport.d.ts +83 -0
  126. package/dist/component/ticketExport.d.ts.map +1 -0
  127. package/dist/component/ticketExport.js +182 -0
  128. package/dist/component/ticketExport.js.map +1 -0
  129. package/dist/component/ticketHistory.d.ts +57 -0
  130. package/dist/component/ticketHistory.d.ts.map +1 -0
  131. package/dist/component/ticketHistory.js +81 -0
  132. package/dist/component/ticketHistory.js.map +1 -0
  133. package/dist/component/ticketMacros.d.ts +141 -0
  134. package/dist/component/ticketMacros.d.ts.map +1 -0
  135. package/dist/component/ticketMacros.js +255 -0
  136. package/dist/component/ticketMacros.js.map +1 -0
  137. package/dist/component/ticketStatuses.d.ts +60 -0
  138. package/dist/component/ticketStatuses.d.ts.map +1 -0
  139. package/dist/component/ticketStatuses.js +110 -0
  140. package/dist/component/ticketStatuses.js.map +1 -0
  141. package/dist/component/ticketTriggers.d.ts +408 -0
  142. package/dist/component/ticketTriggers.d.ts.map +1 -0
  143. package/dist/component/ticketTriggers.js +941 -0
  144. package/dist/component/ticketTriggers.js.map +1 -0
  145. package/dist/component/userProfiles.d.ts +259 -0
  146. package/dist/component/userProfiles.d.ts.map +1 -0
  147. package/dist/component/userProfiles.js +634 -0
  148. package/dist/component/userProfiles.js.map +1 -0
  149. package/dist/component/vendorArticles.d.ts +64 -0
  150. package/dist/component/vendorArticles.d.ts.map +1 -0
  151. package/dist/component/vendorArticles.js +116 -0
  152. package/dist/component/vendorArticles.js.map +1 -0
  153. package/dist/test.d.ts +1302 -0
  154. package/dist/test.d.ts.map +1 -0
  155. package/dist/test.js +7 -0
  156. package/dist/test.js.map +1 -0
  157. package/package.json +71 -0
  158. package/src/client/index.ts +344 -0
  159. package/src/component/_generated/api.ts +110 -0
  160. package/src/component/_generated/component.ts +2460 -0
  161. package/src/component/_generated/dataModel.ts +60 -0
  162. package/src/component/_generated/server.ts +156 -0
  163. package/src/component/apiKeys.ts +229 -0
  164. package/src/component/clinics.ts +136 -0
  165. package/src/component/contracts.ts +136 -0
  166. package/src/component/convex.config.js +2 -0
  167. package/src/component/convex.config.ts +3 -0
  168. package/src/component/crons.ts +18 -0
  169. package/src/component/dashboardStats.ts +141 -0
  170. package/src/component/dashboardStatsCache.ts +145 -0
  171. package/src/component/deviceCategories.ts +280 -0
  172. package/src/component/deviceQuestions.ts +225 -0
  173. package/src/component/deviceRepairHistory.ts +94 -0
  174. package/src/component/deviceStatus.ts +79 -0
  175. package/src/component/devices.ts +645 -0
  176. package/src/component/emailHelpers.ts +38 -0
  177. package/src/component/emails.ts +61 -0
  178. package/src/component/http.ts +231 -0
  179. package/src/component/maintenanceTasks.ts +1003 -0
  180. package/src/component/roles.ts +99 -0
  181. package/src/component/schema.ts +842 -0
  182. package/src/component/slaMonitoring.ts +125 -0
  183. package/src/component/slaRules.ts +231 -0
  184. package/src/component/sparePartOrders.ts +290 -0
  185. package/src/component/spareParts.ts +362 -0
  186. package/src/component/supplierCategories.ts +65 -0
  187. package/src/component/suppliers.ts +234 -0
  188. package/src/component/ticketComments.ts +288 -0
  189. package/src/component/ticketCustomFields.ts +260 -0
  190. package/src/component/ticketExport.ts +220 -0
  191. package/src/component/ticketHistory.ts +106 -0
  192. package/src/component/ticketMacros.ts +291 -0
  193. package/src/component/ticketStatuses.ts +109 -0
  194. package/src/component/ticketTriggers.ts +1152 -0
  195. package/src/component/userProfiles.ts +745 -0
  196. package/src/component/vendorArticles.ts +139 -0
  197. package/src/test.ts +15 -0
@@ -0,0 +1,745 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation } from './_generated/server';
3
+ import { paginationOptsValidator } from 'convex/server';
4
+
5
+ export const getUserProfile = query({
6
+ args: { auth0Id: v.string() },
7
+ handler: async (ctx, args) => {
8
+ const profile = await ctx.db
9
+ .query('user_profiles')
10
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
11
+ .first();
12
+
13
+ return profile;
14
+ },
15
+ });
16
+
17
+ export const getCurrentUserProfile = query({
18
+ args: { auth0Id: v.string() },
19
+ handler: async (ctx, args) => {
20
+ const realProfile = await ctx.db
21
+ .query('user_profiles')
22
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
23
+ .first();
24
+
25
+ if (!realProfile) {
26
+ return null;
27
+ }
28
+
29
+ if (realProfile.isActive === false) {
30
+ return {
31
+ ...realProfile,
32
+ role: null,
33
+ isDisabled: true,
34
+ isImpersonating: false,
35
+ };
36
+ }
37
+
38
+ const realRole = realProfile.roleId ? await ctx.db.get(realProfile.roleId) : null;
39
+ const isAdmin = realRole?.name === 'admin';
40
+
41
+ if (isAdmin && realProfile.impersonatingUserId) {
42
+ const impersonatedProfile = await ctx.db.get(realProfile.impersonatingUserId);
43
+
44
+ if (impersonatedProfile) {
45
+ const impersonatedRole = impersonatedProfile.roleId
46
+ ? await ctx.db.get(impersonatedProfile.roleId)
47
+ : null;
48
+
49
+ return {
50
+ ...impersonatedProfile,
51
+ role: impersonatedRole?.name,
52
+ isImpersonating: true,
53
+ realAdminId: realProfile._id,
54
+ realAdminName: realProfile.name || realProfile.email,
55
+ realAdminEmail: realProfile.email,
56
+ isDisabled: false,
57
+ };
58
+ }
59
+ }
60
+
61
+ return {
62
+ ...realProfile,
63
+ role: realRole?.name,
64
+ isImpersonating: false,
65
+ isDisabled: false,
66
+ };
67
+ },
68
+ });
69
+
70
+ function isPlaceholderAuth0Id(auth0Id: string): boolean {
71
+ return auth0Id.startsWith('placeholder_') ||
72
+ auth0Id.startsWith('manual_') ||
73
+ !auth0Id.includes('|');
74
+ }
75
+
76
+ export const upsertUserProfile = mutation({
77
+ args: {
78
+ auth0Id: v.string(),
79
+ email: v.string(),
80
+ name: v.optional(v.string()),
81
+ roleName: v.optional(v.string()),
82
+ clinicId: v.optional(v.id('clinics')),
83
+ supplierId: v.optional(v.id('suppliers')),
84
+ },
85
+ handler: async (ctx, args) => {
86
+ let existing = await ctx.db
87
+ .query('user_profiles')
88
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
89
+ .first();
90
+
91
+ let shouldUpdateAuth0Id = false;
92
+ if (!existing) {
93
+ const existingByEmail = await ctx.db
94
+ .query('user_profiles')
95
+ .withIndex('by_email', (q) => q.eq('email', args.email))
96
+ .first();
97
+
98
+ if (existingByEmail && isPlaceholderAuth0Id(existingByEmail.auth0Id)) {
99
+ existing = existingByEmail;
100
+ shouldUpdateAuth0Id = true;
101
+ console.log(`🔄 Found pre-created user with email ${args.email}, updating auth0Id from placeholder to real Auth0 ID`);
102
+ }
103
+ }
104
+
105
+ let roleId = existing?.roleId;
106
+ if (args.roleName || !roleId) {
107
+ const roleName = args.roleName || 'user';
108
+ let role = await ctx.db
109
+ .query('roles')
110
+ .withIndex('by_name', (q) => q.eq('name', roleName))
111
+ .first();
112
+
113
+ if (!role) {
114
+ const newRoleId = await ctx.db.insert('roles', {
115
+ name: roleName,
116
+ description: roleName === 'admin' ? 'Administrator with full access' :
117
+ roleName === 'user' ? 'Standard user with limited access' :
118
+ roleName === 'supplier' ? 'Supplier with access to assigned tickets' :
119
+ `${roleName} role`,
120
+ createdAt: Date.now(),
121
+ });
122
+ role = await ctx.db.get(newRoleId);
123
+ }
124
+
125
+ if (role) {
126
+ roleId = role._id;
127
+ }
128
+ }
129
+
130
+ if (existing) {
131
+ const updateData: any = {
132
+ email: args.email,
133
+ name: args.name,
134
+ ...(args.clinicId && { clinicId: args.clinicId }),
135
+ ...(args.supplierId && { supplierId: args.supplierId }),
136
+ };
137
+
138
+ if (shouldUpdateAuth0Id) {
139
+ updateData.auth0Id = args.auth0Id;
140
+ console.log(`✅ Updated auth0Id for user ${args.email}`);
141
+ }
142
+
143
+ if (roleId) {
144
+ updateData.roleId = roleId;
145
+ }
146
+ await ctx.db.patch(existing._id, updateData);
147
+ return existing._id;
148
+ } else {
149
+ const newProfile: any = {
150
+ auth0Id: args.auth0Id,
151
+ email: args.email,
152
+ name: args.name,
153
+ ...(args.clinicId && { clinicId: args.clinicId }),
154
+ ...(args.supplierId && { supplierId: args.supplierId }),
155
+ };
156
+ if (roleId) {
157
+ newProfile.roleId = roleId;
158
+ }
159
+ const profileId = await ctx.db.insert('user_profiles', newProfile);
160
+ return profileId;
161
+ }
162
+ },
163
+ });
164
+
165
+ export const updateUserRole = mutation({
166
+ args: {
167
+ userId: v.id('user_profiles'),
168
+ roleName: v.string(),
169
+ clinicId: v.optional(v.id('clinics')),
170
+ supplierId: v.optional(v.id('suppliers')),
171
+ },
172
+ handler: async (ctx, args) => {
173
+ const newRole = await ctx.db
174
+ .query('roles')
175
+ .withIndex('by_name', (q) => q.eq('name', args.roleName))
176
+ .first();
177
+
178
+ if (!newRole) {
179
+ throw new Error(`Role ${args.roleName} not found`);
180
+ }
181
+
182
+ await ctx.db.patch(args.userId, {
183
+ roleId: newRole._id,
184
+ ...(args.clinicId !== undefined && { clinicId: args.clinicId }),
185
+ ...(args.supplierId !== undefined && { supplierId: args.supplierId }),
186
+ });
187
+
188
+ return args.userId;
189
+ },
190
+ });
191
+
192
+ export const updateUserSupplier = mutation({
193
+ args: {
194
+ userId: v.id('user_profiles'),
195
+ supplierId: v.id('suppliers'),
196
+ },
197
+ handler: async (ctx, args) => {
198
+ await ctx.db.patch(args.userId, {
199
+ supplierId: args.supplierId,
200
+ });
201
+
202
+ return args.userId;
203
+ },
204
+ });
205
+
206
+ export const listUsers = query({
207
+ args: {
208
+ limit: v.optional(v.number()),
209
+ },
210
+ handler: async (ctx, args) => {
211
+ const maxResults = args.limit || 500;
212
+ const users = await ctx.db.query('user_profiles').order('desc').take(maxResults);
213
+
214
+ const usersWithRoles = await Promise.all(
215
+ users.map(async (user) => {
216
+ const role = user.roleId ? await ctx.db.get(user.roleId) : null;
217
+ return {
218
+ ...user,
219
+ role: role?.name || user.role,
220
+ };
221
+ })
222
+ );
223
+
224
+ return usersWithRoles;
225
+ },
226
+ });
227
+
228
+ export const listUsersPaginated = query({
229
+ args: {
230
+ paginationOpts: paginationOptsValidator,
231
+ roleId: v.optional(v.id('roles')),
232
+ isActive: v.optional(v.boolean()),
233
+ searchTerm: v.optional(v.string()),
234
+ },
235
+ handler: async (ctx, args) => {
236
+ let baseQuery;
237
+
238
+ if (args.roleId) {
239
+ baseQuery = ctx.db
240
+ .query('user_profiles')
241
+ .withIndex('by_roleId', (q: any) => q.eq('roleId', args.roleId));
242
+ } else if (args.isActive !== undefined) {
243
+ baseQuery = ctx.db
244
+ .query('user_profiles')
245
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', args.isActive));
246
+ } else {
247
+ baseQuery = ctx.db.query('user_profiles');
248
+ }
249
+
250
+ let filteredQuery = baseQuery.order('desc');
251
+
252
+ if (args.isActive !== undefined && args.roleId) {
253
+ filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('isActive'), args.isActive));
254
+ }
255
+
256
+ const result = await filteredQuery.paginate(args.paginationOpts);
257
+
258
+ let filteredPage = result.page;
259
+
260
+ if (args.searchTerm) {
261
+ const search = args.searchTerm.toLowerCase();
262
+ filteredPage = filteredPage.filter((user: any) =>
263
+ user.name?.toLowerCase().includes(search) ||
264
+ user.email?.toLowerCase().includes(search)
265
+ );
266
+ }
267
+
268
+ const roleIds = [...new Set(filteredPage.map((u: any) => u.roleId).filter(Boolean))];
269
+ const roles = await Promise.all(roleIds.map(id => ctx.db.get(id)));
270
+ const roleMap = new Map(roles.filter(Boolean).map((r: any) => [r._id.toString(), r.name]));
271
+
272
+ const minimalPage = filteredPage.map((user: any) => ({
273
+ _id: user._id,
274
+ auth0Id: user.auth0Id,
275
+ email: user.email,
276
+ name: user.name,
277
+ roleId: user.roleId,
278
+ role: roleMap.get(user.roleId?.toString()) || user.role || 'user',
279
+ isActive: user.isActive,
280
+ clinicId: user.clinicId,
281
+ supplierId: user.supplierId,
282
+ createdAt: user.createdAt || user._creationTime,
283
+ }));
284
+
285
+ return {
286
+ page: minimalPage,
287
+ isDone: result.isDone,
288
+ continueCursor: result.continueCursor,
289
+ };
290
+ },
291
+ });
292
+
293
+ export const getUsersCount = query({
294
+ args: {
295
+ roleId: v.optional(v.id('roles')),
296
+ isActive: v.optional(v.boolean()),
297
+ },
298
+ handler: async (ctx, args) => {
299
+ let query;
300
+
301
+ if (args.roleId) {
302
+ query = ctx.db.query('user_profiles')
303
+ .withIndex('by_roleId', (q: any) => q.eq('roleId', args.roleId));
304
+ } else if (args.isActive !== undefined) {
305
+ query = ctx.db.query('user_profiles')
306
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', args.isActive));
307
+ } else {
308
+ query = ctx.db.query('user_profiles');
309
+ }
310
+
311
+ if (args.isActive !== undefined && args.roleId) {
312
+ query = query.filter((q: any) => q.eq(q.field('isActive'), args.isActive));
313
+ }
314
+
315
+ const users = await query.take(1000);
316
+ return { total: users.length };
317
+ },
318
+ });
319
+
320
+ export const savePrimoUPToken = mutation({
321
+ args: {
322
+ auth0Id: v.string(),
323
+ token: v.string(),
324
+ expiresAt: v.optional(v.number()),
325
+ refreshToken: v.optional(v.string()),
326
+ },
327
+ handler: async (ctx, args) => {
328
+ const existingTokens = await ctx.db
329
+ .query('primoup_tokens')
330
+ .withIndex('by_userId', (q) => q.eq('userId', args.auth0Id))
331
+ .collect();
332
+
333
+ for (const token of existingTokens) {
334
+ await ctx.db.patch(token._id, { isActive: false });
335
+ }
336
+
337
+ await ctx.db.insert('primoup_tokens', {
338
+ userId: args.auth0Id,
339
+ token: args.token,
340
+ refreshToken: args.refreshToken,
341
+ expiresAt: args.expiresAt,
342
+ createdAt: Date.now(),
343
+ isActive: true,
344
+ });
345
+
346
+ await ctx.db.insert('primoup_access_logs', {
347
+ userId: args.auth0Id,
348
+ action: 'login',
349
+ success: true,
350
+ timestamp: Date.now(),
351
+ });
352
+
353
+ return { success: true };
354
+ },
355
+ });
356
+
357
+ export const getPrimoUPToken = query({
358
+ args: { auth0Id: v.string() },
359
+ handler: async (ctx, args) => {
360
+ const activeToken = await ctx.db
361
+ .query('primoup_tokens')
362
+ .withIndex('by_userId_active', (q) =>
363
+ q.eq('userId', args.auth0Id).eq('isActive', true)
364
+ )
365
+ .first();
366
+
367
+ if (!activeToken) {
368
+ return null;
369
+ }
370
+
371
+ if (activeToken.expiresAt && activeToken.expiresAt < Date.now()) {
372
+ return {
373
+ token: null,
374
+ expired: true,
375
+ refreshToken: activeToken.refreshToken,
376
+ };
377
+ }
378
+
379
+ return {
380
+ token: activeToken.token,
381
+ expired: false,
382
+ expiresAt: activeToken.expiresAt,
383
+ tokenId: activeToken._id,
384
+ };
385
+ },
386
+ });
387
+
388
+ export const logPrimoUPAccess = mutation({
389
+ args: {
390
+ auth0Id: v.string(),
391
+ action: v.string(),
392
+ endpoint: v.optional(v.string()),
393
+ success: v.boolean(),
394
+ errorMessage: v.optional(v.string()),
395
+ metadata: v.optional(v.any()),
396
+ },
397
+ handler: async (ctx, args) => {
398
+ await ctx.db.insert('primoup_access_logs', {
399
+ userId: args.auth0Id,
400
+ action: args.action,
401
+ endpoint: args.endpoint,
402
+ success: args.success,
403
+ errorMessage: args.errorMessage,
404
+ timestamp: Date.now(),
405
+ metadata: args.metadata,
406
+ });
407
+
408
+ return { success: true };
409
+ },
410
+ });
411
+
412
+ export const getPrimoUPAccessLogs = query({
413
+ args: {
414
+ auth0Id: v.string(),
415
+ isAdmin: v.optional(v.boolean()),
416
+ userId: v.optional(v.string()),
417
+ limit: v.optional(v.number()),
418
+ },
419
+ handler: async (ctx, args) => {
420
+ const targetUserId = args.isAdmin && args.userId ? args.userId : args.auth0Id;
421
+
422
+ const logs = await ctx.db
423
+ .query('primoup_access_logs')
424
+ .withIndex('by_userId_timestamp', (q) => q.eq('userId', targetUserId))
425
+ .order('desc')
426
+ .take(args.limit || 50);
427
+
428
+ return logs;
429
+ },
430
+ });
431
+
432
+ export const updateUserRoleByEmail = mutation({
433
+ args: {
434
+ email: v.string(),
435
+ roleName: v.string(),
436
+ },
437
+ handler: async (ctx, args) => {
438
+ const user = await ctx.db
439
+ .query('user_profiles')
440
+ .filter((q) => q.eq(q.field('email'), args.email))
441
+ .first();
442
+
443
+ if (!user) {
444
+ throw new Error(`User with email ${args.email} not found`);
445
+ }
446
+
447
+ const role = await ctx.db
448
+ .query('roles')
449
+ .withIndex('by_name', (q) => q.eq('name', args.roleName))
450
+ .first();
451
+
452
+ if (!role) {
453
+ throw new Error(`Role ${args.roleName} not found`);
454
+ }
455
+
456
+ await ctx.db.patch(user._id, {
457
+ roleId: role._id,
458
+ });
459
+
460
+ return {
461
+ success: true,
462
+ userId: user._id,
463
+ email: user.email,
464
+ newRole: args.roleName,
465
+ };
466
+ },
467
+ });
468
+
469
+ export const saveUserClinics = mutation({
470
+ args: {
471
+ auth0Id: v.string(),
472
+ primoupClinics: v.array(v.any()),
473
+ },
474
+ handler: async (ctx, args) => {
475
+ const profile = await ctx.db
476
+ .query('user_profiles')
477
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
478
+ .first();
479
+
480
+ if (!profile) {
481
+ throw new Error('User profile not found');
482
+ }
483
+
484
+ const optimizedClinics = args.primoupClinics.map((clinic: any) => ({
485
+ id: clinic.id,
486
+ name: clinic.name,
487
+ role: clinic.role ? { id: clinic.role.id, name: clinic.role.name, slug: clinic.role.slug } : undefined,
488
+ }));
489
+
490
+ const selectedClinicId = profile.selectedClinicId || (optimizedClinics.length > 0 ? optimizedClinics[0].id : undefined);
491
+
492
+ await ctx.db.patch(profile._id, {
493
+ primoupClinics: optimizedClinics,
494
+ selectedClinicId,
495
+ });
496
+
497
+ return profile._id;
498
+ },
499
+ });
500
+
501
+ export const updateSelectedClinic = mutation({
502
+ args: {
503
+ auth0Id: v.string(),
504
+ clinicId: v.number(),
505
+ },
506
+ handler: async (ctx, args) => {
507
+ const profile = await ctx.db
508
+ .query('user_profiles')
509
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
510
+ .first();
511
+
512
+ if (!profile) {
513
+ throw new Error('User profile not found');
514
+ }
515
+
516
+ if (profile.primoupClinics) {
517
+ const hasClinic = profile.primoupClinics.some((clinic: any) => clinic.id === args.clinicId);
518
+ if (!hasClinic) {
519
+ throw new Error('Clinic not found in user clinics');
520
+ }
521
+ }
522
+
523
+ const clinic = await ctx.db
524
+ .query('clinics')
525
+ .withIndex('by_primoupId', (q) => q.eq('primoupId', args.clinicId.toString()))
526
+ .first();
527
+
528
+ if (!clinic) {
529
+ console.warn(`⚠️ Clinic NOT FOUND in Convex with primoupId: ${args.clinicId}`);
530
+ const newCounter = (profile.clinicChangeCounter || 0) + 1;
531
+ await ctx.db.patch(profile._id, {
532
+ selectedClinicId: args.clinicId,
533
+ clinicId: undefined,
534
+ clinicChangeCounter: newCounter,
535
+ });
536
+ return profile._id;
537
+ }
538
+
539
+ const newCounter = (profile.clinicChangeCounter || 0) + 1;
540
+
541
+ await ctx.db.patch(profile._id, {
542
+ selectedClinicId: args.clinicId,
543
+ clinicId: clinic._id,
544
+ clinicChangeCounter: newCounter,
545
+ });
546
+
547
+ return profile._id;
548
+ },
549
+ });
550
+
551
+ export const startImpersonation = mutation({
552
+ args: {
553
+ auth0Id: v.string(),
554
+ targetUserId: v.id('user_profiles'),
555
+ },
556
+ handler: async (ctx, args) => {
557
+ const currentUser = await ctx.db
558
+ .query('user_profiles')
559
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
560
+ .first();
561
+
562
+ if (!currentUser) {
563
+ throw new Error('User profile not found');
564
+ }
565
+
566
+ const currentRole = currentUser.roleId ? await ctx.db.get(currentUser.roleId) : null;
567
+ if (!currentRole || currentRole.name !== 'admin') {
568
+ throw new Error('Solo gli amministratori possono impersonare altri utenti');
569
+ }
570
+
571
+ const targetUser = await ctx.db.get(args.targetUserId);
572
+ if (!targetUser) {
573
+ throw new Error('Utente target non trovato');
574
+ }
575
+
576
+ if (targetUser._id === currentUser._id) {
577
+ throw new Error('Non puoi impersonare te stesso');
578
+ }
579
+
580
+ await ctx.db.patch(currentUser._id, {
581
+ impersonatingUserId: args.targetUserId,
582
+ });
583
+
584
+ return {
585
+ success: true,
586
+ impersonatedUser: {
587
+ id: targetUser._id,
588
+ name: targetUser.name,
589
+ email: targetUser.email,
590
+ },
591
+ };
592
+ },
593
+ });
594
+
595
+ export const stopImpersonation = mutation({
596
+ args: { auth0Id: v.string() },
597
+ handler: async (ctx, args) => {
598
+ const currentUser = await ctx.db
599
+ .query('user_profiles')
600
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
601
+ .first();
602
+
603
+ if (!currentUser) {
604
+ throw new Error('User profile not found');
605
+ }
606
+
607
+ const currentRole = currentUser.roleId ? await ctx.db.get(currentUser.roleId) : null;
608
+ if (!currentRole || currentRole.name !== 'admin') {
609
+ throw new Error('Solo gli amministratori possono terminare l\'impersonazione');
610
+ }
611
+
612
+ await ctx.db.patch(currentUser._id, {
613
+ impersonatingUserId: undefined,
614
+ });
615
+
616
+ return { success: true };
617
+ },
618
+ });
619
+
620
+ export const updateUserProfile = mutation({
621
+ args: {
622
+ userId: v.id('user_profiles'),
623
+ name: v.optional(v.string()),
624
+ email: v.optional(v.string()),
625
+ roleName: v.optional(v.string()),
626
+ clinicId: v.optional(v.id('clinics')),
627
+ supplierId: v.optional(v.id('suppliers')),
628
+ },
629
+ handler: async (ctx, args) => {
630
+ const targetUser = await ctx.db.get(args.userId);
631
+ if (!targetUser) {
632
+ throw new Error('Target user not found');
633
+ }
634
+
635
+ const updateData: Record<string, any> = {};
636
+
637
+ if (args.name !== undefined) {
638
+ updateData.name = args.name;
639
+ }
640
+
641
+ if (args.email !== undefined) {
642
+ const existingUser = await ctx.db
643
+ .query('user_profiles')
644
+ .withIndex('by_email', (q) => q.eq('email', args.email!))
645
+ .first();
646
+
647
+ if (existingUser && existingUser._id !== args.userId) {
648
+ throw new Error('Email già in uso da un altro utente');
649
+ }
650
+ updateData.email = args.email;
651
+ }
652
+
653
+ if (args.roleName !== undefined) {
654
+ const newRole = await ctx.db
655
+ .query('roles')
656
+ .withIndex('by_name', (q) => q.eq('name', args.roleName!))
657
+ .first();
658
+
659
+ if (!newRole) {
660
+ throw new Error(`Ruolo ${args.roleName} non trovato`);
661
+ }
662
+ updateData.roleId = newRole._id;
663
+ }
664
+
665
+ if (args.clinicId !== undefined) {
666
+ updateData.clinicId = args.clinicId;
667
+ }
668
+
669
+ if (args.supplierId !== undefined) {
670
+ updateData.supplierId = args.supplierId;
671
+ }
672
+
673
+ if (Object.keys(updateData).length > 0) {
674
+ await ctx.db.patch(args.userId, updateData);
675
+ }
676
+
677
+ return args.userId;
678
+ },
679
+ });
680
+
681
+ export const disableUser = mutation({
682
+ args: {
683
+ userId: v.id('user_profiles'),
684
+ callerUserId: v.optional(v.id('user_profiles')),
685
+ },
686
+ handler: async (ctx, args) => {
687
+ if (args.callerUserId && args.userId === args.callerUserId) {
688
+ throw new Error('Non puoi disabilitare te stesso');
689
+ }
690
+
691
+ const targetUser = await ctx.db.get(args.userId);
692
+ if (!targetUser) {
693
+ throw new Error('Utente non trovato');
694
+ }
695
+
696
+ await ctx.db.patch(args.userId, {
697
+ isActive: false,
698
+ });
699
+
700
+ return { success: true, userId: args.userId };
701
+ },
702
+ });
703
+
704
+ export const enableUser = mutation({
705
+ args: {
706
+ userId: v.id('user_profiles'),
707
+ },
708
+ handler: async (ctx, args) => {
709
+ const targetUser = await ctx.db.get(args.userId);
710
+ if (!targetUser) {
711
+ throw new Error('Utente non trovato');
712
+ }
713
+
714
+ await ctx.db.patch(args.userId, {
715
+ isActive: true,
716
+ });
717
+
718
+ return { success: true, userId: args.userId };
719
+ },
720
+ });
721
+
722
+ export const getRealAdminProfile = query({
723
+ args: { auth0Id: v.string() },
724
+ handler: async (ctx, args) => {
725
+ const profile = await ctx.db
726
+ .query('user_profiles')
727
+ .withIndex('by_auth0Id', (q) => q.eq('auth0Id', args.auth0Id))
728
+ .first();
729
+
730
+ if (!profile) {
731
+ return null;
732
+ }
733
+
734
+ const role = profile.roleId ? await ctx.db.get(profile.roleId) : null;
735
+
736
+ return {
737
+ _id: profile._id,
738
+ name: profile.name,
739
+ email: profile.email,
740
+ role: role?.name,
741
+ isImpersonating: !!profile.impersonatingUserId,
742
+ impersonatingUserId: profile.impersonatingUserId,
743
+ };
744
+ },
745
+ });