@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
@@ -20,30 +20,19 @@ export default defineSchema({
20
20
  createdAt: v.optional(v.number()),
21
21
  }).index('by_name', ['name']),
22
22
 
23
- // User profiles with roles
23
+ // Domain-specific user settings (base user data comes from parent app)
24
24
  user_profiles: defineTable({
25
25
  auth0Id: v.string(),
26
- roleId: v.optional(v.id('roles')), // Made optional for backward compatibility
27
- role: v.optional(v.string()), // Legacy field - will be migrated to roleId
28
- clinicId: v.optional(v.id('clinics')),
26
+ clinicId: v.optional(v.string()),
29
27
  supplierId: v.optional(v.id('suppliers')),
30
- email: v.string(),
31
- name: v.optional(v.string()),
32
- // PrimoUP user clinics data
33
- primoupClinics: v.optional(v.array(v.any())), // Array of clinic objects from PrimoUP
34
- selectedClinicId: v.optional(v.number()), // Currently selected PrimoUP clinic ID
35
- clinicChangeCounter: v.optional(v.number()), // Increments every time clinic changes
36
- // Impersonation (admin only)
37
- impersonatingUserId: v.optional(v.id('user_profiles')), // ID of user being impersonated
38
- // Account status
39
- isActive: v.optional(v.boolean()), // Default true, false = account disabilitato
28
+ primoupClinics: v.optional(v.array(v.any())),
29
+ selectedClinicId: v.optional(v.number()),
30
+ clinicChangeCounter: v.optional(v.number()),
31
+ impersonatingUserId: v.optional(v.string()),
40
32
  })
41
33
  .index('by_auth0Id', ['auth0Id'])
42
- .index('by_email', ['email'])
43
- .index('by_roleId', ['roleId'])
44
34
  .index('by_clinicId', ['clinicId'])
45
- .index('by_supplierId', ['supplierId'])
46
- .index('by_isActive', ['isActive']),
35
+ .index('by_supplierId', ['supplierId']),
47
36
 
48
37
  // PrimoUP authentication tokens
49
38
  primoup_tokens: defineTable({
@@ -72,19 +61,9 @@ export default defineSchema({
72
61
  .index('by_timestamp', ['timestamp'])
73
62
  .index('by_userId_timestamp', ['userId', 'timestamp']),
74
63
 
75
- // Clinics
76
- clinics: defineTable({
77
- name: v.string(),
78
- address: v.string(),
79
- contact_email: v.string(),
80
- contact_phone: v.string(),
81
- region: v.optional(v.string()), // Regione della clinica
82
- primoupId: v.optional(v.string()), // PrimoUP clinic ID for sync
83
- }).index('by_primoupId', ['primoupId']),
84
-
85
64
  // Devices
86
65
  devices: defineTable({
87
- clinicId: v.id('clinics'),
66
+ clinicId: v.string(),
88
67
  primoupClinicId: v.optional(v.number()), // PrimoUP clinic ID (for filtering)
89
68
  supplierId: v.optional(v.id('suppliers')), // Fornitore che fornisce assistenza
90
69
  name: v.string(),
@@ -145,8 +124,8 @@ export default defineSchema({
145
124
  // Storico riparazioni attrezzature
146
125
  device_repair_history: defineTable({
147
126
  deviceId: v.id('devices'),
148
- clinicId: v.id('clinics'),
149
- ticketId: v.optional(v.id('maintenance_tasks')),
127
+ clinicId: v.string(),
128
+ ticketId: v.optional(v.string()),
150
129
  sentToRepairAt: v.number(),
151
130
  returnedAt: v.optional(v.number()),
152
131
  statusAfterRepair: v.optional(v.union(
@@ -213,8 +192,8 @@ export default defineSchema({
213
192
  // Contracts
214
193
  contracts: defineTable({
215
194
  supplierId: v.id('suppliers'),
216
- clinicId: v.optional(v.id('clinics')), // Legacy: single clinic (deprecated)
217
- clinicIds: v.optional(v.array(v.id('clinics'))), // New: multiple clinics (empty = all clinics)
195
+ clinicId: v.optional(v.string()), // Legacy: single clinic (deprecated)
196
+ clinicIds: v.optional(v.array(v.string())), // Multiple clinics (empty = all clinics)
218
197
  start_date: v.number(),
219
198
  end_date: v.number(),
220
199
  amount: v.number(),
@@ -281,58 +260,21 @@ export default defineSchema({
281
260
  .index('by_applyToAll', ['applyToAll'])
282
261
  .index('by_parentFieldId', ['parentFieldId']),
283
262
 
284
- // Maintenance tasks (tickets)
285
- maintenance_tasks: defineTable({
286
- ticketNumber: v.optional(v.string()), // Numero ticket incrementale (es: TICK-00001)
287
- title: v.optional(v.string()), // Titolo del ticket
288
- deviceId: v.optional(v.id('devices')), // Opzionale per ticket esterni senza device specifico
289
- clinicId: v.id('clinics'),
290
- supplierId: v.optional(v.id('suppliers')),
291
- created_by: v.string(), // Auth0 user ID
292
- createdByEmail: v.optional(v.string()), // Email del creatore (per webhook)
293
- description: v.string(),
294
- photos: v.array(v.string()), // Legacy base64 - kept for backward compatibility
295
- photoStorageIds: v.optional(v.array(v.id('_storage'))), // New: Convex file storage
296
- status: v.string(), // References ticket_statuses.name
297
- priority: v.optional(v.union(
298
- v.literal('low'),
299
- v.literal('medium'),
300
- v.literal('high')
301
- )), // Priorità del ticket (usata per SLA)
302
- notes: v.optional(v.string()), // Additional notes from supplier/admin
303
- customFields: v.optional(v.any()), // Valori dei campi custom (oggetto dinamico)
263
+ // Ticket device data (links Abaddon ticket to component device/photos)
264
+ ticket_device_data: defineTable({
265
+ ticketId: v.string(), // Abaddon ticket _id (string reference)
266
+ deviceId: v.optional(v.id('devices')),
304
267
  deviceQuestionAnswers: v.optional(v.array(v.object({
305
268
  questionId: v.id('device_questions'),
306
- question: v.string(), // Testo della domanda (salvato per storico)
307
- answer: v.any(), // Risposta (può essere string, number, array, boolean)
308
- }))), // Risposte alle domande specifiche dell'attrezzatura
309
- slaDeadline: v.optional(v.number()), // Scadenza SLA calcolata automaticamente
310
- slaBreached: v.optional(v.boolean()), // True se SLA è stato violato
311
- slaWarningSent: v.optional(v.boolean()), // True se warning email è stata inviata
312
- // External system integration (Abaddon)
313
- externalTicketId: v.optional(v.string()), // ID del ticket nel sistema esterno
314
- externalTicketNumber: v.optional(v.number()), // Numero ticket nel sistema esterno
315
- // Import tracking
316
- importSource: v.optional(v.string()), // Es: "legacy_zammad", "external_api" per ticket importati
317
- legacyTicketId: v.optional(v.number()), // ID originale nel sistema legacy
318
- // External ticket flags
319
- isExternal: v.optional(v.boolean()), // True se ticket creato da API esterna (senza device)
320
- needsAssignment: v.optional(v.boolean()), // True se ticket da gestire (senza fornitore assegnato)
321
- created_at: v.number(),
322
- updated_at: v.number(),
323
- closed_at: v.optional(v.number()), // Data chiusura (quando status diventa completed/cancelled)
269
+ question: v.string(),
270
+ answer: v.any(),
271
+ }))),
272
+ photoStorageIds: v.optional(v.array(v.id('_storage'))),
273
+ isExternal: v.optional(v.boolean()),
274
+ needsAssignment: v.optional(v.boolean()),
324
275
  })
325
- .index('by_ticketNumber', ['ticketNumber'])
326
- .index('by_deviceId', ['deviceId'])
327
- .index('by_clinicId', ['clinicId'])
328
- .index('by_supplierId', ['supplierId'])
329
- .index('by_status', ['status'])
330
- .index('by_status_created_at', ['status', 'created_at'])
331
- .index('by_priority', ['priority'])
332
- .index('by_created_at', ['created_at'])
333
- .index('by_slaDeadline', ['slaDeadline'])
334
- .index('by_externalTicketId', ['externalTicketId'])
335
- .index('by_importSource', ['importSource']),
276
+ .index('by_ticketId', ['ticketId'])
277
+ .index('by_deviceId', ['deviceId']),
336
278
 
337
279
  // Dashboard stats cache - pre-computed every 10 min to avoid bytes read limit
338
280
  dashboard_stats_cache: defineTable({
@@ -457,7 +399,7 @@ export default defineSchema({
457
399
  assignSupplier: v.optional(v.id('suppliers')),
458
400
 
459
401
  // Assegna utente specifico (utente con ruolo supplier)
460
- assignUser: v.optional(v.id('user_profiles')),
402
+ assignUser: v.optional(v.string()),
461
403
 
462
404
  // Imposta SLA (ore)
463
405
  setSlaHours: v.optional(v.number()),
@@ -481,7 +423,7 @@ export default defineSchema({
481
423
  v.literal('specific_user'), // Utente specifico
482
424
  v.literal('specific_supplier') // Fornitore specifico
483
425
  ),
484
- userId: v.optional(v.id('user_profiles')), // Per specific_user
426
+ userId: v.optional(v.string()), // Per specific_user
485
427
  supplierId: v.optional(v.id('suppliers')), // Per specific_supplier
486
428
  }),
487
429
  // Vecchio formato (stringhe) - per retrocompatibilità temporanea
@@ -532,7 +474,7 @@ export default defineSchema({
532
474
  actions: v.object({
533
475
  changeStatus: v.optional(v.string()), // Cambia stato
534
476
  assignSupplier: v.optional(v.id('suppliers')), // Assegna azienda fornitrice
535
- assignUser: v.optional(v.id('user_profiles')), // Assegna utente specifico
477
+ assignUser: v.optional(v.string()), // Assegna utente specifico
536
478
  setSlaHours: v.optional(v.number()), // Imposta SLA
537
479
  applySlaRule: v.optional(v.boolean()), // Applica regola SLA automatica
538
480
  addNote: v.optional(v.string()), // Aggiungi nota automatica
@@ -547,7 +489,7 @@ export default defineSchema({
547
489
  v.literal('specific_user'),
548
490
  v.literal('specific_supplier')
549
491
  ),
550
- userId: v.optional(v.id('user_profiles')),
492
+ userId: v.optional(v.string()),
551
493
  supplierId: v.optional(v.id('suppliers')),
552
494
  }),
553
495
  v.literal('admin'),
@@ -561,7 +503,7 @@ export default defineSchema({
561
503
 
562
504
  createdAt: v.number(),
563
505
  updatedAt: v.number(),
564
- createdBy: v.id('user_profiles'), // Chi ha creato la macro
506
+ createdBy: v.string(), // Chi ha creato la macro
565
507
  })
566
508
  .index('by_createdBy', ['createdBy'])
567
509
  .index('by_isGlobal', ['isGlobal'])
@@ -700,7 +642,7 @@ export default defineSchema({
700
642
 
701
643
  // Ticket comments (Commenti sulle segnalazioni)
702
644
  ticket_comments: defineTable({
703
- taskId: v.id('maintenance_tasks'), // Segnalazione a cui appartiene
645
+ ticketId: v.string(), // Abaddon ticket _id (string reference)
704
646
  authorId: v.string(), // Auth0 user ID dell'autore
705
647
  authorName: v.optional(v.string()), // Nome dell'autore (cached)
706
648
  authorEmail: v.optional(v.string()), // Email dell'autore (cached)
@@ -710,16 +652,16 @@ export default defineSchema({
710
652
  isFromApi: v.optional(v.boolean()), // Se true, commento ricevuto via API esterna
711
653
  attachments: v.optional(v.array(v.id('_storage'))), // Allegati opzionali
712
654
  mentions: v.optional(v.array(v.object({
713
- userId: v.id('user_profiles'),
655
+ userId: v.string(),
714
656
  name: v.string(),
715
657
  email: v.string(),
716
658
  }))), // Utenti taggati nel commento
717
659
  createdAt: v.number(),
718
660
  updatedAt: v.optional(v.number()),
719
661
  })
720
- .index('by_taskId', ['taskId'])
662
+ .index('by_ticketId', ['ticketId'])
721
663
  .index('by_authorId', ['authorId'])
722
- .index('by_taskId_createdAt', ['taskId', 'createdAt']),
664
+ .index('by_ticketId_createdAt', ['ticketId', 'createdAt']),
723
665
 
724
666
  // Spare part orders (Ordini ricambi)
725
667
  spare_part_orders: defineTable({
@@ -786,7 +728,7 @@ export default defineSchema({
786
728
  to: v.union(v.string(), v.array(v.string())),
787
729
  subject: v.string(),
788
730
  html: v.string(),
789
- ticketId: v.optional(v.id('maintenance_tasks')),
731
+ ticketId: v.optional(v.string()),
790
732
  status: v.union(
791
733
  v.literal('sent'),
792
734
  v.literal('failed'),
@@ -802,7 +744,7 @@ export default defineSchema({
802
744
 
803
745
  // Ticket History (Storico eventi ticket)
804
746
  ticket_history: defineTable({
805
- ticketId: v.id('maintenance_tasks'),
747
+ ticketId: v.string(), // Abaddon ticket _id (string reference)
806
748
  eventType: v.union(
807
749
  v.literal('created'), // Ticket creato
808
750
  v.literal('status_change'), // Cambio stato
@@ -1,125 +1,70 @@
1
1
  import { v } from 'convex/values';
2
- import { internalMutation, internalAction, internalQuery } from './_generated/server';
3
- import { internal } from './_generated/api';
2
+ import { mutation, query } from './_generated/server';
4
3
 
5
- // NOTE: Notification sending (sendSLABreachNotifications, sendSLAWarningNotifications)
6
- // is NOT included in the component. The parent app is responsible for:
7
- // 1. Running checkSLAStatus periodically (via cron)
8
- // 2. After breaches/warnings are marked, querying for newly marked tickets
9
- // 3. Sending email notifications via its own emailActions
10
-
11
- export const checkSLAStatus = internalAction({
12
- args: {},
13
- handler: async (ctx): Promise<{ checked: number; warnings: number; breaches: number }> => {
14
- try {
15
- const tickets: any = await ctx.runQuery(internal.slaMonitoring.listTicketsNeedingSLACheck, {});
16
-
17
- const now = Date.now();
18
- let warningsCount = 0;
19
- let breachesCount = 0;
20
-
21
- for (const ticket of tickets) {
22
- const timeUntilDeadline = ticket.slaDeadline - now;
23
- const oneHour = 60 * 60 * 1000;
24
-
25
- if (timeUntilDeadline < 0 && !ticket.slaBreached) {
26
- breachesCount++;
27
-
28
- await ctx.runMutation(internal.slaMonitoring.markSLABreached, {
29
- ticketId: ticket._id,
30
- });
31
- }
32
- else if (timeUntilDeadline > 0 && timeUntilDeadline < oneHour && !ticket.slaWarningSent) {
33
- warningsCount++;
34
-
35
- await ctx.runMutation(internal.slaMonitoring.markSLAWarningSent, {
36
- ticketId: ticket._id,
37
- });
38
- }
39
- }
40
-
41
- return {
42
- checked: tickets.length,
43
- warnings: warningsCount,
44
- breaches: breachesCount,
45
- };
46
- } catch (error) {
47
- console.error('Error checking SLA status:', error);
48
- throw error;
49
- }
4
+ export const evaluateSLA = query({
5
+ args: {
6
+ tickets: v.array(v.object({
7
+ ticketId: v.string(),
8
+ status: v.string(),
9
+ slaDeadline: v.optional(v.number()),
10
+ slaBreached: v.optional(v.boolean()),
11
+ slaWarningSent: v.optional(v.boolean()),
12
+ })),
50
13
  },
51
- });
52
-
53
- export const listTicketsNeedingSLACheck = internalQuery({
54
- args: {},
55
- handler: async (ctx) => {
14
+ handler: async (ctx, args) => {
15
+ const now = Date.now();
16
+ const oneHour = 60 * 60 * 1000;
56
17
  const terminalStatuses = ['closed', 'completed', 'cancelled', 'resolved'];
57
18
 
58
- const ticketsWithSLA = await ctx.db
59
- .query('maintenance_tasks')
60
- .withIndex('by_slaDeadline')
61
- .order('asc')
62
- .take(150);
19
+ const warnings: string[] = [];
20
+ const breaches: string[] = [];
63
21
 
64
- return ticketsWithSLA
65
- .filter(t => t.slaDeadline && !terminalStatuses.includes(t.status))
66
- .map(t => ({
67
- _id: t._id,
68
- ticketNumber: t.ticketNumber,
69
- status: t.status,
70
- slaDeadline: t.slaDeadline,
71
- slaBreached: t.slaBreached,
72
- slaWarningSent: t.slaWarningSent,
73
- }));
74
- },
75
- });
22
+ for (const ticket of args.tickets) {
23
+ if (!ticket.slaDeadline || terminalStatuses.includes(ticket.status)) continue;
76
24
 
77
- export const listAllTicketsForSLA = internalQuery({
78
- args: {},
79
- handler: async (ctx) => {
80
- const terminalStatuses = ['closed', 'completed', 'cancelled', 'resolved'];
25
+ const timeUntilDeadline = ticket.slaDeadline - now;
81
26
 
82
- const tasks = await ctx.db
83
- .query('maintenance_tasks')
84
- .withIndex('by_slaDeadline')
85
- .order('asc')
86
- .take(500);
27
+ if (timeUntilDeadline < 0 && !ticket.slaBreached) {
28
+ breaches.push(ticket.ticketId);
29
+ } else if (timeUntilDeadline > 0 && timeUntilDeadline < oneHour && !ticket.slaWarningSent) {
30
+ warnings.push(ticket.ticketId);
31
+ }
32
+ }
87
33
 
88
- return tasks
89
- .filter(t => t.slaDeadline && !terminalStatuses.includes(t.status))
90
- .map(t => ({
91
- _id: t._id,
92
- ticketNumber: t.ticketNumber,
93
- status: t.status,
94
- supplierId: t.supplierId,
95
- slaDeadline: t.slaDeadline,
96
- slaBreached: t.slaBreached,
97
- slaWarningSent: t.slaWarningSent,
98
- deviceName: '',
99
- clinicName: '',
100
- supplierName: null,
101
- }));
34
+ return { warnings, breaches };
102
35
  },
103
36
  });
104
37
 
105
- export const markSLABreached = internalMutation({
38
+ export const computeSLADeadline = query({
106
39
  args: {
107
- ticketId: v.id('maintenance_tasks'),
40
+ supplierId: v.optional(v.id('suppliers')),
41
+ priority: v.optional(v.string()),
42
+ deviceCategory: v.optional(v.string()),
108
43
  },
109
44
  handler: async (ctx, args) => {
110
- await ctx.db.patch(args.ticketId, {
111
- slaBreached: true,
112
- });
113
- },
114
- });
45
+ if (!args.supplierId || !args.priority) return null;
46
+
47
+ const slaRule = await ctx.db
48
+ .query('sla_rules')
49
+ .withIndex('by_supplierId_category_priority', (q: any) =>
50
+ q.eq('supplierId', args.supplierId)
51
+ .eq('deviceCategory', args.deviceCategory || undefined)
52
+ .eq('priority', args.priority)
53
+ )
54
+ .first();
55
+
56
+ if (!slaRule) {
57
+ const genericRule = await ctx.db
58
+ .query('sla_rules')
59
+ .withIndex('by_supplierId_priority', (q: any) =>
60
+ q.eq('supplierId', args.supplierId).eq('priority', args.priority)
61
+ )
62
+ .first();
63
+
64
+ if (!genericRule) return null;
65
+ return Date.now() + (genericRule.resolutionTimeHours * 60 * 60 * 1000);
66
+ }
115
67
 
116
- export const markSLAWarningSent = internalMutation({
117
- args: {
118
- ticketId: v.id('maintenance_tasks'),
119
- },
120
- handler: async (ctx, args) => {
121
- await ctx.db.patch(args.ticketId, {
122
- slaWarningSent: true,
123
- });
68
+ return Date.now() + (slaRule.resolutionTimeHours * 60 * 60 * 1000);
124
69
  },
125
70
  });
@@ -69,18 +69,49 @@ export const importSparePartsBatch = mutation({
69
69
 
70
70
  export const listSpareParts = query({
71
71
  args: {
72
- limit: v.optional(v.number()),
72
+ page: v.optional(v.number()),
73
+ pageSize: v.optional(v.number()),
74
+ search: v.optional(v.string()),
75
+ clinicId: v.optional(v.number()),
76
+ userRole: v.optional(v.string()),
77
+ userSelectedClinicId: v.optional(v.number()),
73
78
  _triggerReload: v.optional(v.number()),
74
79
  },
75
80
  handler: async (ctx, args) => {
76
- const limit = args.limit || 1000;
81
+ const page = args.page || 1;
82
+ const pageSize = args.pageSize || 40;
77
83
 
78
- const spareParts = await ctx.db
79
- .query('spare_parts')
80
- .order('desc')
81
- .take(limit);
84
+ let allParts: any[];
85
+ const effectiveClinicId = args.clinicId || args.userSelectedClinicId;
82
86
 
83
- return spareParts;
87
+ if (effectiveClinicId) {
88
+ allParts = await ctx.db
89
+ .query('spare_parts')
90
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', effectiveClinicId))
91
+ .collect();
92
+ } else {
93
+ allParts = await ctx.db
94
+ .query('spare_parts')
95
+ .order('desc')
96
+ .collect();
97
+ }
98
+
99
+ if (args.search) {
100
+ const s = args.search.toLowerCase();
101
+ allParts = allParts.filter((p: any) =>
102
+ p.name?.toLowerCase().includes(s) ||
103
+ p.code?.toLowerCase().includes(s) ||
104
+ p.articleName?.toLowerCase().includes(s) ||
105
+ p.brand?.toLowerCase().includes(s)
106
+ );
107
+ }
108
+
109
+ const totalCount = allParts.length;
110
+ const totalPages = Math.ceil(totalCount / pageSize);
111
+ const startIndex = (page - 1) * pageSize;
112
+ const pageParts = allParts.slice(startIndex, startIndex + pageSize);
113
+
114
+ return { page: pageParts, totalCount, totalPages, currentPage: page };
84
115
  },
85
116
  });
86
117
 
@@ -94,7 +125,7 @@ export const listSparePartsByClinic = query({
94
125
 
95
126
  const spareParts = await ctx.db
96
127
  .query('spare_parts')
97
- .withIndex('by_clinicId', (q) => q.eq('clinicId', args.clinicId))
128
+ .withIndex('by_clinicId', (q: any) => q.eq('clinicId', args.clinicId))
98
129
  .take(limit);
99
130
 
100
131
  return spareParts;
@@ -168,9 +199,12 @@ export const listSparePartsPaginated = query({
168
199
  filteredQuery = filteredQuery.filter((q: any) => q.eq(q.field('articleCategoryId'), args.categoryId));
169
200
  }
170
201
 
171
- const result = await filteredQuery.paginate(args.paginationOpts);
202
+ const limit = args.paginationOpts.numItems + 1;
203
+ const allResults = await filteredQuery.take(limit);
204
+ const isDone = allResults.length < limit;
205
+ const page = isDone ? allResults : allResults.slice(0, -1);
172
206
 
173
- let filteredPage = result.page;
207
+ let filteredPage = page;
174
208
 
175
209
  if (args.searchTerm) {
176
210
  const search = args.searchTerm.toLowerCase();
@@ -199,8 +233,8 @@ export const listSparePartsPaginated = query({
199
233
 
200
234
  return {
201
235
  page: minimalPage,
202
- isDone: result.isDone,
203
- continueCursor: result.continueCursor,
236
+ isDone,
237
+ continueCursor: '',
204
238
  };
205
239
  },
206
240
  });