@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,645 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation } from './_generated/server';
3
+
4
+ async function getPhotoUrl(ctx: any, device: any) {
5
+ if (device.photoStorageId) {
6
+ return await ctx.storage.getUrl(device.photoStorageId);
7
+ }
8
+ if (device.photo && device.photo.trim() !== '') {
9
+ return device.photo;
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export const listDevices = query({
15
+ args: {
16
+ paginationOpts: v.optional(v.object({
17
+ numItems: v.number(),
18
+ cursor: v.union(v.string(), v.null()),
19
+ })),
20
+ clinicId: v.optional(v.id('clinics')),
21
+ _triggerReload: v.optional(v.number()),
22
+ userRole: v.optional(v.string()),
23
+ userClinicId: v.optional(v.id('clinics')),
24
+ userSelectedClinicId: v.optional(v.number()),
25
+ },
26
+ handler: async (ctx, args) => {
27
+ const limit = args.paginationOpts?.numItems || 100;
28
+
29
+ let devices: any[];
30
+ if (args.userRole === 'admin') {
31
+ if (args.clinicId) {
32
+ const result = await ctx.db
33
+ .query('devices')
34
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.clinicId))
35
+ .paginate({ numItems: limit, cursor: args.paginationOpts?.cursor || null });
36
+ devices = result.page;
37
+ } else {
38
+ const result = await ctx.db
39
+ .query('devices')
40
+ .paginate({ numItems: limit, cursor: args.paginationOpts?.cursor || null });
41
+ devices = result.page;
42
+ }
43
+ } else if (args.userRole === 'user') {
44
+ try {
45
+ const selectedClinicId = args.userSelectedClinicId;
46
+
47
+ if (selectedClinicId) {
48
+ devices = await ctx.db
49
+ .query('devices')
50
+ .withIndex('by_primoupClinicId', (q: any) => q.eq('primoupClinicId', selectedClinicId))
51
+ .take(limit);
52
+ } else {
53
+ devices = [];
54
+ }
55
+ } catch (error: any) {
56
+ devices = [];
57
+ }
58
+ } else {
59
+ devices = [];
60
+ }
61
+
62
+ const devicesWithUrls = await Promise.all(
63
+ devices.map(async (device) => ({
64
+ ...device,
65
+ photoUrl: await getPhotoUrl(ctx, device),
66
+ }))
67
+ );
68
+
69
+ return devicesWithUrls;
70
+ },
71
+ });
72
+
73
+ export const listDevicesPaginated = query({
74
+ args: {
75
+ paginationOpts: v.object({
76
+ numItems: v.number(),
77
+ cursor: v.union(v.string(), v.null()),
78
+ }),
79
+ category: v.optional(v.string()),
80
+ brand: v.optional(v.string()),
81
+ status: v.optional(v.string()),
82
+ searchTerm: v.optional(v.string()),
83
+ clinicId: v.optional(v.id('clinics')),
84
+ userRole: v.optional(v.string()),
85
+ userClinicId: v.optional(v.id('clinics')),
86
+ userSelectedClinicId: v.optional(v.number()),
87
+ },
88
+ handler: async (ctx, args) => {
89
+ let baseQuery;
90
+
91
+ if (args.userRole === 'admin') {
92
+ if (args.clinicId) {
93
+ baseQuery = ctx.db
94
+ .query('devices')
95
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.clinicId));
96
+ } else if (args.category) {
97
+ baseQuery = ctx.db
98
+ .query('devices')
99
+ .withIndex('by_category', (q: any) => q.eq('category', args.category));
100
+ } else if (args.status) {
101
+ baseQuery = ctx.db
102
+ .query('devices')
103
+ .withIndex('by_status', (q: any) => q.eq('status', args.status));
104
+ } else {
105
+ baseQuery = ctx.db.query('devices');
106
+ }
107
+ } else if (args.userRole === 'user') {
108
+ const selectedClinicId = args.userSelectedClinicId;
109
+
110
+ if (!selectedClinicId) {
111
+ return { page: [], isDone: true, continueCursor: '' };
112
+ }
113
+
114
+ baseQuery = ctx.db
115
+ .query('devices')
116
+ .withIndex('by_primoupClinicId', (q: any) => q.eq('primoupClinicId', selectedClinicId));
117
+ } else {
118
+ return { page: [], isDone: true, continueCursor: '' };
119
+ }
120
+
121
+ let filteredQuery = baseQuery.order('desc');
122
+
123
+ if (args.category && !(args.userRole === 'admin' && !args.clinicId && !args.status)) {
124
+ filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('category'), args.category));
125
+ }
126
+
127
+ if (args.brand) {
128
+ filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('brand'), args.brand));
129
+ }
130
+
131
+ if (args.status && !(args.userRole === 'admin' && !args.clinicId && !args.category)) {
132
+ filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('status'), args.status));
133
+ }
134
+
135
+ const result = await filteredQuery.paginate(args.paginationOpts);
136
+
137
+ let filteredPage = result.page;
138
+
139
+ if (args.searchTerm) {
140
+ const search = args.searchTerm.toLowerCase();
141
+ filteredPage = filteredPage.filter((device: any) =>
142
+ device.name?.toLowerCase().includes(search) ||
143
+ device.brand?.toLowerCase().includes(search) ||
144
+ device.model?.toLowerCase().includes(search) ||
145
+ device.serialNumber?.toLowerCase().includes(search) ||
146
+ device.internalId?.toLowerCase().includes(search)
147
+ );
148
+ }
149
+
150
+ const clinicIds = [...new Set(filteredPage.map((d: any) => d.clinicId).filter(Boolean))];
151
+ const clinics = await Promise.all(clinicIds.map(id => ctx.db.get(id)));
152
+ const clinicMap = new Map(clinics.filter(Boolean).map((c: any) => [c._id.toString(), c.name]));
153
+
154
+ const minimalPage = await Promise.all(
155
+ filteredPage.map(async (device: any) => ({
156
+ _id: device._id,
157
+ name: device.name,
158
+ category: device.category,
159
+ brand: device.brand,
160
+ model: device.model,
161
+ serialNumber: device.serialNumber,
162
+ internalId: device.internalId,
163
+ status: device.status,
164
+ clinicId: device.clinicId,
165
+ clinicName: clinicMap.get(device.clinicId?.toString()) || 'Unknown',
166
+ photoUrl: device.photoStorageId
167
+ ? await ctx.storage.getUrl(device.photoStorageId)
168
+ : null,
169
+ }))
170
+ );
171
+
172
+ return {
173
+ page: minimalPage,
174
+ isDone: result.isDone,
175
+ continueCursor: result.continueCursor,
176
+ };
177
+ },
178
+ });
179
+
180
+ export const getDevicesCount = query({
181
+ args: {
182
+ category: v.optional(v.string()),
183
+ brand: v.optional(v.string()),
184
+ status: v.optional(v.string()),
185
+ clinicId: v.optional(v.id('clinics')),
186
+ userRole: v.optional(v.string()),
187
+ userClinicId: v.optional(v.id('clinics')),
188
+ userSelectedClinicId: v.optional(v.number()),
189
+ },
190
+ handler: async (ctx, args) => {
191
+ let devicesQuery;
192
+
193
+ if (args.userRole === 'admin') {
194
+ if (args.clinicId) {
195
+ devicesQuery = ctx.db.query('devices')
196
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.clinicId));
197
+ } else if (args.category) {
198
+ devicesQuery = ctx.db.query('devices')
199
+ .withIndex('by_category', (q: any) => q.eq('category', args.category));
200
+ } else {
201
+ devicesQuery = ctx.db.query('devices');
202
+ }
203
+ } else if (args.userRole === 'user') {
204
+ const selectedClinicId = args.userSelectedClinicId;
205
+ if (!selectedClinicId) return { total: 0 };
206
+
207
+ devicesQuery = ctx.db.query('devices')
208
+ .withIndex('by_primoupClinicId', (q: any) => q.eq('primoupClinicId', selectedClinicId));
209
+ } else {
210
+ return { total: 0 };
211
+ }
212
+
213
+ if (args.brand) {
214
+ devicesQuery = devicesQuery.filter((q: any) => q.eq(q.field('brand'), args.brand));
215
+ }
216
+ if (args.status && !args.category) {
217
+ devicesQuery = devicesQuery.filter((q: any) => q.eq(q.field('status'), args.status));
218
+ }
219
+
220
+ const devices = await devicesQuery.collect();
221
+ return { total: devices.length };
222
+ },
223
+ });
224
+
225
+ export const getDevice = query({
226
+ args: { deviceId: v.id('devices') },
227
+ handler: async (ctx, args) => {
228
+ const device = await ctx.db.get(args.deviceId);
229
+ if (!device) return null;
230
+
231
+ return {
232
+ ...device,
233
+ photoUrl: await getPhotoUrl(ctx, device),
234
+ };
235
+ },
236
+ });
237
+
238
+ export const getById = query({
239
+ args: { id: v.id('devices') },
240
+ handler: async (ctx, args) => {
241
+ const device = await ctx.db.get(args.id);
242
+ if (!device) return null;
243
+
244
+ return {
245
+ ...device,
246
+ photoUrl: await getPhotoUrl(ctx, device),
247
+ };
248
+ },
249
+ });
250
+
251
+ export const listAllDevicesForMigration = query({
252
+ args: {
253
+ limit: v.optional(v.number()),
254
+ lastCreationTime: v.optional(v.number()),
255
+ },
256
+ handler: async (ctx, args) => {
257
+ const limit = args.limit || 1000;
258
+
259
+ const devices = args.lastCreationTime
260
+ ? await ctx.db
261
+ .query('devices')
262
+ .filter((q) => q.gt(q.field('_creationTime'), args.lastCreationTime!))
263
+ .take(limit)
264
+ : await ctx.db.query('devices').take(limit);
265
+
266
+ return devices;
267
+ },
268
+ });
269
+
270
+ export const generateUploadUrl = mutation({
271
+ args: {},
272
+ handler: async (ctx) => {
273
+ return await ctx.storage.generateUploadUrl();
274
+ },
275
+ });
276
+
277
+ export const createDevice = mutation({
278
+ args: {
279
+ clinicId: v.id('clinics'),
280
+ supplierId: v.optional(v.id('suppliers')),
281
+ name: v.string(),
282
+ category: v.string(),
283
+ brand: v.string(),
284
+ model: v.string(),
285
+ serial_number: v.string(),
286
+ photoStorageId: v.optional(v.id('_storage')),
287
+ status: v.union(v.literal('active'), v.literal('in_maintenance'), v.literal('out_of_service')),
288
+ metadata: v.optional(v.any()),
289
+ internalId: v.optional(v.string()),
290
+ industry40Data: v.optional(v.string()),
291
+ purchaseDate: v.optional(v.number()),
292
+ vendor: v.optional(v.string()),
293
+ purchaseCost: v.optional(v.number()),
294
+ warrantyEndDate: v.optional(v.number()),
295
+ spareParts: v.optional(v.array(v.object({
296
+ name: v.string(),
297
+ code: v.string(),
298
+ photoStorageId: v.optional(v.id('_storage')),
299
+ quantity: v.number(),
300
+ notes: v.optional(v.string()),
301
+ }))),
302
+ maintenanceManualStorageId: v.optional(v.id('_storage')),
303
+ maintenanceReportsStorageIds: v.optional(v.array(v.id('_storage'))),
304
+ conformityDeclarationStorageId: v.optional(v.id('_storage')),
305
+ authorizationsStorageIds: v.optional(v.array(v.id('_storage'))),
306
+ otherAttachmentsStorageIds: v.optional(v.array(v.id('_storage'))),
307
+ },
308
+ handler: async (ctx, args) => {
309
+ const deviceData: any = { ...args };
310
+
311
+ const clinic = await ctx.db.get(args.clinicId);
312
+ if (clinic?.primoupId) {
313
+ const primoupIdNum = parseInt(clinic.primoupId, 10);
314
+ if (!isNaN(primoupIdNum)) {
315
+ deviceData.primoupClinicId = primoupIdNum;
316
+ }
317
+ }
318
+
319
+ const deviceId = await ctx.db.insert('devices', deviceData);
320
+ return deviceId;
321
+ },
322
+ });
323
+
324
+ export const updateDevice = mutation({
325
+ args: {
326
+ deviceId: v.id('devices'),
327
+ clinicId: v.optional(v.id('clinics')),
328
+ primoupClinicId: v.optional(v.number()),
329
+ supplierId: v.optional(v.id('suppliers')),
330
+ name: v.optional(v.string()),
331
+ category: v.optional(v.string()),
332
+ brand: v.optional(v.string()),
333
+ model: v.optional(v.string()),
334
+ serial_number: v.optional(v.string()),
335
+ photoStorageId: v.optional(v.id('_storage')),
336
+ status: v.optional(v.union(v.literal('active'), v.literal('in_maintenance'), v.literal('out_of_service'))),
337
+ metadata: v.optional(v.any()),
338
+ internalId: v.optional(v.string()),
339
+ primoupCategoryId: v.optional(v.string()),
340
+ industry40Data: v.optional(v.string()),
341
+ purchaseDate: v.optional(v.number()),
342
+ vendor: v.optional(v.string()),
343
+ purchaseCost: v.optional(v.number()),
344
+ warrantyEndDate: v.optional(v.number()),
345
+ spareParts: v.optional(v.array(v.object({
346
+ name: v.string(),
347
+ code: v.string(),
348
+ photoStorageId: v.optional(v.id('_storage')),
349
+ quantity: v.number(),
350
+ notes: v.optional(v.string()),
351
+ }))),
352
+ maintenanceManualStorageId: v.optional(v.id('_storage')),
353
+ maintenanceReportsStorageIds: v.optional(v.array(v.id('_storage'))),
354
+ conformityDeclarationStorageId: v.optional(v.id('_storage')),
355
+ authorizationsStorageIds: v.optional(v.array(v.id('_storage'))),
356
+ otherAttachmentsStorageIds: v.optional(v.array(v.id('_storage'))),
357
+ },
358
+ handler: async (ctx, args) => {
359
+ const { deviceId, ...updates } = args;
360
+
361
+ const existingDevice = await ctx.db.get(deviceId);
362
+
363
+ if (existingDevice?.photoStorageId && updates.photoStorageId && existingDevice.photoStorageId !== updates.photoStorageId) {
364
+ try {
365
+ await ctx.storage.delete(existingDevice.photoStorageId);
366
+ } catch (e) {
367
+ // Continue even if delete fails
368
+ }
369
+ }
370
+
371
+ await ctx.db.patch(deviceId, updates);
372
+ return deviceId;
373
+ },
374
+ });
375
+
376
+ export const updateDeviceByUser = mutation({
377
+ args: {
378
+ deviceId: v.id('devices'),
379
+ name: v.optional(v.string()),
380
+ category: v.optional(v.string()),
381
+ brand: v.optional(v.string()),
382
+ model: v.optional(v.string()),
383
+ serial_number: v.optional(v.string()),
384
+ status: v.optional(v.union(v.literal('active'), v.literal('in_maintenance'), v.literal('out_of_service'))),
385
+ industry40Data: v.optional(v.string()),
386
+ userRole: v.string(),
387
+ userSelectedClinicId: v.optional(v.number()),
388
+ userClinicId: v.optional(v.id('clinics')),
389
+ },
390
+ handler: async (ctx, args) => {
391
+ const device = await ctx.db.get(args.deviceId);
392
+ if (!device) {
393
+ throw new Error('Device not found');
394
+ }
395
+
396
+ if (args.userRole === 'user') {
397
+ const selectedClinicId = args.userSelectedClinicId;
398
+
399
+ if (selectedClinicId) {
400
+ if (device.primoupClinicId !== selectedClinicId) {
401
+ throw new Error('Not authorized to update this device');
402
+ }
403
+ } else if (args.userClinicId && args.userClinicId !== device.clinicId) {
404
+ throw new Error('Not authorized to update this device');
405
+ }
406
+ } else if (args.userRole !== 'admin') {
407
+ throw new Error('Not authorized to update devices');
408
+ }
409
+
410
+ const { deviceId, userRole, userSelectedClinicId, userClinicId, ...updates } = args;
411
+ await ctx.db.patch(deviceId, updates);
412
+ return deviceId;
413
+ },
414
+ });
415
+
416
+ export const deleteDevice = mutation({
417
+ args: { deviceId: v.id('devices') },
418
+ handler: async (ctx, args) => {
419
+ const device = await ctx.db.get(args.deviceId);
420
+ if (device?.photoStorageId) {
421
+ await ctx.storage.delete(device.photoStorageId);
422
+ }
423
+
424
+ await ctx.db.delete(args.deviceId);
425
+ return { success: true };
426
+ },
427
+ });
428
+
429
+ export const getDeviceStats = query({
430
+ args: {
431
+ userRole: v.optional(v.string()),
432
+ userClinicId: v.optional(v.id('clinics')),
433
+ },
434
+ handler: async (ctx, args) => {
435
+ try {
436
+ if (args.userRole === 'admin') {
437
+ const LIMIT = 500;
438
+ const [active, inMaintenance, outOfService] = await Promise.all([
439
+ ctx.db.query('devices').withIndex('by_status', (q: any) => q.eq('status', 'active')).take(LIMIT),
440
+ ctx.db.query('devices').withIndex('by_status', (q: any) => q.eq('status', 'in_maintenance')).take(LIMIT),
441
+ ctx.db.query('devices').withIndex('by_status', (q: any) => q.eq('status', 'out_of_service')).take(LIMIT),
442
+ ]);
443
+ return {
444
+ total: active.length + inMaintenance.length + outOfService.length,
445
+ active: active.length,
446
+ in_maintenance: inMaintenance.length,
447
+ out_of_service: outOfService.length,
448
+ };
449
+ } else if (args.userRole === 'user' && args.userClinicId) {
450
+ const devices = await ctx.db
451
+ .query('devices')
452
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.userClinicId!))
453
+ .collect();
454
+ return {
455
+ total: devices.length,
456
+ active: devices.filter((d) => d.status === 'active').length,
457
+ in_maintenance: devices.filter((d) => d.status === 'in_maintenance').length,
458
+ out_of_service: devices.filter((d) => d.status === 'out_of_service').length,
459
+ };
460
+ }
461
+
462
+ return { total: 0, active: 0, in_maintenance: 0, out_of_service: 0 };
463
+ } catch (error) {
464
+ console.error('Error in getDeviceStats:', error);
465
+ return { total: 0, active: 0, in_maintenance: 0, out_of_service: 0 };
466
+ }
467
+ },
468
+ });
469
+
470
+ export const getFileUrl = query({
471
+ args: { storageId: v.id('_storage') },
472
+ handler: async (ctx, args) => {
473
+ const url = await ctx.storage.getUrl(args.storageId);
474
+ return url;
475
+ },
476
+ });
477
+
478
+ export const listAllDevicesForExport = query({
479
+ args: {},
480
+ handler: async (ctx) => {
481
+ const clinics = await ctx.db.query('clinics').take(500);
482
+ const results: any[] = [];
483
+
484
+ for (const clinic of clinics) {
485
+ const clinicDevices = await ctx.db
486
+ .query('devices')
487
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', clinic._id))
488
+ .take(1000);
489
+
490
+ for (const device of clinicDevices) {
491
+ results.push({
492
+ clinicName: clinic.name || 'Sconosciuta',
493
+ category: device.category || '',
494
+ brand: device.brand || '',
495
+ model: device.model || '',
496
+ serial_number: device.serial_number || '',
497
+ quantity: device.quantity || 1,
498
+ status: device.status || 'active',
499
+ });
500
+ }
501
+ }
502
+
503
+ return results;
504
+ },
505
+ });
506
+
507
+ export const getDeviceTemplates = query({
508
+ args: {
509
+ searchQuery: v.optional(v.string()),
510
+ },
511
+ handler: async (ctx, args) => {
512
+ const devices = await ctx.db.query('devices').take(2000);
513
+
514
+ const templateMap = new Map<string, { name: string; category: string; brands: Map<string, Set<string>> }>();
515
+
516
+ for (const device of devices) {
517
+ const name = device.name;
518
+
519
+ if (args.searchQuery) {
520
+ const query = args.searchQuery.toLowerCase();
521
+ if (!name.toLowerCase().includes(query) &&
522
+ !device.brand.toLowerCase().includes(query) &&
523
+ !device.category.toLowerCase().includes(query)) {
524
+ continue;
525
+ }
526
+ }
527
+
528
+ if (!templateMap.has(name)) {
529
+ templateMap.set(name, {
530
+ name,
531
+ category: device.category,
532
+ brands: new Map(),
533
+ });
534
+ }
535
+
536
+ const template = templateMap.get(name)!;
537
+ if (!template.brands.has(device.brand)) {
538
+ template.brands.set(device.brand, new Set());
539
+ }
540
+ template.brands.get(device.brand)!.add(device.model);
541
+ }
542
+
543
+ const templates = Array.from(templateMap.values()).map(t => ({
544
+ name: t.name,
545
+ category: t.category,
546
+ brandOptions: Array.from(t.brands.entries()).map(([brand, models]) => ({
547
+ brand,
548
+ models: Array.from(models),
549
+ })),
550
+ }));
551
+
552
+ templates.sort((a, b) => a.name.localeCompare(b.name));
553
+
554
+ return templates;
555
+ },
556
+ });
557
+
558
+ export const createDeviceByUser = mutation({
559
+ args: {
560
+ name: v.string(),
561
+ category: v.string(),
562
+ brand: v.string(),
563
+ model: v.string(),
564
+ serial_number: v.string(),
565
+ userSelectedClinicId: v.number(),
566
+ userPrimoupClinics: v.optional(v.array(v.any())),
567
+ },
568
+ handler: async (ctx, args) => {
569
+ const selectedClinicId = args.userSelectedClinicId;
570
+
571
+ const primoupClinics = args.userPrimoupClinics || [];
572
+ const selectedPrimoupClinic = primoupClinics.find((c: any) => c.id === selectedClinicId);
573
+
574
+ if (!selectedPrimoupClinic) {
575
+ throw new Error('Selected clinic not found in user profile');
576
+ }
577
+
578
+ let clinic = await ctx.db
579
+ .query('clinics')
580
+ .withIndex('by_primoupId', (q: any) => q.eq('primoupId', String(selectedClinicId)))
581
+ .first();
582
+ if (!clinic) {
583
+ const clinics = await ctx.db.query('clinics').take(500);
584
+ clinic = clinics.find(c => c.name === selectedPrimoupClinic.name) || null;
585
+ }
586
+
587
+ if (!clinic) {
588
+ throw new Error('Clinic not found in database');
589
+ }
590
+
591
+ const deviceId = await ctx.db.insert('devices', {
592
+ clinicId: clinic._id,
593
+ primoupClinicId: selectedClinicId,
594
+ name: args.name,
595
+ category: args.category,
596
+ brand: args.brand,
597
+ model: args.model,
598
+ serial_number: args.serial_number,
599
+ status: 'active',
600
+ quantity: 1,
601
+ });
602
+
603
+ return deviceId;
604
+ },
605
+ });
606
+
607
+ export const listAllDevicesForAdmin = query({
608
+ args: {},
609
+ handler: async (ctx) => {
610
+ const devices = await ctx.db.query('devices').take(3000);
611
+
612
+ return devices.map(device => ({
613
+ _id: device._id,
614
+ name: device.name,
615
+ category: device.category || '',
616
+ brand: device.brand || '',
617
+ model: device.model || '',
618
+ clinicId: device.clinicId,
619
+ }));
620
+ },
621
+ });
622
+
623
+ export const getUniqueBrandsAndModels = query({
624
+ args: {},
625
+ handler: async (ctx) => {
626
+ const devices = await ctx.db.query('devices').take(3000);
627
+
628
+ const brandsSet = new Set<string>();
629
+ const modelsSet = new Set<string>();
630
+
631
+ for (const device of devices) {
632
+ if (device.brand && device.brand.trim()) {
633
+ brandsSet.add(device.brand.trim());
634
+ }
635
+ if (device.model && device.model.trim()) {
636
+ modelsSet.add(device.model.trim());
637
+ }
638
+ }
639
+
640
+ return {
641
+ brands: Array.from(brandsSet).sort(),
642
+ models: Array.from(modelsSet).sort(),
643
+ };
644
+ },
645
+ });
@@ -0,0 +1,38 @@
1
+ import { query } from './_generated/server';
2
+ import { v } from 'convex/values';
3
+
4
+ export const getAdminEmails = query({
5
+ args: {},
6
+ handler: async (ctx) => {
7
+ const adminRole = await ctx.db
8
+ .query('roles')
9
+ .withIndex('by_name', (q) => q.eq('name', 'admin'))
10
+ .first();
11
+ if (!adminRole) return [];
12
+ const adminUsers = await ctx.db
13
+ .query('user_profiles')
14
+ .withIndex('by_roleId', (q) => q.eq('roleId', adminRole._id))
15
+ .collect();
16
+ return adminUsers
17
+ .filter((user) => user.email)
18
+ .map((user) => ({ email: user.email, name: user.name || user.email }));
19
+ },
20
+ });
21
+
22
+ export const getSupplierEmail = query({
23
+ args: { supplierId: v.id('suppliers') },
24
+ handler: async (ctx, args) => {
25
+ const supplier = await ctx.db.get(args.supplierId);
26
+ if (!supplier || !supplier.contact_email) return null;
27
+ return { email: supplier.contact_email, name: supplier.name };
28
+ },
29
+ });
30
+
31
+ export const getClinicEmail = query({
32
+ args: { clinicId: v.id('clinics') },
33
+ handler: async (ctx, args) => {
34
+ const clinic = await ctx.db.get(args.clinicId);
35
+ if (!clinic || !clinic.contact_email) return null;
36
+ return { email: clinic.contact_email, name: clinic.name };
37
+ },
38
+ });