@primocaredentgroup/prescriptions-component 0.1.4 → 0.1.6
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/README.md +16 -0
- package/dist/convex/lib/auth.d.ts +5 -0
- package/dist/convex/lib/auth.d.ts.map +1 -1
- package/dist/convex/lib/auth.js +36 -0
- package/dist/convex/lib/auth.js.map +1 -1
- package/dist/convex/mutations/digitalAssets.d.ts.map +1 -1
- package/dist/convex/mutations/digitalAssets.js +20 -61
- package/dist/convex/mutations/digitalAssets.js.map +1 -1
- package/dist/convex/mutations/operational.d.ts.map +1 -1
- package/dist/convex/mutations/operational.js +19 -54
- package/dist/convex/mutations/operational.js.map +1 -1
- package/dist/convex/mutations/phases.d.ts.map +1 -1
- package/dist/convex/mutations/phases.js +20 -52
- package/dist/convex/mutations/phases.js.map +1 -1
- package/dist/convex/mutations/prescriptions.d.ts.map +1 -1
- package/dist/convex/mutations/prescriptions.js +80 -229
- package/dist/convex/mutations/prescriptions.js.map +1 -1
- package/dist/convex/mutations/syncJobs.d.ts.map +1 -1
- package/dist/convex/mutations/syncJobs.js +6 -16
- package/dist/convex/mutations/syncJobs.js.map +1 -1
- package/dist/convex/prescriptions/fields.d.ts.map +1 -1
- package/dist/convex/prescriptions/fields.js +6 -7
- package/dist/convex/prescriptions/fields.js.map +1 -1
- package/dist/convex/queries/dynamicFields.d.ts.map +1 -1
- package/dist/convex/queries/dynamicFields.js +2 -3
- package/dist/convex/queries/dynamicFields.js.map +1 -1
- package/dist/convex/schema.d.ts +2 -2
- package/dist/convex/schema.d.ts.map +1 -1
- package/dist/convex/schema.js +2 -2
- package/dist/convex/schema.js.map +1 -1
- package/dist/convex/types.d.ts +1 -1
- package/dist/convex/types.d.ts.map +1 -1
- package/package.json +9 -9
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mutation } from "../_generated/server";
|
|
2
2
|
import { ConvexError, v } from "convex/values";
|
|
3
3
|
import { computeClinicalHash, generateIdempotencyKey } from "../lib/utils";
|
|
4
|
-
import { validateClinicalData,
|
|
5
|
-
import {
|
|
4
|
+
import { validateClinicalData, validateDigitalRequirements } from "../lib/validation";
|
|
5
|
+
import { requireIdentityOrThrow } from "../lib/auth";
|
|
6
6
|
import { assertDynamicRequiredComplete } from "../lib/dynamicFieldsStrict";
|
|
7
7
|
import { normalizeClinicalDraftInput } from "../lib/clinicalNormalize";
|
|
8
8
|
// ============================================
|
|
@@ -30,30 +30,11 @@ const calendarDataArgsValidator = v.object({
|
|
|
30
30
|
nextAppointmentPhaseId: v.optional(v.string()),
|
|
31
31
|
nextAppointmentPhaseName: v.optional(v.string()),
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (error instanceof ConvexError) {
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
throw new ConvexError({
|
|
42
|
-
code: "UNAUTHORIZED",
|
|
43
|
-
message: "Accesso non autorizzato",
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function assertPrescriptionAccessOrThrow(user, prescription) {
|
|
48
|
-
try {
|
|
49
|
-
assertPrescriptionAccess(user, prescription);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
throw new ConvexError({
|
|
53
|
-
code: "FORBIDDEN",
|
|
54
|
-
message: error instanceof Error ? error.message : "Forbidden",
|
|
55
|
-
});
|
|
56
|
-
}
|
|
33
|
+
function getActorFromIdentity(identity) {
|
|
34
|
+
return {
|
|
35
|
+
actorUserId: identity.subject,
|
|
36
|
+
actorRole: "HOST",
|
|
37
|
+
};
|
|
57
38
|
}
|
|
58
39
|
// ============================================
|
|
59
40
|
// PRESCRIPTION MUTATIONS
|
|
@@ -74,7 +55,8 @@ export const createDraft = mutation({
|
|
|
74
55
|
},
|
|
75
56
|
handler: async (ctx, args) => {
|
|
76
57
|
const now = Date.now();
|
|
77
|
-
const
|
|
58
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
59
|
+
const actor = getActorFromIdentity(identity);
|
|
78
60
|
// 1. VINCOLO: Verifica unicità pdcItemId
|
|
79
61
|
const existingPrescriptions = await ctx.db
|
|
80
62
|
.query("prescriptions")
|
|
@@ -85,23 +67,7 @@ export const createDraft = mutation({
|
|
|
85
67
|
if (activePrescription) {
|
|
86
68
|
throw new Error(`Esiste già una prescrizione attiva per questo PDC Item (ID: ${activePrescription._id})`);
|
|
87
69
|
}
|
|
88
|
-
// 2.
|
|
89
|
-
const canCreate = user.role === "SECRETARY" ||
|
|
90
|
-
user.adminRole === "admin" ||
|
|
91
|
-
user.adminRole === "superadmin";
|
|
92
|
-
if (!canCreate) {
|
|
93
|
-
throw new ConvexError({
|
|
94
|
-
code: "FORBIDDEN",
|
|
95
|
-
message: "Utente non autorizzato a creare prescrizioni",
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (user.clinicId !== args.clinicId && user.adminRole !== "admin" && user.adminRole !== "superadmin") {
|
|
99
|
-
throw new ConvexError({
|
|
100
|
-
code: "FORBIDDEN",
|
|
101
|
-
message: "Clinic non autorizzata per l'utente autenticato",
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
// 3. Carica flow attivo
|
|
70
|
+
// 2. Carica flow attivo
|
|
105
71
|
const flow = await ctx.db
|
|
106
72
|
.query("flows")
|
|
107
73
|
.withIndex("by_flowKey_status", (q) => q.eq("flowKey", args.flowKey).eq("status", "ACTIVE"))
|
|
@@ -125,7 +91,7 @@ export const createDraft = mutation({
|
|
|
125
91
|
flowKey: args.flowKey,
|
|
126
92
|
flowVersion: flow.version,
|
|
127
93
|
status: "DRAFT",
|
|
128
|
-
createdByUserId:
|
|
94
|
+
createdByUserId: actor.actorUserId,
|
|
129
95
|
createdAt: now,
|
|
130
96
|
updatedAt: now,
|
|
131
97
|
coreRefs: {},
|
|
@@ -180,18 +146,19 @@ export const createDraft = mutation({
|
|
|
180
146
|
operationalDataId: 0, // Will be backfilled
|
|
181
147
|
prescriptionRef: prescriptionId,
|
|
182
148
|
});
|
|
183
|
-
//
|
|
149
|
+
// 8. Audit
|
|
184
150
|
await ctx.db.insert("auditEvents", {
|
|
185
151
|
auditEventId: 0, // Will be backfilled
|
|
186
152
|
prescriptionRef: prescriptionId,
|
|
187
153
|
at: now,
|
|
188
|
-
actorUserId:
|
|
189
|
-
actorRole:
|
|
154
|
+
actorUserId: actor.actorUserId,
|
|
155
|
+
actorRole: actor.actorRole,
|
|
190
156
|
type: "PRESCRIPTION_CREATED",
|
|
191
157
|
payload: {
|
|
192
158
|
pdcItemId: args.pdcItemId,
|
|
193
159
|
flowKey: args.flowKey,
|
|
194
160
|
flowVersion: flow.version,
|
|
161
|
+
actorEmail: identity.email,
|
|
195
162
|
},
|
|
196
163
|
});
|
|
197
164
|
return { prescriptionId, revisionId };
|
|
@@ -226,43 +193,17 @@ export const updateClinicalDraft = mutation({
|
|
|
226
193
|
},
|
|
227
194
|
handler: async (ctx, args) => {
|
|
228
195
|
const now = Date.now();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
user = await requireIdentityUser(ctx);
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
if (error instanceof ConvexError) {
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
throw new ConvexError({
|
|
239
|
-
code: "UNAUTHORIZED",
|
|
240
|
-
message: "Accesso non autorizzato",
|
|
241
|
-
});
|
|
242
|
-
}
|
|
196
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
197
|
+
const actor = getActorFromIdentity(identity);
|
|
243
198
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
244
199
|
if (!prescription) {
|
|
245
200
|
throw new Error("Prescrizione non trovata");
|
|
246
201
|
}
|
|
247
|
-
try {
|
|
248
|
-
assertPrescriptionAccess(user, prescription);
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
throw new ConvexError({
|
|
252
|
-
code: "FORBIDDEN",
|
|
253
|
-
message: error instanceof Error ? error.message : "Forbidden",
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
202
|
// 2. Verifica stato
|
|
257
203
|
if (!["DRAFT", "PENDING_DOCTOR"].includes(prescription.status)) {
|
|
258
204
|
throw new Error(`Non è possibile modificare una prescrizione in stato ${prescription.status}`);
|
|
259
205
|
}
|
|
260
|
-
// 3.
|
|
261
|
-
const permission = canUserPerformAction(user.role, "updateDraft", prescription.status);
|
|
262
|
-
if (!permission.allowed) {
|
|
263
|
-
throw new Error(permission.reason);
|
|
264
|
-
}
|
|
265
|
-
// 4. Carica revisione corrente
|
|
206
|
+
// 3. Carica revisione corrente
|
|
266
207
|
if (!prescription.latestClinicalRevisionId) {
|
|
267
208
|
throw new Error("Nessuna revisione clinica trovata");
|
|
268
209
|
}
|
|
@@ -335,19 +276,20 @@ export const updateClinicalDraft = mutation({
|
|
|
335
276
|
if (normalizedPatch.lineaMargine !== undefined) {
|
|
336
277
|
changedKeys.push("lineaMargine");
|
|
337
278
|
}
|
|
338
|
-
//
|
|
279
|
+
// 9. Audit
|
|
339
280
|
await ctx.db.insert("auditEvents", {
|
|
340
281
|
auditEventId: 0, // Will be backfilled
|
|
341
282
|
prescriptionRef: args.prescriptionId,
|
|
342
283
|
at: now,
|
|
343
|
-
actorUserId:
|
|
344
|
-
actorRole:
|
|
345
|
-
type:
|
|
284
|
+
actorUserId: actor.actorUserId,
|
|
285
|
+
actorRole: actor.actorRole,
|
|
286
|
+
type: "CLINICAL_DRAFT_UPDATED",
|
|
346
287
|
payload: {
|
|
347
288
|
changes: normalizedPatch,
|
|
348
289
|
changedKeys,
|
|
349
290
|
clinicalValidationErrorsCount: clinicalValidationErrors.length,
|
|
350
291
|
newHash,
|
|
292
|
+
actorEmail: identity.email,
|
|
351
293
|
},
|
|
352
294
|
});
|
|
353
295
|
return { ok: true, success: true, clinicalHash: newHash, clinicalValidationErrors };
|
|
@@ -363,22 +305,18 @@ export const submitToDoctor = mutation({
|
|
|
363
305
|
},
|
|
364
306
|
handler: async (ctx, args) => {
|
|
365
307
|
const now = Date.now();
|
|
366
|
-
const
|
|
308
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
309
|
+
const actor = getActorFromIdentity(identity);
|
|
367
310
|
// 1. Carica prescrizione
|
|
368
311
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
369
312
|
if (!prescription) {
|
|
370
313
|
throw new Error("Prescrizione non trovata");
|
|
371
314
|
}
|
|
372
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
373
315
|
// 2. Verifica stato
|
|
374
316
|
if (prescription.status !== "DRAFT") {
|
|
375
317
|
throw new Error(`Non è possibile inviare una prescrizione in stato ${prescription.status}`);
|
|
376
318
|
}
|
|
377
|
-
|
|
378
|
-
if (!permission.allowed) {
|
|
379
|
-
throw new Error(permission.reason);
|
|
380
|
-
}
|
|
381
|
-
// 4. Valida requisiti per prescrizioni digitali
|
|
319
|
+
// 3. Valida requisiti per prescrizioni digitali
|
|
382
320
|
const digitalErrors = validateDigitalRequirements(prescription.prescriptionType, prescription.digitalAssets);
|
|
383
321
|
if (digitalErrors.length > 0) {
|
|
384
322
|
throw new Error(digitalErrors.map(e => e.message).join("; "));
|
|
@@ -388,15 +326,15 @@ export const submitToDoctor = mutation({
|
|
|
388
326
|
status: "PENDING_DOCTOR",
|
|
389
327
|
updatedAt: now,
|
|
390
328
|
});
|
|
391
|
-
//
|
|
329
|
+
// 5. Audit
|
|
392
330
|
await ctx.db.insert("auditEvents", {
|
|
393
331
|
auditEventId: 0, // Will be backfilled
|
|
394
332
|
prescriptionRef: args.prescriptionId,
|
|
395
333
|
at: now,
|
|
396
|
-
actorUserId:
|
|
397
|
-
actorRole:
|
|
334
|
+
actorUserId: actor.actorUserId,
|
|
335
|
+
actorRole: actor.actorRole,
|
|
398
336
|
type: "SUBMITTED_TO_DOCTOR",
|
|
399
|
-
payload: {},
|
|
337
|
+
payload: { actorEmail: identity.email },
|
|
400
338
|
});
|
|
401
339
|
return { success: true };
|
|
402
340
|
},
|
|
@@ -412,24 +350,18 @@ export const signPrescription = mutation({
|
|
|
412
350
|
},
|
|
413
351
|
handler: async (ctx, args) => {
|
|
414
352
|
const now = Date.now();
|
|
415
|
-
const
|
|
353
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
354
|
+
const actor = getActorFromIdentity(identity);
|
|
416
355
|
// 1. Carica prescrizione
|
|
417
356
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
418
357
|
if (!prescription) {
|
|
419
358
|
throw new Error("Prescrizione non trovata");
|
|
420
359
|
}
|
|
421
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
422
360
|
// 2. Verifica stato
|
|
423
361
|
if (prescription.status !== "PENDING_DOCTOR") {
|
|
424
362
|
throw new Error(`Non è possibile firmare una prescrizione in stato ${prescription.status}`);
|
|
425
363
|
}
|
|
426
|
-
|
|
427
|
-
throw new ConvexError({
|
|
428
|
-
code: "FORBIDDEN",
|
|
429
|
-
message: "Solo i medici possono firmare le prescrizioni",
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
// 4. Carica flow e revisione
|
|
364
|
+
// 3. Carica flow e revisione
|
|
433
365
|
const flow = await ctx.db
|
|
434
366
|
.query("flows")
|
|
435
367
|
.withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
|
|
@@ -460,7 +392,7 @@ export const signPrescription = mutation({
|
|
|
460
392
|
// 7. Crea firma
|
|
461
393
|
const signature = {
|
|
462
394
|
signedAt: now,
|
|
463
|
-
signedByDoctorId:
|
|
395
|
+
signedByDoctorId: prescription.doctorId,
|
|
464
396
|
signatureType: "weak-digital",
|
|
465
397
|
signaturePayload: args.signaturePayload,
|
|
466
398
|
clinicalHash: finalHash,
|
|
@@ -515,18 +447,19 @@ export const signPrescription = mutation({
|
|
|
515
447
|
status: "SIGNED",
|
|
516
448
|
updatedAt: now,
|
|
517
449
|
});
|
|
518
|
-
//
|
|
450
|
+
// 10. Audit
|
|
519
451
|
await ctx.db.insert("auditEvents", {
|
|
520
452
|
auditEventId: 0, // Will be backfilled
|
|
521
453
|
prescriptionRef: args.prescriptionId,
|
|
522
454
|
at: now,
|
|
523
|
-
actorUserId:
|
|
524
|
-
actorRole:
|
|
455
|
+
actorUserId: actor.actorUserId,
|
|
456
|
+
actorRole: actor.actorRole,
|
|
525
457
|
type: "PRESCRIPTION_SIGNED",
|
|
526
458
|
payload: {
|
|
527
459
|
clinicalHash: finalHash,
|
|
528
460
|
signedAt: now,
|
|
529
461
|
syncJobIds,
|
|
462
|
+
actorEmail: identity.email,
|
|
530
463
|
},
|
|
531
464
|
});
|
|
532
465
|
return {
|
|
@@ -547,57 +480,17 @@ export const sendToLab = mutation({
|
|
|
547
480
|
},
|
|
548
481
|
handler: async (ctx, args) => {
|
|
549
482
|
const now = Date.now();
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
user = await requireIdentityUser(ctx);
|
|
554
|
-
}
|
|
555
|
-
catch (error) {
|
|
556
|
-
if (error instanceof ConvexError) {
|
|
557
|
-
throw error;
|
|
558
|
-
}
|
|
559
|
-
throw new ConvexError({
|
|
560
|
-
code: "UNAUTHORIZED",
|
|
561
|
-
message: "Accesso non autorizzato",
|
|
562
|
-
});
|
|
563
|
-
}
|
|
483
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
484
|
+
const actor = getActorFromIdentity(identity);
|
|
564
485
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
565
486
|
if (!prescription) {
|
|
566
487
|
throw new Error("Prescrizione non trovata");
|
|
567
488
|
}
|
|
568
|
-
try {
|
|
569
|
-
assertPrescriptionAccess(user, prescription);
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
throw new ConvexError({
|
|
573
|
-
code: "FORBIDDEN",
|
|
574
|
-
message: error instanceof Error ? error.message : "Forbidden",
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
489
|
// 2. Verifica stato (deve essere DRAFT o PENDING_DOCTOR)
|
|
578
490
|
if (prescription.status !== "DRAFT" && prescription.status !== "PENDING_DOCTOR") {
|
|
579
491
|
throw new Error(`Non è possibile inviare al lab una prescrizione in stato ${prescription.status}`);
|
|
580
492
|
}
|
|
581
|
-
// 3.
|
|
582
|
-
// - invio consentito a ruoli autorizzati dal flusso corrente (submit/sign)
|
|
583
|
-
// - auto-firma consentita solo al DOCTOR
|
|
584
|
-
const canSubmit = canUserPerformAction(user.role, "submit", prescription.status).allowed;
|
|
585
|
-
const canSign = canUserPerformAction(user.role, "sign", prescription.status).allowed;
|
|
586
|
-
const canSendToLab = canSubmit || canSign;
|
|
587
|
-
if (!canSendToLab) {
|
|
588
|
-
throw new ConvexError({
|
|
589
|
-
code: "FORBIDDEN",
|
|
590
|
-
message: `Il ruolo ${user.role} non può inviare al laboratorio in stato ${prescription.status}`,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
const canAutoSign = user.role === "DOCTOR";
|
|
594
|
-
if (!canAutoSign) {
|
|
595
|
-
throw new ConvexError({
|
|
596
|
-
code: "FORBIDDEN",
|
|
597
|
-
message: "Invio al laboratorio non consentito: auto-firma disponibile solo per DOCTOR",
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
// 4. Carica flow e revisione
|
|
493
|
+
// 3. Carica flow e revisione
|
|
601
494
|
const flow = await ctx.db
|
|
602
495
|
.query("flows")
|
|
603
496
|
.withIndex("by_flowKey_version", (q) => q.eq("flowKey", prescription.flowKey).eq("version", prescription.flowVersion))
|
|
@@ -633,8 +526,8 @@ export const sendToLab = mutation({
|
|
|
633
526
|
signaturePayload: btoa(JSON.stringify({
|
|
634
527
|
autoSigned: true,
|
|
635
528
|
timestamp: now,
|
|
636
|
-
sentBy:
|
|
637
|
-
sentByRole:
|
|
529
|
+
sentBy: actor.actorUserId,
|
|
530
|
+
sentByRole: actor.actorRole,
|
|
638
531
|
})),
|
|
639
532
|
clinicalHash: finalHash,
|
|
640
533
|
};
|
|
@@ -693,14 +586,15 @@ export const sendToLab = mutation({
|
|
|
693
586
|
auditEventId: 0, // Will be backfilled
|
|
694
587
|
prescriptionRef: args.prescriptionId,
|
|
695
588
|
at: now,
|
|
696
|
-
actorUserId:
|
|
697
|
-
actorRole:
|
|
589
|
+
actorUserId: actor.actorUserId,
|
|
590
|
+
actorRole: actor.actorRole,
|
|
698
591
|
type: "SENT_TO_LAB",
|
|
699
592
|
payload: {
|
|
700
593
|
clinicalHash: finalHash,
|
|
701
594
|
sentAt: now,
|
|
702
595
|
syncJobIds,
|
|
703
596
|
autoSigned: true,
|
|
597
|
+
actorEmail: identity.email,
|
|
704
598
|
},
|
|
705
599
|
});
|
|
706
600
|
return {
|
|
@@ -721,24 +615,18 @@ export const reviseAfterSignature = mutation({
|
|
|
721
615
|
},
|
|
722
616
|
handler: async (ctx, args) => {
|
|
723
617
|
const now = Date.now();
|
|
724
|
-
const
|
|
618
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
619
|
+
const actor = getActorFromIdentity(identity);
|
|
725
620
|
// 1. Carica prescrizione
|
|
726
621
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
727
622
|
if (!prescription) {
|
|
728
623
|
throw new Error("Prescrizione non trovata");
|
|
729
624
|
}
|
|
730
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
731
625
|
// 2. Verifica stato
|
|
732
626
|
if (!["SIGNED", "ERROR"].includes(prescription.status)) {
|
|
733
627
|
throw new Error(`Non è possibile revisionare una prescrizione in stato ${prescription.status}`);
|
|
734
628
|
}
|
|
735
|
-
|
|
736
|
-
throw new ConvexError({
|
|
737
|
-
code: "FORBIDDEN",
|
|
738
|
-
message: "Solo i medici possono creare revisioni",
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
// 4. Carica revisione corrente
|
|
629
|
+
// 3. Carica revisione corrente
|
|
742
630
|
if (!prescription.latestClinicalRevisionId) {
|
|
743
631
|
throw new Error("Nessuna revisione clinica trovata");
|
|
744
632
|
}
|
|
@@ -770,14 +658,15 @@ export const reviseAfterSignature = mutation({
|
|
|
770
658
|
auditEventId: 0, // Will be backfilled
|
|
771
659
|
prescriptionRef: args.prescriptionId,
|
|
772
660
|
at: now,
|
|
773
|
-
actorUserId:
|
|
774
|
-
actorRole:
|
|
661
|
+
actorUserId: actor.actorUserId,
|
|
662
|
+
actorRole: actor.actorRole,
|
|
775
663
|
type: "REVISION_CREATED",
|
|
776
664
|
payload: {
|
|
777
665
|
previousRevisionNumber: currentRevision.revisionNumber,
|
|
778
666
|
newRevisionNumber,
|
|
779
667
|
previousHash: currentRevision.clinicalHash,
|
|
780
668
|
reason: args.reason,
|
|
669
|
+
actorEmail: identity.email,
|
|
781
670
|
},
|
|
782
671
|
});
|
|
783
672
|
return {
|
|
@@ -797,13 +686,13 @@ export const cancelPrescription = mutation({
|
|
|
797
686
|
},
|
|
798
687
|
handler: async (ctx, args) => {
|
|
799
688
|
const now = Date.now();
|
|
800
|
-
const
|
|
689
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
690
|
+
const actor = getActorFromIdentity(identity);
|
|
801
691
|
// 1. Carica prescrizione
|
|
802
692
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
803
693
|
if (!prescription) {
|
|
804
694
|
throw new Error("Prescrizione non trovata");
|
|
805
695
|
}
|
|
806
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
807
696
|
// 2. Verifica stato (non può annullare se già SYNCED)
|
|
808
697
|
if (prescription.status === "SYNCED") {
|
|
809
698
|
throw new Error("Non è possibile annullare una prescrizione già sincronizzata");
|
|
@@ -811,24 +700,20 @@ export const cancelPrescription = mutation({
|
|
|
811
700
|
if (prescription.status === "CANCELLED") {
|
|
812
701
|
throw new Error("La prescrizione è già annullata");
|
|
813
702
|
}
|
|
814
|
-
|
|
815
|
-
if (!permission.allowed) {
|
|
816
|
-
throw new Error(permission.reason);
|
|
817
|
-
}
|
|
818
|
-
// 4. Aggiorna stato
|
|
703
|
+
// 3. Aggiorna stato
|
|
819
704
|
await ctx.db.patch(args.prescriptionId, {
|
|
820
705
|
status: "CANCELLED",
|
|
821
706
|
updatedAt: now,
|
|
822
707
|
});
|
|
823
|
-
//
|
|
708
|
+
// 4. Audit
|
|
824
709
|
await ctx.db.insert("auditEvents", {
|
|
825
710
|
auditEventId: 0, // Will be backfilled
|
|
826
711
|
prescriptionRef: args.prescriptionId,
|
|
827
712
|
at: now,
|
|
828
|
-
actorUserId:
|
|
829
|
-
actorRole:
|
|
713
|
+
actorUserId: actor.actorUserId,
|
|
714
|
+
actorRole: actor.actorRole,
|
|
830
715
|
type: "PRESCRIPTION_CANCELLED",
|
|
831
|
-
payload: { reason: args.reason },
|
|
716
|
+
payload: { reason: args.reason, actorEmail: identity.email },
|
|
832
717
|
});
|
|
833
718
|
return { success: true };
|
|
834
719
|
},
|
|
@@ -844,13 +729,13 @@ export const markAsShipped = mutation({
|
|
|
844
729
|
},
|
|
845
730
|
handler: async (ctx, args) => {
|
|
846
731
|
const now = Date.now();
|
|
847
|
-
const
|
|
732
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
733
|
+
const actor = getActorFromIdentity(identity);
|
|
848
734
|
// 1. Carica prescrizione
|
|
849
735
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
850
736
|
if (!prescription) {
|
|
851
737
|
throw new Error("Prescrizione non trovata");
|
|
852
738
|
}
|
|
853
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
854
739
|
// 2. Verifica stato (deve essere SIGNED o superiore, non CANCELLED)
|
|
855
740
|
const validStatuses = ["SIGNED", "SYNCED", "ERROR"];
|
|
856
741
|
if (!validStatuses.includes(prescription.status)) {
|
|
@@ -860,35 +745,25 @@ export const markAsShipped = mutation({
|
|
|
860
745
|
if (prescription.shippedAt) {
|
|
861
746
|
throw new Error("Questa prescrizione è già stata marcata come spedita");
|
|
862
747
|
}
|
|
863
|
-
//
|
|
864
|
-
const canManageShipment = user.role === "SECRETARY" ||
|
|
865
|
-
user.role === "DOCTOR" ||
|
|
866
|
-
user.adminRole === "admin" ||
|
|
867
|
-
user.adminRole === "superadmin";
|
|
868
|
-
if (!canManageShipment) {
|
|
869
|
-
throw new ConvexError({
|
|
870
|
-
code: "FORBIDDEN",
|
|
871
|
-
message: "Ruolo non autorizzato alla gestione spedizione",
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
// 5. Aggiorna prescrizione
|
|
748
|
+
// 3. Aggiorna prescrizione
|
|
875
749
|
await ctx.db.patch(args.prescriptionId, {
|
|
876
750
|
shippedAt: now,
|
|
877
|
-
shippedBy:
|
|
751
|
+
shippedBy: actor.actorUserId,
|
|
878
752
|
trackingNumber: args.trackingNumber,
|
|
879
753
|
updatedAt: now,
|
|
880
754
|
});
|
|
881
|
-
//
|
|
755
|
+
// 4. Audit
|
|
882
756
|
await ctx.db.insert("auditEvents", {
|
|
883
757
|
auditEventId: 0, // Will be backfilled
|
|
884
758
|
prescriptionRef: args.prescriptionId,
|
|
885
759
|
at: now,
|
|
886
|
-
actorUserId:
|
|
887
|
-
actorRole:
|
|
760
|
+
actorUserId: actor.actorUserId,
|
|
761
|
+
actorRole: actor.actorRole,
|
|
888
762
|
type: "PRESCRIPTION_SHIPPED",
|
|
889
763
|
payload: {
|
|
890
764
|
trackingNumber: args.trackingNumber,
|
|
891
765
|
shippedAt: now,
|
|
766
|
+
actorEmail: identity.email,
|
|
892
767
|
},
|
|
893
768
|
});
|
|
894
769
|
return { success: true, shippedAt: now };
|
|
@@ -903,44 +778,33 @@ export const unmarkAsShipped = mutation({
|
|
|
903
778
|
},
|
|
904
779
|
handler: async (ctx, args) => {
|
|
905
780
|
const now = Date.now();
|
|
906
|
-
const
|
|
781
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
782
|
+
const actor = getActorFromIdentity(identity);
|
|
907
783
|
// 1. Carica prescrizione
|
|
908
784
|
const prescription = await ctx.db.get(args.prescriptionId);
|
|
909
785
|
if (!prescription) {
|
|
910
786
|
throw new Error("Prescrizione non trovata");
|
|
911
787
|
}
|
|
912
|
-
assertPrescriptionAccessOrThrow(user, prescription);
|
|
913
788
|
// 2. Verifica se è spedita
|
|
914
789
|
if (!prescription.shippedAt) {
|
|
915
790
|
throw new Error("Questa prescrizione non è marcata come spedita");
|
|
916
791
|
}
|
|
917
|
-
// 3.
|
|
918
|
-
const canManageShipment = user.role === "SECRETARY" ||
|
|
919
|
-
user.role === "DOCTOR" ||
|
|
920
|
-
user.adminRole === "admin" ||
|
|
921
|
-
user.adminRole === "superadmin";
|
|
922
|
-
if (!canManageShipment) {
|
|
923
|
-
throw new ConvexError({
|
|
924
|
-
code: "FORBIDDEN",
|
|
925
|
-
message: "Ruolo non autorizzato alla gestione spedizione",
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
// 4. Aggiorna prescrizione
|
|
792
|
+
// 3. Aggiorna prescrizione
|
|
929
793
|
await ctx.db.patch(args.prescriptionId, {
|
|
930
794
|
shippedAt: undefined,
|
|
931
795
|
shippedBy: undefined,
|
|
932
796
|
trackingNumber: undefined,
|
|
933
797
|
updatedAt: now,
|
|
934
798
|
});
|
|
935
|
-
//
|
|
799
|
+
// 4. Audit
|
|
936
800
|
await ctx.db.insert("auditEvents", {
|
|
937
801
|
auditEventId: 0, // Will be backfilled
|
|
938
802
|
prescriptionRef: args.prescriptionId,
|
|
939
803
|
at: now,
|
|
940
|
-
actorUserId:
|
|
941
|
-
actorRole:
|
|
804
|
+
actorUserId: actor.actorUserId,
|
|
805
|
+
actorRole: actor.actorRole,
|
|
942
806
|
type: "PRESCRIPTION_SHIPMENT_CANCELLED",
|
|
943
|
-
payload: {},
|
|
807
|
+
payload: { actorEmail: identity.email },
|
|
944
808
|
});
|
|
945
809
|
return { success: true };
|
|
946
810
|
},
|
|
@@ -979,22 +843,8 @@ export const createFromCalendar = mutation({
|
|
|
979
843
|
handler: async (ctx, args) => {
|
|
980
844
|
const now = Date.now();
|
|
981
845
|
const flowKey = args.flowKey ?? "prosthetics-standard";
|
|
982
|
-
const
|
|
983
|
-
const
|
|
984
|
-
user.adminRole === "admin" ||
|
|
985
|
-
user.adminRole === "superadmin";
|
|
986
|
-
if (!canCreate) {
|
|
987
|
-
throw new ConvexError({
|
|
988
|
-
code: "FORBIDDEN",
|
|
989
|
-
message: "Utente non autorizzato a creare prescrizioni",
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
if (user.clinicId !== args.clinicId && user.adminRole !== "admin" && user.adminRole !== "superadmin") {
|
|
993
|
-
throw new ConvexError({
|
|
994
|
-
code: "FORBIDDEN",
|
|
995
|
-
message: "Clinic non autorizzata per l'utente autenticato",
|
|
996
|
-
});
|
|
997
|
-
}
|
|
846
|
+
const identity = await requireIdentityOrThrow(ctx);
|
|
847
|
+
const actor = getActorFromIdentity(identity);
|
|
998
848
|
// =============================================
|
|
999
849
|
// 1. CONTROLLO IDEMPOTENZA
|
|
1000
850
|
// Se esiste già una prescrizione con questa idempotencyKey,
|
|
@@ -1119,7 +969,7 @@ export const createFromCalendar = mutation({
|
|
|
1119
969
|
flowKey: actualFlowKey, // Usa il flow effettivo (potrebbe essere il default)
|
|
1120
970
|
flowVersion: flow.version,
|
|
1121
971
|
status: "DRAFT",
|
|
1122
|
-
createdByUserId:
|
|
972
|
+
createdByUserId: actor.actorUserId, // Derivato dall'identità autenticata
|
|
1123
973
|
createdAt: now,
|
|
1124
974
|
updatedAt: now,
|
|
1125
975
|
coreRefs: {
|
|
@@ -1232,8 +1082,8 @@ export const createFromCalendar = mutation({
|
|
|
1232
1082
|
auditEventId: 0, // Will be backfilled
|
|
1233
1083
|
prescriptionRef: prescriptionId,
|
|
1234
1084
|
at: now,
|
|
1235
|
-
actorUserId:
|
|
1236
|
-
actorRole:
|
|
1085
|
+
actorUserId: actor.actorUserId,
|
|
1086
|
+
actorRole: actor.actorRole,
|
|
1237
1087
|
type: "PRESCRIPTION_CREATED_FROM_CALENDAR",
|
|
1238
1088
|
payload: {
|
|
1239
1089
|
idempotencyKey: args.idempotencyKey,
|
|
@@ -1245,6 +1095,7 @@ export const createFromCalendar = mutation({
|
|
|
1245
1095
|
patientName: `${args.calendarData.patientFirstName} ${args.calendarData.patientLastName}`,
|
|
1246
1096
|
treatmentName: args.calendarData.treatmentName,
|
|
1247
1097
|
phaseName: args.calendarData.phaseName,
|
|
1098
|
+
actorEmail: identity.email,
|
|
1248
1099
|
},
|
|
1249
1100
|
});
|
|
1250
1101
|
// =============================================
|