@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,288 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation, internalQuery } from './_generated/server';
3
+
4
+ export const listComments = query({
5
+ args: {
6
+ taskId: v.id('maintenance_tasks'),
7
+ userRole: v.optional(v.string()),
8
+ },
9
+ handler: async (ctx, args) => {
10
+ const task = await ctx.db.get(args.taskId);
11
+ if (!task) {
12
+ throw new Error('Ticket not found');
13
+ }
14
+
15
+ const comments = await ctx.db
16
+ .query('ticket_comments')
17
+ .withIndex('by_taskId', (q) => q.eq('taskId', args.taskId))
18
+ .collect();
19
+
20
+ const filteredComments = comments.filter((comment) => {
21
+ if (comment.isInternal) {
22
+ return args.userRole === 'admin' || args.userRole === 'supplier';
23
+ }
24
+ return true;
25
+ });
26
+
27
+ const commentsWithUrls = await Promise.all(
28
+ filteredComments.map(async (comment) => {
29
+ let attachmentUrls: string[] = [];
30
+ if (comment.attachments && comment.attachments.length > 0) {
31
+ attachmentUrls = await Promise.all(
32
+ comment.attachments.map(async (storageId) => {
33
+ const url = await ctx.storage.getUrl(storageId);
34
+ return url || '';
35
+ })
36
+ );
37
+ attachmentUrls = attachmentUrls.filter(url => url !== '');
38
+ }
39
+ return {
40
+ ...comment,
41
+ attachmentUrls,
42
+ };
43
+ })
44
+ );
45
+
46
+ return commentsWithUrls.sort((a, b) => a.createdAt - b.createdAt);
47
+ },
48
+ });
49
+
50
+ export const addComment = mutation({
51
+ args: {
52
+ taskId: v.id('maintenance_tasks'),
53
+ content: v.string(),
54
+ isInternal: v.optional(v.boolean()),
55
+ attachments: v.optional(v.array(v.id('_storage'))),
56
+ mentions: v.optional(v.array(v.object({
57
+ userId: v.id('user_profiles'),
58
+ name: v.string(),
59
+ email: v.string(),
60
+ }))),
61
+ authorId: v.string(),
62
+ authorName: v.optional(v.string()),
63
+ authorEmail: v.optional(v.string()),
64
+ authorRole: v.optional(v.string()),
65
+ },
66
+ handler: async (ctx, args) => {
67
+ const task = await ctx.db.get(args.taskId);
68
+ if (!task) {
69
+ throw new Error('Ticket not found');
70
+ }
71
+
72
+ if (args.attachments && args.attachments.length > 5) {
73
+ throw new Error('Maximum 5 attachments allowed');
74
+ }
75
+
76
+ const hasContent = args.content.trim().length > 0;
77
+ const hasAttachments = args.attachments && args.attachments.length > 0;
78
+ if (!hasContent && !hasAttachments) {
79
+ throw new Error('Comment must have either text or attachments');
80
+ }
81
+
82
+ const commentId = await ctx.db.insert('ticket_comments', {
83
+ taskId: args.taskId,
84
+ authorId: args.authorId,
85
+ authorName: args.authorName || args.authorEmail || 'Unknown',
86
+ authorEmail: args.authorEmail || '',
87
+ authorRole: args.authorRole || 'user',
88
+ content: args.content.trim(),
89
+ isInternal: args.isInternal || false,
90
+ isFromApi: false,
91
+ attachments: args.attachments || undefined,
92
+ mentions: args.mentions || undefined,
93
+ createdAt: Date.now(),
94
+ });
95
+
96
+ await ctx.db.patch(args.taskId, {
97
+ updated_at: Date.now(),
98
+ });
99
+
100
+ return commentId;
101
+ },
102
+ });
103
+
104
+ export const generateUploadUrl = mutation({
105
+ args: {},
106
+ handler: async (ctx) => {
107
+ return await ctx.storage.generateUploadUrl();
108
+ },
109
+ });
110
+
111
+ export const addCommentFromApi = mutation({
112
+ args: {
113
+ taskId: v.id('maintenance_tasks'),
114
+ content: v.string(),
115
+ authorEmail: v.optional(v.string()),
116
+ authorName: v.optional(v.string()),
117
+ isInternal: v.optional(v.boolean()),
118
+ },
119
+ handler: async (ctx, args) => {
120
+ const task = await ctx.db.get(args.taskId);
121
+ if (!task) {
122
+ throw new Error('Ticket not found');
123
+ }
124
+
125
+ const commentId = await ctx.db.insert('ticket_comments', {
126
+ taskId: args.taskId,
127
+ authorId: 'api',
128
+ authorName: args.authorName || 'Sistema Esterno',
129
+ authorEmail: args.authorEmail || 'api@external',
130
+ authorRole: 'api',
131
+ content: args.content.trim(),
132
+ isInternal: args.isInternal || false,
133
+ isFromApi: true,
134
+ createdAt: Date.now(),
135
+ });
136
+
137
+ await ctx.db.patch(args.taskId, {
138
+ updated_at: Date.now(),
139
+ });
140
+
141
+ return commentId;
142
+ },
143
+ });
144
+
145
+ export const deleteComment = mutation({
146
+ args: {
147
+ commentId: v.id('ticket_comments'),
148
+ userAuth0Id: v.optional(v.string()),
149
+ userRole: v.optional(v.string()),
150
+ },
151
+ handler: async (ctx, args) => {
152
+ const comment = await ctx.db.get(args.commentId);
153
+ if (!comment) {
154
+ throw new Error('Comment not found');
155
+ }
156
+
157
+ if (args.userRole !== 'admin' && comment.authorId !== args.userAuth0Id) {
158
+ throw new Error('Not authorized to delete this comment');
159
+ }
160
+
161
+ await ctx.db.delete(args.commentId);
162
+ return { success: true };
163
+ },
164
+ });
165
+
166
+ export const updateComment = mutation({
167
+ args: {
168
+ commentId: v.id('ticket_comments'),
169
+ content: v.string(),
170
+ userAuth0Id: v.optional(v.string()),
171
+ userRole: v.optional(v.string()),
172
+ },
173
+ handler: async (ctx, args) => {
174
+ const comment = await ctx.db.get(args.commentId);
175
+ if (!comment) {
176
+ throw new Error('Comment not found');
177
+ }
178
+
179
+ if (args.userRole !== 'admin' && comment.authorId !== args.userAuth0Id) {
180
+ throw new Error('Not authorized to edit this comment');
181
+ }
182
+
183
+ await ctx.db.patch(args.commentId, {
184
+ content: args.content.trim(),
185
+ updatedAt: Date.now(),
186
+ });
187
+
188
+ return { success: true };
189
+ },
190
+ });
191
+
192
+ export const getCommentCount = query({
193
+ args: { taskId: v.id('maintenance_tasks') },
194
+ handler: async (ctx, args) => {
195
+ const comments = await ctx.db
196
+ .query('ticket_comments')
197
+ .withIndex('by_taskId', (q) => q.eq('taskId', args.taskId))
198
+ .collect();
199
+
200
+ return comments.length;
201
+ },
202
+ });
203
+
204
+ export const getUsersForMention = query({
205
+ args: {
206
+ taskId: v.id('maintenance_tasks'),
207
+ searchQuery: v.optional(v.string()),
208
+ excludeUserId: v.optional(v.id('user_profiles')),
209
+ },
210
+ handler: async (ctx, args) => {
211
+ const task = await ctx.db.get(args.taskId);
212
+ if (!task) {
213
+ throw new Error('Ticket not found');
214
+ }
215
+
216
+ let users = await ctx.db
217
+ .query('user_profiles')
218
+ .take(500);
219
+
220
+ users = users.filter(u => u.isActive !== false);
221
+
222
+ if (args.excludeUserId) {
223
+ users = users.filter(u => u._id !== args.excludeUserId);
224
+ }
225
+
226
+ if (args.searchQuery && args.searchQuery.trim()) {
227
+ const search = args.searchQuery.toLowerCase().trim();
228
+ users = users.filter(u =>
229
+ u.name?.toLowerCase().includes(search) ||
230
+ u.email?.toLowerCase().includes(search)
231
+ );
232
+ }
233
+
234
+ const usersWithRoles = await Promise.all(
235
+ users.map(async (u) => {
236
+ const role = u.roleId ? await ctx.db.get(u.roleId) : null;
237
+ const roleName = (role as any)?.name || u.role || 'user';
238
+
239
+ return {
240
+ _id: u._id,
241
+ name: u.name || u.email,
242
+ email: u.email,
243
+ role: roleName,
244
+ };
245
+ })
246
+ );
247
+
248
+ const sortedUsers = usersWithRoles.sort((a, b) => {
249
+ if (a.role === 'admin' && b.role !== 'admin') return -1;
250
+ if (b.role === 'admin' && a.role !== 'admin') return 1;
251
+
252
+ return (a.name || '').localeCompare(b.name || '');
253
+ });
254
+
255
+ return sortedUsers.slice(0, 20);
256
+ },
257
+ });
258
+
259
+ export const listCommentsInternal = internalQuery({
260
+ args: { taskId: v.string() },
261
+ handler: async (ctx, args) => {
262
+ let taskIdTyped;
263
+ try {
264
+ taskIdTyped = args.taskId as any;
265
+ } catch {
266
+ throw new Error('Invalid taskId format');
267
+ }
268
+
269
+ const comments = await ctx.db
270
+ .query('ticket_comments')
271
+ .withIndex('by_taskId', (q) => q.eq('taskId', taskIdTyped))
272
+ .collect();
273
+
274
+ return comments
275
+ .sort((a, b) => a.createdAt - b.createdAt)
276
+ .map((c) => ({
277
+ _id: c._id,
278
+ authorName: c.authorName,
279
+ authorEmail: c.authorEmail,
280
+ authorRole: c.authorRole,
281
+ content: c.content,
282
+ isInternal: c.isInternal,
283
+ isFromApi: c.isFromApi,
284
+ createdAt: c.createdAt,
285
+ updatedAt: c.updatedAt,
286
+ }));
287
+ },
288
+ });
@@ -0,0 +1,260 @@
1
+ import { v } from 'convex/values';
2
+ import { query, mutation } from './_generated/server';
3
+
4
+ // Lista tutti i campi custom attivi per un device specifico
5
+ export const listActiveCustomFieldsForDevice = query({
6
+ args: {
7
+ deviceId: v.id('devices'),
8
+ },
9
+ handler: async (ctx, args) => {
10
+ // Get device to check category
11
+ const device = await ctx.db.get(args.deviceId);
12
+ if (!device) {
13
+ return [];
14
+ }
15
+
16
+ const allFields = await ctx.db
17
+ .query('ticket_custom_fields')
18
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', true))
19
+ .collect();
20
+
21
+ // Filter fields based on applicability
22
+ const applicableFields = allFields.filter((field) => {
23
+ // If applies to all, include it
24
+ if (field.applyToAll) {
25
+ return true;
26
+ }
27
+
28
+ // Check if specific device is included
29
+ if (field.deviceIds && field.deviceIds.includes(args.deviceId)) {
30
+ return true;
31
+ }
32
+
33
+ // Check if device category is included
34
+ if (field.categories && field.categories.includes(device.category)) {
35
+ return true;
36
+ }
37
+
38
+ return false;
39
+ });
40
+
41
+ // Ordina per order
42
+ return applicableFields.sort((a, b) => a.order - b.order);
43
+ },
44
+ });
45
+
46
+ // Lista tutti i campi custom attivi (per retrocompatibilità)
47
+ export const listActiveCustomFields = query({
48
+ args: {},
49
+ handler: async (ctx) => {
50
+ const fields = await ctx.db
51
+ .query('ticket_custom_fields')
52
+ .withIndex('by_isActive', (q: any) => q.eq('isActive', true))
53
+ .collect();
54
+
55
+ // Ordina per order
56
+ return fields.sort((a, b) => a.order - b.order);
57
+ },
58
+ });
59
+
60
+ // Lista tutti i campi custom (admin only)
61
+ export const listAllCustomFields = query({
62
+ args: {},
63
+ handler: async (ctx) => {
64
+ const fields = await ctx.db
65
+ .query('ticket_custom_fields')
66
+ .collect();
67
+
68
+ return fields.sort((a, b) => a.order - b.order);
69
+ },
70
+ });
71
+
72
+ // Crea un nuovo campo custom (admin only)
73
+ export const createCustomField = mutation({
74
+ args: {
75
+ name: v.string(),
76
+ fieldType: v.union(
77
+ v.literal('text'),
78
+ v.literal('textarea'),
79
+ v.literal('number'),
80
+ v.literal('select'),
81
+ v.literal('multiselect'),
82
+ v.literal('date'),
83
+ v.literal('checkbox'),
84
+ v.literal('radio')
85
+ ),
86
+ options: v.optional(v.array(v.string())),
87
+ isRequired: v.boolean(),
88
+ placeholder: v.optional(v.string()),
89
+ helpText: v.optional(v.string()),
90
+ order: v.optional(v.number()),
91
+ applyToAll: v.boolean(),
92
+ categories: v.optional(v.array(v.string())),
93
+ deviceIds: v.optional(v.array(v.id('devices'))),
94
+ parentFieldId: v.optional(v.id('ticket_custom_fields')),
95
+ parentFieldValue: v.optional(v.string()),
96
+ },
97
+ handler: async (ctx, args) => {
98
+ // Se order non è specificato, mettilo alla fine
99
+ let order = args.order;
100
+ if (order === undefined) {
101
+ const allFields = await ctx.db.query('ticket_custom_fields').collect();
102
+ order = allFields.length > 0 ? Math.max(...allFields.map(f => f.order)) + 1 : 0;
103
+ }
104
+
105
+ const fieldId = await ctx.db.insert('ticket_custom_fields', {
106
+ name: args.name,
107
+ fieldType: args.fieldType,
108
+ options: args.options,
109
+ isRequired: args.isRequired,
110
+ placeholder: args.placeholder,
111
+ helpText: args.helpText,
112
+ order,
113
+ isActive: true,
114
+ applyToAll: args.applyToAll,
115
+ categories: args.categories,
116
+ deviceIds: args.deviceIds,
117
+ parentFieldId: args.parentFieldId,
118
+ parentFieldValue: args.parentFieldValue,
119
+ createdAt: Date.now(),
120
+ updatedAt: Date.now(),
121
+ });
122
+
123
+ return fieldId;
124
+ },
125
+ });
126
+
127
+ // Aggiorna un campo custom (admin only)
128
+ export const updateCustomField = mutation({
129
+ args: {
130
+ fieldId: v.id('ticket_custom_fields'),
131
+ name: v.optional(v.string()),
132
+ fieldType: v.optional(v.union(
133
+ v.literal('text'),
134
+ v.literal('textarea'),
135
+ v.literal('number'),
136
+ v.literal('select'),
137
+ v.literal('multiselect'),
138
+ v.literal('date'),
139
+ v.literal('checkbox'),
140
+ v.literal('radio')
141
+ )),
142
+ options: v.optional(v.array(v.string())),
143
+ isRequired: v.optional(v.boolean()),
144
+ placeholder: v.optional(v.string()),
145
+ helpText: v.optional(v.string()),
146
+ order: v.optional(v.number()),
147
+ isActive: v.optional(v.boolean()),
148
+ applyToAll: v.optional(v.boolean()),
149
+ categories: v.optional(v.array(v.string())),
150
+ deviceIds: v.optional(v.array(v.id('devices'))),
151
+ parentFieldId: v.optional(v.id('ticket_custom_fields')),
152
+ parentFieldValue: v.optional(v.string()),
153
+ childFields: v.optional(v.array(v.object({
154
+ parentValue: v.string(),
155
+ childFieldIds: v.array(v.id('ticket_custom_fields')),
156
+ }))),
157
+ },
158
+ handler: async (ctx, args) => {
159
+ const { fieldId, ...updates } = args;
160
+
161
+ await ctx.db.patch(fieldId, {
162
+ ...updates,
163
+ updatedAt: Date.now(),
164
+ });
165
+
166
+ return fieldId;
167
+ },
168
+ });
169
+
170
+ // Elimina un campo custom (admin only)
171
+ export const deleteCustomField = mutation({
172
+ args: {
173
+ fieldId: v.id('ticket_custom_fields'),
174
+ },
175
+ handler: async (ctx, args) => {
176
+ await ctx.db.delete(args.fieldId);
177
+ return { success: true };
178
+ },
179
+ });
180
+
181
+ // Riordina i campi custom (admin only)
182
+ export const reorderCustomFields = mutation({
183
+ args: {
184
+ fieldOrders: v.array(v.object({
185
+ fieldId: v.id('ticket_custom_fields'),
186
+ order: v.number(),
187
+ })),
188
+ },
189
+ handler: async (ctx, args) => {
190
+ for (const { fieldId, order } of args.fieldOrders) {
191
+ await ctx.db.patch(fieldId, {
192
+ order,
193
+ updatedAt: Date.now(),
194
+ });
195
+ }
196
+
197
+ return { success: true };
198
+ },
199
+ });
200
+
201
+ // Get all unique device categories (admin only)
202
+ export const getDeviceCategories = query({
203
+ args: {},
204
+ handler: async (ctx) => {
205
+ const devices = await ctx.db.query('devices').take(3000);
206
+ const categories = new Set(devices.map(d => d.category));
207
+
208
+ return Array.from(categories).sort();
209
+ },
210
+ });
211
+
212
+ // Get child fields for a specific parent field and value
213
+ export const getChildFields = query({
214
+ args: {
215
+ parentFieldId: v.id('ticket_custom_fields'),
216
+ parentValue: v.string(),
217
+ },
218
+ handler: async (ctx, args) => {
219
+ const childFields = await ctx.db
220
+ .query('ticket_custom_fields')
221
+ .withIndex('by_parentFieldId', (q: any) => q.eq('parentFieldId', args.parentFieldId))
222
+ .filter((q) => q.eq(q.field('parentFieldValue'), args.parentValue))
223
+ .filter((q) => q.eq(q.field('isActive'), true))
224
+ .collect();
225
+
226
+ return childFields.sort((a, b) => a.order - b.order);
227
+ },
228
+ });
229
+
230
+ // Get all child fields for a parent (grouped by parent value)
231
+ export const getAllChildFieldsForParent = query({
232
+ args: {
233
+ parentFieldId: v.id('ticket_custom_fields'),
234
+ },
235
+ handler: async (ctx, args) => {
236
+ const childFields = await ctx.db
237
+ .query('ticket_custom_fields')
238
+ .withIndex('by_parentFieldId', (q: any) => q.eq('parentFieldId', args.parentFieldId))
239
+ .filter((q) => q.eq(q.field('isActive'), true))
240
+ .collect();
241
+
242
+ // Group by parent value
243
+ const grouped: Record<string, any[]> = {};
244
+ for (const field of childFields) {
245
+ if (field.parentFieldValue) {
246
+ if (!grouped[field.parentFieldValue]) {
247
+ grouped[field.parentFieldValue] = [];
248
+ }
249
+ grouped[field.parentFieldValue].push(field);
250
+ }
251
+ }
252
+
253
+ // Sort each group
254
+ for (const key in grouped) {
255
+ grouped[key].sort((a, b) => a.order - b.order);
256
+ }
257
+
258
+ return grouped;
259
+ },
260
+ });