@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,1152 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation } from './_generated/server';
3
+ import { api } from './_generated/api';
4
+
5
+ export const listAllTriggers = query({
6
+ args: {},
7
+ handler: async (ctx) => {
8
+ const triggers = await ctx.db
9
+ .query('ticket_triggers')
10
+ .collect();
11
+
12
+ return triggers.sort((a, b) => a.priority - b.priority);
13
+ },
14
+ });
15
+
16
+ export const listActiveTriggers = query({
17
+ args: {},
18
+ handler: async (ctx) => {
19
+ const triggers = await ctx.db
20
+ .query('ticket_triggers')
21
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', true))
22
+ .collect();
23
+
24
+ return triggers.sort((a, b) => a.priority - b.priority);
25
+ },
26
+ });
27
+
28
+ export const getTrigger = query({
29
+ args: { triggerId: v.id('ticket_triggers') },
30
+ handler: async (ctx, args) => {
31
+ return await ctx.db.get(args.triggerId);
32
+ },
33
+ });
34
+
35
+ export const createTrigger = mutation({
36
+ args: {
37
+ name: v.string(),
38
+ description: v.optional(v.string()),
39
+ priority: v.optional(v.number()),
40
+ conditions: v.object({
41
+ conditionLogic: v.optional(v.union(v.literal('AND'), v.literal('OR'))),
42
+ conditionsList: v.optional(v.array(v.object({
43
+ type: v.union(
44
+ v.literal('trigger_event'),
45
+ v.literal('status'),
46
+ v.literal('category'),
47
+ v.literal('brand'),
48
+ v.literal('model'),
49
+ v.literal('region'),
50
+ v.literal('priority'),
51
+ v.literal('custom_field')
52
+ ),
53
+ negated: v.boolean(),
54
+ value: v.union(v.string(), v.array(v.string())),
55
+ nextOperator: v.union(v.literal('AND'), v.literal('OR')),
56
+ customFieldId: v.optional(v.id('ticket_custom_fields')),
57
+ customFieldOperator: v.optional(v.union(
58
+ v.literal('equals'),
59
+ v.literal('not_equals'),
60
+ v.literal('contains'),
61
+ v.literal('greater_than'),
62
+ v.literal('less_than'),
63
+ v.literal('is_empty'),
64
+ v.literal('is_not_empty')
65
+ )),
66
+ }))),
67
+ categories: v.optional(v.array(v.string())),
68
+ applyToAllCategories: v.boolean(),
69
+ regions: v.optional(v.array(v.string())),
70
+ applyToAllRegions: v.optional(v.boolean()),
71
+ brands: v.optional(v.array(v.string())),
72
+ brandOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
73
+ models: v.optional(v.array(v.string())),
74
+ modelOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
75
+ statuses: v.optional(v.array(v.string())),
76
+ statusOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
77
+ customFieldConditions: v.optional(v.array(v.object({
78
+ fieldId: v.id('ticket_custom_fields'),
79
+ operator: v.union(
80
+ v.literal('equals'),
81
+ v.literal('not_equals'),
82
+ v.literal('contains'),
83
+ v.literal('greater_than'),
84
+ v.literal('less_than'),
85
+ v.literal('is_empty'),
86
+ v.literal('is_not_empty')
87
+ ),
88
+ value: v.optional(v.any()),
89
+ }))),
90
+ triggerOn: v.union(
91
+ v.literal('create'),
92
+ v.literal('status_change'),
93
+ v.literal('update'),
94
+ v.literal('sla_warning'),
95
+ v.literal('sla_breach'),
96
+ v.literal('spare_part_order_created'),
97
+ v.literal('spare_part_order_status_change')
98
+ ),
99
+ sparePartOrderFilters: v.optional(v.object({
100
+ supplierIds: v.optional(v.array(v.id('suppliers'))),
101
+ applyToAllSuppliers: v.optional(v.boolean()),
102
+ statuses: v.optional(v.array(v.string())),
103
+ })),
104
+ }),
105
+ actions: v.object({
106
+ changeStatus: v.optional(v.string()),
107
+ assignSupplier: v.optional(v.id('suppliers')),
108
+ setSlaHours: v.optional(v.number()),
109
+ applySlaRule: v.optional(v.boolean()),
110
+ setPriority: v.optional(v.union(
111
+ v.literal('low'),
112
+ v.literal('medium'),
113
+ v.literal('high')
114
+ )),
115
+ addNote: v.optional(v.string()),
116
+ sendNotification: v.optional(v.object({
117
+ recipients: v.array(v.object({
118
+ type: v.union(
119
+ v.literal('admin'),
120
+ v.literal('supplier'),
121
+ v.literal('ticket_creator'),
122
+ v.literal('ticket_assignee'),
123
+ v.literal('specific_user'),
124
+ v.literal('specific_supplier')
125
+ ),
126
+ userId: v.optional(v.id('user_profiles')),
127
+ supplierId: v.optional(v.id('suppliers')),
128
+ })),
129
+ message: v.string(),
130
+ })),
131
+ requireAttachment: v.optional(v.boolean()),
132
+ sparePartOrderActions: v.optional(v.object({
133
+ changeOrderStatus: v.optional(v.string()),
134
+ addOrderNote: v.optional(v.string()),
135
+ sendOrderNotification: v.optional(v.object({
136
+ recipients: v.array(v.union(
137
+ v.literal('admin'),
138
+ v.literal('supplier'),
139
+ v.literal('order_creator')
140
+ )),
141
+ message: v.string(),
142
+ })),
143
+ })),
144
+ }),
145
+ createdBy: v.string(),
146
+ },
147
+ handler: async (ctx, args) => {
148
+ let priority = args.priority;
149
+ if (priority === undefined) {
150
+ const lastTrigger = await ctx.db
151
+ .query('ticket_triggers')
152
+ .withIndex('by_priority')
153
+ .order('desc')
154
+ .first();
155
+ priority = lastTrigger ? lastTrigger.priority + 1 : 0;
156
+ }
157
+
158
+ const triggerId = await ctx.db.insert('ticket_triggers', {
159
+ name: args.name,
160
+ description: args.description,
161
+ isActive: true,
162
+ priority,
163
+ conditions: args.conditions,
164
+ actions: args.actions,
165
+ createdAt: Date.now(),
166
+ updatedAt: Date.now(),
167
+ createdBy: args.createdBy,
168
+ });
169
+
170
+ return triggerId;
171
+ },
172
+ });
173
+
174
+ export const updateTrigger = mutation({
175
+ args: {
176
+ triggerId: v.id('ticket_triggers'),
177
+ name: v.optional(v.string()),
178
+ description: v.optional(v.string()),
179
+ priority: v.optional(v.number()),
180
+ isActive: v.optional(v.boolean()),
181
+ conditions: v.optional(v.object({
182
+ conditionLogic: v.optional(v.union(v.literal('AND'), v.literal('OR'))),
183
+ conditionsList: v.optional(v.array(v.object({
184
+ type: v.union(
185
+ v.literal('trigger_event'),
186
+ v.literal('status'),
187
+ v.literal('category'),
188
+ v.literal('brand'),
189
+ v.literal('model'),
190
+ v.literal('region'),
191
+ v.literal('priority'),
192
+ v.literal('custom_field')
193
+ ),
194
+ negated: v.boolean(),
195
+ value: v.union(v.string(), v.array(v.string())),
196
+ nextOperator: v.union(v.literal('AND'), v.literal('OR')),
197
+ customFieldId: v.optional(v.id('ticket_custom_fields')),
198
+ customFieldOperator: v.optional(v.union(
199
+ v.literal('equals'),
200
+ v.literal('not_equals'),
201
+ v.literal('contains'),
202
+ v.literal('greater_than'),
203
+ v.literal('less_than'),
204
+ v.literal('is_empty'),
205
+ v.literal('is_not_empty')
206
+ )),
207
+ }))),
208
+ categories: v.optional(v.array(v.string())),
209
+ applyToAllCategories: v.boolean(),
210
+ regions: v.optional(v.array(v.string())),
211
+ applyToAllRegions: v.optional(v.boolean()),
212
+ brands: v.optional(v.array(v.string())),
213
+ brandOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
214
+ models: v.optional(v.array(v.string())),
215
+ modelOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
216
+ statuses: v.optional(v.array(v.string())),
217
+ statusOperator: v.optional(v.union(v.literal('is'), v.literal('is_not'))),
218
+ customFieldConditions: v.optional(v.array(v.object({
219
+ fieldId: v.id('ticket_custom_fields'),
220
+ operator: v.union(
221
+ v.literal('equals'),
222
+ v.literal('not_equals'),
223
+ v.literal('contains'),
224
+ v.literal('greater_than'),
225
+ v.literal('less_than'),
226
+ v.literal('is_empty'),
227
+ v.literal('is_not_empty')
228
+ ),
229
+ value: v.optional(v.any()),
230
+ }))),
231
+ triggerOn: v.union(
232
+ v.literal('create'),
233
+ v.literal('status_change'),
234
+ v.literal('update'),
235
+ v.literal('sla_warning'),
236
+ v.literal('sla_breach'),
237
+ v.literal('spare_part_order_created'),
238
+ v.literal('spare_part_order_status_change')
239
+ ),
240
+ sparePartOrderFilters: v.optional(v.object({
241
+ supplierIds: v.optional(v.array(v.id('suppliers'))),
242
+ applyToAllSuppliers: v.optional(v.boolean()),
243
+ statuses: v.optional(v.array(v.string())),
244
+ })),
245
+ })),
246
+ actions: v.optional(v.object({
247
+ changeStatus: v.optional(v.string()),
248
+ assignSupplier: v.optional(v.id('suppliers')),
249
+ setSlaHours: v.optional(v.number()),
250
+ applySlaRule: v.optional(v.boolean()),
251
+ setPriority: v.optional(v.union(
252
+ v.literal('low'),
253
+ v.literal('medium'),
254
+ v.literal('high')
255
+ )),
256
+ addNote: v.optional(v.string()),
257
+ sendNotification: v.optional(v.object({
258
+ recipients: v.array(v.object({
259
+ type: v.union(
260
+ v.literal('admin'),
261
+ v.literal('supplier'),
262
+ v.literal('ticket_creator'),
263
+ v.literal('ticket_assignee'),
264
+ v.literal('specific_user'),
265
+ v.literal('specific_supplier')
266
+ ),
267
+ userId: v.optional(v.id('user_profiles')),
268
+ supplierId: v.optional(v.id('suppliers')),
269
+ })),
270
+ message: v.string(),
271
+ })),
272
+ requireAttachment: v.optional(v.boolean()),
273
+ sparePartOrderActions: v.optional(v.object({
274
+ changeOrderStatus: v.optional(v.string()),
275
+ addOrderNote: v.optional(v.string()),
276
+ sendOrderNotification: v.optional(v.object({
277
+ recipients: v.array(v.union(
278
+ v.literal('admin'),
279
+ v.literal('supplier'),
280
+ v.literal('order_creator')
281
+ )),
282
+ message: v.string(),
283
+ })),
284
+ })),
285
+ })),
286
+ },
287
+ handler: async (ctx, args) => {
288
+ const { triggerId, ...updates } = args;
289
+
290
+ await ctx.db.patch(triggerId, {
291
+ ...updates,
292
+ updatedAt: Date.now(),
293
+ });
294
+
295
+ return triggerId;
296
+ },
297
+ });
298
+
299
+ export const deleteTrigger = mutation({
300
+ args: { triggerId: v.id('ticket_triggers') },
301
+ handler: async (ctx, args) => {
302
+ await ctx.db.delete(args.triggerId);
303
+ return { success: true };
304
+ },
305
+ });
306
+
307
+ export const toggleTrigger = mutation({
308
+ args: {
309
+ triggerId: v.id('ticket_triggers'),
310
+ isActive: v.boolean(),
311
+ },
312
+ handler: async (ctx, args) => {
313
+ await ctx.db.patch(args.triggerId, {
314
+ isActive: args.isActive,
315
+ updatedAt: Date.now(),
316
+ });
317
+
318
+ return { success: true };
319
+ },
320
+ });
321
+
322
+ export const duplicateTrigger = mutation({
323
+ args: {
324
+ triggerId: v.id('ticket_triggers'),
325
+ newName: v.optional(v.string()),
326
+ createdBy: v.string(),
327
+ },
328
+ handler: async (ctx, args) => {
329
+ const originalTrigger = await ctx.db.get(args.triggerId);
330
+ if (!originalTrigger) {
331
+ throw new Error('Trigger non trovato');
332
+ }
333
+
334
+ const lastTrigger = await ctx.db
335
+ .query('ticket_triggers')
336
+ .withIndex('by_priority')
337
+ .order('desc')
338
+ .first();
339
+ const maxPriority = lastTrigger ? lastTrigger.priority + 1 : 0;
340
+
341
+ const duplicateName = args.newName?.trim() || `${originalTrigger.name} (copia)`;
342
+
343
+ const newTriggerId = await ctx.db.insert('ticket_triggers', {
344
+ name: duplicateName,
345
+ description: originalTrigger.description,
346
+ isActive: false,
347
+ priority: maxPriority,
348
+ conditions: originalTrigger.conditions,
349
+ actions: originalTrigger.actions,
350
+ createdAt: Date.now(),
351
+ updatedAt: Date.now(),
352
+ createdBy: args.createdBy,
353
+ });
354
+
355
+ return newTriggerId;
356
+ },
357
+ });
358
+
359
+ export const reorderTriggers = mutation({
360
+ args: {
361
+ triggerOrders: v.array(v.object({
362
+ triggerId: v.id('ticket_triggers'),
363
+ priority: v.number(),
364
+ })),
365
+ },
366
+ handler: async (ctx, args) => {
367
+ for (const { triggerId, priority } of args.triggerOrders) {
368
+ await ctx.db.patch(triggerId, {
369
+ priority,
370
+ updatedAt: Date.now(),
371
+ });
372
+ }
373
+
374
+ return { success: true };
375
+ },
376
+ });
377
+
378
+ export const executeTriggers = mutation({
379
+ args: {
380
+ ticketId: v.id('maintenance_tasks'),
381
+ triggerOn: v.union(
382
+ v.literal('create'),
383
+ v.literal('status_change'),
384
+ v.literal('update'),
385
+ v.literal('sla_warning'),
386
+ v.literal('sla_breach')
387
+ ),
388
+ oldStatus: v.optional(v.string()),
389
+ },
390
+ handler: async (ctx, args) => {
391
+ const ticket = await ctx.db.get(args.ticketId);
392
+ if (!ticket) {
393
+ throw new Error('Ticket not found');
394
+ }
395
+
396
+ const device = ticket.deviceId ? await ctx.db.get(ticket.deviceId) : null;
397
+
398
+ const clinic = await ctx.db.get(ticket.clinicId);
399
+
400
+ const triggers = await ctx.db
401
+ .query('ticket_triggers')
402
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', true))
403
+ .collect();
404
+
405
+ const sortedTriggers = triggers.sort((a, b) => a.priority - b.priority);
406
+
407
+ const evaluateCondition = (condition: any): boolean => {
408
+ const { type, negated, value } = condition;
409
+ let result = false;
410
+
411
+ switch (type) {
412
+ case 'trigger_event':
413
+ result = value === args.triggerOn;
414
+ break;
415
+ case 'status':
416
+ const statusValues = Array.isArray(value) ? value : [value];
417
+ result = statusValues.includes(ticket.status);
418
+ break;
419
+ case 'category':
420
+ const categoryValues = Array.isArray(value) ? value : [value];
421
+ result = device ? categoryValues.includes(device.category) : false;
422
+ break;
423
+ case 'brand':
424
+ const brandValues = Array.isArray(value) ? value : [value];
425
+ result = device ? brandValues.includes(device.brand) : false;
426
+ break;
427
+ case 'model':
428
+ const modelValues = Array.isArray(value) ? value : [value];
429
+ result = device ? modelValues.includes(device.model) : false;
430
+ break;
431
+ case 'region':
432
+ const regionValues = Array.isArray(value) ? value : [value];
433
+ const clinicRegion = (clinic as any)?.region;
434
+ result = clinicRegion ? regionValues.includes(clinicRegion) : false;
435
+ break;
436
+ case 'priority':
437
+ const priorityValues = Array.isArray(value) ? value : [value];
438
+ result = priorityValues.includes((ticket as any).priority);
439
+ break;
440
+ case 'custom_field':
441
+ const { customFieldId, customFieldOperator } = condition;
442
+ if (!customFieldId) {
443
+ result = true;
444
+ break;
445
+ }
446
+ const fieldValue = ticket.customFields?.[customFieldId];
447
+ const operator = customFieldOperator || 'equals';
448
+
449
+ switch (operator) {
450
+ case 'equals':
451
+ result = fieldValue === value;
452
+ break;
453
+ case 'not_equals':
454
+ result = fieldValue !== value;
455
+ break;
456
+ case 'contains':
457
+ result = fieldValue && String(fieldValue).includes(String(value));
458
+ break;
459
+ case 'greater_than':
460
+ result = fieldValue && Number(fieldValue) > Number(value);
461
+ break;
462
+ case 'less_than':
463
+ result = fieldValue && Number(fieldValue) < Number(value);
464
+ break;
465
+ case 'is_empty':
466
+ result = fieldValue === undefined || fieldValue === null || fieldValue === '';
467
+ break;
468
+ case 'is_not_empty':
469
+ result = fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
470
+ break;
471
+ default:
472
+ result = true;
473
+ }
474
+ break;
475
+ default:
476
+ result = true;
477
+ }
478
+
479
+ return negated ? !result : result;
480
+ };
481
+
482
+ const evaluateConditionsWithOperators = (conditions: any[]): boolean => {
483
+ if (conditions.length === 0) return true;
484
+ if (conditions.length === 1) return evaluateCondition(conditions[0]);
485
+
486
+ const results = conditions.map(c => evaluateCondition(c));
487
+
488
+ const andGroups: boolean[][] = [[]];
489
+ let currentGroup = 0;
490
+
491
+ for (let i = 0; i < conditions.length; i++) {
492
+ andGroups[currentGroup].push(results[i]);
493
+
494
+ if (i < conditions.length - 1) {
495
+ const nextOp = conditions[i].nextOperator || 'AND';
496
+ if (nextOp === 'OR') {
497
+ currentGroup++;
498
+ andGroups[currentGroup] = [];
499
+ }
500
+ }
501
+ }
502
+
503
+ const groupResults = andGroups.map(group => group.every(r => r));
504
+ return groupResults.some(r => r);
505
+ };
506
+
507
+ for (const trigger of sortedTriggers) {
508
+ const triggerConditions = trigger.conditions as any;
509
+
510
+ if (triggerConditions.triggerOn && triggerConditions.triggerOn !== args.triggerOn) {
511
+ continue;
512
+ }
513
+
514
+ if (triggerConditions.conditionsList && triggerConditions.conditionsList.length > 0) {
515
+ const shouldExecute = evaluateConditionsWithOperators(triggerConditions.conditionsList);
516
+
517
+ if (!shouldExecute) {
518
+ continue;
519
+ }
520
+ } else {
521
+ if (trigger.conditions.triggerOn !== args.triggerOn) {
522
+ continue;
523
+ }
524
+
525
+ const conditionLogic = trigger.conditions.conditionLogic || 'AND';
526
+ const conditionResults: boolean[] = [];
527
+
528
+ let categoryMatch = true;
529
+ if (!trigger.conditions.applyToAllCategories && device) {
530
+ if (!trigger.conditions.categories || !trigger.conditions.categories.includes(device.category)) {
531
+ categoryMatch = false;
532
+ }
533
+ }
534
+ conditionResults.push(categoryMatch);
535
+
536
+ let brandMatch = true;
537
+ if (triggerConditions.brands && triggerConditions.brands.length > 0 && device) {
538
+ const brandOperator = triggerConditions.brandOperator || 'is';
539
+ const brandInList = triggerConditions.brands.includes(device.brand);
540
+
541
+ if (brandOperator === 'is') {
542
+ brandMatch = brandInList;
543
+ } else {
544
+ brandMatch = !brandInList;
545
+ }
546
+ }
547
+ conditionResults.push(brandMatch);
548
+
549
+ let modelMatch = true;
550
+ if (triggerConditions.models && triggerConditions.models.length > 0 && device) {
551
+ const modelOperator = triggerConditions.modelOperator || 'is';
552
+ const modelInList = triggerConditions.models.includes(device.model);
553
+
554
+ if (modelOperator === 'is') {
555
+ modelMatch = modelInList;
556
+ } else {
557
+ modelMatch = !modelInList;
558
+ }
559
+ }
560
+ conditionResults.push(modelMatch);
561
+
562
+ let regionMatch = true;
563
+ if (trigger.conditions.applyToAllRegions === false) {
564
+ const clinicRegion = (clinic as any)?.region;
565
+
566
+ if (!clinicRegion || !trigger.conditions.regions || !trigger.conditions.regions.includes(clinicRegion)) {
567
+ regionMatch = false;
568
+ }
569
+ }
570
+ conditionResults.push(regionMatch);
571
+
572
+ let statusMatch = true;
573
+ if (trigger.conditions.statuses && trigger.conditions.statuses.length > 0) {
574
+ const statusOperator = trigger.conditions.statusOperator || 'is';
575
+ const statusInList = trigger.conditions.statuses.includes(ticket.status);
576
+
577
+ if (statusOperator === 'is') {
578
+ statusMatch = statusInList;
579
+ } else {
580
+ statusMatch = !statusInList;
581
+ }
582
+ }
583
+ conditionResults.push(statusMatch);
584
+
585
+ let customFieldsMatch = true;
586
+ if (trigger.conditions.customFieldConditions && trigger.conditions.customFieldConditions.length > 0) {
587
+ for (const condition of trigger.conditions.customFieldConditions) {
588
+ const fieldValue = ticket.customFields?.[condition.fieldId];
589
+ let conditionMet = false;
590
+
591
+ switch (condition.operator) {
592
+ case 'equals':
593
+ conditionMet = fieldValue === condition.value;
594
+ break;
595
+ case 'not_equals':
596
+ conditionMet = fieldValue !== condition.value;
597
+ break;
598
+ case 'contains':
599
+ conditionMet = fieldValue && String(fieldValue).includes(String(condition.value));
600
+ break;
601
+ case 'greater_than':
602
+ conditionMet = fieldValue && Number(fieldValue) > Number(condition.value);
603
+ break;
604
+ case 'less_than':
605
+ conditionMet = fieldValue && Number(fieldValue) < Number(condition.value);
606
+ break;
607
+ case 'is_empty':
608
+ conditionMet = fieldValue === undefined || fieldValue === null || fieldValue === '';
609
+ break;
610
+ case 'is_not_empty':
611
+ conditionMet = fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
612
+ break;
613
+ }
614
+
615
+ if (!conditionMet) {
616
+ customFieldsMatch = false;
617
+ break;
618
+ }
619
+ }
620
+ }
621
+ conditionResults.push(customFieldsMatch);
622
+
623
+ let shouldExecute = false;
624
+ if (conditionLogic === 'OR') {
625
+ shouldExecute = conditionResults.some(result => result === true);
626
+ } else {
627
+ shouldExecute = conditionResults.every(result => result === true);
628
+ }
629
+
630
+ if (!shouldExecute) {
631
+ continue;
632
+ }
633
+ }
634
+
635
+ const updates: any = {};
636
+
637
+ const actions = trigger.actions as any;
638
+ if (actions.setPriority) {
639
+ updates.priority = actions.setPriority;
640
+ }
641
+
642
+ if (trigger.actions.changeStatus) {
643
+ updates.status = trigger.actions.changeStatus;
644
+ }
645
+
646
+ if (trigger.actions.assignSupplier) {
647
+ updates.supplierId = trigger.actions.assignSupplier;
648
+ }
649
+
650
+ if (actions.applySlaRule && actions.assignSupplier) {
651
+ const priority = actions.setPriority || (ticket as any).priority || 'medium';
652
+ const slaRule = await ctx.db
653
+ .query('sla_rules')
654
+ .withIndex('by_supplierId_priority', (q: any) =>
655
+ q.eq('supplierId', actions.assignSupplier).eq('priority', priority)
656
+ )
657
+ .first();
658
+
659
+ if (slaRule) {
660
+ const slaDeadline = Date.now() + (slaRule.resolutionTimeHours * 60 * 60 * 1000);
661
+ updates.slaDeadline = slaDeadline;
662
+ }
663
+ } else if (trigger.actions.setSlaHours) {
664
+ const slaDeadline = Date.now() + (trigger.actions.setSlaHours * 60 * 60 * 1000);
665
+ updates.slaDeadline = slaDeadline;
666
+ }
667
+
668
+ if (trigger.actions.addNote) {
669
+ const currentNotes = ticket.notes || '';
670
+ const newNote = `[Trigger: ${trigger.name}] ${trigger.actions.addNote}`;
671
+ updates.notes = currentNotes ? `${currentNotes}\n\n${newNote}` : newNote;
672
+ }
673
+
674
+ if (Object.keys(updates).length > 0) {
675
+ updates.updated_at = Date.now();
676
+ await ctx.db.patch(args.ticketId, updates);
677
+
678
+ try {
679
+ const actionDescriptions: string[] = [];
680
+ if (updates.status) actionDescriptions.push(`Stato → ${updates.status}`);
681
+ if (updates.supplierId) {
682
+ const supplier = await ctx.db.get(updates.supplierId);
683
+ actionDescriptions.push(`Assegnato a ${(supplier as any)?.name || 'fornitore'}`);
684
+ }
685
+ if (updates.priority) actionDescriptions.push(`Priorità → ${updates.priority}`);
686
+ if (updates.slaDeadline) actionDescriptions.push(`SLA impostato`);
687
+
688
+ await ctx.runMutation(api.ticketHistory.recordHistoryEvent, {
689
+ ticketId: args.ticketId,
690
+ eventType: 'trigger_executed',
691
+ triggerName: trigger.name,
692
+ triggerId: trigger._id,
693
+ oldValue: updates.status ? ticket.status : undefined,
694
+ newValue: updates.status,
695
+ oldSupplierId: updates.supplierId ? ticket.supplierId : undefined,
696
+ newSupplierId: updates.supplierId,
697
+ notes: actionDescriptions.join(', '),
698
+ isSystemAction: true,
699
+ });
700
+ } catch (error) {
701
+ console.error('Error recording trigger history:', error);
702
+ }
703
+ }
704
+
705
+ }
706
+
707
+ return { success: true };
708
+ },
709
+ });
710
+
711
+ export const checkAttachmentRequired = query({
712
+ args: {
713
+ deviceId: v.optional(v.id('devices')),
714
+ clinicId: v.optional(v.id('clinics')),
715
+ status: v.optional(v.string()),
716
+ priority: v.optional(v.union(v.literal('low'), v.literal('medium'), v.literal('high'))),
717
+ customFields: v.optional(v.any()),
718
+ },
719
+ handler: async (ctx, args) => {
720
+ try {
721
+ const triggers = await ctx.db
722
+ .query('ticket_triggers')
723
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', true))
724
+ .collect();
725
+
726
+ const attachmentTriggers = triggers.filter((t: any) =>
727
+ t.actions?.requireAttachment === true
728
+ );
729
+
730
+ if (attachmentTriggers.length === 0) {
731
+ return { required: false, matchingTriggers: [] };
732
+ }
733
+
734
+ let device = null;
735
+ let clinic = null;
736
+
737
+ try {
738
+ if (args.deviceId) {
739
+ device = await ctx.db.get(args.deviceId);
740
+ }
741
+ } catch (e) {
742
+ console.error('Error fetching device:', e);
743
+ }
744
+
745
+ try {
746
+ if (args.clinicId) {
747
+ clinic = await ctx.db.get(args.clinicId);
748
+ }
749
+ } catch (e) {
750
+ console.error('Error fetching clinic:', e);
751
+ }
752
+
753
+ const evaluateCondition = (condition: any): boolean => {
754
+ const { type, negated, value } = condition;
755
+ let result = false;
756
+
757
+ switch (type) {
758
+ case 'trigger_event':
759
+ result = value === 'create';
760
+ break;
761
+ case 'status':
762
+ const statusValues = Array.isArray(value) ? value : [value];
763
+ result = args.status ? statusValues.includes(args.status) : false;
764
+ break;
765
+ case 'category':
766
+ const categoryValues = Array.isArray(value) ? value : [value];
767
+ result = device ? categoryValues.includes((device as any).category) : false;
768
+ break;
769
+ case 'brand':
770
+ const brandValues = Array.isArray(value) ? value : [value];
771
+ result = device ? brandValues.includes((device as any).brand) : false;
772
+ break;
773
+ case 'model':
774
+ const modelValues = Array.isArray(value) ? value : [value];
775
+ result = device ? modelValues.includes((device as any).model) : false;
776
+ break;
777
+ case 'region':
778
+ const regionValues = Array.isArray(value) ? value : [value];
779
+ const clinicRegion = (clinic as any)?.region;
780
+ result = clinicRegion ? regionValues.includes(clinicRegion) : false;
781
+ break;
782
+ case 'priority':
783
+ const priorityValues = Array.isArray(value) ? value : [value];
784
+ result = args.priority ? priorityValues.includes(args.priority) : false;
785
+ break;
786
+ case 'custom_field':
787
+ const { customFieldId, customFieldOperator } = condition;
788
+ if (!customFieldId) {
789
+ result = true;
790
+ break;
791
+ }
792
+ const fieldValue = args.customFields?.[customFieldId];
793
+ const operator = customFieldOperator || 'equals';
794
+
795
+ switch (operator) {
796
+ case 'equals':
797
+ result = fieldValue === value;
798
+ break;
799
+ case 'not_equals':
800
+ result = fieldValue !== value;
801
+ break;
802
+ case 'contains':
803
+ result = fieldValue && String(fieldValue).includes(String(value));
804
+ break;
805
+ case 'greater_than':
806
+ result = fieldValue && Number(fieldValue) > Number(value);
807
+ break;
808
+ case 'less_than':
809
+ result = fieldValue && Number(fieldValue) < Number(value);
810
+ break;
811
+ case 'is_empty':
812
+ result = fieldValue === undefined || fieldValue === null || fieldValue === '';
813
+ break;
814
+ case 'is_not_empty':
815
+ result = fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
816
+ break;
817
+ default:
818
+ result = true;
819
+ }
820
+ break;
821
+ default:
822
+ result = true;
823
+ }
824
+
825
+ return negated ? !result : result;
826
+ };
827
+
828
+ const evaluateConditionsWithOperators = (conditions: any[]): boolean => {
829
+ if (conditions.length === 0) return true;
830
+ if (conditions.length === 1) return evaluateCondition(conditions[0]);
831
+
832
+ const results = conditions.map(c => evaluateCondition(c));
833
+
834
+ const andGroups: boolean[][] = [[]];
835
+ let currentGroup = 0;
836
+
837
+ for (let i = 0; i < conditions.length; i++) {
838
+ andGroups[currentGroup].push(results[i]);
839
+
840
+ if (i < conditions.length - 1) {
841
+ const nextOp = conditions[i].nextOperator || 'AND';
842
+ if (nextOp === 'OR') {
843
+ currentGroup++;
844
+ andGroups[currentGroup] = [];
845
+ }
846
+ }
847
+ }
848
+
849
+ const groupResults = andGroups.map(group => group.every(r => r));
850
+ return groupResults.some(r => r);
851
+ };
852
+
853
+ const matchingTriggers: Array<{ id: string; name: string }> = [];
854
+
855
+ for (const trigger of attachmentTriggers) {
856
+ const triggerConditions = trigger.conditions as any;
857
+ let matches = false;
858
+
859
+ if (triggerConditions.conditionsList && triggerConditions.conditionsList.length > 0) {
860
+ matches = evaluateConditionsWithOperators(triggerConditions.conditionsList);
861
+ } else {
862
+ if (triggerConditions.triggerOn !== 'create') {
863
+ continue;
864
+ }
865
+
866
+ let categoryMatch = true;
867
+ if (!triggerConditions.applyToAllCategories && device) {
868
+ if (!triggerConditions.categories || !triggerConditions.categories.includes((device as any).category)) {
869
+ categoryMatch = false;
870
+ }
871
+ }
872
+
873
+ let brandMatch = true;
874
+ if (triggerConditions.brands && triggerConditions.brands.length > 0 && device) {
875
+ const brandOperator = triggerConditions.brandOperator || 'is';
876
+ const brandInList = triggerConditions.brands.includes((device as any).brand);
877
+ brandMatch = brandOperator === 'is' ? brandInList : !brandInList;
878
+ }
879
+
880
+ let modelMatch = true;
881
+ if (triggerConditions.models && triggerConditions.models.length > 0 && device) {
882
+ const modelOperator = triggerConditions.modelOperator || 'is';
883
+ const modelInList = triggerConditions.models.includes((device as any).model);
884
+ modelMatch = modelOperator === 'is' ? modelInList : !modelInList;
885
+ }
886
+
887
+ let regionMatch = true;
888
+ if (triggerConditions.applyToAllRegions === false) {
889
+ const clinicRegion = (clinic as any)?.region;
890
+ if (!clinicRegion || !triggerConditions.regions || !triggerConditions.regions.includes(clinicRegion)) {
891
+ regionMatch = false;
892
+ }
893
+ }
894
+
895
+ matches = categoryMatch && brandMatch && modelMatch && regionMatch;
896
+ }
897
+
898
+ if (matches) {
899
+ matchingTriggers.push({
900
+ id: trigger._id,
901
+ name: trigger.name,
902
+ });
903
+ }
904
+ }
905
+
906
+ return {
907
+ required: matchingTriggers.length > 0,
908
+ matchingTriggers,
909
+ };
910
+ } catch (error) {
911
+ console.error('Error in checkAttachmentRequired:', error);
912
+ return { required: false, matchingTriggers: [] };
913
+ }
914
+ },
915
+ });
916
+
917
+ export const simulateTrigger = query({
918
+ args: {
919
+ triggerId: v.id('ticket_triggers'),
920
+ simulationData: v.object({
921
+ triggerOn: v.union(
922
+ v.literal('create'),
923
+ v.literal('status_change'),
924
+ v.literal('update'),
925
+ v.literal('sla_warning'),
926
+ v.literal('sla_breach')
927
+ ),
928
+ status: v.optional(v.string()),
929
+ category: v.optional(v.string()),
930
+ brand: v.optional(v.string()),
931
+ model: v.optional(v.string()),
932
+ region: v.optional(v.string()),
933
+ priority: v.optional(v.union(v.literal('low'), v.literal('medium'), v.literal('high'))),
934
+ customFields: v.optional(v.any()),
935
+ }),
936
+ },
937
+ handler: async (ctx, args) => {
938
+ const trigger = await ctx.db.get(args.triggerId);
939
+ if (!trigger) {
940
+ throw new Error('Trigger non trovato');
941
+ }
942
+
943
+ const { simulationData } = args;
944
+ const triggerConditions = trigger.conditions as any;
945
+
946
+ const conditionResults: Array<{
947
+ type: string;
948
+ expected: any;
949
+ actual: any;
950
+ negated: boolean;
951
+ result: boolean;
952
+ description: string;
953
+ }> = [];
954
+
955
+ const evaluateCondition = (condition: any): boolean => {
956
+ const { type, negated, value } = condition;
957
+ let result = false;
958
+ let actual: any = null;
959
+ let description = '';
960
+
961
+ switch (type) {
962
+ case 'trigger_event':
963
+ actual = simulationData.triggerOn;
964
+ result = value === simulationData.triggerOn;
965
+ description = `Evento: ${value}`;
966
+ break;
967
+ case 'status':
968
+ const statusValues = Array.isArray(value) ? value : [value];
969
+ actual = simulationData.status || '(non specificato)';
970
+ result = simulationData.status ? statusValues.includes(simulationData.status) : false;
971
+ description = `Stato in: [${statusValues.join(', ')}]`;
972
+ break;
973
+ case 'category':
974
+ const categoryValues = Array.isArray(value) ? value : [value];
975
+ actual = simulationData.category || '(non specificato)';
976
+ result = simulationData.category ? categoryValues.includes(simulationData.category) : false;
977
+ description = `Categoria in: [${categoryValues.join(', ')}]`;
978
+ break;
979
+ case 'brand':
980
+ const brandValues = Array.isArray(value) ? value : [value];
981
+ actual = simulationData.brand || '(non specificato)';
982
+ result = simulationData.brand ? brandValues.includes(simulationData.brand) : false;
983
+ description = `Marca in: [${brandValues.join(', ')}]`;
984
+ break;
985
+ case 'model':
986
+ const modelValues = Array.isArray(value) ? value : [value];
987
+ actual = simulationData.model || '(non specificato)';
988
+ result = simulationData.model ? modelValues.includes(simulationData.model) : false;
989
+ description = `Modello in: [${modelValues.join(', ')}]`;
990
+ break;
991
+ case 'region':
992
+ const regionValues = Array.isArray(value) ? value : [value];
993
+ actual = simulationData.region || '(non specificato)';
994
+ result = simulationData.region ? regionValues.includes(simulationData.region) : false;
995
+ description = `Regione in: [${regionValues.join(', ')}]`;
996
+ break;
997
+ case 'priority':
998
+ const priorityValues = Array.isArray(value) ? value : [value];
999
+ actual = simulationData.priority || '(non specificato)';
1000
+ result = simulationData.priority ? priorityValues.includes(simulationData.priority) : false;
1001
+ description = `Priorità in: [${priorityValues.join(', ')}]`;
1002
+ break;
1003
+ case 'custom_field':
1004
+ const { customFieldId, customFieldOperator } = condition;
1005
+ if (!customFieldId) {
1006
+ result = true;
1007
+ description = 'Campo custom (non configurato)';
1008
+ break;
1009
+ }
1010
+ const fieldValue = simulationData.customFields?.[customFieldId];
1011
+ actual = fieldValue ?? '(non specificato)';
1012
+ const operator = customFieldOperator || 'equals';
1013
+
1014
+ switch (operator) {
1015
+ case 'equals':
1016
+ result = fieldValue === value;
1017
+ description = `Campo custom = ${value}`;
1018
+ break;
1019
+ case 'not_equals':
1020
+ result = fieldValue !== value;
1021
+ description = `Campo custom ≠ ${value}`;
1022
+ break;
1023
+ case 'contains':
1024
+ result = fieldValue && String(fieldValue).includes(String(value));
1025
+ description = `Campo custom contiene "${value}"`;
1026
+ break;
1027
+ case 'greater_than':
1028
+ result = fieldValue && Number(fieldValue) > Number(value);
1029
+ description = `Campo custom > ${value}`;
1030
+ break;
1031
+ case 'less_than':
1032
+ result = fieldValue && Number(fieldValue) < Number(value);
1033
+ description = `Campo custom < ${value}`;
1034
+ break;
1035
+ case 'is_empty':
1036
+ result = fieldValue === undefined || fieldValue === null || fieldValue === '';
1037
+ description = 'Campo custom è vuoto';
1038
+ break;
1039
+ case 'is_not_empty':
1040
+ result = fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
1041
+ description = 'Campo custom non è vuoto';
1042
+ break;
1043
+ default:
1044
+ result = true;
1045
+ description = 'Campo custom (operatore sconosciuto)';
1046
+ }
1047
+ break;
1048
+ default:
1049
+ result = true;
1050
+ description = `Tipo sconosciuto: ${type}`;
1051
+ }
1052
+
1053
+ const finalResult = negated ? !result : result;
1054
+
1055
+ conditionResults.push({
1056
+ type,
1057
+ expected: value,
1058
+ actual,
1059
+ negated,
1060
+ result: finalResult,
1061
+ description: negated ? `NON (${description})` : description,
1062
+ });
1063
+
1064
+ return finalResult;
1065
+ };
1066
+
1067
+ let wouldTrigger = false;
1068
+
1069
+ if (triggerConditions.conditionsList && triggerConditions.conditionsList.length > 0) {
1070
+ const results = triggerConditions.conditionsList.map((c: any) => evaluateCondition(c));
1071
+
1072
+ const andGroups: boolean[][] = [[]];
1073
+ let currentGroup = 0;
1074
+
1075
+ for (let i = 0; i < triggerConditions.conditionsList.length; i++) {
1076
+ andGroups[currentGroup].push(results[i]);
1077
+
1078
+ if (i < triggerConditions.conditionsList.length - 1) {
1079
+ const nextOp = triggerConditions.conditionsList[i].nextOperator || 'AND';
1080
+ if (nextOp === 'OR') {
1081
+ currentGroup++;
1082
+ andGroups[currentGroup] = [];
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ const groupResults = andGroups.map(group => group.every(r => r));
1088
+ wouldTrigger = groupResults.some(r => r);
1089
+ } else {
1090
+ if (triggerConditions.triggerOn !== simulationData.triggerOn) {
1091
+ conditionResults.push({
1092
+ type: 'trigger_event',
1093
+ expected: triggerConditions.triggerOn,
1094
+ actual: simulationData.triggerOn,
1095
+ negated: false,
1096
+ result: false,
1097
+ description: `Evento: ${triggerConditions.triggerOn}`,
1098
+ });
1099
+ wouldTrigger = false;
1100
+ } else {
1101
+ wouldTrigger = true;
1102
+ }
1103
+ }
1104
+
1105
+ const actions = trigger.actions as any;
1106
+ const actionDescriptions: string[] = [];
1107
+
1108
+ if (actions?.changeStatus) {
1109
+ actionDescriptions.push(`📊 Cambierebbe stato a: "${actions.changeStatus}"`);
1110
+ }
1111
+ if (actions?.assignSupplier) {
1112
+ const supplier = await ctx.db.get(actions.assignSupplier);
1113
+ actionDescriptions.push(`👤 Assegnerebbe a: ${(supplier as any)?.name || 'Fornitore sconosciuto'}`);
1114
+ }
1115
+ if (actions?.setPriority) {
1116
+ const priorityLabels: Record<string, string> = { low: '🟢 Bassa', medium: '🟡 Media', high: '🟠 Alta' };
1117
+ actionDescriptions.push(`⚡ Cambierebbe priorità a: ${priorityLabels[actions.setPriority] || actions.setPriority}`);
1118
+ }
1119
+ if (actions?.setSlaHours) {
1120
+ actionDescriptions.push(`⏱️ Imposterebbe SLA: ${actions.setSlaHours} ore`);
1121
+ }
1122
+ if (actions?.applySlaRule) {
1123
+ actionDescriptions.push(`⏱️ Applicherebbe regola SLA automatica`);
1124
+ }
1125
+ if (actions?.addNote) {
1126
+ actionDescriptions.push(`📝 Aggiungerebbe nota: "${actions.addNote.substring(0, 50)}${actions.addNote.length > 50 ? '...' : ''}"`);
1127
+ }
1128
+ if (actions?.sendNotification) {
1129
+ const recipientTypes: Record<string, string> = {
1130
+ admin: 'Admin',
1131
+ supplier: 'Fornitore assegnato',
1132
+ ticket_creator: 'Creatore ticket',
1133
+ ticket_assignee: 'Assegnatario',
1134
+ specific_user: 'Utente specifico',
1135
+ specific_supplier: 'Fornitore specifico',
1136
+ };
1137
+ const recipients = actions.sendNotification.recipients?.map((r: any) =>
1138
+ typeof r === 'string' ? r : recipientTypes[r.type] || r.type
1139
+ ).join(', ');
1140
+ actionDescriptions.push(`📧 Invierebbe email a: ${recipients}`);
1141
+ }
1142
+
1143
+ return {
1144
+ triggerId: trigger._id,
1145
+ triggerName: trigger.name,
1146
+ wouldTrigger,
1147
+ conditionResults,
1148
+ actionDescriptions,
1149
+ isActive: trigger.isActive,
1150
+ };
1151
+ },
1152
+ });