@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,125 @@
|
|
|
1
|
+
import { v } from 'convex/values';
|
|
2
|
+
import { internalMutation, internalAction, internalQuery } from './_generated/server';
|
|
3
|
+
import { internal } from './_generated/api';
|
|
4
|
+
|
|
5
|
+
// NOTE: Notification sending (sendSLABreachNotifications, sendSLAWarningNotifications)
|
|
6
|
+
// is NOT included in the component. The parent app is responsible for:
|
|
7
|
+
// 1. Running checkSLAStatus periodically (via cron)
|
|
8
|
+
// 2. After breaches/warnings are marked, querying for newly marked tickets
|
|
9
|
+
// 3. Sending email notifications via its own emailActions
|
|
10
|
+
|
|
11
|
+
export const checkSLAStatus = internalAction({
|
|
12
|
+
args: {},
|
|
13
|
+
handler: async (ctx): Promise<{ checked: number; warnings: number; breaches: number }> => {
|
|
14
|
+
try {
|
|
15
|
+
const tickets: any = await ctx.runQuery(internal.slaMonitoring.listTicketsNeedingSLACheck, {});
|
|
16
|
+
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
let warningsCount = 0;
|
|
19
|
+
let breachesCount = 0;
|
|
20
|
+
|
|
21
|
+
for (const ticket of tickets) {
|
|
22
|
+
const timeUntilDeadline = ticket.slaDeadline - now;
|
|
23
|
+
const oneHour = 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
if (timeUntilDeadline < 0 && !ticket.slaBreached) {
|
|
26
|
+
breachesCount++;
|
|
27
|
+
|
|
28
|
+
await ctx.runMutation(internal.slaMonitoring.markSLABreached, {
|
|
29
|
+
ticketId: ticket._id,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else if (timeUntilDeadline > 0 && timeUntilDeadline < oneHour && !ticket.slaWarningSent) {
|
|
33
|
+
warningsCount++;
|
|
34
|
+
|
|
35
|
+
await ctx.runMutation(internal.slaMonitoring.markSLAWarningSent, {
|
|
36
|
+
ticketId: ticket._id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
checked: tickets.length,
|
|
43
|
+
warnings: warningsCount,
|
|
44
|
+
breaches: breachesCount,
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error checking SLA status:', error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const listTicketsNeedingSLACheck = internalQuery({
|
|
54
|
+
args: {},
|
|
55
|
+
handler: async (ctx) => {
|
|
56
|
+
const terminalStatuses = ['closed', 'completed', 'cancelled', 'resolved'];
|
|
57
|
+
|
|
58
|
+
const ticketsWithSLA = await ctx.db
|
|
59
|
+
.query('maintenance_tasks')
|
|
60
|
+
.withIndex('by_slaDeadline')
|
|
61
|
+
.order('asc')
|
|
62
|
+
.take(150);
|
|
63
|
+
|
|
64
|
+
return ticketsWithSLA
|
|
65
|
+
.filter(t => t.slaDeadline && !terminalStatuses.includes(t.status))
|
|
66
|
+
.map(t => ({
|
|
67
|
+
_id: t._id,
|
|
68
|
+
ticketNumber: t.ticketNumber,
|
|
69
|
+
status: t.status,
|
|
70
|
+
slaDeadline: t.slaDeadline,
|
|
71
|
+
slaBreached: t.slaBreached,
|
|
72
|
+
slaWarningSent: t.slaWarningSent,
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const listAllTicketsForSLA = internalQuery({
|
|
78
|
+
args: {},
|
|
79
|
+
handler: async (ctx) => {
|
|
80
|
+
const terminalStatuses = ['closed', 'completed', 'cancelled', 'resolved'];
|
|
81
|
+
|
|
82
|
+
const tasks = await ctx.db
|
|
83
|
+
.query('maintenance_tasks')
|
|
84
|
+
.withIndex('by_slaDeadline')
|
|
85
|
+
.order('asc')
|
|
86
|
+
.take(500);
|
|
87
|
+
|
|
88
|
+
return tasks
|
|
89
|
+
.filter(t => t.slaDeadline && !terminalStatuses.includes(t.status))
|
|
90
|
+
.map(t => ({
|
|
91
|
+
_id: t._id,
|
|
92
|
+
ticketNumber: t.ticketNumber,
|
|
93
|
+
status: t.status,
|
|
94
|
+
supplierId: t.supplierId,
|
|
95
|
+
slaDeadline: t.slaDeadline,
|
|
96
|
+
slaBreached: t.slaBreached,
|
|
97
|
+
slaWarningSent: t.slaWarningSent,
|
|
98
|
+
deviceName: '',
|
|
99
|
+
clinicName: '',
|
|
100
|
+
supplierName: null,
|
|
101
|
+
}));
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const markSLABreached = internalMutation({
|
|
106
|
+
args: {
|
|
107
|
+
ticketId: v.id('maintenance_tasks'),
|
|
108
|
+
},
|
|
109
|
+
handler: async (ctx, args) => {
|
|
110
|
+
await ctx.db.patch(args.ticketId, {
|
|
111
|
+
slaBreached: true,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const markSLAWarningSent = internalMutation({
|
|
117
|
+
args: {
|
|
118
|
+
ticketId: v.id('maintenance_tasks'),
|
|
119
|
+
},
|
|
120
|
+
handler: async (ctx, args) => {
|
|
121
|
+
await ctx.db.patch(args.ticketId, {
|
|
122
|
+
slaWarningSent: true,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { v } from 'convex/values';
|
|
2
|
+
import { query, mutation } from './_generated/server';
|
|
3
|
+
|
|
4
|
+
// List all SLA rules (admin only)
|
|
5
|
+
export const listSLARules = query({
|
|
6
|
+
args: {},
|
|
7
|
+
handler: async (ctx) => {
|
|
8
|
+
const rules = await ctx.db.query('sla_rules').collect();
|
|
9
|
+
|
|
10
|
+
// Enrich with supplier info
|
|
11
|
+
const enrichedRules = await Promise.all(
|
|
12
|
+
rules.map(async (rule) => {
|
|
13
|
+
const supplier = await ctx.db.get(rule.supplierId);
|
|
14
|
+
return {
|
|
15
|
+
...rule,
|
|
16
|
+
supplierName: supplier?.name || 'Unknown',
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return enrichedRules;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Get all unique device categories (admin only)
|
|
26
|
+
export const getDeviceCategories = query({
|
|
27
|
+
args: {},
|
|
28
|
+
handler: async (ctx) => {
|
|
29
|
+
const devices = await ctx.db.query('devices').take(3000);
|
|
30
|
+
const categories = [...new Set(devices.map(d => d.category))].sort();
|
|
31
|
+
|
|
32
|
+
return categories;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Get SLA rules for a specific supplier
|
|
37
|
+
export const getSLARulesBySupplier = query({
|
|
38
|
+
args: { supplierId: v.id('suppliers') },
|
|
39
|
+
handler: async (ctx, args) => {
|
|
40
|
+
const rules = await ctx.db
|
|
41
|
+
.query('sla_rules')
|
|
42
|
+
.withIndex('by_supplierId', (q: any) => q.eq('supplierId', args.supplierId))
|
|
43
|
+
.collect();
|
|
44
|
+
|
|
45
|
+
return rules;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Get SLA rule for supplier, priority and optionally device category
|
|
50
|
+
export const getSLARule = query({
|
|
51
|
+
args: {
|
|
52
|
+
supplierId: v.id('suppliers'),
|
|
53
|
+
priority: v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
|
|
54
|
+
deviceCategory: v.optional(v.string()),
|
|
55
|
+
},
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
// Prima cerca una regola specifica per la categoria
|
|
58
|
+
if (args.deviceCategory) {
|
|
59
|
+
const specificRule = await ctx.db
|
|
60
|
+
.query('sla_rules')
|
|
61
|
+
.withIndex('by_supplierId_category_priority', (q: any) =>
|
|
62
|
+
q.eq('supplierId', args.supplierId)
|
|
63
|
+
.eq('deviceCategory', args.deviceCategory)
|
|
64
|
+
.eq('priority', args.priority)
|
|
65
|
+
)
|
|
66
|
+
.first();
|
|
67
|
+
|
|
68
|
+
if (specificRule) {
|
|
69
|
+
return specificRule;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Se non trova una regola specifica, cerca una regola generica (senza categoria)
|
|
74
|
+
const genericRule = await ctx.db
|
|
75
|
+
.query('sla_rules')
|
|
76
|
+
.withIndex('by_supplierId_category_priority', (q: any) =>
|
|
77
|
+
q.eq('supplierId', args.supplierId)
|
|
78
|
+
.eq('deviceCategory', undefined)
|
|
79
|
+
.eq('priority', args.priority)
|
|
80
|
+
)
|
|
81
|
+
.first();
|
|
82
|
+
|
|
83
|
+
return genericRule;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Create SLA rule (admin only)
|
|
88
|
+
export const createSLARule = mutation({
|
|
89
|
+
args: {
|
|
90
|
+
name: v.optional(v.string()),
|
|
91
|
+
supplierId: v.id('suppliers'),
|
|
92
|
+
deviceCategory: v.optional(v.string()), // Opzionale = SLA generico
|
|
93
|
+
priority: v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
|
|
94
|
+
responseTimeHours: v.number(),
|
|
95
|
+
resolutionTimeHours: v.number(),
|
|
96
|
+
description: v.optional(v.string()),
|
|
97
|
+
},
|
|
98
|
+
handler: async (ctx, args) => {
|
|
99
|
+
// Verify supplier exists
|
|
100
|
+
const supplier = await ctx.db.get(args.supplierId);
|
|
101
|
+
if (!supplier) {
|
|
102
|
+
throw new Error(`Supplier with ID ${args.supplierId} not found`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if rule already exists for this supplier, category and priority
|
|
106
|
+
const existing = await ctx.db
|
|
107
|
+
.query('sla_rules')
|
|
108
|
+
.withIndex('by_supplierId_category_priority', (q: any) =>
|
|
109
|
+
q.eq('supplierId', args.supplierId)
|
|
110
|
+
.eq('deviceCategory', args.deviceCategory)
|
|
111
|
+
.eq('priority', args.priority)
|
|
112
|
+
)
|
|
113
|
+
.first();
|
|
114
|
+
|
|
115
|
+
if (existing) {
|
|
116
|
+
const priorityLabels: Record<string, string> = {
|
|
117
|
+
low: 'Bassa',
|
|
118
|
+
medium: 'Media',
|
|
119
|
+
high: 'Alta'
|
|
120
|
+
};
|
|
121
|
+
const categoryText = args.deviceCategory
|
|
122
|
+
? `per la categoria "${args.deviceCategory}"`
|
|
123
|
+
: 'generica (tutte le attrezzature)';
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Esiste già una regola SLA ${categoryText} per il fornitore "${supplier?.name}" con priorità "${priorityLabels[args.priority]}". ` +
|
|
126
|
+
`Modifica la regola esistente o scegli un'altra combinazione.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ruleId = await ctx.db.insert('sla_rules', {
|
|
131
|
+
name: args.name,
|
|
132
|
+
supplierId: args.supplierId,
|
|
133
|
+
deviceCategory: args.deviceCategory,
|
|
134
|
+
priority: args.priority,
|
|
135
|
+
responseTimeHours: args.responseTimeHours,
|
|
136
|
+
resolutionTimeHours: args.resolutionTimeHours,
|
|
137
|
+
description: args.description,
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
updatedAt: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return ruleId;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Update SLA rule (admin only)
|
|
147
|
+
export const updateSLARule = mutation({
|
|
148
|
+
args: {
|
|
149
|
+
ruleId: v.id('sla_rules'),
|
|
150
|
+
name: v.optional(v.string()),
|
|
151
|
+
supplierId: v.optional(v.id('suppliers')),
|
|
152
|
+
deviceCategory: v.optional(v.string()),
|
|
153
|
+
priority: v.optional(v.union(v.literal('low'), v.literal('medium'), v.literal('high'))),
|
|
154
|
+
responseTimeHours: v.number(),
|
|
155
|
+
resolutionTimeHours: v.number(),
|
|
156
|
+
description: v.optional(v.string()),
|
|
157
|
+
},
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
// Get current rule
|
|
160
|
+
const currentRule = await ctx.db.get(args.ruleId);
|
|
161
|
+
if (!currentRule) {
|
|
162
|
+
throw new Error('Regola SLA non trovata');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Determine new values
|
|
166
|
+
const newSupplierId = args.supplierId || currentRule.supplierId;
|
|
167
|
+
const newDeviceCategory = args.deviceCategory !== undefined ? args.deviceCategory : currentRule.deviceCategory;
|
|
168
|
+
const newPriority = args.priority || currentRule.priority;
|
|
169
|
+
|
|
170
|
+
// Check if the new combination already exists (excluding current rule)
|
|
171
|
+
if (args.supplierId || args.deviceCategory !== undefined || args.priority) {
|
|
172
|
+
const existing = await ctx.db
|
|
173
|
+
.query('sla_rules')
|
|
174
|
+
.withIndex('by_supplierId_category_priority', (q: any) =>
|
|
175
|
+
q.eq('supplierId', newSupplierId)
|
|
176
|
+
.eq('deviceCategory', newDeviceCategory || undefined)
|
|
177
|
+
.eq('priority', newPriority)
|
|
178
|
+
)
|
|
179
|
+
.first();
|
|
180
|
+
|
|
181
|
+
if (existing && existing._id !== args.ruleId) {
|
|
182
|
+
const supplier = await ctx.db.get(newSupplierId);
|
|
183
|
+
const priorityLabels: Record<string, string> = {
|
|
184
|
+
low: 'Bassa',
|
|
185
|
+
medium: 'Media',
|
|
186
|
+
high: 'Alta'
|
|
187
|
+
};
|
|
188
|
+
const categoryText = newDeviceCategory
|
|
189
|
+
? `per la categoria "${newDeviceCategory}"`
|
|
190
|
+
: 'generica (tutte le attrezzature)';
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Esiste già una regola SLA ${categoryText} per il fornitore "${supplier?.name}" con priorità "${priorityLabels[newPriority]}". ` +
|
|
193
|
+
`Modifica la regola esistente o scegli un'altra combinazione.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const updates: any = {
|
|
199
|
+
responseTimeHours: args.responseTimeHours,
|
|
200
|
+
resolutionTimeHours: args.resolutionTimeHours,
|
|
201
|
+
description: args.description,
|
|
202
|
+
updatedAt: Date.now(),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (args.name) {
|
|
206
|
+
updates.name = args.name;
|
|
207
|
+
}
|
|
208
|
+
if (args.supplierId) {
|
|
209
|
+
updates.supplierId = args.supplierId;
|
|
210
|
+
}
|
|
211
|
+
if (args.deviceCategory !== undefined) {
|
|
212
|
+
updates.deviceCategory = args.deviceCategory || undefined;
|
|
213
|
+
}
|
|
214
|
+
if (args.priority) {
|
|
215
|
+
updates.priority = args.priority;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await ctx.db.patch(args.ruleId, updates);
|
|
219
|
+
|
|
220
|
+
return args.ruleId;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Delete SLA rule (admin only)
|
|
225
|
+
export const deleteSLARule = mutation({
|
|
226
|
+
args: { ruleId: v.id('sla_rules') },
|
|
227
|
+
handler: async (ctx, args) => {
|
|
228
|
+
await ctx.db.delete(args.ruleId);
|
|
229
|
+
return { success: true };
|
|
230
|
+
},
|
|
231
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { v } from 'convex/values';
|
|
2
|
+
import { mutation, query, internalMutation } from './_generated/server';
|
|
3
|
+
import { internal } from './_generated/api';
|
|
4
|
+
|
|
5
|
+
function generateOrderNumber(): string {
|
|
6
|
+
const date = new Date();
|
|
7
|
+
const year = date.getFullYear();
|
|
8
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
9
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
10
|
+
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
|
11
|
+
return `ORD-${year}${month}${day}-${random}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const createOrder = mutation({
|
|
15
|
+
args: {
|
|
16
|
+
clinicId: v.number(),
|
|
17
|
+
supplierId: v.id('suppliers'),
|
|
18
|
+
items: v.array(v.object({
|
|
19
|
+
sparePartId: v.id('spare_parts'),
|
|
20
|
+
articleId: v.number(),
|
|
21
|
+
articleName: v.string(),
|
|
22
|
+
code: v.optional(v.string()),
|
|
23
|
+
quantity: v.number(),
|
|
24
|
+
unitPrice: v.optional(v.number()),
|
|
25
|
+
totalPrice: v.optional(v.number()),
|
|
26
|
+
notes: v.optional(v.string()),
|
|
27
|
+
})),
|
|
28
|
+
notes: v.optional(v.string()),
|
|
29
|
+
requestedDeliveryDate: v.optional(v.number()),
|
|
30
|
+
createdBy: v.string(),
|
|
31
|
+
},
|
|
32
|
+
handler: async (ctx, args) => {
|
|
33
|
+
const orderNumber = generateOrderNumber();
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
|
|
36
|
+
const totalAmount = args.items.reduce((sum, item) => {
|
|
37
|
+
return sum + (item.totalPrice || 0);
|
|
38
|
+
}, 0);
|
|
39
|
+
|
|
40
|
+
const orderId = await ctx.db.insert('spare_part_orders', {
|
|
41
|
+
orderNumber,
|
|
42
|
+
clinicId: args.clinicId,
|
|
43
|
+
supplierId: args.supplierId,
|
|
44
|
+
status: 'pending',
|
|
45
|
+
items: args.items,
|
|
46
|
+
totalAmount,
|
|
47
|
+
notes: args.notes,
|
|
48
|
+
requestedDeliveryDate: args.requestedDeliveryDate,
|
|
49
|
+
createdBy: args.createdBy,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await ctx.scheduler.runAfter(0, internal.sparePartOrders.executeSparePartOrderTriggers, {
|
|
55
|
+
orderId,
|
|
56
|
+
eventType: 'spare_part_order_created',
|
|
57
|
+
previousStatus: undefined,
|
|
58
|
+
newStatus: 'pending',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { orderId, orderNumber };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const listOrdersByClinic = query({
|
|
66
|
+
args: {
|
|
67
|
+
clinicId: v.number(),
|
|
68
|
+
limit: v.optional(v.number()),
|
|
69
|
+
},
|
|
70
|
+
handler: async (ctx, args) => {
|
|
71
|
+
const limit = args.limit || 100;
|
|
72
|
+
|
|
73
|
+
const orders = await ctx.db
|
|
74
|
+
.query('spare_part_orders')
|
|
75
|
+
.withIndex('by_clinicId', (q) => q.eq('clinicId', args.clinicId))
|
|
76
|
+
.order('desc')
|
|
77
|
+
.take(limit);
|
|
78
|
+
|
|
79
|
+
return orders;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const listAllOrders = query({
|
|
84
|
+
args: {
|
|
85
|
+
limit: v.optional(v.number()),
|
|
86
|
+
},
|
|
87
|
+
handler: async (ctx, args) => {
|
|
88
|
+
const limit = args.limit || 100;
|
|
89
|
+
|
|
90
|
+
const orders = await ctx.db
|
|
91
|
+
.query('spare_part_orders')
|
|
92
|
+
.withIndex('by_createdAt')
|
|
93
|
+
.order('desc')
|
|
94
|
+
.take(limit);
|
|
95
|
+
|
|
96
|
+
return orders;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const getOrder = query({
|
|
101
|
+
args: { orderId: v.id('spare_part_orders') },
|
|
102
|
+
handler: async (ctx, args) => {
|
|
103
|
+
return await ctx.db.get(args.orderId);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const updateOrderStatus = mutation({
|
|
108
|
+
args: {
|
|
109
|
+
orderId: v.id('spare_part_orders'),
|
|
110
|
+
status: v.union(
|
|
111
|
+
v.literal('draft'),
|
|
112
|
+
v.literal('pending'),
|
|
113
|
+
v.literal('confirmed'),
|
|
114
|
+
v.literal('shipped'),
|
|
115
|
+
v.literal('delivered'),
|
|
116
|
+
v.literal('cancelled')
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
handler: async (ctx, args) => {
|
|
120
|
+
const order = await ctx.db.get(args.orderId);
|
|
121
|
+
const previousStatus = order?.status;
|
|
122
|
+
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const updates: any = {
|
|
125
|
+
status: args.status,
|
|
126
|
+
updatedAt: now,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (args.status === 'confirmed') {
|
|
130
|
+
updates.confirmedAt = now;
|
|
131
|
+
} else if (args.status === 'shipped') {
|
|
132
|
+
updates.shippedAt = now;
|
|
133
|
+
} else if (args.status === 'delivered') {
|
|
134
|
+
updates.deliveredAt = now;
|
|
135
|
+
updates.actualDeliveryDate = now;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await ctx.db.patch(args.orderId, updates);
|
|
139
|
+
|
|
140
|
+
if (previousStatus !== args.status) {
|
|
141
|
+
await ctx.scheduler.runAfter(0, internal.sparePartOrders.executeSparePartOrderTriggers, {
|
|
142
|
+
orderId: args.orderId,
|
|
143
|
+
eventType: 'spare_part_order_status_change',
|
|
144
|
+
previousStatus,
|
|
145
|
+
newStatus: args.status,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { success: true };
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export const updateOrder = mutation({
|
|
154
|
+
args: {
|
|
155
|
+
orderId: v.id('spare_part_orders'),
|
|
156
|
+
items: v.optional(v.array(v.object({
|
|
157
|
+
sparePartId: v.id('spare_parts'),
|
|
158
|
+
articleId: v.number(),
|
|
159
|
+
articleName: v.string(),
|
|
160
|
+
code: v.optional(v.string()),
|
|
161
|
+
quantity: v.number(),
|
|
162
|
+
unitPrice: v.optional(v.number()),
|
|
163
|
+
totalPrice: v.optional(v.number()),
|
|
164
|
+
notes: v.optional(v.string()),
|
|
165
|
+
}))),
|
|
166
|
+
notes: v.optional(v.string()),
|
|
167
|
+
requestedDeliveryDate: v.optional(v.number()),
|
|
168
|
+
},
|
|
169
|
+
handler: async (ctx, args) => {
|
|
170
|
+
const { orderId, ...updates } = args;
|
|
171
|
+
|
|
172
|
+
if (updates.items) {
|
|
173
|
+
const totalAmount = updates.items.reduce((sum, item) => {
|
|
174
|
+
return sum + (item.totalPrice || 0);
|
|
175
|
+
}, 0);
|
|
176
|
+
(updates as any).totalAmount = totalAmount;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
(updates as any).updatedAt = Date.now();
|
|
180
|
+
|
|
181
|
+
await ctx.db.patch(orderId, updates);
|
|
182
|
+
|
|
183
|
+
return { success: true };
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const deleteOrder = mutation({
|
|
188
|
+
args: {
|
|
189
|
+
orderId: v.id('spare_part_orders'),
|
|
190
|
+
},
|
|
191
|
+
handler: async (ctx, args) => {
|
|
192
|
+
await ctx.db.delete(args.orderId);
|
|
193
|
+
return { success: true };
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export const listOrdersBySupplier = query({
|
|
198
|
+
args: {
|
|
199
|
+
supplierId: v.id('suppliers'),
|
|
200
|
+
limit: v.optional(v.number()),
|
|
201
|
+
},
|
|
202
|
+
handler: async (ctx, args) => {
|
|
203
|
+
const limit = args.limit || 100;
|
|
204
|
+
|
|
205
|
+
const orders = await ctx.db
|
|
206
|
+
.query('spare_part_orders')
|
|
207
|
+
.withIndex('by_supplierId', (q) => q.eq('supplierId', args.supplierId))
|
|
208
|
+
.order('desc')
|
|
209
|
+
.take(limit);
|
|
210
|
+
|
|
211
|
+
return orders;
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const executeSparePartOrderTriggers = internalMutation({
|
|
216
|
+
args: {
|
|
217
|
+
orderId: v.id('spare_part_orders'),
|
|
218
|
+
eventType: v.union(
|
|
219
|
+
v.literal('spare_part_order_created'),
|
|
220
|
+
v.literal('spare_part_order_status_change')
|
|
221
|
+
),
|
|
222
|
+
previousStatus: v.optional(v.string()),
|
|
223
|
+
newStatus: v.string(),
|
|
224
|
+
},
|
|
225
|
+
handler: async (ctx, args) => {
|
|
226
|
+
const order = await ctx.db.get(args.orderId);
|
|
227
|
+
if (!order) {
|
|
228
|
+
console.log('Order not found for trigger execution');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const triggers = await ctx.db
|
|
233
|
+
.query('ticket_triggers')
|
|
234
|
+
.withIndex('by_isActive', (q) => q.eq('isActive', true))
|
|
235
|
+
.collect();
|
|
236
|
+
|
|
237
|
+
const sparePartTriggers = triggers.filter(t =>
|
|
238
|
+
t.conditions.triggerOn === args.eventType
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
sparePartTriggers.sort((a, b) => a.priority - b.priority);
|
|
242
|
+
|
|
243
|
+
for (const trigger of sparePartTriggers) {
|
|
244
|
+
try {
|
|
245
|
+
const conditions = trigger.conditions;
|
|
246
|
+
const filters = conditions.sparePartOrderFilters;
|
|
247
|
+
|
|
248
|
+
if (filters && !filters.applyToAllSuppliers && filters.supplierIds && filters.supplierIds.length > 0) {
|
|
249
|
+
if (!filters.supplierIds.includes(order.supplierId)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (args.eventType === 'spare_part_order_status_change' && filters?.statuses && filters.statuses.length > 0) {
|
|
255
|
+
if (!filters.statuses.includes(args.newStatus)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const actions = trigger.actions.sparePartOrderActions;
|
|
261
|
+
if (!actions) continue;
|
|
262
|
+
|
|
263
|
+
if (actions.changeOrderStatus && actions.changeOrderStatus !== args.newStatus) {
|
|
264
|
+
const validStatuses = ['draft', 'pending', 'confirmed', 'shipped', 'delivered', 'cancelled'];
|
|
265
|
+
if (validStatuses.includes(actions.changeOrderStatus)) {
|
|
266
|
+
await ctx.db.patch(args.orderId, {
|
|
267
|
+
status: actions.changeOrderStatus as 'draft' | 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled',
|
|
268
|
+
updatedAt: Date.now(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (actions.addOrderNote) {
|
|
274
|
+
const currentNotes = order.notes || '';
|
|
275
|
+
const newNote = `[${new Date().toLocaleString('it-IT')}] ${actions.addOrderNote}`;
|
|
276
|
+
await ctx.db.patch(args.orderId, {
|
|
277
|
+
notes: currentNotes ? `${currentNotes}\n${newNote}` : newNote,
|
|
278
|
+
updatedAt: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// NOTE: sendOrderNotification is handled by the parent app (emailActions).
|
|
283
|
+
// The component marks data; the parent app is responsible for sending emails.
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error(`Error executing trigger "${trigger.name}":`, error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
});
|