@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.
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +233 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +94 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +1444 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/apiKeys.d.ts +69 -0
- package/dist/component/apiKeys.d.ts.map +1 -0
- package/dist/component/apiKeys.js +207 -0
- package/dist/component/apiKeys.js.map +1 -0
- package/dist/component/clinics.d.ts +103 -0
- package/dist/component/clinics.d.ts.map +1 -0
- package/dist/component/clinics.js +126 -0
- package/dist/component/clinics.js.map +1 -0
- package/dist/component/contracts.d.ts +85 -0
- package/dist/component/contracts.d.ts.map +1 -0
- package/dist/component/contracts.js +115 -0
- package/dist/component/contracts.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/crons.d.ts +3 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +7 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/dashboardStats.d.ts +14 -0
- package/dist/component/dashboardStats.d.ts.map +1 -0
- package/dist/component/dashboardStats.js +136 -0
- package/dist/component/dashboardStats.js.map +1 -0
- package/dist/component/dashboardStatsCache.d.ts +32 -0
- package/dist/component/dashboardStatsCache.d.ts.map +1 -0
- package/dist/component/dashboardStatsCache.js +129 -0
- package/dist/component/dashboardStatsCache.js.map +1 -0
- package/dist/component/deviceCategories.d.ts +108 -0
- package/dist/component/deviceCategories.d.ts.map +1 -0
- package/dist/component/deviceCategories.js +254 -0
- package/dist/component/deviceCategories.js.map +1 -0
- package/dist/component/deviceQuestions.d.ts +129 -0
- package/dist/component/deviceQuestions.d.ts.map +1 -0
- package/dist/component/deviceQuestions.js +175 -0
- package/dist/component/deviceQuestions.js.map +1 -0
- package/dist/component/deviceRepairHistory.d.ts +30 -0
- package/dist/component/deviceRepairHistory.d.ts.map +1 -0
- package/dist/component/deviceRepairHistory.js +84 -0
- package/dist/component/deviceRepairHistory.js.map +1 -0
- package/dist/component/deviceStatus.d.ts +63 -0
- package/dist/component/deviceStatus.d.ts.map +1 -0
- package/dist/component/deviceStatus.js +58 -0
- package/dist/component/deviceStatus.js.map +1 -0
- package/dist/component/devices.d.ts +299 -0
- package/dist/component/devices.d.ts.map +1 -0
- package/dist/component/devices.js +587 -0
- package/dist/component/devices.js.map +1 -0
- package/dist/component/emailHelpers.d.ts +17 -0
- package/dist/component/emailHelpers.d.ts.map +1 -0
- package/dist/component/emailHelpers.js +39 -0
- package/dist/component/emailHelpers.js.map +1 -0
- package/dist/component/emails.d.ts +56 -0
- package/dist/component/emails.d.ts.map +1 -0
- package/dist/component/emails.js +58 -0
- package/dist/component/emails.js.map +1 -0
- package/dist/component/http.d.ts +3 -0
- package/dist/component/http.d.ts.map +1 -0
- package/dist/component/http.js +229 -0
- package/dist/component/http.js.map +1 -0
- package/dist/component/maintenanceTasks.d.ts +733 -0
- package/dist/component/maintenanceTasks.d.ts.map +1 -0
- package/dist/component/maintenanceTasks.js +937 -0
- package/dist/component/maintenanceTasks.js.map +1 -0
- package/dist/component/roles.d.ts +75 -0
- package/dist/component/roles.d.ts.map +1 -0
- package/dist/component/roles.js +98 -0
- package/dist/component/roles.js.map +1 -0
- package/dist/component/schema.d.ts +1295 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +724 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/slaMonitoring.d.ts +32 -0
- package/dist/component/slaMonitoring.d.ts.map +1 -0
- package/dist/component/slaMonitoring.js +111 -0
- package/dist/component/slaMonitoring.js.map +1 -0
- package/dist/component/slaRules.d.ts +72 -0
- package/dist/component/slaRules.d.ts.map +1 -0
- package/dist/component/slaRules.js +193 -0
- package/dist/component/slaRules.js.map +1 -0
- package/dist/component/sparePartOrders.d.ts +177 -0
- package/dist/component/sparePartOrders.d.ts.map +1 -0
- package/dist/component/sparePartOrders.js +243 -0
- package/dist/component/sparePartOrders.js.map +1 -0
- package/dist/component/spareParts.d.ts +472 -0
- package/dist/component/spareParts.d.ts.map +1 -0
- package/dist/component/spareParts.js +319 -0
- package/dist/component/spareParts.js.map +1 -0
- package/dist/component/supplierCategories.d.ts +22 -0
- package/dist/component/supplierCategories.d.ts.map +1 -0
- package/dist/component/supplierCategories.js +64 -0
- package/dist/component/supplierCategories.js.map +1 -0
- package/dist/component/suppliers.d.ts +94 -0
- package/dist/component/suppliers.d.ts.map +1 -0
- package/dist/component/suppliers.js +195 -0
- package/dist/component/suppliers.js.map +1 -0
- package/dist/component/ticketComments.d.ts +89 -0
- package/dist/component/ticketComments.d.ts.map +1 -0
- package/dist/component/ticketComments.js +246 -0
- package/dist/component/ticketComments.js.map +1 -0
- package/dist/component/ticketCustomFields.d.ts +149 -0
- package/dist/component/ticketCustomFields.d.ts.map +1 -0
- package/dist/component/ticketCustomFields.js +215 -0
- package/dist/component/ticketCustomFields.js.map +1 -0
- package/dist/component/ticketExport.d.ts +83 -0
- package/dist/component/ticketExport.d.ts.map +1 -0
- package/dist/component/ticketExport.js +182 -0
- package/dist/component/ticketExport.js.map +1 -0
- package/dist/component/ticketHistory.d.ts +57 -0
- package/dist/component/ticketHistory.d.ts.map +1 -0
- package/dist/component/ticketHistory.js +81 -0
- package/dist/component/ticketHistory.js.map +1 -0
- package/dist/component/ticketMacros.d.ts +141 -0
- package/dist/component/ticketMacros.d.ts.map +1 -0
- package/dist/component/ticketMacros.js +255 -0
- package/dist/component/ticketMacros.js.map +1 -0
- package/dist/component/ticketStatuses.d.ts +60 -0
- package/dist/component/ticketStatuses.d.ts.map +1 -0
- package/dist/component/ticketStatuses.js +110 -0
- package/dist/component/ticketStatuses.js.map +1 -0
- package/dist/component/ticketTriggers.d.ts +408 -0
- package/dist/component/ticketTriggers.d.ts.map +1 -0
- package/dist/component/ticketTriggers.js +941 -0
- package/dist/component/ticketTriggers.js.map +1 -0
- package/dist/component/userProfiles.d.ts +259 -0
- package/dist/component/userProfiles.d.ts.map +1 -0
- package/dist/component/userProfiles.js +634 -0
- package/dist/component/userProfiles.js.map +1 -0
- package/dist/component/vendorArticles.d.ts +64 -0
- package/dist/component/vendorArticles.d.ts.map +1 -0
- package/dist/component/vendorArticles.js +116 -0
- package/dist/component/vendorArticles.js.map +1 -0
- package/dist/test.d.ts +1302 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +7 -0
- package/dist/test.js.map +1 -0
- package/package.json +71 -0
- package/src/client/index.ts +344 -0
- package/src/component/_generated/api.ts +110 -0
- package/src/component/_generated/component.ts +2460 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/apiKeys.ts +229 -0
- package/src/component/clinics.ts +136 -0
- package/src/component/contracts.ts +136 -0
- package/src/component/convex.config.js +2 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/crons.ts +18 -0
- package/src/component/dashboardStats.ts +141 -0
- package/src/component/dashboardStatsCache.ts +145 -0
- package/src/component/deviceCategories.ts +280 -0
- package/src/component/deviceQuestions.ts +225 -0
- package/src/component/deviceRepairHistory.ts +94 -0
- package/src/component/deviceStatus.ts +79 -0
- package/src/component/devices.ts +645 -0
- package/src/component/emailHelpers.ts +38 -0
- package/src/component/emails.ts +61 -0
- package/src/component/http.ts +231 -0
- package/src/component/maintenanceTasks.ts +1003 -0
- package/src/component/roles.ts +99 -0
- package/src/component/schema.ts +842 -0
- package/src/component/slaMonitoring.ts +125 -0
- package/src/component/slaRules.ts +231 -0
- package/src/component/sparePartOrders.ts +290 -0
- package/src/component/spareParts.ts +362 -0
- package/src/component/supplierCategories.ts +65 -0
- package/src/component/suppliers.ts +234 -0
- package/src/component/ticketComments.ts +288 -0
- package/src/component/ticketCustomFields.ts +260 -0
- package/src/component/ticketExport.ts +220 -0
- package/src/component/ticketHistory.ts +106 -0
- package/src/component/ticketMacros.ts +291 -0
- package/src/component/ticketStatuses.ts +109 -0
- package/src/component/ticketTriggers.ts +1152 -0
- package/src/component/userProfiles.ts +745 -0
- package/src/component/vendorArticles.ts +139 -0
- 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
|
+
});
|