@blackcode_sa/metaestetics-api 1.6.4 → 1.6.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/dist/admin/index.d.mts +236 -2
- package/dist/admin/index.d.ts +236 -2
- package/dist/admin/index.js +11251 -10447
- package/dist/admin/index.mjs +11251 -10447
- package/dist/backoffice/index.d.mts +2 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/index.d.mts +50 -77
- package/dist/index.d.ts +50 -77
- package/dist/index.js +77 -305
- package/dist/index.mjs +78 -306
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/README.md +128 -0
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
- package/src/admin/booking/README.md +125 -0
- package/src/admin/booking/booking.admin.ts +638 -3
- package/src/admin/calendar/calendar.admin.service.ts +183 -0
- package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
- package/src/admin/mailing/base.mailing.service.ts +1 -1
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/notifications/notifications.admin.ts +397 -1
- package/src/backoffice/types/product.types.ts +2 -0
- package/src/services/appointment/appointment.service.ts +89 -182
- package/src/services/procedure/procedure.service.ts +1 -0
- package/src/types/appointment/index.ts +3 -1
- package/src/types/notifications/index.ts +4 -2
- package/src/types/procedure/index.ts +7 -0
- package/src/validations/appointment.schema.ts +2 -3
- package/src/validations/procedure.schema.ts +3 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as admin from "firebase-admin";
|
|
2
|
+
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
2
3
|
import {
|
|
3
4
|
BookingAvailabilityCalculator,
|
|
4
5
|
BookingAvailabilityRequest,
|
|
@@ -6,9 +7,72 @@ import {
|
|
|
6
7
|
AvailableSlot,
|
|
7
8
|
} from "./";
|
|
8
9
|
import { CalendarEventStatus, CalendarEventType } from "../../types/calendar";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
Clinic,
|
|
12
|
+
CLINICS_COLLECTION,
|
|
13
|
+
ClinicGroup,
|
|
14
|
+
CLINIC_GROUPS_COLLECTION,
|
|
15
|
+
} from "../../types/clinic";
|
|
16
|
+
import {
|
|
17
|
+
Practitioner,
|
|
18
|
+
PRACTITIONERS_COLLECTION,
|
|
19
|
+
} from "../../types/practitioner";
|
|
20
|
+
import {
|
|
21
|
+
Procedure,
|
|
22
|
+
PROCEDURES_COLLECTION,
|
|
23
|
+
ProcedureSummaryInfo,
|
|
24
|
+
} from "../../types/procedure";
|
|
25
|
+
import {
|
|
26
|
+
PatientProfile,
|
|
27
|
+
PATIENTS_COLLECTION,
|
|
28
|
+
PatientSensitiveInfo,
|
|
29
|
+
PATIENT_SENSITIVE_INFO_COLLECTION,
|
|
30
|
+
Gender,
|
|
31
|
+
} from "../../types/patient";
|
|
32
|
+
import {
|
|
33
|
+
Appointment,
|
|
34
|
+
AppointmentStatus,
|
|
35
|
+
PaymentStatus,
|
|
36
|
+
APPOINTMENTS_COLLECTION,
|
|
37
|
+
ProcedureExtendedInfo,
|
|
38
|
+
} from "../../types/appointment";
|
|
39
|
+
import { Currency } from "../../backoffice/types/static/pricing.types";
|
|
40
|
+
import {
|
|
41
|
+
DocumentTemplate as AppDocumentTemplate,
|
|
42
|
+
FilledDocument,
|
|
43
|
+
FilledDocumentStatus,
|
|
44
|
+
USER_FORMS_SUBCOLLECTION,
|
|
45
|
+
DOCTOR_FORMS_SUBCOLLECTION,
|
|
46
|
+
} from "../../types/documentation-templates";
|
|
47
|
+
import {
|
|
48
|
+
ClinicInfo,
|
|
49
|
+
PractitionerProfileInfo,
|
|
50
|
+
PatientProfileInfo,
|
|
51
|
+
} from "../../types/profile";
|
|
52
|
+
import { Category } from "../../backoffice/types/category.types";
|
|
53
|
+
import { Subcategory } from "../../backoffice/types/subcategory.types";
|
|
54
|
+
import { Technology } from "../../backoffice/types/technology.types";
|
|
55
|
+
import { Product } from "../../backoffice/types/product.types";
|
|
56
|
+
import {
|
|
57
|
+
CalendarEvent,
|
|
58
|
+
CalendarEventTime,
|
|
59
|
+
CalendarSyncStatus,
|
|
60
|
+
ProcedureInfo as CalendarProcedureInfo,
|
|
61
|
+
CALENDAR_COLLECTION,
|
|
62
|
+
} from "../../types/calendar";
|
|
63
|
+
import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
|
|
64
|
+
import { LinkedFormInfo } from "../../types/appointment";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Interface for the data required by orchestrateAppointmentCreation
|
|
68
|
+
*/
|
|
69
|
+
export interface OrchestrateAppointmentCreationData {
|
|
70
|
+
patientId: string;
|
|
71
|
+
procedureId: string;
|
|
72
|
+
appointmentStartTime: admin.firestore.Timestamp;
|
|
73
|
+
appointmentEndTime: admin.firestore.Timestamp;
|
|
74
|
+
patientNotes?: string | null;
|
|
75
|
+
}
|
|
12
76
|
|
|
13
77
|
/**
|
|
14
78
|
* Admin service for handling booking-related operations.
|
|
@@ -16,6 +80,7 @@ import { Procedure } from "../../types/procedure";
|
|
|
16
80
|
*/
|
|
17
81
|
export class BookingAdmin {
|
|
18
82
|
private db: admin.firestore.Firestore;
|
|
83
|
+
private documentManagerAdmin: DocumentManagerAdminService;
|
|
19
84
|
|
|
20
85
|
/**
|
|
21
86
|
* Creates a new BookingAdmin instance
|
|
@@ -23,6 +88,7 @@ export class BookingAdmin {
|
|
|
23
88
|
*/
|
|
24
89
|
constructor(firestore?: admin.firestore.Firestore) {
|
|
25
90
|
this.db = firestore || admin.firestore();
|
|
91
|
+
this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
|
|
26
92
|
}
|
|
27
93
|
|
|
28
94
|
/**
|
|
@@ -231,4 +297,573 @@ export class BookingAdmin {
|
|
|
231
297
|
return [];
|
|
232
298
|
}
|
|
233
299
|
}
|
|
300
|
+
|
|
301
|
+
private _generateCalendarProcedureInfo(
|
|
302
|
+
procedure: Procedure
|
|
303
|
+
): CalendarProcedureInfo {
|
|
304
|
+
return {
|
|
305
|
+
name: procedure.name,
|
|
306
|
+
description: procedure.description,
|
|
307
|
+
duration: procedure.duration, // in minutes
|
|
308
|
+
price: procedure.price,
|
|
309
|
+
currency: procedure.currency,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Orchestrates the creation of a new appointment, including data aggregation.
|
|
315
|
+
* This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
|
|
316
|
+
*
|
|
317
|
+
* @param data - Data required to create the appointment.
|
|
318
|
+
* @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
|
|
319
|
+
* @returns Promise resolving to an object indicating success, and appointmentId or an error message.
|
|
320
|
+
*/
|
|
321
|
+
async orchestrateAppointmentCreation(
|
|
322
|
+
data: OrchestrateAppointmentCreationData,
|
|
323
|
+
authenticatedUserId: string
|
|
324
|
+
): Promise<{
|
|
325
|
+
success: boolean;
|
|
326
|
+
appointmentId?: string;
|
|
327
|
+
appointmentData?: Appointment;
|
|
328
|
+
practitionerCalendarEventId?: string;
|
|
329
|
+
patientCalendarEventId?: string;
|
|
330
|
+
clinicCalendarEventId?: string;
|
|
331
|
+
error?: string;
|
|
332
|
+
}> {
|
|
333
|
+
console.log(
|
|
334
|
+
`[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
|
|
335
|
+
);
|
|
336
|
+
const batch = this.db.batch();
|
|
337
|
+
const adminTsNow = admin.firestore.Timestamp.now();
|
|
338
|
+
const serverTimestampValue = admin.firestore.FieldValue.serverTimestamp();
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// --- 1. Input Validation ---
|
|
342
|
+
if (
|
|
343
|
+
!data.patientId ||
|
|
344
|
+
!data.procedureId ||
|
|
345
|
+
!data.appointmentStartTime ||
|
|
346
|
+
!data.appointmentEndTime
|
|
347
|
+
) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
error:
|
|
351
|
+
"Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime.",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
data.appointmentEndTime.toMillis() <=
|
|
356
|
+
data.appointmentStartTime.toMillis()
|
|
357
|
+
) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: "Appointment end time must be after start time.",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (authenticatedUserId !== data.patientId) {
|
|
364
|
+
console.warn(
|
|
365
|
+
`[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- 2. Fetch Core Procedure Data ---
|
|
370
|
+
const procedureRef = this.db
|
|
371
|
+
.collection(PROCEDURES_COLLECTION)
|
|
372
|
+
.doc(data.procedureId);
|
|
373
|
+
const procedureDoc = await procedureRef.get();
|
|
374
|
+
if (!procedureDoc.exists) {
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: `Procedure ${data.procedureId} not found.`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const procedure = procedureDoc.data() as Procedure;
|
|
381
|
+
|
|
382
|
+
// --- 3. Fetch Clinic and then other Primary Documents ---
|
|
383
|
+
// Fetch clinic first to get its clinicGroupId
|
|
384
|
+
const clinicRef = this.db
|
|
385
|
+
.collection(CLINICS_COLLECTION)
|
|
386
|
+
.doc(procedure.clinicBranchId);
|
|
387
|
+
const clinicSnap = await clinicRef.get(); // Await here directly
|
|
388
|
+
if (!clinicSnap.exists) {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
error: `Clinic ${procedure.clinicBranchId} not found.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const clinicData = clinicSnap.data() as Clinic; // Now clinicData is available
|
|
395
|
+
|
|
396
|
+
// Define other refs using clinicData for clinicGroupRef
|
|
397
|
+
const practitionerRef = this.db
|
|
398
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
399
|
+
.doc(procedure.practitionerId);
|
|
400
|
+
const patientProfileRef = this.db
|
|
401
|
+
.collection(PATIENTS_COLLECTION)
|
|
402
|
+
.doc(data.patientId);
|
|
403
|
+
const patientSensitiveRef = this.db
|
|
404
|
+
.collection(PATIENTS_COLLECTION)
|
|
405
|
+
.doc(data.patientId)
|
|
406
|
+
.collection(PATIENT_SENSITIVE_INFO_COLLECTION)
|
|
407
|
+
.doc(data.patientId);
|
|
408
|
+
const clinicGroupRef = this.db
|
|
409
|
+
.collection(CLINIC_GROUPS_COLLECTION)
|
|
410
|
+
.doc(clinicData.clinicGroupId); // Use clinicData here
|
|
411
|
+
|
|
412
|
+
const [
|
|
413
|
+
practitionerSnap,
|
|
414
|
+
patientProfileSnap,
|
|
415
|
+
patientSensitiveSnap,
|
|
416
|
+
clinicGroupSnap,
|
|
417
|
+
] = await Promise.all([
|
|
418
|
+
// clinicRef.get() is already done via clinicSnap
|
|
419
|
+
practitionerRef.get(),
|
|
420
|
+
patientProfileRef.get(),
|
|
421
|
+
patientSensitiveRef.get(),
|
|
422
|
+
clinicGroupRef.get(), // Fetch the defined clinicGroupRef
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
if (!practitionerSnap.exists)
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
error: `Practitioner ${procedure.practitionerId} not found.`,
|
|
429
|
+
};
|
|
430
|
+
if (!patientProfileSnap.exists)
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
error: `PatientProfile ${data.patientId} not found.`,
|
|
434
|
+
};
|
|
435
|
+
if (!clinicGroupSnap.exists)
|
|
436
|
+
return {
|
|
437
|
+
success: false,
|
|
438
|
+
error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const practitionerData = practitionerSnap.data() as Practitioner;
|
|
442
|
+
const patientProfileData = patientProfileSnap.data() as PatientProfile;
|
|
443
|
+
const patientSensitiveData = patientSensitiveSnap.exists
|
|
444
|
+
? (patientSensitiveSnap.data() as PatientSensitiveInfo)
|
|
445
|
+
: undefined;
|
|
446
|
+
const clinicGroupData = clinicGroupSnap.data() as ClinicGroup;
|
|
447
|
+
|
|
448
|
+
// --- 4. Determine initialStatus (based on clinic settings) ---
|
|
449
|
+
const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
|
|
450
|
+
const initialAppointmentStatus = autoConfirm
|
|
451
|
+
? AppointmentStatus.CONFIRMED
|
|
452
|
+
: AppointmentStatus.PENDING;
|
|
453
|
+
const initialCalendarEventStatus = autoConfirm
|
|
454
|
+
? CalendarEventStatus.CONFIRMED
|
|
455
|
+
: CalendarEventStatus.PENDING;
|
|
456
|
+
|
|
457
|
+
// --- 5. Aggregate Information (Snapshots) ---
|
|
458
|
+
const clinicInfo: ClinicInfo = {
|
|
459
|
+
id: clinicSnap.id,
|
|
460
|
+
name: clinicData.name,
|
|
461
|
+
featuredPhoto: clinicData.coverPhoto || clinicData.logo || "",
|
|
462
|
+
description: clinicData.description,
|
|
463
|
+
location: clinicData.location,
|
|
464
|
+
contactInfo: clinicData.contactInfo,
|
|
465
|
+
};
|
|
466
|
+
const practitionerInfo: PractitionerProfileInfo = {
|
|
467
|
+
id: practitionerSnap.id,
|
|
468
|
+
practitionerPhoto: practitionerData.basicInfo.profileImageUrl || null,
|
|
469
|
+
name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
470
|
+
email: practitionerData.basicInfo.email,
|
|
471
|
+
phone: practitionerData.basicInfo.phoneNumber || null,
|
|
472
|
+
certification: practitionerData.certification,
|
|
473
|
+
};
|
|
474
|
+
const patientInfo: PatientProfileInfo = {
|
|
475
|
+
id: patientProfileSnap.id,
|
|
476
|
+
fullName:
|
|
477
|
+
`${patientSensitiveData?.firstName || ""} ${
|
|
478
|
+
patientSensitiveData?.lastName || ""
|
|
479
|
+
}`.trim() || patientProfileData.displayName,
|
|
480
|
+
email: patientSensitiveData?.email || "",
|
|
481
|
+
phone:
|
|
482
|
+
patientSensitiveData?.phoneNumber ||
|
|
483
|
+
patientProfileData.phoneNumber ||
|
|
484
|
+
null,
|
|
485
|
+
dateOfBirth:
|
|
486
|
+
patientSensitiveData?.dateOfBirth ||
|
|
487
|
+
patientProfileData.dateOfBirth ||
|
|
488
|
+
admin.firestore.Timestamp.now(),
|
|
489
|
+
gender: patientSensitiveData?.gender || Gender.OTHER,
|
|
490
|
+
};
|
|
491
|
+
const procedureCategory = procedure.category as Category;
|
|
492
|
+
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
493
|
+
const procedureTechnology = procedure.technology as Technology;
|
|
494
|
+
const procedureProduct = procedure.product as Product;
|
|
495
|
+
|
|
496
|
+
const procedureInfo: ProcedureSummaryInfo = {
|
|
497
|
+
id: procedure.id,
|
|
498
|
+
name: procedure.name,
|
|
499
|
+
description: procedure.description,
|
|
500
|
+
family: procedure.family,
|
|
501
|
+
categoryName: procedureCategory?.name || "",
|
|
502
|
+
subcategoryName: procedureSubCategory?.name || "",
|
|
503
|
+
technologyName: procedureTechnology?.name || "",
|
|
504
|
+
price: procedure.price,
|
|
505
|
+
pricingMeasure: procedure.pricingMeasure,
|
|
506
|
+
currency: procedure.currency,
|
|
507
|
+
duration: procedure.duration,
|
|
508
|
+
clinicId: procedure.clinicBranchId,
|
|
509
|
+
clinicName: clinicData.name,
|
|
510
|
+
practitionerId: procedure.practitionerId,
|
|
511
|
+
practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
512
|
+
photo: procedure.photos?.[0] || "",
|
|
513
|
+
brandName: procedureProduct?.brandName || "",
|
|
514
|
+
productName: procedureProduct?.name || "",
|
|
515
|
+
};
|
|
516
|
+
const procedureExtendedInfo: ProcedureExtendedInfo = {
|
|
517
|
+
id: procedure.id,
|
|
518
|
+
name: procedure.name,
|
|
519
|
+
description: procedure.description,
|
|
520
|
+
cost: procedure.price,
|
|
521
|
+
duration: procedure.duration,
|
|
522
|
+
procedureFamily: procedure.family,
|
|
523
|
+
procedureCategoryId: procedureCategory?.id || "",
|
|
524
|
+
procedureCategoryName: procedureCategory?.name || "",
|
|
525
|
+
procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
526
|
+
procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
527
|
+
procedureTechnologyId: procedureTechnology?.id || "",
|
|
528
|
+
procedureTechnologyName: procedureTechnology?.name || "",
|
|
529
|
+
procedureProductBrandId: procedureProduct.brandId || "",
|
|
530
|
+
procedureProductBrandName: procedureProduct.brandName || "",
|
|
531
|
+
procedureProductId: procedureProduct.id || "",
|
|
532
|
+
procedureProductName: procedureProduct.name || "",
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// --- 6. Determine pendingUserFormsIds and linkedFormIds ---
|
|
536
|
+
let pendingUserFormsIds: string[] = [];
|
|
537
|
+
let linkedFormIds: string[] = [];
|
|
538
|
+
if (
|
|
539
|
+
procedure.documentationTemplates &&
|
|
540
|
+
Array.isArray(procedure.documentationTemplates)
|
|
541
|
+
) {
|
|
542
|
+
pendingUserFormsIds = procedure.documentationTemplates
|
|
543
|
+
.filter(
|
|
544
|
+
(template: AppDocumentTemplate) =>
|
|
545
|
+
template.isUserForm && template.isRequired
|
|
546
|
+
)
|
|
547
|
+
.map((template: AppDocumentTemplate) => template.id);
|
|
548
|
+
linkedFormIds = procedure.documentationTemplates.map(
|
|
549
|
+
(template: AppDocumentTemplate) => template.id
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- 7. Construct New Appointment Object ---
|
|
554
|
+
const newAppointmentId = this.db
|
|
555
|
+
.collection(APPOINTMENTS_COLLECTION)
|
|
556
|
+
.doc().id;
|
|
557
|
+
|
|
558
|
+
const eventTimeForCalendarEvents: CalendarEventTime = {
|
|
559
|
+
start: new FirebaseClientTimestamp(
|
|
560
|
+
data.appointmentStartTime.seconds,
|
|
561
|
+
data.appointmentStartTime.nanoseconds
|
|
562
|
+
),
|
|
563
|
+
end: new FirebaseClientTimestamp(
|
|
564
|
+
data.appointmentEndTime.seconds,
|
|
565
|
+
data.appointmentEndTime.nanoseconds
|
|
566
|
+
),
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Practitioner Calendar Event
|
|
570
|
+
const practitionerCalendarEventId = this.db
|
|
571
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
572
|
+
.doc(practitionerData.id)
|
|
573
|
+
.collection(CALENDAR_COLLECTION)
|
|
574
|
+
.doc().id;
|
|
575
|
+
const practitionerCalendarEventData: CalendarEvent = {
|
|
576
|
+
id: practitionerCalendarEventId,
|
|
577
|
+
appointmentId: newAppointmentId,
|
|
578
|
+
clinicBranchId: clinicData.id,
|
|
579
|
+
clinicBranchInfo: clinicInfo,
|
|
580
|
+
practitionerProfileId: practitionerData.id,
|
|
581
|
+
practitionerProfileInfo: practitionerInfo,
|
|
582
|
+
patientProfileId: patientProfileData.id,
|
|
583
|
+
patientProfileInfo: patientInfo,
|
|
584
|
+
procedureId: procedure.id,
|
|
585
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
586
|
+
eventName: `Appointment: ${procedure.name} with ${patientInfo.fullName}`,
|
|
587
|
+
eventLocation: clinicData.location,
|
|
588
|
+
eventTime: eventTimeForCalendarEvents,
|
|
589
|
+
description: procedure.description || undefined,
|
|
590
|
+
status: initialCalendarEventStatus,
|
|
591
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
592
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
593
|
+
createdAt: serverTimestampValue as any,
|
|
594
|
+
updatedAt: serverTimestampValue as any,
|
|
595
|
+
};
|
|
596
|
+
batch.set(
|
|
597
|
+
this.db
|
|
598
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
599
|
+
.doc(practitionerData.id)
|
|
600
|
+
.collection(CALENDAR_COLLECTION)
|
|
601
|
+
.doc(practitionerCalendarEventId),
|
|
602
|
+
practitionerCalendarEventData
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Patient Calendar Event
|
|
606
|
+
const patientCalendarEventId = this.db
|
|
607
|
+
.collection(PATIENTS_COLLECTION)
|
|
608
|
+
.doc(patientProfileData.id)
|
|
609
|
+
.collection(CALENDAR_COLLECTION)
|
|
610
|
+
.doc().id;
|
|
611
|
+
const patientCalendarEventData: CalendarEvent = {
|
|
612
|
+
id: patientCalendarEventId,
|
|
613
|
+
appointmentId: newAppointmentId,
|
|
614
|
+
clinicBranchId: clinicData.id,
|
|
615
|
+
clinicBranchInfo: clinicInfo,
|
|
616
|
+
practitionerProfileId: practitionerData.id,
|
|
617
|
+
practitionerProfileInfo: practitionerInfo,
|
|
618
|
+
procedureId: procedure.id,
|
|
619
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
620
|
+
eventName: `Appointment: ${procedure.name} at ${clinicData.name}`,
|
|
621
|
+
eventLocation: clinicData.location,
|
|
622
|
+
eventTime: eventTimeForCalendarEvents,
|
|
623
|
+
description: data.patientNotes || undefined,
|
|
624
|
+
status: initialCalendarEventStatus,
|
|
625
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
626
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
627
|
+
createdAt: serverTimestampValue as any,
|
|
628
|
+
updatedAt: serverTimestampValue as any,
|
|
629
|
+
};
|
|
630
|
+
batch.set(
|
|
631
|
+
this.db
|
|
632
|
+
.collection(PATIENTS_COLLECTION)
|
|
633
|
+
.doc(patientProfileData.id)
|
|
634
|
+
.collection(CALENDAR_COLLECTION)
|
|
635
|
+
.doc(patientCalendarEventId),
|
|
636
|
+
patientCalendarEventData
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
// Clinic Calendar Event
|
|
640
|
+
const clinicCalendarEventId = this.db
|
|
641
|
+
.collection(CLINICS_COLLECTION)
|
|
642
|
+
.doc(clinicData.id)
|
|
643
|
+
.collection(CALENDAR_COLLECTION)
|
|
644
|
+
.doc().id;
|
|
645
|
+
const clinicCalendarEventData: CalendarEvent = {
|
|
646
|
+
id: clinicCalendarEventId,
|
|
647
|
+
appointmentId: newAppointmentId,
|
|
648
|
+
clinicBranchId: clinicData.id,
|
|
649
|
+
clinicBranchInfo: clinicInfo,
|
|
650
|
+
practitionerProfileId: practitionerData.id,
|
|
651
|
+
practitionerProfileInfo: practitionerInfo,
|
|
652
|
+
patientProfileId: patientProfileData.id,
|
|
653
|
+
patientProfileInfo: patientInfo,
|
|
654
|
+
procedureId: procedure.id,
|
|
655
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
656
|
+
eventName: `Appointment: ${procedure.name} for ${patientInfo.fullName} with ${practitionerInfo.name}`,
|
|
657
|
+
eventLocation: clinicData.location,
|
|
658
|
+
eventTime: eventTimeForCalendarEvents,
|
|
659
|
+
description: data.patientNotes || undefined,
|
|
660
|
+
status: initialCalendarEventStatus,
|
|
661
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
662
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
663
|
+
createdAt: serverTimestampValue as any,
|
|
664
|
+
updatedAt: serverTimestampValue as any,
|
|
665
|
+
};
|
|
666
|
+
batch.set(
|
|
667
|
+
this.db
|
|
668
|
+
.collection(CLINICS_COLLECTION)
|
|
669
|
+
.doc(clinicData.id)
|
|
670
|
+
.collection(CALENDAR_COLLECTION)
|
|
671
|
+
.doc(clinicCalendarEventId),
|
|
672
|
+
clinicCalendarEventData
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// --- Initialize Pending/Draft Filled Documents and get form IDs ---
|
|
676
|
+
let initializedFormsInfo: LinkedFormInfo[] = [];
|
|
677
|
+
let pendingUserFormTemplateIds: string[] = [];
|
|
678
|
+
let allLinkedFormTemplateIds: string[] = [];
|
|
679
|
+
|
|
680
|
+
if (
|
|
681
|
+
procedure.documentationTemplates &&
|
|
682
|
+
Array.isArray(procedure.documentationTemplates) &&
|
|
683
|
+
procedure.documentationTemplates.length > 0
|
|
684
|
+
) {
|
|
685
|
+
const formInitResult =
|
|
686
|
+
this.documentManagerAdmin.batchInitializeAppointmentForms(
|
|
687
|
+
batch,
|
|
688
|
+
newAppointmentId,
|
|
689
|
+
procedure.id, // Pass the actual procedureId for the forms
|
|
690
|
+
procedure.documentationTemplates as AppDocumentTemplate[],
|
|
691
|
+
data.patientId,
|
|
692
|
+
procedure.practitionerId,
|
|
693
|
+
procedure.clinicBranchId,
|
|
694
|
+
adminTsNow.toMillis()
|
|
695
|
+
);
|
|
696
|
+
initializedFormsInfo = formInitResult.initializedFormsInfo;
|
|
697
|
+
pendingUserFormTemplateIds = formInitResult.pendingUserFormsIds;
|
|
698
|
+
allLinkedFormTemplateIds = formInitResult.allLinkedTemplateIds;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// --- Construct Appointment Object ---
|
|
702
|
+
const newAppointmentData: Appointment = {
|
|
703
|
+
id: newAppointmentId,
|
|
704
|
+
calendarEventId: practitionerCalendarEventId,
|
|
705
|
+
clinicBranchId: procedure.clinicBranchId,
|
|
706
|
+
clinicInfo,
|
|
707
|
+
practitionerId: procedure.practitionerId,
|
|
708
|
+
practitionerInfo,
|
|
709
|
+
patientId: data.patientId,
|
|
710
|
+
patientInfo,
|
|
711
|
+
procedureId: data.procedureId,
|
|
712
|
+
procedureInfo: this._generateProcedureSummaryInfo(
|
|
713
|
+
procedure,
|
|
714
|
+
clinicData,
|
|
715
|
+
practitionerData
|
|
716
|
+
),
|
|
717
|
+
procedureExtendedInfo: this._generateProcedureExtendedInfo(procedure),
|
|
718
|
+
status: initialAppointmentStatus,
|
|
719
|
+
bookingTime: new FirebaseClientTimestamp(
|
|
720
|
+
adminTsNow.seconds,
|
|
721
|
+
adminTsNow.nanoseconds
|
|
722
|
+
),
|
|
723
|
+
confirmationTime:
|
|
724
|
+
initialAppointmentStatus === AppointmentStatus.CONFIRMED
|
|
725
|
+
? new FirebaseClientTimestamp(
|
|
726
|
+
adminTsNow.seconds,
|
|
727
|
+
adminTsNow.nanoseconds
|
|
728
|
+
)
|
|
729
|
+
: null,
|
|
730
|
+
appointmentStartTime: new FirebaseClientTimestamp(
|
|
731
|
+
data.appointmentStartTime.seconds,
|
|
732
|
+
data.appointmentStartTime.nanoseconds
|
|
733
|
+
),
|
|
734
|
+
appointmentEndTime: new FirebaseClientTimestamp(
|
|
735
|
+
data.appointmentEndTime.seconds,
|
|
736
|
+
data.appointmentEndTime.nanoseconds
|
|
737
|
+
),
|
|
738
|
+
cost: procedure.price,
|
|
739
|
+
currency: procedure.currency,
|
|
740
|
+
paymentStatus:
|
|
741
|
+
procedure.price > 0
|
|
742
|
+
? PaymentStatus.UNPAID
|
|
743
|
+
: PaymentStatus.NOT_APPLICABLE,
|
|
744
|
+
patientNotes: data.patientNotes || null,
|
|
745
|
+
blockingConditions: procedure.blockingConditions || [],
|
|
746
|
+
contraindications: (procedure as any).contraindications || [],
|
|
747
|
+
preProcedureRequirements: procedure.preRequirements || [],
|
|
748
|
+
postProcedureRequirements: procedure.postRequirements || [],
|
|
749
|
+
pendingUserFormsIds: pendingUserFormTemplateIds,
|
|
750
|
+
linkedFormIds: allLinkedFormTemplateIds,
|
|
751
|
+
completedPreRequirements: [],
|
|
752
|
+
completedPostRequirements: [],
|
|
753
|
+
linkedForms: initializedFormsInfo,
|
|
754
|
+
media: [],
|
|
755
|
+
reviewInfo: null,
|
|
756
|
+
finalizedDetails: undefined,
|
|
757
|
+
internalNotes: null,
|
|
758
|
+
cancellationReason: null,
|
|
759
|
+
cancellationTime: null,
|
|
760
|
+
canceledBy: undefined,
|
|
761
|
+
rescheduleTime: null,
|
|
762
|
+
procedureActualStartTime: null,
|
|
763
|
+
actualDurationMinutes: undefined,
|
|
764
|
+
isRecurring: false,
|
|
765
|
+
recurringAppointmentId: null,
|
|
766
|
+
isArchived: false,
|
|
767
|
+
createdAt: new FirebaseClientTimestamp(
|
|
768
|
+
adminTsNow.seconds,
|
|
769
|
+
adminTsNow.nanoseconds
|
|
770
|
+
),
|
|
771
|
+
updatedAt: new FirebaseClientTimestamp(
|
|
772
|
+
adminTsNow.seconds,
|
|
773
|
+
adminTsNow.nanoseconds
|
|
774
|
+
),
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
batch.set(
|
|
778
|
+
this.db.collection(APPOINTMENTS_COLLECTION).doc(newAppointmentId),
|
|
779
|
+
newAppointmentData
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
// Commit Batch
|
|
783
|
+
await batch.commit();
|
|
784
|
+
|
|
785
|
+
console.log(
|
|
786
|
+
`[BookingAdmin] Appointment ${newAppointmentId} and associated calendar events created successfully.`
|
|
787
|
+
);
|
|
788
|
+
return {
|
|
789
|
+
success: true,
|
|
790
|
+
appointmentId: newAppointmentId,
|
|
791
|
+
appointmentData: newAppointmentData,
|
|
792
|
+
practitionerCalendarEventId,
|
|
793
|
+
patientCalendarEventId,
|
|
794
|
+
clinicCalendarEventId,
|
|
795
|
+
};
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error(
|
|
798
|
+
"[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
|
|
799
|
+
error
|
|
800
|
+
);
|
|
801
|
+
const errorMessage =
|
|
802
|
+
error instanceof Error
|
|
803
|
+
? error.message
|
|
804
|
+
: "Unknown server error during appointment creation.";
|
|
805
|
+
return { success: false, error: errorMessage };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private _generateProcedureSummaryInfo(
|
|
810
|
+
procedure: Procedure,
|
|
811
|
+
clinicData: Clinic,
|
|
812
|
+
practitionerData: Practitioner
|
|
813
|
+
): ProcedureSummaryInfo {
|
|
814
|
+
const procedureCategory = procedure.category as Category;
|
|
815
|
+
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
816
|
+
const procedureTechnology = procedure.technology as Technology;
|
|
817
|
+
const procedureProduct = procedure.product as Product;
|
|
818
|
+
return {
|
|
819
|
+
id: procedure.id,
|
|
820
|
+
name: procedure.name,
|
|
821
|
+
description: procedure.description,
|
|
822
|
+
family: procedure.family,
|
|
823
|
+
categoryName: procedureCategory?.name || "",
|
|
824
|
+
subcategoryName: procedureSubCategory?.name || "",
|
|
825
|
+
technologyName: procedureTechnology?.name || "",
|
|
826
|
+
price: procedure.price,
|
|
827
|
+
pricingMeasure: procedure.pricingMeasure,
|
|
828
|
+
currency: procedure.currency,
|
|
829
|
+
duration: procedure.duration,
|
|
830
|
+
clinicId: procedure.clinicBranchId,
|
|
831
|
+
clinicName: clinicData.name,
|
|
832
|
+
practitionerId: procedure.practitionerId,
|
|
833
|
+
practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
834
|
+
photo:
|
|
835
|
+
(procedureTechnology as any)?.photos?.[0]?.url ||
|
|
836
|
+
(procedureProduct as any)?.photos?.[0]?.url ||
|
|
837
|
+
"",
|
|
838
|
+
brandName: (procedureProduct as any)?.brand?.name || "",
|
|
839
|
+
productName: procedureProduct?.name || "",
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private _generateProcedureExtendedInfo(
|
|
844
|
+
procedure: Procedure
|
|
845
|
+
): ProcedureExtendedInfo {
|
|
846
|
+
const procedureCategory = procedure.category as Category;
|
|
847
|
+
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
848
|
+
const procedureTechnology = procedure.technology as Technology;
|
|
849
|
+
const procedureProduct = procedure.product as Product;
|
|
850
|
+
return {
|
|
851
|
+
id: procedure.id,
|
|
852
|
+
name: procedure.name,
|
|
853
|
+
description: procedure.description,
|
|
854
|
+
cost: procedure.price,
|
|
855
|
+
duration: procedure.duration,
|
|
856
|
+
procedureFamily: procedure.family,
|
|
857
|
+
procedureCategoryId: procedureCategory?.id || "",
|
|
858
|
+
procedureCategoryName: procedureCategory?.name || "",
|
|
859
|
+
procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
860
|
+
procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
861
|
+
procedureTechnologyId: procedureTechnology?.id || "",
|
|
862
|
+
procedureTechnologyName: procedureTechnology?.name || "",
|
|
863
|
+
procedureProductBrandId: (procedureProduct as any)?.brand?.id || "",
|
|
864
|
+
procedureProductBrandName: (procedureProduct as any)?.brand?.name || "",
|
|
865
|
+
procedureProductId: procedureProduct?.id || "",
|
|
866
|
+
procedureProductName: procedureProduct?.name || "",
|
|
867
|
+
};
|
|
868
|
+
}
|
|
234
869
|
}
|