@blackcode_sa/metaestetics-api 1.6.2 → 1.6.4

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 (37) hide show
  1. package/dist/admin/index.d.mts +228 -25
  2. package/dist/admin/index.d.ts +228 -25
  3. package/dist/admin/index.js +35867 -2493
  4. package/dist/admin/index.mjs +35856 -2464
  5. package/dist/backoffice/index.d.mts +252 -1
  6. package/dist/backoffice/index.d.ts +252 -1
  7. package/dist/backoffice/index.js +86 -12
  8. package/dist/backoffice/index.mjs +86 -13
  9. package/dist/index.d.mts +1417 -554
  10. package/dist/index.d.ts +1417 -554
  11. package/dist/index.js +1393 -687
  12. package/dist/index.mjs +1423 -711
  13. package/package.json +1 -1
  14. package/src/admin/index.ts +15 -1
  15. package/src/admin/notifications/notifications.admin.ts +1 -1
  16. package/src/admin/requirements/README.md +128 -0
  17. package/src/admin/requirements/patient-requirements.admin.service.ts +482 -0
  18. package/src/index.ts +16 -1
  19. package/src/services/appointment/appointment.service.ts +315 -86
  20. package/src/services/clinic/clinic-admin.service.ts +3 -0
  21. package/src/services/clinic/clinic-group.service.ts +8 -0
  22. package/src/services/documentation-templates/documentation-template.service.ts +24 -16
  23. package/src/services/documentation-templates/filled-document.service.ts +253 -136
  24. package/src/services/patient/patient.service.ts +31 -1
  25. package/src/services/patient/patientRequirements.service.ts +285 -0
  26. package/src/services/patient/utils/practitioner.utils.ts +79 -1
  27. package/src/types/appointment/index.ts +134 -10
  28. package/src/types/documentation-templates/index.ts +34 -2
  29. package/src/types/notifications/README.md +77 -0
  30. package/src/types/notifications/index.ts +154 -27
  31. package/src/types/patient/index.ts +6 -0
  32. package/src/types/patient/patient-requirements.ts +81 -0
  33. package/src/validations/appointment.schema.ts +300 -62
  34. package/src/validations/documentation-templates/template.schema.ts +55 -0
  35. package/src/validations/documentation-templates.schema.ts +9 -14
  36. package/src/validations/notification.schema.ts +3 -3
  37. package/src/validations/patient/patient-requirements.schema.ts +75 -0
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export { CalendarServiceV2 } from "./services/calendar/calendar-refactored.servi
33
33
  export { SyncedCalendarsService } from "./services/calendar/synced-calendars.service";
34
34
  export { ReviewService } from "./services/reviews/reviews.service";
35
35
  export { AppointmentService } from "./services/appointment/appointment.service";
36
+ export { PatientRequirementsService } from "./services/patient/patientRequirements.service";
36
37
 
37
38
  // Backoffice services
38
39
  export { BrandService } from "./backoffice/services/brand.service";
@@ -74,7 +75,6 @@ export type {
74
75
  PreRequirementNotification,
75
76
  PostRequirementNotification,
76
77
  AppointmentReminderNotification,
77
- AppointmentNotification,
78
78
  } from "./types/notifications";
79
79
  export { NotificationType, NotificationStatus } from "./types/notifications";
80
80
 
@@ -346,3 +346,18 @@ export {
346
346
  reviewSchema,
347
347
  createReviewSchema,
348
348
  } from "./validations/reviews.schema";
349
+
350
+ // Patient Requirement Types
351
+ export type {
352
+ PatientRequirementInstance,
353
+ PatientRequirementInstruction,
354
+ } from "./types/patient/patient-requirements";
355
+ export {
356
+ PatientInstructionStatus,
357
+ PatientRequirementOverallStatus,
358
+ PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME,
359
+ } from "./types/patient/patient-requirements";
360
+
361
+ // Patient Requirement Validation Schemas (if they exist and are for client use)
362
+ // Assuming the path, add if it exists and is needed:
363
+ // export * from "./validations/patient/patient-requirements.schema";
@@ -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,13 +81,15 @@ 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
 
@@ -580,61 +589,67 @@ export class AppointmentService extends BaseService {
580
589
  *
581
590
  * @param appointmentId ID of the appointment
582
591
  * @param newStatus New status to set
583
- * @param cancellationReason Required if canceling the appointment
584
- * @param canceledBy Required if canceling the appointment
592
+ * @param details Optional details for the status change
585
593
  * @returns The updated appointment
586
594
  */
587
595
  async updateAppointmentStatus(
588
596
  appointmentId: string,
589
597
  newStatus: AppointmentStatus,
590
- cancellationReason?: string,
591
- canceledBy?: "patient" | "clinic" | "practitioner" | "system"
598
+ details?: {
599
+ cancellationReason?: string;
600
+ canceledBy?: "patient" | "clinic" | "practitioner" | "system";
601
+ }
592
602
  ): Promise<Appointment> {
593
603
  console.log(
594
604
  `[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
595
605
  );
606
+ const updateData: UpdateAppointmentData = {
607
+ status: newStatus,
608
+ updatedAt: serverTimestamp(),
609
+ };
596
610
 
597
- // Create update data based on the new status
598
- const updateData: UpdateAppointmentData = { status: newStatus };
599
-
600
- // Add cancellation details if applicable
601
611
  if (
602
612
  newStatus === AppointmentStatus.CANCELED_CLINIC ||
603
- newStatus === AppointmentStatus.CANCELED_PATIENT
613
+ newStatus === AppointmentStatus.CANCELED_PATIENT ||
614
+ newStatus === AppointmentStatus.CANCELED_PATIENT_RESCHEDULED
604
615
  ) {
605
- if (!cancellationReason) {
606
- throw new Error(
607
- "Cancellation reason is required when canceling an appointment"
608
- );
616
+ if (!details?.cancellationReason) {
617
+ throw new Error("Cancellation reason is required when canceling.");
609
618
  }
610
- if (!canceledBy) {
611
- throw new Error(
612
- "Canceled by is required when canceling an appointment"
613
- );
619
+ if (!details?.canceledBy) {
620
+ throw new Error("Canceled by is required when canceling.");
614
621
  }
615
-
616
- updateData.cancellationReason = cancellationReason;
617
- updateData.canceledBy = canceledBy;
622
+ updateData.cancellationReason = details.cancellationReason;
623
+ updateData.canceledBy = details.canceledBy;
624
+ updateData.cancellationTime = Timestamp.now();
618
625
  }
619
626
 
620
- // Add confirmation time if confirming
621
627
  if (newStatus === AppointmentStatus.CONFIRMED) {
622
628
  updateData.confirmationTime = Timestamp.now();
623
629
  }
624
630
 
631
+ if (newStatus === AppointmentStatus.RESCHEDULED_BY_CLINIC) {
632
+ updateData.rescheduleTime = Timestamp.now();
633
+ }
634
+
625
635
  return this.updateAppointment(appointmentId, updateData);
626
636
  }
627
637
 
628
638
  /**
629
- * Confirms an appointment.
630
- *
631
- * @param appointmentId ID of the appointment to confirm
632
- * @returns The confirmed appointment
639
+ * Confirms a PENDING appointment by an Admin/Clinic.
633
640
  */
634
- async confirmAppointment(appointmentId: string): Promise<Appointment> {
641
+ async confirmAppointmentAdmin(appointmentId: string): Promise<Appointment> {
635
642
  console.log(
636
- `[APPOINTMENT_SERVICE] Confirming appointment: ${appointmentId}`
643
+ `[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`
637
644
  );
645
+ const appointment = await this.getAppointmentById(appointmentId);
646
+ if (!appointment)
647
+ throw new Error(`Appointment ${appointmentId} not found.`);
648
+ if (appointment.status !== AppointmentStatus.PENDING) {
649
+ throw new Error(
650
+ `Appointment ${appointmentId} is not in PENDING state to be confirmed.`
651
+ );
652
+ }
638
653
  return this.updateAppointmentStatus(
639
654
  appointmentId,
640
655
  AppointmentStatus.CONFIRMED
@@ -642,125 +657,340 @@ export class AppointmentService extends BaseService {
642
657
  }
643
658
 
644
659
  /**
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
660
+ * Cancels an appointment by the User (Patient).
650
661
  */
651
- async cancelAppointmentByClinic(
662
+ async cancelAppointmentUser(
652
663
  appointmentId: string,
653
664
  reason: string
654
665
  ): Promise<Appointment> {
655
666
  console.log(
656
- `[APPOINTMENT_SERVICE] Canceling appointment by clinic: ${appointmentId}`
667
+ `[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`
657
668
  );
658
669
  return this.updateAppointmentStatus(
659
670
  appointmentId,
660
- AppointmentStatus.CANCELED_CLINIC,
661
- reason,
662
- "clinic"
671
+ AppointmentStatus.CANCELED_PATIENT,
672
+ {
673
+ cancellationReason: reason,
674
+ canceledBy: "patient",
675
+ }
663
676
  );
664
677
  }
665
678
 
666
679
  /**
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
680
+ * Cancels an appointment by an Admin/Clinic.
672
681
  */
673
- async cancelAppointmentByPatient(
682
+ async cancelAppointmentAdmin(
674
683
  appointmentId: string,
675
684
  reason: string
676
685
  ): Promise<Appointment> {
677
686
  console.log(
678
- `[APPOINTMENT_SERVICE] Canceling appointment by patient: ${appointmentId}`
687
+ `[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`
679
688
  );
680
689
  return this.updateAppointmentStatus(
681
690
  appointmentId,
682
- AppointmentStatus.CANCELED_PATIENT,
683
- reason,
684
- "patient"
691
+ AppointmentStatus.CANCELED_CLINIC,
692
+ {
693
+ cancellationReason: reason,
694
+ canceledBy: "clinic",
695
+ }
685
696
  );
686
697
  }
687
698
 
688
699
  /**
689
- * Marks an appointment as checked in.
690
- *
691
- * @param appointmentId ID of the appointment
692
- * @returns The updated appointment
700
+ * Admin proposes to reschedule an appointment.
701
+ * Sets status to RESCHEDULED_BY_CLINIC and updates times.
702
+ */
703
+ async rescheduleAppointmentAdmin(
704
+ appointmentId: string,
705
+ newStartTime: Timestamp,
706
+ newEndTime: Timestamp
707
+ ): Promise<Appointment> {
708
+ console.log(
709
+ `[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${appointmentId}`
710
+ );
711
+ if (newEndTime.toMillis() <= newStartTime.toMillis()) {
712
+ throw new Error("New end time must be after new start time.");
713
+ }
714
+ const updateData: UpdateAppointmentData = {
715
+ status: AppointmentStatus.RESCHEDULED_BY_CLINIC,
716
+ appointmentStartTime: newStartTime,
717
+ appointmentEndTime: newEndTime,
718
+ rescheduleTime: Timestamp.now(),
719
+ confirmationTime: null,
720
+ updatedAt: serverTimestamp(),
721
+ };
722
+ return this.updateAppointment(appointmentId, updateData);
723
+ }
724
+
725
+ /**
726
+ * User confirms a reschedule proposed by the clinic.
727
+ * Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
693
728
  */
694
- async checkInAppointment(appointmentId: string): Promise<Appointment> {
729
+ async rescheduleAppointmentConfirmUser(
730
+ appointmentId: string
731
+ ): Promise<Appointment> {
695
732
  console.log(
696
- `[APPOINTMENT_SERVICE] Checking in appointment: ${appointmentId}`
733
+ `[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`
697
734
  );
735
+ const appointment = await this.getAppointmentById(appointmentId);
736
+ if (!appointment)
737
+ throw new Error(`Appointment ${appointmentId} not found.`);
738
+ if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
739
+ throw new Error(
740
+ `Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
741
+ );
742
+ }
698
743
  return this.updateAppointmentStatus(
699
744
  appointmentId,
700
- AppointmentStatus.CHECKED_IN
745
+ AppointmentStatus.CONFIRMED
701
746
  );
702
747
  }
703
748
 
704
749
  /**
705
- * Marks an appointment as in progress.
706
- *
707
- * @param appointmentId ID of the appointment
708
- * @returns The updated appointment
750
+ * User rejects a reschedule proposed by the clinic.
751
+ * Status changes from RESCHEDULED_BY_CLINIC to CANCELED_PATIENT_RESCHEDULED.
709
752
  */
710
- async startAppointment(appointmentId: string): Promise<Appointment> {
711
- console.log(`[APPOINTMENT_SERVICE] Starting appointment: ${appointmentId}`);
753
+ async rescheduleAppointmentRejectUser(
754
+ appointmentId: string,
755
+ reason: string
756
+ ): Promise<Appointment> {
757
+ console.log(
758
+ `[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`
759
+ );
760
+ const appointment = await this.getAppointmentById(appointmentId);
761
+ if (!appointment)
762
+ throw new Error(`Appointment ${appointmentId} not found.`);
763
+ if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
764
+ throw new Error(
765
+ `Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
766
+ );
767
+ }
712
768
  return this.updateAppointmentStatus(
713
769
  appointmentId,
714
- AppointmentStatus.IN_PROGRESS
770
+ AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
771
+ {
772
+ cancellationReason: reason,
773
+ canceledBy: "patient",
774
+ }
715
775
  );
716
776
  }
717
777
 
718
778
  /**
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
779
+ * Admin checks in a patient for their appointment.
780
+ * Requires all pending user forms to be completed.
724
781
  */
725
- async completeAppointment(
782
+ async checkInPatientAdmin(appointmentId: string): Promise<Appointment> {
783
+ console.log(
784
+ `[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`
785
+ );
786
+ const appointment = await this.getAppointmentById(appointmentId);
787
+ if (!appointment)
788
+ throw new Error(`Appointment ${appointmentId} not found.`);
789
+
790
+ if (
791
+ appointment.pendingUserFormsIds &&
792
+ appointment.pendingUserFormsIds.length > 0
793
+ ) {
794
+ throw new Error(
795
+ `Cannot check in: Patient has ${
796
+ appointment.pendingUserFormsIds.length
797
+ } pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(
798
+ ", "
799
+ )}`
800
+ );
801
+ }
802
+ if (
803
+ appointment.status !== AppointmentStatus.CONFIRMED &&
804
+ appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC
805
+ ) {
806
+ console.warn(
807
+ `Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`
808
+ );
809
+ }
810
+
811
+ return this.updateAppointmentStatus(
812
+ appointmentId,
813
+ AppointmentStatus.CHECKED_IN
814
+ );
815
+ }
816
+
817
+ /**
818
+ * Doctor starts the appointment procedure.
819
+ */
820
+ async startAppointmentDoctor(appointmentId: string): Promise<Appointment> {
821
+ console.log(
822
+ `[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`
823
+ );
824
+ const appointment = await this.getAppointmentById(appointmentId);
825
+ if (!appointment)
826
+ throw new Error(`Appointment ${appointmentId} not found.`);
827
+ if (appointment.status !== AppointmentStatus.CHECKED_IN) {
828
+ throw new Error(
829
+ `Appointment ${appointmentId} must be CHECKED_IN to start.`
830
+ );
831
+ }
832
+ // Update status and set procedureActualStartTime
833
+ const updateData: UpdateAppointmentData = {
834
+ status: AppointmentStatus.IN_PROGRESS,
835
+ procedureActualStartTime: Timestamp.now(), // Set actual start time
836
+ updatedAt: serverTimestamp(),
837
+ };
838
+ return this.updateAppointment(appointmentId, updateData);
839
+ }
840
+
841
+ /**
842
+ * Doctor completes and finalizes the appointment.
843
+ */
844
+ async completeAppointmentDoctor(
726
845
  appointmentId: string,
727
- actualDurationMinutes?: number
846
+ finalizationNotes: string,
847
+ actualDurationMinutesInput?: number // Renamed to avoid conflict if we calculate
728
848
  ): Promise<Appointment> {
729
849
  console.log(
730
- `[APPOINTMENT_SERVICE] Completing appointment: ${appointmentId}`
850
+ `[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`
731
851
  );
852
+ const currentUser = this.auth.currentUser;
853
+ if (!currentUser)
854
+ throw new Error("Authentication required to complete appointment.");
855
+
856
+ const appointment = await this.getAppointmentById(appointmentId);
857
+ if (!appointment)
858
+ throw new Error(`Appointment ${appointmentId} not found.`);
859
+
860
+ let calculatedDurationMinutes = actualDurationMinutesInput;
861
+ const procedureCompletionTime = Timestamp.now();
862
+
863
+ // Calculate duration if not provided and actual start time is available
864
+ if (
865
+ calculatedDurationMinutes === undefined &&
866
+ appointment.procedureActualStartTime
867
+ ) {
868
+ const startTimeMillis = appointment.procedureActualStartTime.toMillis();
869
+ const endTimeMillis = procedureCompletionTime.toMillis();
870
+ if (endTimeMillis > startTimeMillis) {
871
+ calculatedDurationMinutes = Math.round(
872
+ (endTimeMillis - startTimeMillis) / 60000
873
+ );
874
+ }
875
+ }
732
876
 
733
877
  const updateData: UpdateAppointmentData = {
734
878
  status: AppointmentStatus.COMPLETED,
735
- actualDurationMinutes,
879
+ actualDurationMinutes: calculatedDurationMinutes, // Use calculated or provided duration
880
+ finalizedDetails: {
881
+ by: currentUser.uid, // This is used ID, not practitioner's profile ID (just so we know who completed the appointment)
882
+ at: procedureCompletionTime, // Use consistent completion timestamp
883
+ notes: finalizationNotes,
884
+ },
885
+ // Optionally update appointmentEndTime to the actual completion time
886
+ // appointmentEndTime: procedureCompletionTime,
887
+ updatedAt: serverTimestamp(),
736
888
  };
737
-
738
889
  return this.updateAppointment(appointmentId, updateData);
739
890
  }
740
891
 
741
892
  /**
742
- * Marks an appointment as no-show.
743
- *
744
- * @param appointmentId ID of the appointment
745
- * @returns The updated appointment
893
+ * Admin marks an appointment as No-Show.
746
894
  */
747
- async markNoShow(appointmentId: string): Promise<Appointment> {
895
+ async markNoShowAdmin(appointmentId: string): Promise<Appointment> {
748
896
  console.log(
749
- `[APPOINTMENT_SERVICE] Marking appointment as no-show: ${appointmentId}`
897
+ `[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`
750
898
  );
899
+ const appointment = await this.getAppointmentById(appointmentId);
900
+ if (!appointment)
901
+ throw new Error(`Appointment ${appointmentId} not found.`);
902
+ if (
903
+ Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()
904
+ ) {
905
+ throw new Error("Cannot mark no-show before appointment start time.");
906
+ }
751
907
  return this.updateAppointmentStatus(
752
908
  appointmentId,
753
- AppointmentStatus.NO_SHOW
909
+ AppointmentStatus.NO_SHOW,
910
+ {
911
+ cancellationReason: "Patient did not show up for the appointment.",
912
+ canceledBy: "clinic",
913
+ }
754
914
  );
755
915
  }
756
916
 
917
+ /**
918
+ * Adds a media item to an appointment.
919
+ */
920
+ async addMediaToAppointment(
921
+ appointmentId: string,
922
+ mediaItemData: Omit<AppointmentMediaItem, "id" | "uploadedAt">
923
+ ): Promise<Appointment> {
924
+ console.log(
925
+ `[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`
926
+ );
927
+ const currentUser = this.auth.currentUser;
928
+ if (!currentUser) throw new Error("Authentication required.");
929
+
930
+ const newMediaItem: AppointmentMediaItem = {
931
+ ...mediaItemData,
932
+ id: this.generateId(),
933
+ uploadedAt: Timestamp.now(),
934
+ uploadedBy: currentUser.uid,
935
+ };
936
+
937
+ const updateData: UpdateAppointmentData = {
938
+ media: arrayUnion(newMediaItem) as any,
939
+ updatedAt: serverTimestamp(),
940
+ };
941
+ return this.updateAppointment(appointmentId, updateData);
942
+ }
943
+
944
+ /**
945
+ * Removes a media item from an appointment.
946
+ */
947
+ async removeMediaFromAppointment(
948
+ appointmentId: string,
949
+ mediaItemId: string
950
+ ): Promise<Appointment> {
951
+ console.log(
952
+ `[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`
953
+ );
954
+ const appointment = await this.getAppointmentById(appointmentId);
955
+ if (!appointment || !appointment.media) {
956
+ throw new Error("Appointment or media list not found.");
957
+ }
958
+ const mediaToRemove = appointment.media.find((m) => m.id === mediaItemId);
959
+ if (!mediaToRemove) {
960
+ throw new Error(`Media item ${mediaItemId} not found in appointment.`);
961
+ }
962
+
963
+ const updateData: UpdateAppointmentData = {
964
+ media: arrayRemove(mediaToRemove) as any,
965
+ updatedAt: serverTimestamp(),
966
+ };
967
+ return this.updateAppointment(appointmentId, updateData);
968
+ }
969
+
970
+ /**
971
+ * Adds or updates review information for an appointment.
972
+ */
973
+ async addReviewToAppointment(
974
+ appointmentId: string,
975
+ reviewData: Omit<PatientReviewInfo, "reviewedAt" | "reviewId">
976
+ ): Promise<Appointment> {
977
+ console.log(
978
+ `[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`
979
+ );
980
+ const newReviewInfo: PatientReviewInfo = {
981
+ ...reviewData,
982
+ reviewId: this.generateId(),
983
+ reviewedAt: Timestamp.now(),
984
+ };
985
+ const updateData: UpdateAppointmentData = {
986
+ reviewInfo: newReviewInfo,
987
+ updatedAt: serverTimestamp(),
988
+ };
989
+ return this.updateAppointment(appointmentId, updateData);
990
+ }
991
+
757
992
  /**
758
993
  * 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
764
994
  */
765
995
  async updatePaymentStatus(
766
996
  appointmentId: string,
@@ -770,12 +1000,11 @@ export class AppointmentService extends BaseService {
770
1000
  console.log(
771
1001
  `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
772
1002
  );
773
-
774
1003
  const updateData: UpdateAppointmentData = {
775
1004
  paymentStatus,
776
1005
  paymentTransactionId: paymentTransactionId || null,
1006
+ updatedAt: serverTimestamp(),
777
1007
  };
778
-
779
1008
  return this.updateAppointment(appointmentId, updateData);
780
1009
  }
781
1010
 
@@ -197,4 +197,7 @@ export class ClinicAdminService extends BaseService {
197
197
  this.getClinicService()
198
198
  );
199
199
  }
200
+
201
+ // TODO: Add more methods for clinic admins for managing permissions, editing profiles by the admin, or by the clinic group owner and so on
202
+ // Generally refactor admin permissions and clinic group permissions and systems for admin management
200
203
  }
@@ -253,4 +253,12 @@ export class ClinicGroupService extends BaseService {
253
253
  this.app
254
254
  );
255
255
  }
256
+
257
+ // TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
258
+
259
+ // TODO: Refactor admin token methods not to add tokens to the clinicGroup document,
260
+ // but to add them to a subcollection called adminTokens that belongs to a specific clinicGroup document
261
+
262
+ // TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
263
+ // TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
256
264
  }
@@ -23,10 +23,7 @@ import {
23
23
  DocumentTemplate,
24
24
  UpdateDocumentTemplateData,
25
25
  } from "../../types";
26
- import {
27
- FILLED_DOCUMENTS_COLLECTION,
28
- DOCUMENTATION_TEMPLATES_COLLECTION,
29
- } from "../../types";
26
+ import { DOCUMENTATION_TEMPLATES_COLLECTION } from "../../types";
30
27
  import {
31
28
  createDocumentTemplateSchema,
32
29
  updateDocumentTemplateSchema,
@@ -76,6 +73,9 @@ export class DocumentationTemplateService extends BaseService {
76
73
  version: 1,
77
74
  isActive: true,
78
75
  tags: validatedData.tags || [],
76
+ isUserForm: validatedData.isUserForm || false,
77
+ isRequired: validatedData.isRequired || false,
78
+ sortingOrder: validatedData.sortingOrder || 0,
79
79
  };
80
80
 
81
81
  // Save to Firestore
@@ -123,30 +123,38 @@ export class DocumentationTemplateService extends BaseService {
123
123
  // Process elements if provided
124
124
  let updatedElements = template.elements;
125
125
  if (validatedData.elements) {
126
- // Generate IDs for new elements
127
126
  updatedElements = validatedData.elements.map((element) => ({
128
127
  ...element,
129
- id: this.generateId(),
128
+ id: (element as any).id || this.generateId(),
130
129
  })) as DocumentElement[];
131
130
  }
132
131
 
133
- // Create update object
134
- const updateData: Partial<DocumentTemplate> = {
135
- ...validatedData,
132
+ const updatePayload: Partial<DocumentTemplate> = {
136
133
  elements: updatedElements,
137
134
  updatedAt: Date.now(),
138
135
  version: template.version + 1,
139
136
  };
140
137
 
141
- // Update in Firestore
138
+ // Conditionally add fields from validatedData to avoid overwriting with undefined
139
+ if (validatedData.title !== undefined)
140
+ updatePayload.title = validatedData.title;
141
+ if (validatedData.description !== undefined)
142
+ updatePayload.description = validatedData.description;
143
+ if (validatedData.isActive !== undefined)
144
+ updatePayload.isActive = validatedData.isActive;
145
+ if (validatedData.tags !== undefined)
146
+ updatePayload.tags = validatedData.tags;
147
+ if (validatedData.isUserForm !== undefined)
148
+ updatePayload.isUserForm = validatedData.isUserForm;
149
+ if (validatedData.isRequired !== undefined)
150
+ updatePayload.isRequired = validatedData.isRequired;
151
+ if (validatedData.sortingOrder !== undefined)
152
+ updatePayload.sortingOrder = validatedData.sortingOrder;
153
+
142
154
  const docRef = doc(this.collectionRef, templateId);
143
- await updateDoc(docRef, updateData);
155
+ await updateDoc(docRef, updatePayload);
144
156
 
145
- // Return updated template
146
- return {
147
- ...template,
148
- ...updateData,
149
- } as DocumentTemplate;
157
+ return { ...template, ...updatePayload } as DocumentTemplate;
150
158
  }
151
159
 
152
160
  /**