@primocaredentgroup/elettromedicali 0.1.0 → 0.1.1

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 (150) hide show
  1. package/dist/client/index.d.ts +0 -2
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +2 -28
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +4 -4
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/component.d.ts +165 -218
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/contracts.d.ts +9 -9
  10. package/dist/component/contracts.d.ts.map +1 -1
  11. package/dist/component/contracts.js +7 -13
  12. package/dist/component/contracts.js.map +1 -1
  13. package/dist/component/crons.d.ts.map +1 -1
  14. package/dist/component/crons.js +1 -2
  15. package/dist/component/crons.js.map +1 -1
  16. package/dist/component/dashboardStats.d.ts +8 -3
  17. package/dist/component/dashboardStats.d.ts.map +1 -1
  18. package/dist/component/dashboardStats.js +24 -39
  19. package/dist/component/dashboardStats.js.map +1 -1
  20. package/dist/component/dashboardStatsCache.d.ts +5 -11
  21. package/dist/component/dashboardStatsCache.d.ts.map +1 -1
  22. package/dist/component/dashboardStatsCache.js +12 -53
  23. package/dist/component/dashboardStatsCache.js.map +1 -1
  24. package/dist/component/deviceCategories.d.ts +22 -15
  25. package/dist/component/deviceCategories.d.ts.map +1 -1
  26. package/dist/component/deviceCategories.js +10 -4
  27. package/dist/component/deviceCategories.js.map +1 -1
  28. package/dist/component/deviceQuestions.d.ts +36 -27
  29. package/dist/component/deviceQuestions.d.ts.map +1 -1
  30. package/dist/component/deviceQuestions.js +22 -5
  31. package/dist/component/deviceQuestions.js.map +1 -1
  32. package/dist/component/deviceRepairHistory.d.ts +3 -3
  33. package/dist/component/deviceRepairHistory.js +1 -1
  34. package/dist/component/deviceRepairHistory.js.map +1 -1
  35. package/dist/component/deviceStatus.d.ts +8 -57
  36. package/dist/component/deviceStatus.d.ts.map +1 -1
  37. package/dist/component/deviceStatus.js +32 -30
  38. package/dist/component/deviceStatus.js.map +1 -1
  39. package/dist/component/devices.d.ts +39 -22
  40. package/dist/component/devices.d.ts.map +1 -1
  41. package/dist/component/devices.js +85 -96
  42. package/dist/component/devices.js.map +1 -1
  43. package/dist/component/emailHelpers.d.ts +10 -3
  44. package/dist/component/emailHelpers.d.ts.map +1 -1
  45. package/dist/component/emailHelpers.js +9 -20
  46. package/dist/component/emailHelpers.js.map +1 -1
  47. package/dist/component/emails.d.ts +5 -5
  48. package/dist/component/emails.js +2 -2
  49. package/dist/component/emails.js.map +1 -1
  50. package/dist/component/http.d.ts.map +1 -1
  51. package/dist/component/http.js +3 -108
  52. package/dist/component/http.js.map +1 -1
  53. package/dist/component/migrationHelpers.d.ts +29 -0
  54. package/dist/component/migrationHelpers.d.ts.map +1 -0
  55. package/dist/component/migrationHelpers.js +84 -0
  56. package/dist/component/migrationHelpers.js.map +1 -0
  57. package/dist/component/roles.d.ts +1 -0
  58. package/dist/component/roles.d.ts.map +1 -1
  59. package/dist/component/roles.js +5 -6
  60. package/dist/component/roles.js.map +1 -1
  61. package/dist/component/schema.d.ts +69 -150
  62. package/dist/component/schema.d.ts.map +1 -1
  63. package/dist/component/schema.js +35 -88
  64. package/dist/component/schema.js.map +1 -1
  65. package/dist/component/slaMonitoring.d.ts +16 -30
  66. package/dist/component/slaMonitoring.d.ts.map +1 -1
  67. package/dist/component/slaMonitoring.js +48 -99
  68. package/dist/component/slaMonitoring.js.map +1 -1
  69. package/dist/component/spareParts.d.ts +11 -48
  70. package/dist/component/spareParts.d.ts.map +1 -1
  71. package/dist/component/spareParts.js +41 -11
  72. package/dist/component/spareParts.js.map +1 -1
  73. package/dist/component/suppliers.d.ts +38 -19
  74. package/dist/component/suppliers.d.ts.map +1 -1
  75. package/dist/component/suppliers.js +63 -44
  76. package/dist/component/suppliers.js.map +1 -1
  77. package/dist/component/ticketComments.d.ts +18 -12
  78. package/dist/component/ticketComments.d.ts.map +1 -1
  79. package/dist/component/ticketComments.js +28 -59
  80. package/dist/component/ticketComments.js.map +1 -1
  81. package/dist/component/ticketDeviceData.d.ts +63 -0
  82. package/dist/component/ticketDeviceData.d.ts.map +1 -0
  83. package/dist/component/ticketDeviceData.js +103 -0
  84. package/dist/component/ticketDeviceData.js.map +1 -0
  85. package/dist/component/ticketExport.d.ts +22 -40
  86. package/dist/component/ticketExport.d.ts.map +1 -1
  87. package/dist/component/ticketExport.js +43 -109
  88. package/dist/component/ticketExport.js.map +1 -1
  89. package/dist/component/ticketHistory.d.ts +4 -4
  90. package/dist/component/ticketHistory.d.ts.map +1 -1
  91. package/dist/component/ticketHistory.js +6 -9
  92. package/dist/component/ticketHistory.js.map +1 -1
  93. package/dist/component/ticketMacros.d.ts +19 -18
  94. package/dist/component/ticketMacros.d.ts.map +1 -1
  95. package/dist/component/ticketMacros.js +24 -30
  96. package/dist/component/ticketMacros.js.map +1 -1
  97. package/dist/component/ticketStatuses.d.ts +1 -0
  98. package/dist/component/ticketStatuses.d.ts.map +1 -1
  99. package/dist/component/ticketStatuses.js +5 -6
  100. package/dist/component/ticketStatuses.js.map +1 -1
  101. package/dist/component/ticketTriggers.d.ts +36 -16
  102. package/dist/component/ticketTriggers.d.ts.map +1 -1
  103. package/dist/component/ticketTriggers.js +115 -153
  104. package/dist/component/ticketTriggers.js.map +1 -1
  105. package/dist/component/userProfiles.d.ts +25 -120
  106. package/dist/component/userProfiles.d.ts.map +1 -1
  107. package/dist/component/userProfiles.js +73 -384
  108. package/dist/component/userProfiles.js.map +1 -1
  109. package/dist/test.d.ts +69 -150
  110. package/dist/test.d.ts.map +1 -1
  111. package/package.json +12 -3
  112. package/src/client/index.ts +2 -30
  113. package/src/component/_generated/api.ts +4 -4
  114. package/src/component/_generated/component.ts +228 -350
  115. package/src/component/contracts.ts +7 -14
  116. package/src/component/crons.ts +2 -7
  117. package/src/component/dashboardStats.ts +24 -41
  118. package/src/component/dashboardStatsCache.ts +12 -61
  119. package/src/component/deviceCategories.ts +12 -4
  120. package/src/component/deviceQuestions.ts +28 -5
  121. package/src/component/deviceRepairHistory.ts +1 -1
  122. package/src/component/deviceStatus.ts +43 -45
  123. package/src/component/devices.ts +87 -106
  124. package/src/component/emailHelpers.ts +9 -19
  125. package/src/component/emails.ts +2 -2
  126. package/src/component/http.ts +3 -108
  127. package/src/component/migrationHelpers.ts +96 -0
  128. package/src/component/roles.ts +5 -6
  129. package/src/component/schema.ts +35 -93
  130. package/src/component/slaMonitoring.ts +52 -107
  131. package/src/component/spareParts.ts +46 -12
  132. package/src/component/suppliers.ts +71 -48
  133. package/src/component/ticketComments.ts +28 -71
  134. package/src/component/ticketDeviceData.ts +113 -0
  135. package/src/component/ticketExport.ts +52 -137
  136. package/src/component/ticketHistory.ts +6 -9
  137. package/src/component/ticketMacros.ts +25 -37
  138. package/src/component/ticketStatuses.ts +5 -6
  139. package/src/component/ticketTriggers.ts +121 -217
  140. package/src/component/userProfiles.ts +67 -451
  141. package/dist/component/clinics.d.ts +0 -103
  142. package/dist/component/clinics.d.ts.map +0 -1
  143. package/dist/component/clinics.js +0 -126
  144. package/dist/component/clinics.js.map +0 -1
  145. package/dist/component/maintenanceTasks.d.ts +0 -733
  146. package/dist/component/maintenanceTasks.d.ts.map +0 -1
  147. package/dist/component/maintenanceTasks.js +0 -937
  148. package/dist/component/maintenanceTasks.js.map +0 -1
  149. package/src/component/clinics.ts +0 -136
  150. package/src/component/maintenanceTasks.ts +0 -1003
@@ -13,60 +13,83 @@ async function getPhotoUrl(ctx: any, device: any) {
13
13
 
14
14
  export const listDevices = query({
15
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')),
16
+ page: v.optional(v.number()),
17
+ pageSize: v.optional(v.number()),
18
+ clinicId: v.optional(v.string()),
19
+ search: v.optional(v.string()),
20
+ category: v.optional(v.string()),
21
+ status: v.optional(v.string()),
21
22
  _triggerReload: v.optional(v.number()),
22
23
  userRole: v.optional(v.string()),
23
- userClinicId: v.optional(v.id('clinics')),
24
+ userClinicId: v.optional(v.string()),
24
25
  userSelectedClinicId: v.optional(v.number()),
26
+ userSupplierId: v.optional(v.id('suppliers')),
25
27
  },
26
28
  handler: async (ctx, args) => {
27
- const limit = args.paginationOpts?.numItems || 100;
29
+ const page = args.page || 1;
30
+ const pageSize = args.pageSize || 40;
28
31
 
29
- let devices: any[];
32
+ let baseQuery;
30
33
  if (args.userRole === 'admin') {
31
34
  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;
35
+ baseQuery = ctx.db.query('devices')
36
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.clinicId));
37
+ } else if (args.userSelectedClinicId) {
38
+ baseQuery = ctx.db.query('devices')
39
+ .withIndex('by_primoupClinicId', (q: any) => q.eq('primoupClinicId', args.userSelectedClinicId));
37
40
  } else {
38
- const result = await ctx.db
39
- .query('devices')
40
- .paginate({ numItems: limit, cursor: args.paginationOpts?.cursor || null });
41
- devices = result.page;
41
+ baseQuery = ctx.db.query('devices');
42
+ }
43
+ } else if (args.userRole === 'supplier') {
44
+ if (args.userSupplierId) {
45
+ baseQuery = ctx.db.query('devices')
46
+ .withIndex('by_supplierId', (q: any) => q.eq('supplierId', args.userSupplierId));
47
+ } else {
48
+ return { page: [], totalCount: 0, totalPages: 0, currentPage: page };
42
49
  }
43
50
  } 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 = [];
51
+ if (args.userSelectedClinicId) {
52
+ baseQuery = ctx.db.query('devices')
53
+ .withIndex('by_primoupClinicId', (q: any) => q.eq('primoupClinicId', args.userSelectedClinicId));
54
+ } else {
55
+ return { page: [], totalCount: 0, totalPages: 0, currentPage: page };
57
56
  }
58
57
  } else {
59
- devices = [];
58
+ return { page: [], totalCount: 0, totalPages: 0, currentPage: page };
60
59
  }
61
60
 
61
+ let allDevices = await baseQuery.collect();
62
+
63
+ if (args.search) {
64
+ const s = args.search.toLowerCase();
65
+ allDevices = allDevices.filter((d: any) =>
66
+ d.name?.toLowerCase().includes(s) ||
67
+ d.brand?.toLowerCase().includes(s) ||
68
+ d.model?.toLowerCase().includes(s) ||
69
+ d.serialNumber?.toLowerCase().includes(s) ||
70
+ d.internalId?.toLowerCase().includes(s)
71
+ );
72
+ }
73
+ if (args.category) {
74
+ allDevices = allDevices.filter((d: any) => d.category === args.category);
75
+ }
76
+ if (args.status) {
77
+ allDevices = allDevices.filter((d: any) => d.status === args.status);
78
+ }
79
+
80
+ const totalCount = allDevices.length;
81
+ const totalPages = Math.ceil(totalCount / pageSize);
82
+ const startIndex = (page - 1) * pageSize;
83
+ const pageDevices = allDevices.slice(startIndex, startIndex + pageSize);
84
+
62
85
  const devicesWithUrls = await Promise.all(
63
- devices.map(async (device) => ({
86
+ pageDevices.map(async (device: any) => ({
64
87
  ...device,
65
88
  photoUrl: await getPhotoUrl(ctx, device),
66
89
  }))
67
90
  );
68
91
 
69
- return devicesWithUrls;
92
+ return { page: devicesWithUrls, totalCount, totalPages, currentPage: page };
70
93
  },
71
94
  });
72
95
 
@@ -80,9 +103,9 @@ export const listDevicesPaginated = query({
80
103
  brand: v.optional(v.string()),
81
104
  status: v.optional(v.string()),
82
105
  searchTerm: v.optional(v.string()),
83
- clinicId: v.optional(v.id('clinics')),
106
+ clinicId: v.optional(v.string()),
84
107
  userRole: v.optional(v.string()),
85
- userClinicId: v.optional(v.id('clinics')),
108
+ userClinicId: v.optional(v.string()),
86
109
  userSelectedClinicId: v.optional(v.number()),
87
110
  },
88
111
  handler: async (ctx, args) => {
@@ -132,9 +155,12 @@ export const listDevicesPaginated = query({
132
155
  filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('status'), args.status));
133
156
  }
134
157
 
135
- const result = await filteredQuery.paginate(args.paginationOpts);
158
+ const limit = args.paginationOpts.numItems + 1;
159
+ const allResults = await filteredQuery.take(limit);
160
+ const isDone = allResults.length < limit;
161
+ const page = isDone ? allResults : allResults.slice(0, -1);
136
162
 
137
- let filteredPage = result.page;
163
+ let filteredPage = page;
138
164
 
139
165
  if (args.searchTerm) {
140
166
  const search = args.searchTerm.toLowerCase();
@@ -147,10 +173,6 @@ export const listDevicesPaginated = query({
147
173
  );
148
174
  }
149
175
 
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
176
  const minimalPage = await Promise.all(
155
177
  filteredPage.map(async (device: any) => ({
156
178
  _id: device._id,
@@ -162,7 +184,7 @@ export const listDevicesPaginated = query({
162
184
  internalId: device.internalId,
163
185
  status: device.status,
164
186
  clinicId: device.clinicId,
165
- clinicName: clinicMap.get(device.clinicId?.toString()) || 'Unknown',
187
+ clinicName: '',
166
188
  photoUrl: device.photoStorageId
167
189
  ? await ctx.storage.getUrl(device.photoStorageId)
168
190
  : null,
@@ -171,8 +193,8 @@ export const listDevicesPaginated = query({
171
193
 
172
194
  return {
173
195
  page: minimalPage,
174
- isDone: result.isDone,
175
- continueCursor: result.continueCursor,
196
+ isDone,
197
+ continueCursor: '',
176
198
  };
177
199
  },
178
200
  });
@@ -182,9 +204,9 @@ export const getDevicesCount = query({
182
204
  category: v.optional(v.string()),
183
205
  brand: v.optional(v.string()),
184
206
  status: v.optional(v.string()),
185
- clinicId: v.optional(v.id('clinics')),
207
+ clinicId: v.optional(v.string()),
186
208
  userRole: v.optional(v.string()),
187
- userClinicId: v.optional(v.id('clinics')),
209
+ userClinicId: v.optional(v.string()),
188
210
  userSelectedClinicId: v.optional(v.number()),
189
211
  },
190
212
  handler: async (ctx, args) => {
@@ -276,7 +298,8 @@ export const generateUploadUrl = mutation({
276
298
 
277
299
  export const createDevice = mutation({
278
300
  args: {
279
- clinicId: v.id('clinics'),
301
+ clinicId: v.string(),
302
+ primoupClinicId: v.optional(v.number()),
280
303
  supplierId: v.optional(v.id('suppliers')),
281
304
  name: v.string(),
282
305
  category: v.string(),
@@ -308,14 +331,6 @@ export const createDevice = mutation({
308
331
  handler: async (ctx, args) => {
309
332
  const deviceData: any = { ...args };
310
333
 
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
334
  const deviceId = await ctx.db.insert('devices', deviceData);
320
335
  return deviceId;
321
336
  },
@@ -324,7 +339,7 @@ export const createDevice = mutation({
324
339
  export const updateDevice = mutation({
325
340
  args: {
326
341
  deviceId: v.id('devices'),
327
- clinicId: v.optional(v.id('clinics')),
342
+ clinicId: v.optional(v.string()),
328
343
  primoupClinicId: v.optional(v.number()),
329
344
  supplierId: v.optional(v.id('suppliers')),
330
345
  name: v.optional(v.string()),
@@ -385,7 +400,7 @@ export const updateDeviceByUser = mutation({
385
400
  industry40Data: v.optional(v.string()),
386
401
  userRole: v.string(),
387
402
  userSelectedClinicId: v.optional(v.number()),
388
- userClinicId: v.optional(v.id('clinics')),
403
+ userClinicId: v.optional(v.string()),
389
404
  },
390
405
  handler: async (ctx, args) => {
391
406
  const device = await ctx.db.get(args.deviceId);
@@ -429,7 +444,7 @@ export const deleteDevice = mutation({
429
444
  export const getDeviceStats = query({
430
445
  args: {
431
446
  userRole: v.optional(v.string()),
432
- userClinicId: v.optional(v.id('clinics')),
447
+ userClinicId: v.optional(v.string()),
433
448
  },
434
449
  handler: async (ctx, args) => {
435
450
  try {
@@ -478,29 +493,17 @@ export const getFileUrl = query({
478
493
  export const listAllDevicesForExport = query({
479
494
  args: {},
480
495
  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;
496
+ const devices = await ctx.db.query('devices').take(5000);
497
+ return devices.map(device => ({
498
+ clinicId: device.clinicId,
499
+ clinicName: '',
500
+ category: device.category || '',
501
+ brand: device.brand || '',
502
+ model: device.model || '',
503
+ serial_number: device.serial_number || '',
504
+ quantity: device.quantity || 1,
505
+ status: device.status || 'active',
506
+ }));
504
507
  },
505
508
  });
506
509
 
@@ -557,40 +560,18 @@ export const getDeviceTemplates = query({
557
560
 
558
561
  export const createDeviceByUser = mutation({
559
562
  args: {
563
+ clinicId: v.string(),
560
564
  name: v.string(),
561
565
  category: v.string(),
562
566
  brand: v.string(),
563
567
  model: v.string(),
564
568
  serial_number: v.string(),
565
569
  userSelectedClinicId: v.number(),
566
- userPrimoupClinics: v.optional(v.array(v.any())),
567
570
  },
568
571
  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
572
  const deviceId = await ctx.db.insert('devices', {
592
- clinicId: clinic._id,
593
- primoupClinicId: selectedClinicId,
573
+ clinicId: args.clinicId,
574
+ primoupClinicId: args.userSelectedClinicId,
594
575
  name: args.name,
595
576
  category: args.category,
596
577
  brand: args.brand,
@@ -2,20 +2,11 @@ import { query } from './_generated/server';
2
2
  import { v } from 'convex/values';
3
3
 
4
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 }));
5
+ args: {
6
+ adminEmails: v.optional(v.array(v.object({ email: v.string(), name: v.string() }))),
7
+ },
8
+ handler: async (_ctx, args) => {
9
+ return args.adminEmails || [];
19
10
  },
20
11
  });
21
12
 
@@ -29,10 +20,9 @@ export const getSupplierEmail = query({
29
20
  });
30
21
 
31
22
  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 };
23
+ args: { clinicId: v.string(), clinicEmail: v.optional(v.string()), clinicName: v.optional(v.string()) },
24
+ handler: async (_ctx, args) => {
25
+ if (!args.clinicEmail) return null;
26
+ return { email: args.clinicEmail, name: args.clinicName || 'Clinica' };
37
27
  },
38
28
  });
@@ -7,7 +7,7 @@ export const logEmail = mutation({
7
7
  to: v.union(v.string(), v.array(v.string())),
8
8
  subject: v.string(),
9
9
  html: v.string(),
10
- ticketId: v.optional(v.id('maintenance_tasks')),
10
+ ticketId: v.optional(v.string()),
11
11
  status: v.union(v.literal('sent'), v.literal('failed'), v.literal('pending')),
12
12
  resendId: v.optional(v.string()),
13
13
  errorMessage: v.optional(v.string()),
@@ -28,7 +28,7 @@ export const logEmail = mutation({
28
28
  });
29
29
 
30
30
  export const getEmailLogsByTicket = query({
31
- args: { ticketId: v.id('maintenance_tasks') },
31
+ args: { ticketId: v.string() },
32
32
  handler: async (ctx, args) => {
33
33
  return await ctx.db
34
34
  .query('email_logs')
@@ -54,7 +54,7 @@ http.route({
54
54
 
55
55
  try {
56
56
  const commentId = await ctx.runMutation(api.ticketComments.addCommentFromApi, {
57
- taskId: ticketId,
57
+ ticketId,
58
58
  content: content.trim(),
59
59
  authorEmail: authorEmail || undefined,
60
60
  authorName: authorName || undefined,
@@ -99,7 +99,7 @@ http.route({
99
99
  const ticketId = url.searchParams.get('ticketId');
100
100
  if (!ticketId) return errorResponse('Parametro "ticketId" obbligatorio', 400);
101
101
 
102
- const comments = await ctx.runQuery(internal.ticketComments.listCommentsInternal, { taskId: ticketId });
102
+ const comments = await ctx.runQuery(internal.ticketComments.listCommentsInternal, { ticketId });
103
103
  await ctx.runMutation(internal.apiKeys.recordApiKeyUsage, { keyId: keyData.keyId });
104
104
 
105
105
  return jsonResponse({ success: true, comments, count: comments.length });
@@ -110,111 +110,6 @@ http.route({
110
110
  }),
111
111
  });
112
112
 
113
- http.route({
114
- path: '/api/tickets/get',
115
- method: 'OPTIONS',
116
- handler: httpAction(async () => {
117
- return new Response(null, { status: 204, headers: corsHeaders });
118
- }),
119
- });
120
-
121
- http.route({
122
- path: '/api/tickets/get',
123
- method: 'GET',
124
- handler: httpAction(async (ctx, request) => {
125
- try {
126
- const apiKey = request.headers.get('X-API-Key');
127
- if (!apiKey) return errorResponse('API key mancante', 401);
128
-
129
- const keyData = await ctx.runQuery(api.apiKeys.validateApiKey, { plainKey: apiKey });
130
- if (!keyData) return errorResponse('API key non valida', 401);
131
- if (!keyData.permissions.includes('tickets:read')) {
132
- return errorResponse('API key non ha il permesso "tickets:read"', 403);
133
- }
134
-
135
- const url = new URL(request.url);
136
- const ticketId = url.searchParams.get('ticketId');
137
- if (!ticketId) return errorResponse('Parametro "ticketId" obbligatorio', 400);
138
-
139
- const ticket = await ctx.runQuery(internal.maintenanceTasks.getTicketInternal, { taskId: ticketId });
140
- if (!ticket) return errorResponse('Ticket non trovato', 404);
141
-
142
- await ctx.runMutation(internal.apiKeys.recordApiKeyUsage, { keyId: keyData.keyId });
143
- return jsonResponse({ success: true, ticket });
144
- } catch (error: any) {
145
- console.error('Error in /api/tickets/get:', error);
146
- return errorResponse('Errore interno: ' + error.message, 500);
147
- }
148
- }),
149
- });
150
-
151
- http.route({
152
- path: '/api/tickets/create',
153
- method: 'OPTIONS',
154
- handler: httpAction(async () => {
155
- return new Response(null, { status: 204, headers: corsHeaders });
156
- }),
157
- });
158
-
159
- http.route({
160
- path: '/api/tickets/create',
161
- method: 'POST',
162
- handler: httpAction(async (ctx, request) => {
163
- try {
164
- const apiKey = request.headers.get('X-API-Key');
165
- if (!apiKey) return errorResponse('API key mancante. Usa header X-API-Key', 401);
166
-
167
- const keyData = await ctx.runQuery(api.apiKeys.validateApiKey, { plainKey: apiKey });
168
- if (!keyData) return errorResponse('API key non valida o scaduta', 401);
169
- if (!keyData.permissions.includes('tickets:create')) {
170
- return errorResponse('API key non ha il permesso "tickets:create"', 403);
171
- }
172
-
173
- let body: any;
174
- try { body = await request.json(); } catch { return errorResponse('Body JSON non valido', 400); }
175
-
176
- const { clinicId, primoupId, clinicName, description, title, externalTicketId, externalTicketNumber, creatorEmail, creatorName, priority } = body;
177
-
178
- if (!clinicId && !primoupId && !clinicName) {
179
- return errorResponse('Devi fornire almeno uno tra: clinicId, primoupId, clinicName', 400);
180
- }
181
- if (!description || typeof description !== 'string' || description.trim().length === 0) {
182
- return errorResponse('Campo "description" obbligatorio e non vuoto', 400);
183
- }
184
-
185
- const clinic = await ctx.runQuery(internal.maintenanceTasks.findClinicInternal, {
186
- clinicId, primoupId: primoupId?.toString(), clinicName,
187
- });
188
- if (!clinic) return errorResponse('Clinica non trovata', 404);
189
-
190
- const validPriorities = ['low', 'medium', 'high'];
191
- if (priority && !validPriorities.includes(priority)) {
192
- return errorResponse(`Priorità non valida. Usa: ${validPriorities.join(', ')}`, 400);
193
- }
194
-
195
- try {
196
- const taskId = await ctx.runMutation(internal.maintenanceTasks.createExternalTicket, {
197
- clinicId: clinic._id as any,
198
- description: description.trim(),
199
- title: title || undefined,
200
- externalTicketId: externalTicketId || undefined,
201
- externalTicketNumber: externalTicketNumber || undefined,
202
- creatorEmail: creatorEmail || undefined,
203
- creatorName: creatorName || undefined,
204
- priority: priority || undefined,
205
- });
206
- await ctx.runMutation(internal.apiKeys.recordApiKeyUsage, { keyId: keyData.keyId });
207
- return jsonResponse({ success: true, ticketId: taskId, message: 'Segnalazione creata con successo.' });
208
- } catch (error: any) {
209
- return errorResponse('Errore nella creazione del ticket: ' + error.message, 500);
210
- }
211
- } catch (error: any) {
212
- console.error('Error in /api/tickets/create:', error);
213
- return errorResponse('Errore interno del server: ' + error.message, 500);
214
- }
215
- }),
216
- });
217
-
218
113
  http.route({
219
114
  path: '/api/health',
220
115
  method: 'GET',
@@ -223,7 +118,7 @@ http.route({
223
118
  success: true,
224
119
  status: 'healthy',
225
120
  timestamp: new Date().toISOString(),
226
- version: '1.0.0',
121
+ version: '2.0.0',
227
122
  });
228
123
  }),
229
124
  });
@@ -0,0 +1,96 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation } from './_generated/server';
3
+
4
+ export const listMaintenanceTasksForMigration = query({
5
+ args: {
6
+ cursor: v.optional(v.number()),
7
+ batchSize: v.optional(v.number()),
8
+ },
9
+ handler: async (ctx, args) => {
10
+ // This function reads from ticket_comments and ticket_history
11
+ // to reconstruct a list of known ticket IDs for migration purposes.
12
+ // Since maintenance_tasks table has been removed, this is a no-op placeholder.
13
+ // The actual migration should be run BEFORE removing the maintenance_tasks table,
14
+ // or data should be exported to a JSON file first.
15
+ return [];
16
+ },
17
+ });
18
+
19
+ export const updateCommentsTicketId = mutation({
20
+ args: {
21
+ oldTicketId: v.string(),
22
+ newTicketId: v.string(),
23
+ },
24
+ handler: async (ctx, args) => {
25
+ const comments = await ctx.db
26
+ .query('ticket_comments')
27
+ .withIndex('by_ticketId', (q) => q.eq('ticketId', args.oldTicketId))
28
+ .collect();
29
+
30
+ for (const comment of comments) {
31
+ await ctx.db.patch(comment._id, { ticketId: args.newTicketId });
32
+ }
33
+
34
+ return { updated: comments.length };
35
+ },
36
+ });
37
+
38
+ export const updateHistoryTicketId = mutation({
39
+ args: {
40
+ oldTicketId: v.string(),
41
+ newTicketId: v.string(),
42
+ },
43
+ handler: async (ctx, args) => {
44
+ const entries = await ctx.db
45
+ .query('ticket_history')
46
+ .withIndex('by_ticketId', (q) => q.eq('ticketId', args.oldTicketId))
47
+ .collect();
48
+
49
+ for (const entry of entries) {
50
+ await ctx.db.patch(entry._id, { ticketId: args.newTicketId });
51
+ }
52
+
53
+ return { updated: entries.length };
54
+ },
55
+ });
56
+
57
+ export const updateEmailLogsTicketId = mutation({
58
+ args: {
59
+ oldTicketId: v.string(),
60
+ newTicketId: v.string(),
61
+ },
62
+ handler: async (ctx, args) => {
63
+ const logs = await ctx.db
64
+ .query('email_logs')
65
+ .withIndex('by_ticketId', (q) => q.eq('ticketId', args.oldTicketId))
66
+ .collect();
67
+
68
+ for (const log of logs) {
69
+ await ctx.db.patch(log._id, { ticketId: args.newTicketId });
70
+ }
71
+
72
+ return { updated: logs.length };
73
+ },
74
+ });
75
+
76
+ export const updateRepairHistoryTicketId = mutation({
77
+ args: {
78
+ oldTicketId: v.string(),
79
+ newTicketId: v.string(),
80
+ },
81
+ handler: async (ctx, args) => {
82
+ const entries = await ctx.db
83
+ .query('device_repair_history')
84
+ .collect();
85
+
86
+ let updated = 0;
87
+ for (const entry of entries) {
88
+ if (entry.ticketId === args.oldTicketId) {
89
+ await ctx.db.patch(entry._id, { ticketId: args.newTicketId });
90
+ updated++;
91
+ }
92
+ }
93
+
94
+ return { updated };
95
+ },
96
+ });
@@ -80,17 +80,16 @@ export const updateRole = mutation({
80
80
  });
81
81
 
82
82
  export const deleteRole = mutation({
83
- args: { roleId: v.id('roles') },
83
+ args: {
84
+ roleId: v.id('roles'),
85
+ hasAssignedUsers: v.optional(v.boolean()),
86
+ },
84
87
  handler: async (ctx, args) => {
85
88
  const role = await ctx.db.get(args.roleId);
86
89
  if (role && (role.name === 'admin' || role.name === 'user' || role.name === 'supplier')) {
87
90
  throw new Error('Cannot delete system roles (admin, user, supplier)');
88
91
  }
89
- const usersWithRole = await ctx.db
90
- .query('user_profiles')
91
- .withIndex('by_roleId', (q) => q.eq('roleId', args.roleId))
92
- .first();
93
- if (usersWithRole) {
92
+ if (args.hasAssignedUsers) {
94
93
  throw new Error('Cannot delete role that is assigned to users');
95
94
  }
96
95
  await ctx.db.delete(args.roleId);