@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,724 @@
1
+ import { defineSchema, defineTable } from 'convex/server';
2
+ import { v } from 'convex/values';
3
+ export default defineSchema({
4
+ // Roles table
5
+ roles: defineTable({
6
+ name: v.string(),
7
+ description: v.string(),
8
+ policies: v.optional(v.object({
9
+ canViewAllClinics: v.optional(v.boolean()),
10
+ canEditClinics: v.optional(v.boolean()),
11
+ canViewAllDevices: v.optional(v.boolean()),
12
+ canEditDevices: v.optional(v.boolean()),
13
+ canViewAllTickets: v.optional(v.boolean()),
14
+ canEditTickets: v.optional(v.boolean()),
15
+ canManageUsers: v.optional(v.boolean()),
16
+ canManageRoles: v.optional(v.boolean()),
17
+ canViewReports: v.optional(v.boolean()),
18
+ })),
19
+ createdAt: v.optional(v.number()),
20
+ }).index('by_name', ['name']),
21
+ // User profiles with roles
22
+ user_profiles: defineTable({
23
+ auth0Id: v.string(),
24
+ roleId: v.optional(v.id('roles')), // Made optional for backward compatibility
25
+ role: v.optional(v.string()), // Legacy field - will be migrated to roleId
26
+ clinicId: v.optional(v.id('clinics')),
27
+ supplierId: v.optional(v.id('suppliers')),
28
+ email: v.string(),
29
+ name: v.optional(v.string()),
30
+ // PrimoUP user clinics data
31
+ primoupClinics: v.optional(v.array(v.any())), // Array of clinic objects from PrimoUP
32
+ selectedClinicId: v.optional(v.number()), // Currently selected PrimoUP clinic ID
33
+ clinicChangeCounter: v.optional(v.number()), // Increments every time clinic changes
34
+ // Impersonation (admin only)
35
+ impersonatingUserId: v.optional(v.id('user_profiles')), // ID of user being impersonated
36
+ // Account status
37
+ isActive: v.optional(v.boolean()), // Default true, false = account disabilitato
38
+ })
39
+ .index('by_auth0Id', ['auth0Id'])
40
+ .index('by_email', ['email'])
41
+ .index('by_roleId', ['roleId'])
42
+ .index('by_clinicId', ['clinicId'])
43
+ .index('by_supplierId', ['supplierId'])
44
+ .index('by_isActive', ['isActive']),
45
+ // PrimoUP authentication tokens
46
+ primoup_tokens: defineTable({
47
+ userId: v.string(), // Auth0 user ID
48
+ token: v.string(),
49
+ refreshToken: v.optional(v.string()),
50
+ expiresAt: v.optional(v.number()),
51
+ createdAt: v.number(),
52
+ lastUsedAt: v.optional(v.number()),
53
+ isActive: v.boolean(), // Only one active token per user
54
+ })
55
+ .index('by_userId', ['userId'])
56
+ .index('by_userId_active', ['userId', 'isActive']),
57
+ // PrimoUP access logs
58
+ primoup_access_logs: defineTable({
59
+ userId: v.string(), // Auth0 user ID
60
+ action: v.string(), // 'login', 'refresh', 'api_call', etc.
61
+ endpoint: v.optional(v.string()), // API endpoint called
62
+ success: v.boolean(),
63
+ errorMessage: v.optional(v.string()),
64
+ timestamp: v.number(),
65
+ metadata: v.optional(v.any()), // Additional info
66
+ })
67
+ .index('by_userId', ['userId'])
68
+ .index('by_timestamp', ['timestamp'])
69
+ .index('by_userId_timestamp', ['userId', 'timestamp']),
70
+ // Clinics
71
+ clinics: defineTable({
72
+ name: v.string(),
73
+ address: v.string(),
74
+ contact_email: v.string(),
75
+ contact_phone: v.string(),
76
+ region: v.optional(v.string()), // Regione della clinica
77
+ primoupId: v.optional(v.string()), // PrimoUP clinic ID for sync
78
+ }).index('by_primoupId', ['primoupId']),
79
+ // Devices
80
+ devices: defineTable({
81
+ clinicId: v.id('clinics'),
82
+ primoupClinicId: v.optional(v.number()), // PrimoUP clinic ID (for filtering)
83
+ supplierId: v.optional(v.id('suppliers')), // Fornitore che fornisce assistenza
84
+ name: v.string(),
85
+ category: v.string(),
86
+ brand: v.string(),
87
+ model: v.string(),
88
+ serial_number: v.string(),
89
+ photo: v.optional(v.string()), // Legacy base64 - will be migrated
90
+ photoStorageId: v.optional(v.id('_storage')), // New: Convex file storage ID
91
+ status: v.union(v.literal('active'), v.literal('in_maintenance'), v.literal('out_of_service')),
92
+ metadata: v.optional(v.any()),
93
+ // Nuovi campi per enhancement
94
+ // Identificazione
95
+ internalId: v.optional(v.string()), // ID interno azienda (warehouse_article id)
96
+ articleId: v.optional(v.string()), // Article ID from PrimoUP (article id)
97
+ primoupCategoryId: v.optional(v.string()), // PrimoUP category ID (article_category_id)
98
+ industry40Data: v.optional(v.string()), // Dati Industry 4.0
99
+ quantity: v.optional(v.number()), // Quantità disponibile in questa clinica
100
+ // Acquisto
101
+ purchaseDate: v.optional(v.number()), // Data acquisto (timestamp)
102
+ vendor: v.optional(v.string()), // Venditore
103
+ purchaseCost: v.optional(v.number()), // Costo acquisto
104
+ // Garanzia
105
+ warrantyEndDate: v.optional(v.number()), // Fine garanzia (timestamp)
106
+ // Ricambi
107
+ spareParts: v.optional(v.array(v.object({
108
+ name: v.string(),
109
+ code: v.string(),
110
+ photoStorageId: v.optional(v.id('_storage')),
111
+ quantity: v.number(),
112
+ notes: v.optional(v.string()),
113
+ }))),
114
+ // Allegati
115
+ maintenanceManualStorageId: v.optional(v.id('_storage')), // Manuale manutenzione PDF
116
+ maintenanceReportsStorageIds: v.optional(v.array(v.id('_storage'))), // Verbali manutenzione programmata
117
+ conformityDeclarationStorageId: v.optional(v.id('_storage')), // Dichiarazione conformità
118
+ authorizationsStorageIds: v.optional(v.array(v.id('_storage'))), // Autorizzazioni
119
+ otherAttachmentsStorageIds: v.optional(v.array(v.id('_storage'))), // Altri allegati
120
+ })
121
+ .index('by_clinicId', ['clinicId'])
122
+ .index('by_primoupClinicId', ['primoupClinicId'])
123
+ .index('by_supplierId', ['supplierId'])
124
+ .index('by_category', ['category'])
125
+ .index('by_status', ['status'])
126
+ .index('by_internalId', ['internalId'])
127
+ .index('by_articleId', ['articleId'])
128
+ .index('by_primoupCategoryId', ['primoupCategoryId']),
129
+ // Storico riparazioni attrezzature
130
+ device_repair_history: defineTable({
131
+ deviceId: v.id('devices'),
132
+ clinicId: v.id('clinics'),
133
+ ticketId: v.optional(v.id('maintenance_tasks')),
134
+ sentToRepairAt: v.number(),
135
+ returnedAt: v.optional(v.number()),
136
+ statusAfterRepair: v.optional(v.union(v.literal('active'), v.literal('out_of_service'))),
137
+ notes: v.optional(v.string()),
138
+ createdBy: v.string(),
139
+ returnedBy: v.optional(v.string()),
140
+ })
141
+ .index('by_deviceId', ['deviceId'])
142
+ .index('by_clinicId', ['clinicId'])
143
+ .index('by_deviceId_sentAt', ['deviceId', 'sentToRepairAt']),
144
+ // Supplier Categories
145
+ supplier_categories: defineTable({
146
+ name: v.string(),
147
+ description: v.optional(v.string()),
148
+ createdAt: v.number(),
149
+ }).index('by_name', ['name']),
150
+ // SLA Rules
151
+ sla_rules: defineTable({
152
+ name: v.optional(v.string()), // Nome SLA (opzionale per retrocompatibilità)
153
+ supplierId: v.id('suppliers'),
154
+ deviceCategory: v.optional(v.string()), // Categoria attrezzatura (opzionale = SLA generico)
155
+ priority: v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
156
+ responseTimeHours: v.number(), // Tempo di risposta in ore
157
+ resolutionTimeHours: v.number(), // Tempo di risoluzione in ore
158
+ description: v.optional(v.string()),
159
+ createdAt: v.number(),
160
+ updatedAt: v.number(),
161
+ })
162
+ .index('by_supplierId', ['supplierId'])
163
+ .index('by_supplierId_priority', ['supplierId', 'priority'])
164
+ .index('by_supplierId_category_priority', ['supplierId', 'deviceCategory', 'priority']),
165
+ // Suppliers
166
+ suppliers: defineTable({
167
+ name: v.string(),
168
+ contact_email: v.string(),
169
+ contact_phone: v.string(),
170
+ categories: v.array(v.string()),
171
+ sla_days: v.number(),
172
+ notes: v.optional(v.string()),
173
+ primoupId: v.optional(v.string()), // PrimoUP vendor ID for sync
174
+ type: v.optional(v.string()), // warehouse, clinic, lab
175
+ }).index('by_primoupId', ['primoupId']),
176
+ // Vendor Articles (pivot table: which vendor sells which article/device)
177
+ vendor_articles: defineTable({
178
+ vendorId: v.id('suppliers'), // Convex supplier ID
179
+ deviceId: v.id('devices'), // Convex device ID (article)
180
+ primoupVendorId: v.optional(v.string()), // PrimoUP vendor ID
181
+ primoupArticleId: v.optional(v.string()), // PrimoUP article ID
182
+ metadata: v.optional(v.any()),
183
+ })
184
+ .index('by_vendorId', ['vendorId'])
185
+ .index('by_deviceId', ['deviceId'])
186
+ .index('by_vendorId_deviceId', ['vendorId', 'deviceId'])
187
+ .index('by_primoupVendorId', ['primoupVendorId'])
188
+ .index('by_primoupArticleId', ['primoupArticleId']),
189
+ // Contracts
190
+ contracts: defineTable({
191
+ supplierId: v.id('suppliers'),
192
+ clinicId: v.optional(v.id('clinics')), // Legacy: single clinic (deprecated)
193
+ clinicIds: v.optional(v.array(v.id('clinics'))), // New: multiple clinics (empty = all clinics)
194
+ start_date: v.number(),
195
+ end_date: v.number(),
196
+ amount: v.number(),
197
+ type: v.union(v.literal('full'), v.literal('on_demand'), v.literal('mixed')),
198
+ description: v.optional(v.string()), // Description of the contract
199
+ pdfStorageId: v.optional(v.id('_storage')), // PDF file stored in Convex
200
+ pdf_url: v.optional(v.string()), // Legacy: external URL
201
+ related_devices: v.array(v.id('devices')),
202
+ })
203
+ .index('by_supplierId', ['supplierId'])
204
+ .index('by_clinicId', ['clinicId']),
205
+ // Ticket statuses (configurable)
206
+ ticket_statuses: defineTable({
207
+ name: v.string(), // e.g., 'open', 'assigned_to_supplier', 'in_progress', 'completed', 'cancelled'
208
+ slug: v.optional(v.string()), // Slug per API esterna (Abaddon): 'open', 'in_progress', 'waiting_customer', 'closed', etc.
209
+ label: v.string(), // e.g., 'Aperto', 'Assegnato', 'In Corso', 'Completato', 'Annullato'
210
+ color: v.string(), // e.g., 'warning', 'info', 'primary', 'success', 'danger'
211
+ order: v.number(), // Order for display
212
+ isActive: v.boolean(), // Can be disabled without deleting
213
+ createdAt: v.number(),
214
+ })
215
+ .index('by_name', ['name'])
216
+ .index('by_slug', ['slug'])
217
+ .index('by_order', ['order'])
218
+ .index('by_isActive', ['isActive']),
219
+ // Custom fields configuration (admin-defined fields for tickets)
220
+ ticket_custom_fields: defineTable({
221
+ name: v.string(), // Nome del campo (es. "Priorità", "Tipo di guasto")
222
+ fieldType: v.union(v.literal('text'), v.literal('textarea'), v.literal('number'), v.literal('select'), v.literal('multiselect'), v.literal('date'), v.literal('checkbox'), v.literal('radio')),
223
+ options: v.optional(v.array(v.string())), // Per select/multiselect/radio
224
+ isRequired: v.boolean(),
225
+ placeholder: v.optional(v.string()),
226
+ helpText: v.optional(v.string()),
227
+ order: v.number(), // Ordine di visualizzazione nel form
228
+ isActive: v.boolean(), // Può essere disabilitato senza cancellare
229
+ // Filtri di applicabilità
230
+ applyToAll: v.boolean(), // Se true, si applica a tutte le attrezzature
231
+ categories: v.optional(v.array(v.string())), // Categorie di attrezzature (es. ["Riuniti", "Autoclavi"])
232
+ deviceIds: v.optional(v.array(v.id('devices'))), // Specifiche attrezzature
233
+ // Campi dipendenti/concatenati
234
+ parentFieldId: v.optional(v.id('ticket_custom_fields')), // Campo padre (se è un campo figlio)
235
+ parentFieldValue: v.optional(v.string()), // Valore del campo padre che attiva questo campo
236
+ // Struttura alternativa: mappa di campi figli per ogni opzione
237
+ childFields: v.optional(v.array(v.object({
238
+ parentValue: v.string(), // Valore dell'opzione che attiva i figli
239
+ childFieldIds: v.array(v.id('ticket_custom_fields')), // Array di campi figli
240
+ }))),
241
+ createdAt: v.number(),
242
+ updatedAt: v.number(),
243
+ })
244
+ .index('by_order', ['order'])
245
+ .index('by_isActive', ['isActive'])
246
+ .index('by_applyToAll', ['applyToAll'])
247
+ .index('by_parentFieldId', ['parentFieldId']),
248
+ // Maintenance tasks (tickets)
249
+ maintenance_tasks: defineTable({
250
+ ticketNumber: v.optional(v.string()), // Numero ticket incrementale (es: TICK-00001)
251
+ title: v.optional(v.string()), // Titolo del ticket
252
+ deviceId: v.optional(v.id('devices')), // Opzionale per ticket esterni senza device specifico
253
+ clinicId: v.id('clinics'),
254
+ supplierId: v.optional(v.id('suppliers')),
255
+ created_by: v.string(), // Auth0 user ID
256
+ createdByEmail: v.optional(v.string()), // Email del creatore (per webhook)
257
+ description: v.string(),
258
+ photos: v.array(v.string()), // Legacy base64 - kept for backward compatibility
259
+ photoStorageIds: v.optional(v.array(v.id('_storage'))), // New: Convex file storage
260
+ status: v.string(), // References ticket_statuses.name
261
+ priority: v.optional(v.union(v.literal('low'), v.literal('medium'), v.literal('high'))), // Priorità del ticket (usata per SLA)
262
+ notes: v.optional(v.string()), // Additional notes from supplier/admin
263
+ customFields: v.optional(v.any()), // Valori dei campi custom (oggetto dinamico)
264
+ deviceQuestionAnswers: v.optional(v.array(v.object({
265
+ questionId: v.id('device_questions'),
266
+ question: v.string(), // Testo della domanda (salvato per storico)
267
+ answer: v.any(), // Risposta (può essere string, number, array, boolean)
268
+ }))), // Risposte alle domande specifiche dell'attrezzatura
269
+ slaDeadline: v.optional(v.number()), // Scadenza SLA calcolata automaticamente
270
+ slaBreached: v.optional(v.boolean()), // True se SLA è stato violato
271
+ slaWarningSent: v.optional(v.boolean()), // True se warning email è stata inviata
272
+ // External system integration (Abaddon)
273
+ externalTicketId: v.optional(v.string()), // ID del ticket nel sistema esterno
274
+ externalTicketNumber: v.optional(v.number()), // Numero ticket nel sistema esterno
275
+ // Import tracking
276
+ importSource: v.optional(v.string()), // Es: "legacy_zammad", "external_api" per ticket importati
277
+ legacyTicketId: v.optional(v.number()), // ID originale nel sistema legacy
278
+ // External ticket flags
279
+ isExternal: v.optional(v.boolean()), // True se ticket creato da API esterna (senza device)
280
+ needsAssignment: v.optional(v.boolean()), // True se ticket da gestire (senza fornitore assegnato)
281
+ created_at: v.number(),
282
+ updated_at: v.number(),
283
+ closed_at: v.optional(v.number()), // Data chiusura (quando status diventa completed/cancelled)
284
+ })
285
+ .index('by_ticketNumber', ['ticketNumber'])
286
+ .index('by_deviceId', ['deviceId'])
287
+ .index('by_clinicId', ['clinicId'])
288
+ .index('by_supplierId', ['supplierId'])
289
+ .index('by_status', ['status'])
290
+ .index('by_status_created_at', ['status', 'created_at'])
291
+ .index('by_priority', ['priority'])
292
+ .index('by_created_at', ['created_at'])
293
+ .index('by_slaDeadline', ['slaDeadline'])
294
+ .index('by_externalTicketId', ['externalTicketId'])
295
+ .index('by_importSource', ['importSource']),
296
+ // Dashboard stats cache - pre-computed every 10 min to avoid bytes read limit
297
+ dashboard_stats_cache: defineTable({
298
+ lastUpdated: v.number(),
299
+ totalClinics: v.number(),
300
+ totalSuppliers: v.number(),
301
+ totalDevices: v.number(),
302
+ deviceCountByStatus: v.any(), // { active: N, in_maintenance: N, out_of_service: N }
303
+ ticketStats: v.any(), // { open: N, closed: N, ... }
304
+ totalTickets: v.number(),
305
+ }),
306
+ // Push subscriptions for notifications
307
+ push_subscriptions: defineTable({
308
+ userId: v.string(), // Auth0 user ID
309
+ subscription: v.any(), // PushSubscription object
310
+ }).index('by_userId', ['userId']),
311
+ // Ticket automation triggers
312
+ ticket_triggers: defineTable({
313
+ name: v.string(), // Nome del trigger (es. "Auto-assegna Riuniti urgenti")
314
+ description: v.optional(v.string()),
315
+ isActive: v.boolean(),
316
+ priority: v.number(), // Ordine di esecuzione (più basso = più alta priorità)
317
+ // CONDIZIONI (quando si attiva il trigger)
318
+ conditions: v.object({
319
+ // Logica di combinazione condizioni (AND = tutte devono essere vere, OR = almeno una deve essere vera)
320
+ // DEPRECATO: usare conditionsList per operatori individuali
321
+ conditionLogic: v.optional(v.union(v.literal('AND'), v.literal('OR'))), // Default: AND
322
+ // NUOVO: Lista condizioni con operatori individuali tra ogni coppia
323
+ // Se presente, ha priorità sui campi legacy
324
+ conditionsList: v.optional(v.array(v.object({
325
+ type: v.union(v.literal('trigger_event'), // Evento trigger (create, status_change, etc.)
326
+ v.literal('status'), // Stato ticket
327
+ v.literal('category'), // Categoria attrezzatura
328
+ v.literal('brand'), // Marca attrezzatura
329
+ v.literal('model'), // Modello attrezzatura
330
+ v.literal('region'), // Regione clinica
331
+ v.literal('priority'), // Priorità ticket
332
+ v.literal('custom_field') // Campo custom
333
+ ),
334
+ negated: v.boolean(), // NON (nega la condizione)
335
+ value: v.union(v.string(), v.array(v.string())), // Valore o valori
336
+ nextOperator: v.union(v.literal('AND'), v.literal('OR')), // Operatore verso la prossima condizione
337
+ // Per custom_field
338
+ customFieldId: v.optional(v.id('ticket_custom_fields')),
339
+ customFieldOperator: v.optional(v.union(v.literal('equals'), v.literal('not_equals'), v.literal('contains'), v.literal('greater_than'), v.literal('less_than'), v.literal('is_empty'), v.literal('is_not_empty'))),
340
+ }))),
341
+ // Filtri per categorie attrezzature (LEGACY)
342
+ categories: v.optional(v.array(v.string())), // Es. ["Riuniti", "Autoclavi"]
343
+ applyToAllCategories: v.boolean(), // Se true, ignora categories
344
+ // Filtri per regioni cliniche (LEGACY)
345
+ regions: v.optional(v.array(v.string())), // Es. ["Lombardia", "Lazio"]
346
+ applyToAllRegions: v.optional(v.boolean()), // Se true, ignora regions
347
+ // Filtri per marche (LEGACY)
348
+ brands: v.optional(v.array(v.string())),
349
+ brandOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
350
+ // Filtri per modelli (LEGACY)
351
+ models: v.optional(v.array(v.string())),
352
+ modelOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
353
+ // Filtri per stati (LEGACY)
354
+ statuses: v.optional(v.array(v.string())), // Es. ["open", "in_progress"]
355
+ statusOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))), // Default: 'is'
356
+ // Filtri per campi custom (condizioni AND)
357
+ customFieldConditions: v.optional(v.array(v.object({
358
+ fieldId: v.id('ticket_custom_fields'),
359
+ operator: v.union(v.literal('equals'), // Uguale a
360
+ v.literal('not_equals'), // Diverso da
361
+ v.literal('contains'), // Contiene (per testo)
362
+ v.literal('greater_than'), // Maggiore di (per numeri)
363
+ v.literal('less_than'), // Minore di (per numeri)
364
+ v.literal('is_empty'), // È vuoto
365
+ v.literal('is_not_empty') // Non è vuoto
366
+ ),
367
+ value: v.optional(v.any()), // Valore da confrontare
368
+ }))),
369
+ // Trigger su eventi specifici
370
+ triggerOn: v.union(v.literal('create'), // Alla creazione ticket
371
+ v.literal('status_change'), // Al cambio stato ticket
372
+ v.literal('update'), // Ad ogni modifica ticket
373
+ v.literal('sla_warning'), // Quando SLA sta per scadere
374
+ v.literal('sla_breach'), // Quando SLA è scaduto
375
+ // Eventi ordini ricambi
376
+ v.literal('spare_part_order_created'), // Alla creazione ordine ricambio
377
+ v.literal('spare_part_order_status_change') // Al cambio stato ordine ricambio
378
+ ),
379
+ // Filtri specifici per ordini ricambi
380
+ sparePartOrderFilters: v.optional(v.object({
381
+ supplierIds: v.optional(v.array(v.id('suppliers'))), // Fornitori specifici
382
+ applyToAllSuppliers: v.optional(v.boolean()), // Se true, applica a tutti
383
+ statuses: v.optional(v.array(v.string())), // Stati ordine (pending, approved, etc.)
384
+ })),
385
+ }),
386
+ // AZIONI (cosa fa il trigger)
387
+ actions: v.object({
388
+ // Cambia stato
389
+ changeStatus: v.optional(v.string()), // Nome del nuovo stato
390
+ // Assegna fornitore (azienda)
391
+ assignSupplier: v.optional(v.id('suppliers')),
392
+ // Assegna utente specifico (utente con ruolo supplier)
393
+ assignUser: v.optional(v.id('user_profiles')),
394
+ // Imposta SLA (ore)
395
+ setSlaHours: v.optional(v.number()),
396
+ // Applica regola SLA automatica (basata su fornitore e priorità)
397
+ applySlaRule: v.optional(v.boolean()),
398
+ // Aggiungi nota automatica
399
+ addNote: v.optional(v.string()),
400
+ // Invia notifica
401
+ sendNotification: v.optional(v.object({
402
+ recipients: v.array(v.union(
403
+ // Nuovo formato (oggetti)
404
+ v.object({
405
+ type: v.union(v.literal('admin'), // Tutti gli admin
406
+ v.literal('supplier'), // Fornitore assegnato al ticket
407
+ v.literal('ticket_creator'), // Utente che ha aperto il ticket
408
+ v.literal('ticket_assignee'), // Utente assegnatario del ticket
409
+ v.literal('specific_user'), // Utente specifico
410
+ v.literal('specific_supplier') // Fornitore specifico
411
+ ),
412
+ userId: v.optional(v.id('user_profiles')), // Per specific_user
413
+ supplierId: v.optional(v.id('suppliers')), // Per specific_supplier
414
+ }),
415
+ // Vecchio formato (stringhe) - per retrocompatibilità temporanea
416
+ v.literal('admin'), v.literal('supplier'), v.literal('clinic'))),
417
+ message: v.string(),
418
+ })),
419
+ // Imposta priorità (se esiste campo custom priorità)
420
+ setPriority: v.optional(v.string()),
421
+ // Richiedi allegato obbligatorio
422
+ requireAttachment: v.optional(v.boolean()),
423
+ // Azioni specifiche per ordini ricambi
424
+ sparePartOrderActions: v.optional(v.object({
425
+ changeOrderStatus: v.optional(v.string()), // Cambia stato ordine
426
+ addOrderNote: v.optional(v.string()), // Aggiungi nota all'ordine
427
+ sendOrderNotification: v.optional(v.object({
428
+ recipients: v.array(v.union(v.literal('admin'), v.literal('supplier'), v.literal('order_creator'))),
429
+ message: v.string(),
430
+ })),
431
+ })),
432
+ }),
433
+ createdAt: v.number(),
434
+ updatedAt: v.number(),
435
+ createdBy: v.string(), // Auth0 user ID
436
+ })
437
+ .index('by_isActive', ['isActive'])
438
+ .index('by_priority', ['priority'])
439
+ .index('by_createdAt', ['createdAt']),
440
+ // Ticket Macros (azioni manuali sui ticket)
441
+ ticket_macros: defineTable({
442
+ name: v.string(), // Nome della macro
443
+ description: v.optional(v.string()),
444
+ isGlobal: v.boolean(), // true = visibile a tutti, false = solo creatore
445
+ isActive: v.boolean(),
446
+ // Azioni (stesse dei trigger)
447
+ actions: v.object({
448
+ changeStatus: v.optional(v.string()), // Cambia stato
449
+ assignSupplier: v.optional(v.id('suppliers')), // Assegna azienda fornitrice
450
+ assignUser: v.optional(v.id('user_profiles')), // Assegna utente specifico
451
+ setSlaHours: v.optional(v.number()), // Imposta SLA
452
+ applySlaRule: v.optional(v.boolean()), // Applica regola SLA automatica
453
+ addNote: v.optional(v.string()), // Aggiungi nota automatica
454
+ sendNotification: v.optional(v.object({
455
+ recipients: v.array(v.union(v.object({
456
+ type: v.union(v.literal('admin'), v.literal('supplier'), v.literal('ticket_creator'), v.literal('ticket_assignee'), v.literal('specific_user'), v.literal('specific_supplier')),
457
+ userId: v.optional(v.id('user_profiles')),
458
+ supplierId: v.optional(v.id('suppliers')),
459
+ }), v.literal('admin'), v.literal('supplier'), v.literal('clinic'))),
460
+ message: v.string(),
461
+ })),
462
+ setPriority: v.optional(v.string()), // Imposta priorità
463
+ }),
464
+ createdAt: v.number(),
465
+ updatedAt: v.number(),
466
+ createdBy: v.id('user_profiles'), // Chi ha creato la macro
467
+ })
468
+ .index('by_createdBy', ['createdBy'])
469
+ .index('by_isGlobal', ['isGlobal'])
470
+ .index('by_isActive', ['isActive']),
471
+ // Device Questions (Domande per attrezzature)
472
+ device_questions: defineTable({
473
+ deviceId: v.id('devices'), // Attrezzatura specifica
474
+ question: v.string(), // Testo della domanda
475
+ questionType: v.union(v.literal('text'), // Risposta testuale libera
476
+ v.literal('textarea'), // Risposta testuale lunga
477
+ v.literal('select'), // Scelta singola
478
+ v.literal('multiselect'), // Scelta multipla
479
+ v.literal('yes_no'), // Sì/No
480
+ v.literal('number'), // Numero
481
+ v.literal('date') // Data
482
+ ),
483
+ options: v.optional(v.array(v.string())), // Opzioni per select/multiselect
484
+ isRequired: v.boolean(), // Se la risposta è obbligatoria
485
+ order: v.number(), // Ordine di visualizzazione
486
+ placeholder: v.optional(v.string()), // Testo placeholder
487
+ helpText: v.optional(v.string()), // Testo di aiuto
488
+ // Logica condizionale - Mostra questa domanda solo se le condizioni sono soddisfatte
489
+ conditionalLogic: v.optional(v.object({
490
+ enabled: v.boolean(), // Se la logica condizionale è abilitata
491
+ conditions: v.array(v.object({
492
+ questionId: v.id('device_questions'), // ID della domanda da cui dipende
493
+ operator: v.union(v.literal('equals'), // uguale a
494
+ v.literal('not_equals'), // diverso da
495
+ v.literal('contains'), // contiene (per multiselect/text)
496
+ v.literal('not_contains'), // non contiene
497
+ v.literal('greater_than'), // maggiore di (per number)
498
+ v.literal('less_than'), // minore di (per number)
499
+ v.literal('is_empty'), // è vuoto
500
+ v.literal('is_not_empty') // non è vuoto
501
+ ),
502
+ value: v.any(), // Valore da confrontare (può essere string, number, boolean, array)
503
+ })),
504
+ logic: v.union(v.literal('AND'), // Tutte le condizioni devono essere vere
505
+ v.literal('OR') // Almeno una condizione deve essere vera
506
+ ),
507
+ })),
508
+ isActive: v.boolean(), // Se la domanda è attiva
509
+ createdAt: v.number(),
510
+ updatedAt: v.number(),
511
+ createdBy: v.string(), // Auth0 user ID dell'admin che l'ha creata
512
+ })
513
+ .index('by_deviceId', ['deviceId'])
514
+ .index('by_deviceId_order', ['deviceId', 'order'])
515
+ .index('by_isActive', ['isActive']),
516
+ // Spare parts (Ricambi)
517
+ spare_parts: defineTable({
518
+ // Warehouse item fields (colonne 1-16)
519
+ warehouseItemId: v.number(), // id from warehouse_items
520
+ name: v.string(),
521
+ quantity: v.optional(v.number()),
522
+ dentalUnitQuantity: v.optional(v.number()),
523
+ minQuantity: v.optional(v.number()),
524
+ minimumStockAmount: v.optional(v.number()),
525
+ lot: v.optional(v.string()),
526
+ netPrice: v.optional(v.number()),
527
+ deadlineDate: v.optional(v.string()),
528
+ warehouseOrisId: v.optional(v.string()),
529
+ articleId: v.number(),
530
+ clinicId: v.number(), // PrimoUP clinic ID
531
+ vendorId: v.optional(v.number()),
532
+ warehouseCreatedAt: v.optional(v.string()),
533
+ warehouseUpdatedAt: v.optional(v.string()),
534
+ warehouseDeletedAt: v.optional(v.string()),
535
+ // Article fields (colonne 17-36)
536
+ articleInternalId: v.optional(v.number()), // second id column
537
+ code: v.optional(v.string()),
538
+ barcode: v.optional(v.string()),
539
+ articleName: v.optional(v.string()),
540
+ brand: v.optional(v.string()),
541
+ model: v.optional(v.string()),
542
+ articleCategoryId: v.optional(v.number()),
543
+ mu: v.optional(v.string()), // unit of measure
544
+ sellingUnit: v.optional(v.string()),
545
+ warehouseUnit: v.optional(v.string()),
546
+ case: v.optional(v.string()),
547
+ package: v.optional(v.string()),
548
+ articleOrisId: v.optional(v.string()),
549
+ articleDeadlineDate: v.optional(v.string()),
550
+ alternativeArticleId: v.optional(v.number()),
551
+ type: v.optional(v.string()),
552
+ labelInfosLength: v.optional(v.number()),
553
+ articleCreatedAt: v.optional(v.string()),
554
+ articleUpdatedAt: v.optional(v.string()),
555
+ articleDeletedAt: v.optional(v.string()),
556
+ // Category fields (colonne 37-44)
557
+ categoryId: v.optional(v.number()), // third id column
558
+ categoryName: v.optional(v.string()),
559
+ parentId: v.optional(v.number()),
560
+ activeWarehouseCategory: v.optional(v.number()),
561
+ categoryOrisId: v.optional(v.string()),
562
+ categoryType: v.optional(v.string()),
563
+ categoryCreatedAt: v.optional(v.string()),
564
+ categoryUpdatedAt: v.optional(v.string()),
565
+ })
566
+ .index('by_clinicId', ['clinicId'])
567
+ .index('by_articleId', ['articleId'])
568
+ .index('by_warehouseItemId', ['warehouseItemId'])
569
+ .index('by_vendorId', ['vendorId'])
570
+ .index('by_articleCategoryId', ['articleCategoryId']),
571
+ // API Keys (Chiavi per accesso API esterno)
572
+ api_keys: defineTable({
573
+ name: v.string(), // Nome descrittivo della chiave (es. "Integrazione PrimoUp")
574
+ keyHash: v.string(), // Hash SHA-256 della chiave (la chiave originale non viene salvata!)
575
+ keyPrefix: v.string(), // Primi 8 caratteri della chiave (per identificazione)
576
+ permissions: v.array(v.string()), // Permessi: ["comments:add", "tickets:read", etc.]
577
+ createdBy: v.string(), // Auth0 ID dell'admin che l'ha creata
578
+ createdByEmail: v.optional(v.string()), // Email per riferimento
579
+ isActive: v.boolean(), // Se false, la chiave è revocata
580
+ lastUsedAt: v.optional(v.number()), // Ultimo utilizzo
581
+ usageCount: v.optional(v.number()), // Conteggio utilizzi
582
+ expiresAt: v.optional(v.number()), // Scadenza opzionale
583
+ revokedAt: v.optional(v.number()), // Data revoca
584
+ revokedBy: v.optional(v.string()), // Chi ha revocato
585
+ notes: v.optional(v.string()), // Note opzionali
586
+ createdAt: v.number(),
587
+ })
588
+ .index('by_keyHash', ['keyHash'])
589
+ .index('by_keyPrefix', ['keyPrefix'])
590
+ .index('by_isActive', ['isActive'])
591
+ .index('by_createdBy', ['createdBy']),
592
+ // Ticket comments (Commenti sulle segnalazioni)
593
+ ticket_comments: defineTable({
594
+ taskId: v.id('maintenance_tasks'), // Segnalazione a cui appartiene
595
+ authorId: v.string(), // Auth0 user ID dell'autore
596
+ authorName: v.optional(v.string()), // Nome dell'autore (cached)
597
+ authorEmail: v.optional(v.string()), // Email dell'autore (cached)
598
+ authorRole: v.optional(v.string()), // Ruolo dell'autore (admin, user, supplier)
599
+ content: v.string(), // Contenuto del commento
600
+ isInternal: v.optional(v.boolean()), // Se true, visibile solo ad admin e fornitori
601
+ isFromApi: v.optional(v.boolean()), // Se true, commento ricevuto via API esterna
602
+ attachments: v.optional(v.array(v.id('_storage'))), // Allegati opzionali
603
+ mentions: v.optional(v.array(v.object({
604
+ userId: v.id('user_profiles'),
605
+ name: v.string(),
606
+ email: v.string(),
607
+ }))), // Utenti taggati nel commento
608
+ createdAt: v.number(),
609
+ updatedAt: v.optional(v.number()),
610
+ })
611
+ .index('by_taskId', ['taskId'])
612
+ .index('by_authorId', ['authorId'])
613
+ .index('by_taskId_createdAt', ['taskId', 'createdAt']),
614
+ // Spare part orders (Ordini ricambi)
615
+ spare_part_orders: defineTable({
616
+ orderNumber: v.string(), // Numero ordine generato automaticamente
617
+ clinicId: v.number(), // PrimoUP clinic ID
618
+ supplierId: v.id('suppliers'), // Fornitore
619
+ status: v.union(v.literal('draft'), // Bozza
620
+ v.literal('pending'), // In attesa
621
+ v.literal('confirmed'), // Confermato
622
+ v.literal('shipped'), // Spedito
623
+ v.literal('delivered'), // Consegnato
624
+ v.literal('cancelled') // Annullato
625
+ ),
626
+ items: v.array(v.object({
627
+ sparePartId: v.id('spare_parts'),
628
+ articleId: v.number(),
629
+ articleName: v.string(),
630
+ code: v.optional(v.string()),
631
+ quantity: v.number(),
632
+ unitPrice: v.optional(v.number()),
633
+ totalPrice: v.optional(v.number()),
634
+ notes: v.optional(v.string()),
635
+ })),
636
+ totalAmount: v.optional(v.number()),
637
+ notes: v.optional(v.string()),
638
+ requestedDeliveryDate: v.optional(v.number()),
639
+ actualDeliveryDate: v.optional(v.number()),
640
+ createdBy: v.string(), // Auth0 user ID
641
+ createdAt: v.number(),
642
+ updatedAt: v.number(),
643
+ confirmedAt: v.optional(v.number()),
644
+ shippedAt: v.optional(v.number()),
645
+ deliveredAt: v.optional(v.number()),
646
+ })
647
+ .index('by_clinicId', ['clinicId'])
648
+ .index('by_supplierId', ['supplierId'])
649
+ .index('by_status', ['status'])
650
+ .index('by_orderNumber', ['orderNumber'])
651
+ .index('by_createdAt', ['createdAt']),
652
+ // Device Categories (Categorie attrezzature da PrimoUP)
653
+ device_categories: defineTable({
654
+ primoupId: v.number(), // ID originale da PrimoUP
655
+ name: v.string(), // Nome categoria
656
+ parentId: v.optional(v.number()), // ID categoria padre (per gerarchia)
657
+ activeWarehouseCategory: v.optional(v.number()), // 0 o 1
658
+ orisId: v.optional(v.string()), // ID ORIS
659
+ type: v.optional(v.string()), // clinic, warehouse, lab
660
+ note: v.optional(v.string()), // null, "1" (attrezzature), "2" (ricambi), "3" (entrambi)
661
+ createdAt: v.optional(v.string()), // Data creazione originale da PrimoUP
662
+ updatedAt: v.optional(v.string()), // Data aggiornamento originale da PrimoUP
663
+ importedAt: v.number(), // Timestamp import in Convex
664
+ })
665
+ .index('by_primoupId', ['primoupId'])
666
+ .index('by_parentId', ['parentId'])
667
+ .index('by_note', ['note'])
668
+ .index('by_type', ['type'])
669
+ .index('by_name', ['name']),
670
+ // Email Logs (Log invio email via Resend)
671
+ email_logs: defineTable({
672
+ from: v.string(),
673
+ to: v.union(v.string(), v.array(v.string())),
674
+ subject: v.string(),
675
+ html: v.string(),
676
+ ticketId: v.optional(v.id('maintenance_tasks')),
677
+ status: v.union(v.literal('sent'), v.literal('failed'), v.literal('pending')),
678
+ resendId: v.optional(v.string()), // ID email da Resend
679
+ errorMessage: v.optional(v.string()),
680
+ sentAt: v.number(),
681
+ })
682
+ .index('by_ticketId', ['ticketId'])
683
+ .index('by_status', ['status'])
684
+ .index('by_sentAt', ['sentAt']),
685
+ // Ticket History (Storico eventi ticket)
686
+ ticket_history: defineTable({
687
+ ticketId: v.id('maintenance_tasks'),
688
+ eventType: v.union(v.literal('created'), // Ticket creato
689
+ v.literal('status_change'), // Cambio stato
690
+ v.literal('assignee_change'), // Cambio assegnatario (fornitore)
691
+ v.literal('priority_change'), // Cambio priorità
692
+ v.literal('trigger_executed'), // Trigger eseguito
693
+ v.literal('comment_added'), // Commento aggiunto
694
+ v.literal('sla_set'), // SLA impostato
695
+ v.literal('sla_warning'), // Warning SLA
696
+ v.literal('sla_breach'), // SLA violato
697
+ v.literal('updated') // Modifica generica
698
+ ),
699
+ // Dettagli evento
700
+ oldValue: v.optional(v.string()), // Valore precedente (es. stato precedente)
701
+ newValue: v.optional(v.string()), // Nuovo valore (es. nuovo stato)
702
+ // Per trigger
703
+ triggerName: v.optional(v.string()), // Nome del trigger che ha eseguito l'azione
704
+ triggerId: v.optional(v.id('ticket_triggers')), // ID del trigger
705
+ // Per assegnatario
706
+ oldSupplierId: v.optional(v.id('suppliers')),
707
+ newSupplierId: v.optional(v.id('suppliers')),
708
+ oldSupplierName: v.optional(v.string()),
709
+ newSupplierName: v.optional(v.string()),
710
+ // Chi ha fatto l'azione
711
+ performedBy: v.optional(v.string()), // Auth0 user ID (null se sistema/trigger)
712
+ performedByName: v.optional(v.string()), // Nome utente
713
+ performedByEmail: v.optional(v.string()), // Email utente
714
+ isSystemAction: v.boolean(), // True se azione automatica (trigger/sistema)
715
+ // Note aggiuntive
716
+ notes: v.optional(v.string()),
717
+ timestamp: v.number(),
718
+ })
719
+ .index('by_ticketId', ['ticketId'])
720
+ .index('by_ticketId_timestamp', ['ticketId', 'timestamp'])
721
+ .index('by_eventType', ['eventType'])
722
+ .index('by_timestamp', ['timestamp']),
723
+ });
724
+ //# sourceMappingURL=schema.js.map