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