@blackcode_sa/metaestetics-api 1.6.3 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/admin/index.d.mts +439 -25
  2. package/dist/admin/index.d.ts +439 -25
  3. package/dist/admin/index.js +36107 -2493
  4. package/dist/admin/index.mjs +36093 -2461
  5. package/dist/backoffice/index.d.mts +254 -1
  6. package/dist/backoffice/index.d.ts +254 -1
  7. package/dist/backoffice/index.js +86 -12
  8. package/dist/backoffice/index.mjs +86 -13
  9. package/dist/index.d.mts +1434 -621
  10. package/dist/index.d.ts +1434 -621
  11. package/dist/index.js +1381 -970
  12. package/dist/index.mjs +1433 -1016
  13. package/package.json +1 -1
  14. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +321 -0
  15. package/src/admin/booking/booking.admin.ts +376 -3
  16. package/src/admin/index.ts +15 -1
  17. package/src/admin/notifications/notifications.admin.ts +1 -1
  18. package/src/admin/requirements/README.md +128 -0
  19. package/src/admin/requirements/patient-requirements.admin.service.ts +482 -0
  20. package/src/backoffice/types/product.types.ts +2 -0
  21. package/src/index.ts +16 -1
  22. package/src/services/appointment/appointment.service.ts +386 -250
  23. package/src/services/clinic/clinic-admin.service.ts +3 -0
  24. package/src/services/clinic/clinic-group.service.ts +8 -0
  25. package/src/services/documentation-templates/documentation-template.service.ts +24 -16
  26. package/src/services/documentation-templates/filled-document.service.ts +253 -136
  27. package/src/services/patient/patientRequirements.service.ts +285 -0
  28. package/src/services/procedure/procedure.service.ts +1 -0
  29. package/src/types/appointment/index.ts +136 -11
  30. package/src/types/documentation-templates/index.ts +34 -2
  31. package/src/types/notifications/README.md +77 -0
  32. package/src/types/notifications/index.ts +154 -27
  33. package/src/types/patient/patient-requirements.ts +81 -0
  34. package/src/types/procedure/index.ts +7 -0
  35. package/src/validations/appointment.schema.ts +298 -62
  36. package/src/validations/documentation-templates/template.schema.ts +55 -0
  37. package/src/validations/documentation-templates.schema.ts +9 -14
  38. package/src/validations/notification.schema.ts +3 -3
  39. package/src/validations/patient/patient-requirements.schema.ts +75 -0
  40. package/src/validations/procedure.schema.ts +3 -0
@@ -3,6 +3,8 @@ import {
3
3
  Timestamp,
4
4
  DocumentSnapshot,
5
5
  serverTimestamp,
6
+ arrayUnion,
7
+ arrayRemove,
6
8
  } from "firebase/firestore";
7
9
  import { Auth } from "firebase/auth";
8
10
  import { FirebaseApp } from "firebase/app";
@@ -15,6 +17,9 @@ import {
15
17
  UpdateAppointmentData,
16
18
  SearchAppointmentsParams,
17
19
  PaymentStatus,
20
+ AppointmentMediaItem,
21
+ PatientReviewInfo,
22
+ LinkedFormInfo,
18
23
  } from "../../types/appointment";
19
24
  import {
20
25
  createAppointmentSchema,
@@ -27,6 +32,7 @@ import { CalendarServiceV2 } from "../calendar/calendar-refactored.service";
27
32
  import { PatientService } from "../patient/patient.service";
28
33
  import { PractitionerService } from "../practitioner/practitioner.service";
29
34
  import { ClinicService } from "../clinic/clinic.service";
35
+ import { FilledDocumentService } from "../documentation-templates/filled-document.service";
30
36
 
31
37
  // Import utility functions
32
38
  import {
@@ -54,6 +60,7 @@ export class AppointmentService extends BaseService {
54
60
  private patientService: PatientService;
55
61
  private practitionerService: PractitionerService;
56
62
  private clinicService: ClinicService;
63
+ private filledDocumentService: FilledDocumentService;
57
64
  private functions: Functions;
58
65
 
59
66
  /**
@@ -74,110 +81,18 @@ export class AppointmentService extends BaseService {
74
81
  calendarService: CalendarServiceV2,
75
82
  patientService: PatientService,
76
83
  practitionerService: PractitionerService,
77
- clinicService: ClinicService
84
+ clinicService: ClinicService,
85
+ filledDocumentService: FilledDocumentService
78
86
  ) {
79
87
  super(db, auth, app);
80
88
  this.calendarService = calendarService;
81
89
  this.patientService = patientService;
82
90
  this.practitionerService = practitionerService;
83
91
  this.clinicService = clinicService;
92
+ this.filledDocumentService = filledDocumentService;
84
93
  this.functions = getFunctions(app, "europe-west6"); // Initialize Firebase Functions with the correct region
85
94
  }
86
95
 
87
- /**
88
- * Test method using the callable function version of getAvailableBookingSlots
89
- * For development and testing purposes only - not for production use
90
- *
91
- * @param clinicId ID of the clinic
92
- * @param practitionerId ID of the practitioner
93
- * @param procedureId ID of the procedure
94
- * @param startDate Start date of the time range to check
95
- * @param endDate End date of the time range to check
96
- * @returns Test result from the callable function
97
- */
98
- async testGetAvailableBookingSlots(
99
- clinicId: string,
100
- practitionerId: string,
101
- procedureId: string,
102
- startDate: Date,
103
- endDate: Date
104
- ): Promise<any> {
105
- try {
106
- console.log(
107
- `[APPOINTMENT_SERVICE] Testing callable function for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
108
- );
109
-
110
- // Get the callable function
111
- const getAvailableBookingSlotsCallable = httpsCallable(
112
- this.functions,
113
- "getAvailableBookingSlots"
114
- );
115
-
116
- // Call the function with the required parameters
117
- const result = await getAvailableBookingSlotsCallable({
118
- clinicId,
119
- practitionerId,
120
- procedureId,
121
- timeframe: {
122
- start: startDate.getTime(),
123
- end: endDate.getTime(),
124
- },
125
- });
126
-
127
- console.log(
128
- "[APPOINTMENT_SERVICE] Callable function test result:",
129
- result.data
130
- );
131
-
132
- return result.data;
133
- } catch (error) {
134
- console.error(
135
- "[APPOINTMENT_SERVICE] Error testing callable function:",
136
- error
137
- );
138
- throw error;
139
- }
140
- }
141
-
142
- /**
143
- * Gets available booking slots for a specific clinic, practitioner, and procedure.
144
- *
145
- * @param clinicId ID of the clinic
146
- * @param practitionerId ID of the practitioner
147
- * @param procedureId ID of the procedure
148
- * @param startDate Start date of the time range to check
149
- * @param endDate End date of the time range to check
150
- * @returns Array of available booking slots
151
- */
152
- async getAvailableBookingSlots(
153
- clinicId: string,
154
- practitionerId: string,
155
- procedureId: string,
156
- startDate: Date,
157
- endDate: Date
158
- ): Promise<AvailableSlot[]> {
159
- try {
160
- console.log(
161
- `[APPOINTMENT_SERVICE] Getting available booking slots for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
162
- );
163
-
164
- // Just call our HTTP implementation since the callable function isn't working in the browser
165
- return this.getAvailableBookingSlotsHttp(
166
- clinicId,
167
- practitionerId,
168
- procedureId,
169
- startDate,
170
- endDate
171
- );
172
- } catch (error) {
173
- console.error(
174
- "[APPOINTMENT_SERVICE] Error getting available booking slots:",
175
- error
176
- );
177
- throw error;
178
- }
179
- }
180
-
181
96
  /**
182
97
  * Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
183
98
  * This is an alternative implementation using direct HTTP request instead of callable function.
@@ -319,42 +234,108 @@ export class AppointmentService extends BaseService {
319
234
  }
320
235
 
321
236
  /**
322
- * Creates a new appointment.
237
+ * Creates an appointment via the Cloud Function orchestrateAppointmentCreation
323
238
  *
324
- * @param data Data needed to create the appointment
239
+ * @param data - CreateAppointmentData object
325
240
  * @returns The created appointment
326
241
  */
327
- async createAppointment(data: CreateAppointmentData): Promise<Appointment> {
242
+ async createAppointmentHttp(
243
+ data: CreateAppointmentData
244
+ ): Promise<Appointment> {
328
245
  try {
329
- console.log("[APPOINTMENT_SERVICE] Creating appointment");
246
+ console.log(
247
+ "[APPOINTMENT_SERVICE] Creating appointment via cloud function"
248
+ );
330
249
 
331
- // Validate input data
332
- const validatedData = await createAppointmentSchema.parseAsync(data);
250
+ // Get the authenticated user's ID token
251
+ const currentUser = this.auth.currentUser;
252
+ if (!currentUser) {
253
+ throw new Error("User must be authenticated to create an appointment");
254
+ }
255
+ const idToken = await currentUser.getIdToken();
333
256
 
334
- // Fetch all required aggregated information
335
- const aggregatedInfo = await fetchAggregatedInfoUtil(
336
- this.db,
337
- validatedData.clinicBranchId,
338
- validatedData.practitionerId,
339
- validatedData.patientId,
340
- validatedData.procedureId
341
- );
257
+ // Construct the function URL for the Express app endpoint
258
+ const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
342
259
 
343
- // Create the appointment using the utility function
344
- const appointment = await createAppointmentUtil(
345
- this.db,
346
- validatedData as CreateAppointmentData,
347
- aggregatedInfo,
348
- this.generateId.bind(this)
260
+ // Prepare request data for the Cloud Function
261
+ // Map CreateAppointmentData to OrchestrateAppointmentCreationData format
262
+ const requestData = {
263
+ patientId: data.patientId,
264
+ procedureId: data.procedureId,
265
+ appointmentStartTime: data.appointmentStartTime.toMillis
266
+ ? data.appointmentStartTime.toMillis()
267
+ : new Date(data.appointmentStartTime as any).getTime(),
268
+ appointmentEndTime: data.appointmentEndTime.toMillis
269
+ ? data.appointmentEndTime.toMillis()
270
+ : new Date(data.appointmentEndTime as any).getTime(),
271
+ patientNotes: data.patientNotes || null,
272
+ };
273
+
274
+ console.log(
275
+ `[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
349
276
  );
350
277
 
278
+ // Make the HTTP request with expanded CORS options for browser
279
+ const response = await fetch(functionUrl, {
280
+ method: "POST",
281
+ mode: "cors",
282
+ cache: "no-cache",
283
+ credentials: "omit",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Authorization: `Bearer ${idToken}`,
287
+ },
288
+ redirect: "follow",
289
+ referrerPolicy: "no-referrer",
290
+ body: JSON.stringify(requestData),
291
+ });
292
+
351
293
  console.log(
352
- `[APPOINTMENT_SERVICE] Appointment created with ID: ${appointment.id}`
294
+ `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
353
295
  );
354
296
 
355
- return appointment;
297
+ // Check if the request was successful
298
+ if (!response.ok) {
299
+ const errorText = await response.text();
300
+ console.error(
301
+ `[APPOINTMENT_SERVICE] Error response details: ${errorText}`
302
+ );
303
+ throw new Error(
304
+ `Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
305
+ );
306
+ }
307
+
308
+ // Parse the response
309
+ const result = await response.json();
310
+
311
+ if (!result.success) {
312
+ throw new Error(result.error || "Failed to create appointment");
313
+ }
314
+
315
+ // If the backend returns the full appointment data
316
+ if (result.appointmentData) {
317
+ console.log(
318
+ `[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`
319
+ );
320
+ return result.appointmentData;
321
+ }
322
+
323
+ // If only the ID is returned, fetch the complete appointment
324
+ const createdAppointment = await this.getAppointmentById(
325
+ result.appointmentId
326
+ );
327
+ if (!createdAppointment) {
328
+ throw new Error(
329
+ `Failed to retrieve created appointment with ID: ${result.appointmentId}`
330
+ );
331
+ }
332
+
333
+ return createdAppointment;
356
334
  } catch (error) {
357
- console.error("[APPOINTMENT_SERVICE] Error creating appointment:", error);
335
+ console.error(
336
+ "[APPOINTMENT_SERVICE] Error creating appointment via cloud function:",
337
+ error
338
+ );
358
339
  throw error;
359
340
  }
360
341
  }
@@ -580,61 +561,67 @@ export class AppointmentService extends BaseService {
580
561
  *
581
562
  * @param appointmentId ID of the appointment
582
563
  * @param newStatus New status to set
583
- * @param cancellationReason Required if canceling the appointment
584
- * @param canceledBy Required if canceling the appointment
564
+ * @param details Optional details for the status change
585
565
  * @returns The updated appointment
586
566
  */
587
567
  async updateAppointmentStatus(
588
568
  appointmentId: string,
589
569
  newStatus: AppointmentStatus,
590
- cancellationReason?: string,
591
- canceledBy?: "patient" | "clinic" | "practitioner" | "system"
570
+ details?: {
571
+ cancellationReason?: string;
572
+ canceledBy?: "patient" | "clinic" | "practitioner" | "system";
573
+ }
592
574
  ): Promise<Appointment> {
593
575
  console.log(
594
576
  `[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
595
577
  );
578
+ const updateData: UpdateAppointmentData = {
579
+ status: newStatus,
580
+ updatedAt: serverTimestamp(),
581
+ };
596
582
 
597
- // Create update data based on the new status
598
- const updateData: UpdateAppointmentData = { status: newStatus };
599
-
600
- // Add cancellation details if applicable
601
583
  if (
602
584
  newStatus === AppointmentStatus.CANCELED_CLINIC ||
603
- newStatus === AppointmentStatus.CANCELED_PATIENT
585
+ newStatus === AppointmentStatus.CANCELED_PATIENT ||
586
+ newStatus === AppointmentStatus.CANCELED_PATIENT_RESCHEDULED
604
587
  ) {
605
- if (!cancellationReason) {
606
- throw new Error(
607
- "Cancellation reason is required when canceling an appointment"
608
- );
588
+ if (!details?.cancellationReason) {
589
+ throw new Error("Cancellation reason is required when canceling.");
609
590
  }
610
- if (!canceledBy) {
611
- throw new Error(
612
- "Canceled by is required when canceling an appointment"
613
- );
591
+ if (!details?.canceledBy) {
592
+ throw new Error("Canceled by is required when canceling.");
614
593
  }
615
-
616
- updateData.cancellationReason = cancellationReason;
617
- updateData.canceledBy = canceledBy;
594
+ updateData.cancellationReason = details.cancellationReason;
595
+ updateData.canceledBy = details.canceledBy;
596
+ updateData.cancellationTime = Timestamp.now();
618
597
  }
619
598
 
620
- // Add confirmation time if confirming
621
599
  if (newStatus === AppointmentStatus.CONFIRMED) {
622
600
  updateData.confirmationTime = Timestamp.now();
623
601
  }
624
602
 
603
+ if (newStatus === AppointmentStatus.RESCHEDULED_BY_CLINIC) {
604
+ updateData.rescheduleTime = Timestamp.now();
605
+ }
606
+
625
607
  return this.updateAppointment(appointmentId, updateData);
626
608
  }
627
609
 
628
610
  /**
629
- * Confirms an appointment.
630
- *
631
- * @param appointmentId ID of the appointment to confirm
632
- * @returns The confirmed appointment
611
+ * Confirms a PENDING appointment by an Admin/Clinic.
633
612
  */
634
- async confirmAppointment(appointmentId: string): Promise<Appointment> {
613
+ async confirmAppointmentAdmin(appointmentId: string): Promise<Appointment> {
635
614
  console.log(
636
- `[APPOINTMENT_SERVICE] Confirming appointment: ${appointmentId}`
615
+ `[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`
637
616
  );
617
+ const appointment = await this.getAppointmentById(appointmentId);
618
+ if (!appointment)
619
+ throw new Error(`Appointment ${appointmentId} not found.`);
620
+ if (appointment.status !== AppointmentStatus.PENDING) {
621
+ throw new Error(
622
+ `Appointment ${appointmentId} is not in PENDING state to be confirmed.`
623
+ );
624
+ }
638
625
  return this.updateAppointmentStatus(
639
626
  appointmentId,
640
627
  AppointmentStatus.CONFIRMED
@@ -642,184 +629,354 @@ export class AppointmentService extends BaseService {
642
629
  }
643
630
 
644
631
  /**
645
- * Cancels an appointment from the clinic side.
646
- *
647
- * @param appointmentId ID of the appointment to cancel
648
- * @param reason Reason for cancellation
649
- * @returns The canceled appointment
632
+ * Cancels an appointment by the User (Patient).
650
633
  */
651
- async cancelAppointmentByClinic(
634
+ async cancelAppointmentUser(
652
635
  appointmentId: string,
653
636
  reason: string
654
637
  ): Promise<Appointment> {
655
638
  console.log(
656
- `[APPOINTMENT_SERVICE] Canceling appointment by clinic: ${appointmentId}`
639
+ `[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`
657
640
  );
658
641
  return this.updateAppointmentStatus(
659
642
  appointmentId,
660
- AppointmentStatus.CANCELED_CLINIC,
661
- reason,
662
- "clinic"
643
+ AppointmentStatus.CANCELED_PATIENT,
644
+ {
645
+ cancellationReason: reason,
646
+ canceledBy: "patient",
647
+ }
663
648
  );
664
649
  }
665
650
 
666
651
  /**
667
- * Cancels an appointment from the patient side.
668
- *
669
- * @param appointmentId ID of the appointment to cancel
670
- * @param reason Reason for cancellation
671
- * @returns The canceled appointment
652
+ * Cancels an appointment by an Admin/Clinic.
672
653
  */
673
- async cancelAppointmentByPatient(
654
+ async cancelAppointmentAdmin(
674
655
  appointmentId: string,
675
656
  reason: string
676
657
  ): Promise<Appointment> {
677
658
  console.log(
678
- `[APPOINTMENT_SERVICE] Canceling appointment by patient: ${appointmentId}`
659
+ `[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`
679
660
  );
680
661
  return this.updateAppointmentStatus(
681
662
  appointmentId,
682
- AppointmentStatus.CANCELED_PATIENT,
683
- reason,
684
- "patient"
663
+ AppointmentStatus.CANCELED_CLINIC,
664
+ {
665
+ cancellationReason: reason,
666
+ canceledBy: "clinic",
667
+ }
685
668
  );
686
669
  }
687
670
 
688
671
  /**
689
- * Marks an appointment as checked in.
690
- *
691
- * @param appointmentId ID of the appointment
692
- * @returns The updated appointment
672
+ * Admin proposes to reschedule an appointment.
673
+ * Sets status to RESCHEDULED_BY_CLINIC and updates times.
674
+ */
675
+ async rescheduleAppointmentAdmin(
676
+ appointmentId: string,
677
+ newStartTime: Timestamp,
678
+ newEndTime: Timestamp
679
+ ): Promise<Appointment> {
680
+ console.log(
681
+ `[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${appointmentId}`
682
+ );
683
+ if (newEndTime.toMillis() <= newStartTime.toMillis()) {
684
+ throw new Error("New end time must be after new start time.");
685
+ }
686
+ const updateData: UpdateAppointmentData = {
687
+ status: AppointmentStatus.RESCHEDULED_BY_CLINIC,
688
+ appointmentStartTime: newStartTime,
689
+ appointmentEndTime: newEndTime,
690
+ rescheduleTime: Timestamp.now(),
691
+ confirmationTime: null,
692
+ updatedAt: serverTimestamp(),
693
+ };
694
+ return this.updateAppointment(appointmentId, updateData);
695
+ }
696
+
697
+ /**
698
+ * User confirms a reschedule proposed by the clinic.
699
+ * Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
693
700
  */
694
- async checkInAppointment(appointmentId: string): Promise<Appointment> {
701
+ async rescheduleAppointmentConfirmUser(
702
+ appointmentId: string
703
+ ): Promise<Appointment> {
695
704
  console.log(
696
- `[APPOINTMENT_SERVICE] Checking in appointment: ${appointmentId}`
705
+ `[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`
697
706
  );
707
+ const appointment = await this.getAppointmentById(appointmentId);
708
+ if (!appointment)
709
+ throw new Error(`Appointment ${appointmentId} not found.`);
710
+ if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
711
+ throw new Error(
712
+ `Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
713
+ );
714
+ }
698
715
  return this.updateAppointmentStatus(
699
716
  appointmentId,
700
- AppointmentStatus.CHECKED_IN
717
+ AppointmentStatus.CONFIRMED
701
718
  );
702
719
  }
703
720
 
704
721
  /**
705
- * Marks an appointment as in progress.
706
- *
707
- * @param appointmentId ID of the appointment
708
- * @returns The updated appointment
722
+ * User rejects a reschedule proposed by the clinic.
723
+ * Status changes from RESCHEDULED_BY_CLINIC to CANCELED_PATIENT_RESCHEDULED.
709
724
  */
710
- async startAppointment(appointmentId: string): Promise<Appointment> {
711
- console.log(`[APPOINTMENT_SERVICE] Starting appointment: ${appointmentId}`);
725
+ async rescheduleAppointmentRejectUser(
726
+ appointmentId: string,
727
+ reason: string
728
+ ): Promise<Appointment> {
729
+ console.log(
730
+ `[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`
731
+ );
732
+ const appointment = await this.getAppointmentById(appointmentId);
733
+ if (!appointment)
734
+ throw new Error(`Appointment ${appointmentId} not found.`);
735
+ if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
736
+ throw new Error(
737
+ `Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
738
+ );
739
+ }
712
740
  return this.updateAppointmentStatus(
713
741
  appointmentId,
714
- AppointmentStatus.IN_PROGRESS
742
+ AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
743
+ {
744
+ cancellationReason: reason,
745
+ canceledBy: "patient",
746
+ }
715
747
  );
716
748
  }
717
749
 
718
750
  /**
719
- * Marks an appointment as completed.
720
- *
721
- * @param appointmentId ID of the appointment
722
- * @param actualDurationMinutes Actual duration of the appointment in minutes
723
- * @returns The updated appointment
751
+ * Admin checks in a patient for their appointment.
752
+ * Requires all pending user forms to be completed.
724
753
  */
725
- async completeAppointment(
754
+ async checkInPatientAdmin(appointmentId: string): Promise<Appointment> {
755
+ console.log(
756
+ `[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`
757
+ );
758
+ const appointment = await this.getAppointmentById(appointmentId);
759
+ if (!appointment)
760
+ throw new Error(`Appointment ${appointmentId} not found.`);
761
+
762
+ if (
763
+ appointment.pendingUserFormsIds &&
764
+ appointment.pendingUserFormsIds.length > 0
765
+ ) {
766
+ throw new Error(
767
+ `Cannot check in: Patient has ${
768
+ appointment.pendingUserFormsIds.length
769
+ } pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(
770
+ ", "
771
+ )}`
772
+ );
773
+ }
774
+ if (
775
+ appointment.status !== AppointmentStatus.CONFIRMED &&
776
+ appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC
777
+ ) {
778
+ console.warn(
779
+ `Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`
780
+ );
781
+ }
782
+
783
+ return this.updateAppointmentStatus(
784
+ appointmentId,
785
+ AppointmentStatus.CHECKED_IN
786
+ );
787
+ }
788
+
789
+ /**
790
+ * Doctor starts the appointment procedure.
791
+ */
792
+ async startAppointmentDoctor(appointmentId: string): Promise<Appointment> {
793
+ console.log(
794
+ `[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`
795
+ );
796
+ const appointment = await this.getAppointmentById(appointmentId);
797
+ if (!appointment)
798
+ throw new Error(`Appointment ${appointmentId} not found.`);
799
+ if (appointment.status !== AppointmentStatus.CHECKED_IN) {
800
+ throw new Error(
801
+ `Appointment ${appointmentId} must be CHECKED_IN to start.`
802
+ );
803
+ }
804
+ // Update status and set procedureActualStartTime
805
+ const updateData: UpdateAppointmentData = {
806
+ status: AppointmentStatus.IN_PROGRESS,
807
+ procedureActualStartTime: Timestamp.now(), // Set actual start time
808
+ updatedAt: serverTimestamp(),
809
+ };
810
+ return this.updateAppointment(appointmentId, updateData);
811
+ }
812
+
813
+ /**
814
+ * Doctor completes and finalizes the appointment.
815
+ */
816
+ async completeAppointmentDoctor(
726
817
  appointmentId: string,
727
- actualDurationMinutes?: number
818
+ finalizationNotes: string,
819
+ actualDurationMinutesInput?: number // Renamed to avoid conflict if we calculate
728
820
  ): Promise<Appointment> {
729
821
  console.log(
730
- `[APPOINTMENT_SERVICE] Completing appointment: ${appointmentId}`
822
+ `[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`
731
823
  );
824
+ const currentUser = this.auth.currentUser;
825
+ if (!currentUser)
826
+ throw new Error("Authentication required to complete appointment.");
827
+
828
+ const appointment = await this.getAppointmentById(appointmentId);
829
+ if (!appointment)
830
+ throw new Error(`Appointment ${appointmentId} not found.`);
831
+
832
+ let calculatedDurationMinutes = actualDurationMinutesInput;
833
+ const procedureCompletionTime = Timestamp.now();
834
+
835
+ // Calculate duration if not provided and actual start time is available
836
+ if (
837
+ calculatedDurationMinutes === undefined &&
838
+ appointment.procedureActualStartTime
839
+ ) {
840
+ const startTimeMillis = appointment.procedureActualStartTime.toMillis();
841
+ const endTimeMillis = procedureCompletionTime.toMillis();
842
+ if (endTimeMillis > startTimeMillis) {
843
+ calculatedDurationMinutes = Math.round(
844
+ (endTimeMillis - startTimeMillis) / 60000
845
+ );
846
+ }
847
+ }
732
848
 
733
849
  const updateData: UpdateAppointmentData = {
734
850
  status: AppointmentStatus.COMPLETED,
735
- actualDurationMinutes,
851
+ actualDurationMinutes: calculatedDurationMinutes, // Use calculated or provided duration
852
+ finalizedDetails: {
853
+ by: currentUser.uid, // This is used ID, not practitioner's profile ID (just so we know who completed the appointment)
854
+ at: procedureCompletionTime, // Use consistent completion timestamp
855
+ notes: finalizationNotes,
856
+ },
857
+ // Optionally update appointmentEndTime to the actual completion time
858
+ // appointmentEndTime: procedureCompletionTime,
859
+ updatedAt: serverTimestamp(),
736
860
  };
737
-
738
861
  return this.updateAppointment(appointmentId, updateData);
739
862
  }
740
863
 
741
864
  /**
742
- * Marks an appointment as no-show.
743
- *
744
- * @param appointmentId ID of the appointment
745
- * @returns The updated appointment
865
+ * Admin marks an appointment as No-Show.
746
866
  */
747
- async markNoShow(appointmentId: string): Promise<Appointment> {
867
+ async markNoShowAdmin(appointmentId: string): Promise<Appointment> {
748
868
  console.log(
749
- `[APPOINTMENT_SERVICE] Marking appointment as no-show: ${appointmentId}`
869
+ `[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`
750
870
  );
871
+ const appointment = await this.getAppointmentById(appointmentId);
872
+ if (!appointment)
873
+ throw new Error(`Appointment ${appointmentId} not found.`);
874
+ if (
875
+ Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()
876
+ ) {
877
+ throw new Error("Cannot mark no-show before appointment start time.");
878
+ }
751
879
  return this.updateAppointmentStatus(
752
880
  appointmentId,
753
- AppointmentStatus.NO_SHOW
881
+ AppointmentStatus.NO_SHOW,
882
+ {
883
+ cancellationReason: "Patient did not show up for the appointment.",
884
+ canceledBy: "clinic",
885
+ }
754
886
  );
755
887
  }
756
888
 
757
889
  /**
758
- * Updates the payment status of an appointment.
759
- *
760
- * @param appointmentId ID of the appointment
761
- * @param paymentStatus New payment status
762
- * @param paymentTransactionId Optional transaction ID for the payment
763
- * @returns The updated appointment
890
+ * Adds a media item to an appointment.
764
891
  */
765
- async updatePaymentStatus(
892
+ async addMediaToAppointment(
766
893
  appointmentId: string,
767
- paymentStatus: PaymentStatus,
768
- paymentTransactionId?: string
894
+ mediaItemData: Omit<AppointmentMediaItem, "id" | "uploadedAt">
769
895
  ): Promise<Appointment> {
770
896
  console.log(
771
- `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
897
+ `[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`
772
898
  );
899
+ const currentUser = this.auth.currentUser;
900
+ if (!currentUser) throw new Error("Authentication required.");
901
+
902
+ const newMediaItem: AppointmentMediaItem = {
903
+ ...mediaItemData,
904
+ id: this.generateId(),
905
+ uploadedAt: Timestamp.now(),
906
+ uploadedBy: currentUser.uid,
907
+ };
773
908
 
774
909
  const updateData: UpdateAppointmentData = {
775
- paymentStatus,
776
- paymentTransactionId: paymentTransactionId || null,
910
+ media: arrayUnion(newMediaItem) as any,
911
+ updatedAt: serverTimestamp(),
777
912
  };
778
-
779
913
  return this.updateAppointment(appointmentId, updateData);
780
914
  }
781
915
 
782
916
  /**
783
- * Marks pre-procedure requirements as completed.
784
- *
785
- * @param appointmentId ID of the appointment
786
- * @param requirementIds IDs of the requirements to mark as completed
787
- * @returns The updated appointment
917
+ * Removes a media item from an appointment.
788
918
  */
789
- async completePreRequirements(
919
+ async removeMediaFromAppointment(
790
920
  appointmentId: string,
791
- requirementIds: string[]
921
+ mediaItemId: string
792
922
  ): Promise<Appointment> {
793
923
  console.log(
794
- `[APPOINTMENT_SERVICE] Marking pre-requirements as completed for appointment: ${appointmentId}`
924
+ `[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`
795
925
  );
926
+ const appointment = await this.getAppointmentById(appointmentId);
927
+ if (!appointment || !appointment.media) {
928
+ throw new Error("Appointment or media list not found.");
929
+ }
930
+ const mediaToRemove = appointment.media.find((m) => m.id === mediaItemId);
931
+ if (!mediaToRemove) {
932
+ throw new Error(`Media item ${mediaItemId} not found in appointment.`);
933
+ }
796
934
 
797
935
  const updateData: UpdateAppointmentData = {
798
- completedPreRequirements: requirementIds,
936
+ media: arrayRemove(mediaToRemove) as any,
937
+ updatedAt: serverTimestamp(),
799
938
  };
800
-
801
939
  return this.updateAppointment(appointmentId, updateData);
802
940
  }
803
941
 
804
942
  /**
805
- * Marks post-procedure requirements as completed.
806
- *
807
- * @param appointmentId ID of the appointment
808
- * @param requirementIds IDs of the requirements to mark as completed
809
- * @returns The updated appointment
943
+ * Adds or updates review information for an appointment.
810
944
  */
811
- async completePostRequirements(
945
+ async addReviewToAppointment(
812
946
  appointmentId: string,
813
- requirementIds: string[]
947
+ reviewData: Omit<PatientReviewInfo, "reviewedAt" | "reviewId">
814
948
  ): Promise<Appointment> {
815
949
  console.log(
816
- `[APPOINTMENT_SERVICE] Marking post-requirements as completed for appointment: ${appointmentId}`
950
+ `[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`
817
951
  );
818
-
952
+ const newReviewInfo: PatientReviewInfo = {
953
+ ...reviewData,
954
+ reviewId: this.generateId(),
955
+ reviewedAt: Timestamp.now(),
956
+ };
819
957
  const updateData: UpdateAppointmentData = {
820
- completedPostRequirements: requirementIds,
958
+ reviewInfo: newReviewInfo,
959
+ updatedAt: serverTimestamp(),
821
960
  };
961
+ return this.updateAppointment(appointmentId, updateData);
962
+ }
822
963
 
964
+ /**
965
+ * Updates the payment status of an appointment.
966
+ */
967
+ async updatePaymentStatus(
968
+ appointmentId: string,
969
+ paymentStatus: PaymentStatus,
970
+ paymentTransactionId?: string
971
+ ): Promise<Appointment> {
972
+ console.log(
973
+ `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
974
+ );
975
+ const updateData: UpdateAppointmentData = {
976
+ paymentStatus,
977
+ paymentTransactionId: paymentTransactionId || null,
978
+ updatedAt: serverTimestamp(),
979
+ };
823
980
  return this.updateAppointment(appointmentId, updateData);
824
981
  }
825
982
 
@@ -844,25 +1001,4 @@ export class AppointmentService extends BaseService {
844
1001
 
845
1002
  return this.updateAppointment(appointmentId, updateData);
846
1003
  }
847
-
848
- /**
849
- * Debug helper: Get the current user's ID token for testing purposes
850
- * Use this token in Postman with Authorization: Bearer TOKEN
851
- */
852
- async getDebugToken(): Promise<string | null> {
853
- try {
854
- const currentUser = this.auth.currentUser;
855
- if (!currentUser) {
856
- console.log("[APPOINTMENT_SERVICE] No user is signed in");
857
- return null;
858
- }
859
-
860
- const idToken = await currentUser.getIdToken();
861
- console.log("[APPOINTMENT_SERVICE] Debug token:", idToken);
862
- return idToken;
863
- } catch (error) {
864
- console.error("[APPOINTMENT_SERVICE] Error getting debug token:", error);
865
- return null;
866
- }
867
- }
868
1004
  }