@blackcode_sa/metaestetics-api 1.12.20 → 1.12.21

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/index.js CHANGED
@@ -143,7 +143,7 @@ __export(index_exports, {
143
143
  module.exports = __toCommonJS(index_exports);
144
144
 
145
145
  // src/services/appointment/appointment.service.ts
146
- var import_firestore2 = require("firebase/firestore");
146
+ var import_firestore4 = require("firebase/firestore");
147
147
  var import_functions = require("firebase/functions");
148
148
 
149
149
  // src/services/base.service.ts
@@ -707,1968 +707,2201 @@ var rescheduleAppointmentSchema = import_zod3.z.object({
707
707
  "New end time must be a valid timestamp, Date object, number, or string"
708
708
  )
709
709
  });
710
+ var zonePhotoUploadSchema = import_zod3.z.object({
711
+ appointmentId: import_zod3.z.string().min(MIN_STRING_LENGTH, "Appointment ID is required"),
712
+ zoneId: import_zod3.z.string().min(MIN_STRING_LENGTH, "Zone ID is required"),
713
+ photoType: import_zod3.z.enum(["before", "after"], {
714
+ required_error: 'Photo type must be either "before" or "after"'
715
+ }),
716
+ file: import_zod3.z.any().refine((file) => {
717
+ return file instanceof File || file instanceof Blob || file && typeof file.size === "number" && typeof file.type === "string";
718
+ }, "File must be a valid File or Blob object"),
719
+ notes: import_zod3.z.string().max(MAX_STRING_LENGTH_LONG, "Notes too long").optional()
720
+ });
710
721
 
711
- // src/services/appointment/utils/appointment.utils.ts
722
+ // src/services/media/media.service.ts
712
723
  var import_firestore = require("firebase/firestore");
713
-
714
- // src/types/calendar/index.ts
715
- var CalendarEventStatus = /* @__PURE__ */ ((CalendarEventStatus4) => {
716
- CalendarEventStatus4["PENDING"] = "pending";
717
- CalendarEventStatus4["CONFIRMED"] = "confirmed";
718
- CalendarEventStatus4["REJECTED"] = "rejected";
719
- CalendarEventStatus4["CANCELED"] = "canceled";
720
- CalendarEventStatus4["RESCHEDULED"] = "rescheduled";
721
- CalendarEventStatus4["COMPLETED"] = "completed";
722
- CalendarEventStatus4["NO_SHOW"] = "no_show";
723
- return CalendarEventStatus4;
724
- })(CalendarEventStatus || {});
725
- var CalendarSyncStatus = /* @__PURE__ */ ((CalendarSyncStatus4) => {
726
- CalendarSyncStatus4["INTERNAL"] = "internal";
727
- CalendarSyncStatus4["EXTERNAL"] = "external";
728
- return CalendarSyncStatus4;
729
- })(CalendarSyncStatus || {});
730
- var CalendarEventType = /* @__PURE__ */ ((CalendarEventType3) => {
731
- CalendarEventType3["APPOINTMENT"] = "appointment";
732
- CalendarEventType3["BLOCKING"] = "blocking";
733
- CalendarEventType3["BREAK"] = "break";
734
- CalendarEventType3["FREE_DAY"] = "free_day";
735
- CalendarEventType3["OTHER"] = "other";
736
- return CalendarEventType3;
737
- })(CalendarEventType || {});
738
- var CALENDAR_COLLECTION = "calendar";
739
- var SearchLocationEnum = /* @__PURE__ */ ((SearchLocationEnum2) => {
740
- SearchLocationEnum2["PRACTITIONER"] = "practitioner";
741
- SearchLocationEnum2["PATIENT"] = "patient";
742
- SearchLocationEnum2["CLINIC"] = "clinic";
743
- return SearchLocationEnum2;
744
- })(SearchLocationEnum || {});
745
-
746
- // src/types/practitioner/index.ts
747
- var PRACTITIONERS_COLLECTION = "practitioners";
748
- var REGISTER_TOKENS_COLLECTION = "register_tokens";
749
- var PractitionerStatus = /* @__PURE__ */ ((PractitionerStatus2) => {
750
- PractitionerStatus2["DRAFT"] = "draft";
751
- PractitionerStatus2["ACTIVE"] = "active";
752
- return PractitionerStatus2;
753
- })(PractitionerStatus || {});
754
- var PractitionerTokenStatus = /* @__PURE__ */ ((PractitionerTokenStatus2) => {
755
- PractitionerTokenStatus2["ACTIVE"] = "active";
756
- PractitionerTokenStatus2["USED"] = "used";
757
- PractitionerTokenStatus2["EXPIRED"] = "expired";
758
- PractitionerTokenStatus2["REVOKED"] = "revoked";
759
- return PractitionerTokenStatus2;
760
- })(PractitionerTokenStatus || {});
761
-
762
- // src/types/clinic/preferences.types.ts
763
- var PracticeType = /* @__PURE__ */ ((PracticeType2) => {
764
- PracticeType2["GENERAL_PRACTICE"] = "general_practice";
765
- PracticeType2["DENTAL"] = "dental";
766
- PracticeType2["DERMATOLOGY"] = "dermatology";
767
- PracticeType2["CARDIOLOGY"] = "cardiology";
768
- PracticeType2["ORTHOPEDICS"] = "orthopedics";
769
- PracticeType2["GYNECOLOGY"] = "gynecology";
770
- PracticeType2["PEDIATRICS"] = "pediatrics";
771
- PracticeType2["OPHTHALMOLOGY"] = "ophthalmology";
772
- PracticeType2["NEUROLOGY"] = "neurology";
773
- PracticeType2["PSYCHIATRY"] = "psychiatry";
774
- PracticeType2["UROLOGY"] = "urology";
775
- PracticeType2["ONCOLOGY"] = "oncology";
776
- PracticeType2["ENDOCRINOLOGY"] = "endocrinology";
777
- PracticeType2["GASTROENTEROLOGY"] = "gastroenterology";
778
- PracticeType2["PULMONOLOGY"] = "pulmonology";
779
- PracticeType2["RHEUMATOLOGY"] = "rheumatology";
780
- PracticeType2["PHYSICAL_THERAPY"] = "physical_therapy";
781
- PracticeType2["NUTRITION"] = "nutrition";
782
- PracticeType2["ALTERNATIVE_MEDICINE"] = "alternative_medicine";
783
- PracticeType2["OTHER"] = "other";
784
- return PracticeType2;
785
- })(PracticeType || {});
786
- var Language = /* @__PURE__ */ ((Language2) => {
787
- Language2["ENGLISH"] = "english";
788
- Language2["GERMAN"] = "german";
789
- Language2["ITALIAN"] = "italian";
790
- Language2["FRENCH"] = "french";
791
- Language2["SPANISH"] = "spanish";
792
- return Language2;
793
- })(Language || {});
794
- var ClinicTag = /* @__PURE__ */ ((ClinicTag6) => {
795
- ClinicTag6["PARKING"] = "parking";
796
- ClinicTag6["WIFI"] = "wifi";
797
- ClinicTag6["LUXURY_WAITING"] = "luxury_waiting";
798
- ClinicTag6["REFRESHMENTS"] = "refreshments";
799
- ClinicTag6["PRIVATE_ROOMS"] = "private_rooms";
800
- ClinicTag6["RECOVERY_AREA"] = "recovery_area";
801
- ClinicTag6["CARD_PAYMENT"] = "card_payment";
802
- ClinicTag6["FINANCING"] = "financing";
803
- ClinicTag6["FREE_CONSULTATION"] = "free_consultation";
804
- ClinicTag6["VIRTUAL_CONSULTATION"] = "virtual_consultation";
805
- ClinicTag6["BEFORE_AFTER_PHOTOS"] = "before_after_photos";
806
- ClinicTag6["AFTERCARE_SUPPORT"] = "aftercare_support";
807
- ClinicTag6["BOTOX"] = "botox";
808
- ClinicTag6["DERMAL_FILLERS"] = "dermal_fillers";
809
- ClinicTag6["LASER_HAIR_REMOVAL"] = "laser_hair_removal";
810
- ClinicTag6["LASER_SKIN_RESURFACING"] = "laser_skin_resurfacing";
811
- ClinicTag6["CHEMICAL_PEELS"] = "chemical_peels";
812
- ClinicTag6["MICRONEEDLING"] = "microneedling";
813
- ClinicTag6["COOLSCULPTING"] = "coolsculpting";
814
- ClinicTag6["THREAD_LIFT"] = "thread_lift";
815
- ClinicTag6["LIP_ENHANCEMENT"] = "lip_enhancement";
816
- ClinicTag6["RHINOPLASTY"] = "rhinoplasty";
817
- ClinicTag6["SKIN_TIGHTENING"] = "skin_tightening";
818
- ClinicTag6["FAT_DISSOLVING"] = "fat_dissolving";
819
- ClinicTag6["PRP_TREATMENT"] = "prp_treatment";
820
- ClinicTag6["HYDRAFACIAL"] = "hydrafacial";
821
- ClinicTag6["IPL_PHOTOFACIAL"] = "ipl_photofacial";
822
- ClinicTag6["BODY_CONTOURING"] = "body_contouring";
823
- ClinicTag6["FACELIFT"] = "facelift";
824
- ClinicTag6["RHINOPLASTY_SURGICAL"] = "rhinoplasty_surgical";
825
- ClinicTag6["BREAST_AUGMENTATION"] = "breast_augmentation";
826
- ClinicTag6["BREAST_REDUCTION"] = "breast_reduction";
827
- ClinicTag6["BREAST_LIFT"] = "breast_lift";
828
- ClinicTag6["TUMMY_TUCK"] = "tummy_tuck";
829
- ClinicTag6["LIPOSUCTION"] = "liposuction";
830
- ClinicTag6["BBL"] = "bbl";
831
- ClinicTag6["MOMMY_MAKEOVER"] = "mommy_makeover";
832
- ClinicTag6["ARM_LIFT"] = "arm_lift";
833
- ClinicTag6["THIGH_LIFT"] = "thigh_lift";
834
- ClinicTag6["EYELID_SURGERY"] = "eyelid_surgery";
835
- ClinicTag6["BROW_LIFT"] = "brow_lift";
836
- ClinicTag6["NECK_LIFT"] = "neck_lift";
837
- ClinicTag6["OTOPLASTY"] = "otoplasty";
838
- ClinicTag6["LABIAPLASTY"] = "labiaplasty";
839
- ClinicTag6["ONLINE_BOOKING"] = "online_booking";
840
- ClinicTag6["MOBILE_APP"] = "mobile_app";
841
- ClinicTag6["SMS_NOTIFICATIONS"] = "sms_notifications";
842
- ClinicTag6["EMAIL_NOTIFICATIONS"] = "email_notifications";
843
- ClinicTag6["VIRTUAL_TRY_ON"] = "virtual_try_on";
844
- ClinicTag6["SKIN_ANALYSIS"] = "skin_analysis";
845
- ClinicTag6["TREATMENT_TRACKING"] = "treatment_tracking";
846
- ClinicTag6["LOYALTY_PROGRAM"] = "loyalty_program";
847
- ClinicTag6["ENGLISH"] = "english";
848
- ClinicTag6["GERMAN"] = "german";
849
- ClinicTag6["FRENCH"] = "french";
850
- ClinicTag6["SPANISH"] = "spanish";
851
- ClinicTag6["ITALIAN"] = "italian";
852
- ClinicTag6["DUTCH"] = "dutch";
853
- ClinicTag6["RUSSIAN"] = "russian";
854
- ClinicTag6["PORTUGUESE"] = "portuguese";
855
- ClinicTag6["OPEN_24_7"] = "open_24_7";
856
- ClinicTag6["WEEKEND_HOURS"] = "weekend_hours";
857
- ClinicTag6["EXTENDED_HOURS"] = "extended_hours";
858
- ClinicTag6["HOLIDAY_HOURS"] = "holiday_hours";
859
- return ClinicTag6;
860
- })(ClinicTag || {});
861
- var ClinicPhotoTag = /* @__PURE__ */ ((ClinicPhotoTag2) => {
862
- ClinicPhotoTag2["BUILDING_EXTERIOR"] = "building_exterior";
863
- ClinicPhotoTag2["ENTRANCE"] = "entrance";
864
- ClinicPhotoTag2["PARKING"] = "parking";
865
- ClinicPhotoTag2["RECEPTION"] = "reception";
866
- ClinicPhotoTag2["WAITING_ROOM"] = "waiting_room";
867
- ClinicPhotoTag2["HALLWAY"] = "hallway";
868
- ClinicPhotoTag2["EXAM_ROOM"] = "exam_room";
869
- ClinicPhotoTag2["TREATMENT_ROOM"] = "treatment_room";
870
- ClinicPhotoTag2["LABORATORY"] = "laboratory";
871
- ClinicPhotoTag2["XRAY_ROOM"] = "xray_room";
872
- ClinicPhotoTag2["ULTRASOUND_ROOM"] = "ultrasound_room";
873
- ClinicPhotoTag2["DENTAL_OFFICE"] = "dental_office";
874
- ClinicPhotoTag2["OPERATING_ROOM"] = "operating_room";
875
- ClinicPhotoTag2["RECOVERY_ROOM"] = "recovery_room";
876
- ClinicPhotoTag2["MEDICAL_EQUIPMENT"] = "medical_equipment";
877
- ClinicPhotoTag2["PHARMACY"] = "pharmacy";
878
- ClinicPhotoTag2["CAFETERIA"] = "cafeteria";
879
- ClinicPhotoTag2["CHILDREN_AREA"] = "children_area";
880
- ClinicPhotoTag2["STAFF"] = "staff";
881
- ClinicPhotoTag2["OTHER"] = "other";
882
- return ClinicPhotoTag2;
883
- })(ClinicPhotoTag || {});
884
-
885
- // src/types/clinic/practitioner-invite.types.ts
886
- var PRACTITIONER_INVITES_COLLECTION = "practitioner-invites";
887
- var PractitionerInviteStatus = /* @__PURE__ */ ((PractitionerInviteStatus2) => {
888
- PractitionerInviteStatus2["PENDING"] = "pending";
889
- PractitionerInviteStatus2["ACCEPTED"] = "accepted";
890
- PractitionerInviteStatus2["REJECTED"] = "rejected";
891
- PractitionerInviteStatus2["CANCELLED"] = "cancelled";
892
- return PractitionerInviteStatus2;
893
- })(PractitionerInviteStatus || {});
894
-
895
- // src/types/clinic/index.ts
896
- var CLINIC_GROUPS_COLLECTION = "clinic_groups";
897
- var CLINIC_ADMINS_COLLECTION = "clinic_admins";
898
- var CLINICS_COLLECTION = "clinics";
899
- var AdminTokenStatus = /* @__PURE__ */ ((AdminTokenStatus2) => {
900
- AdminTokenStatus2["ACTIVE"] = "active";
901
- AdminTokenStatus2["USED"] = "used";
902
- AdminTokenStatus2["EXPIRED"] = "expired";
903
- return AdminTokenStatus2;
904
- })(AdminTokenStatus || {});
905
- var SubscriptionModel = /* @__PURE__ */ ((SubscriptionModel2) => {
906
- SubscriptionModel2["NO_SUBSCRIPTION"] = "no_subscription";
907
- SubscriptionModel2["BASIC"] = "basic";
908
- SubscriptionModel2["PREMIUM"] = "premium";
909
- SubscriptionModel2["ENTERPRISE"] = "enterprise";
910
- return SubscriptionModel2;
911
- })(SubscriptionModel || {});
912
- var SubscriptionStatus = /* @__PURE__ */ ((SubscriptionStatus2) => {
913
- SubscriptionStatus2["PENDING"] = "pending";
914
- SubscriptionStatus2["ACTIVE"] = "active";
915
- SubscriptionStatus2["PENDING_CANCELLATION"] = "pending_cancellation";
916
- SubscriptionStatus2["CANCELED"] = "canceled";
917
- SubscriptionStatus2["PAST_DUE"] = "past_due";
918
- SubscriptionStatus2["TRIALING"] = "trialing";
919
- return SubscriptionStatus2;
920
- })(SubscriptionStatus || {});
921
- var BillingTransactionType = /* @__PURE__ */ ((BillingTransactionType2) => {
922
- BillingTransactionType2["SUBSCRIPTION_CREATED"] = "subscription_created";
923
- BillingTransactionType2["SUBSCRIPTION_ACTIVATED"] = "subscription_activated";
924
- BillingTransactionType2["SUBSCRIPTION_RENEWED"] = "subscription_renewed";
925
- BillingTransactionType2["SUBSCRIPTION_UPDATED"] = "subscription_updated";
926
- BillingTransactionType2["SUBSCRIPTION_CANCELED"] = "subscription_canceled";
927
- BillingTransactionType2["SUBSCRIPTION_REACTIVATED"] = "subscription_reactivated";
928
- BillingTransactionType2["SUBSCRIPTION_DELETED"] = "subscription_deleted";
929
- return BillingTransactionType2;
930
- })(BillingTransactionType || {});
931
-
932
- // src/types/patient/medical-info.types.ts
933
- var PATIENT_MEDICAL_INFO_COLLECTION = "medical_info";
934
- var DEFAULT_MEDICAL_INFO = {
935
- vitalStats: {},
936
- blockingConditions: [],
937
- contraindications: [],
938
- allergies: [],
939
- currentMedications: []
940
- };
941
-
942
- // src/types/patient/index.ts
943
- var PATIENTS_COLLECTION = "patients";
944
- var PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
945
- var PATIENT_MEDICAL_HISTORY_COLLECTION = "medical-history";
946
- var PATIENT_APPOINTMENTS_COLLECTION = "appointments";
947
- var PATIENT_LOCATION_INFO_COLLECTION = "location-info";
948
- var Gender = /* @__PURE__ */ ((Gender2) => {
949
- Gender2["MALE"] = "male";
950
- Gender2["FEMALE"] = "female";
951
- Gender2["TRANSGENDER_MALE"] = "transgender_male";
952
- Gender2["TRANSGENDER_FEMALE"] = "transgender_female";
953
- Gender2["PREFER_NOT_TO_SAY"] = "prefer_not_to_say";
954
- Gender2["OTHER"] = "other";
955
- return Gender2;
956
- })(Gender || {});
957
-
958
- // src/types/procedure/index.ts
959
- var PROCEDURES_COLLECTION = "procedures";
960
-
961
- // src/backoffice/types/technology.types.ts
962
- var TECHNOLOGIES_COLLECTION = "technologies";
963
-
964
- // src/services/appointment/utils/appointment.utils.ts
965
- async function updateAppointmentUtil(db, appointmentId, data) {
966
- try {
967
- const appointmentRef = (0, import_firestore.doc)(db, APPOINTMENTS_COLLECTION, appointmentId);
968
- const appointmentDoc = await (0, import_firestore.getDoc)(appointmentRef);
969
- if (!appointmentDoc.exists()) {
970
- throw new Error(`Appointment with ID ${appointmentId} not found`);
971
- }
972
- const currentAppointment = appointmentDoc.data();
973
- let completedPreRequirements = currentAppointment.completedPreRequirements || [];
974
- let completedPostRequirements = currentAppointment.completedPostRequirements || [];
975
- if (data.completedPreRequirements) {
976
- const validPreReqIds = currentAppointment.preProcedureRequirements.map(
977
- (req) => req.id
978
- );
979
- if (Array.isArray(data.completedPreRequirements)) {
980
- const invalidPreReqIds = data.completedPreRequirements.filter(
981
- (id) => !validPreReqIds.includes(id)
982
- );
983
- if (invalidPreReqIds.length > 0) {
984
- throw new Error(
985
- `Invalid pre-requirement IDs: ${invalidPreReqIds.join(", ")}`
986
- );
987
- }
988
- completedPreRequirements = [
989
- .../* @__PURE__ */ new Set([
990
- ...completedPreRequirements,
991
- ...data.completedPreRequirements
992
- ])
993
- ];
994
- }
995
- }
996
- if (data.completedPostRequirements) {
997
- const validPostReqIds = currentAppointment.postProcedureRequirements.map(
998
- (req) => req.id
999
- );
1000
- if (Array.isArray(data.completedPostRequirements)) {
1001
- const invalidPostReqIds = data.completedPostRequirements.filter(
1002
- (id) => !validPostReqIds.includes(id)
1003
- );
1004
- if (invalidPostReqIds.length > 0) {
1005
- throw new Error(
1006
- `Invalid post-requirement IDs: ${invalidPostReqIds.join(", ")}`
1007
- );
1008
- }
1009
- completedPostRequirements = [
1010
- .../* @__PURE__ */ new Set([
1011
- ...completedPostRequirements,
1012
- ...data.completedPostRequirements
1013
- ])
1014
- ];
1015
- }
1016
- }
1017
- const updateData = {
1018
- ...data,
1019
- completedPreRequirements: Array.isArray(data.completedPreRequirements) ? completedPreRequirements : data.completedPreRequirements,
1020
- completedPostRequirements: Array.isArray(data.completedPostRequirements) ? completedPostRequirements : data.completedPostRequirements,
1021
- updatedAt: (0, import_firestore.serverTimestamp)()
1022
- };
1023
- Object.keys(updateData).forEach((key) => {
1024
- if (updateData[key] === void 0) {
1025
- delete updateData[key];
1026
- }
1027
- });
1028
- if (data.status && data.status !== currentAppointment.status) {
1029
- if (data.status === "confirmed" /* CONFIRMED */ && !updateData.confirmationTime) {
1030
- updateData.confirmationTime = import_firestore.Timestamp.now();
1031
- }
1032
- if (currentAppointment.calendarEventId) {
1033
- await updateCalendarEventStatus(
1034
- db,
1035
- currentAppointment.calendarEventId,
1036
- data.status
1037
- );
1038
- }
1039
- }
1040
- await (0, import_firestore.updateDoc)(appointmentRef, updateData);
1041
- const updatedAppointmentDoc = await (0, import_firestore.getDoc)(appointmentRef);
1042
- if (!updatedAppointmentDoc.exists()) {
1043
- throw new Error(
1044
- `Failed to retrieve updated appointment ${appointmentId}`
1045
- );
1046
- }
1047
- return updatedAppointmentDoc.data();
1048
- } catch (error) {
1049
- console.error(`Error updating appointment ${appointmentId}:`, error);
1050
- throw error;
724
+ var import_storage2 = require("firebase/storage");
725
+ var import_firestore2 = require("firebase/firestore");
726
+ var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
727
+ MediaAccessLevel2["PUBLIC"] = "public";
728
+ MediaAccessLevel2["PRIVATE"] = "private";
729
+ MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
730
+ return MediaAccessLevel2;
731
+ })(MediaAccessLevel || {});
732
+ var MEDIA_METADATA_COLLECTION = "media_metadata";
733
+ var MediaService = class extends BaseService {
734
+ constructor(...args) {
735
+ super(...args);
1051
736
  }
1052
- }
1053
- async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus) {
1054
- try {
1055
- const calendarEventRef = (0, import_firestore.doc)(db, CALENDAR_COLLECTION, calendarEventId);
1056
- const calendarEventDoc = await (0, import_firestore.getDoc)(calendarEventRef);
1057
- if (!calendarEventDoc.exists()) {
1058
- console.warn(`Calendar event with ID ${calendarEventId} not found`);
1059
- return;
737
+ /**
738
+ * Upload a media file, store its metadata, and return the metadata including the URL.
739
+ * @param file - The file to upload.
740
+ * @param ownerId - ID of the owner (user, patient, clinic, etc.).
741
+ * @param accessLevel - Access level (public, private, confidential).
742
+ * @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
743
+ * @param originalFileName - Optional: the original name of the file, if not using file.name.
744
+ * @returns Promise with the media metadata.
745
+ */
746
+ async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
747
+ const mediaId = this.generateId();
748
+ const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
749
+ const uniqueFileName = `${mediaId}-${fileNameToUse}`;
750
+ const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
751
+ console.log(`[MediaService] Uploading file to: ${filePath}`);
752
+ const storageRef = (0, import_storage2.ref)(this.storage, filePath);
753
+ try {
754
+ const uploadResult = await (0, import_storage2.uploadBytes)(storageRef, file, {
755
+ contentType: file.type
756
+ });
757
+ console.log("[MediaService] File uploaded successfully", uploadResult);
758
+ const downloadURL = await (0, import_storage2.getDownloadURL)(uploadResult.ref);
759
+ console.log("[MediaService] Got download URL:", downloadURL);
760
+ const metadata = {
761
+ id: mediaId,
762
+ name: fileNameToUse,
763
+ url: downloadURL,
764
+ contentType: file.type,
765
+ size: file.size,
766
+ createdAt: import_firestore.Timestamp.now(),
767
+ accessLevel,
768
+ ownerId,
769
+ collectionName,
770
+ path: filePath
771
+ };
772
+ const metadataDocRef = (0, import_firestore2.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
773
+ await (0, import_firestore2.setDoc)(metadataDocRef, metadata);
774
+ console.log("[MediaService] Metadata stored in Firestore:", mediaId);
775
+ return metadata;
776
+ } catch (error) {
777
+ console.error("[MediaService] Error during media upload:", error);
778
+ throw error;
1060
779
  }
1061
- let calendarStatus;
1062
- switch (appointmentStatus) {
1063
- case "confirmed" /* CONFIRMED */:
1064
- calendarStatus = "confirmed";
1065
- break;
1066
- case "canceled_patient" /* CANCELED_PATIENT */:
1067
- case "canceled_clinic" /* CANCELED_CLINIC */:
1068
- calendarStatus = "canceled";
1069
- break;
1070
- case "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */:
1071
- calendarStatus = "rescheduled";
1072
- break;
1073
- case "completed" /* COMPLETED */:
1074
- calendarStatus = "completed";
1075
- break;
1076
- default:
1077
- return;
780
+ }
781
+ /**
782
+ * Get media metadata from Firestore by its ID.
783
+ * @param mediaId - ID of the media.
784
+ * @returns Promise with the media metadata or null if not found.
785
+ */
786
+ async getMediaMetadata(mediaId) {
787
+ console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
788
+ const docRef = (0, import_firestore2.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
789
+ const docSnap = await (0, import_firestore2.getDoc)(docRef);
790
+ if (docSnap.exists()) {
791
+ console.log("[MediaService] Metadata found:", docSnap.data());
792
+ return docSnap.data();
1078
793
  }
1079
- await (0, import_firestore.updateDoc)(calendarEventRef, {
1080
- status: calendarStatus,
1081
- updatedAt: (0, import_firestore.serverTimestamp)()
1082
- });
1083
- } catch (error) {
1084
- console.error(`Error updating calendar event ${calendarEventId}:`, error);
794
+ console.log("[MediaService] No metadata found for ID:", mediaId);
795
+ return null;
1085
796
  }
1086
- }
1087
- async function getAppointmentByIdUtil(db, appointmentId) {
1088
- try {
1089
- const appointmentDoc = await (0, import_firestore.getDoc)(
1090
- (0, import_firestore.doc)(db, APPOINTMENTS_COLLECTION, appointmentId)
797
+ /**
798
+ * Get media metadata from Firestore by its public URL.
799
+ * @param url - The public URL of the media file.
800
+ * @returns Promise with the media metadata or null if not found.
801
+ */
802
+ async getMediaMetadataByUrl(url) {
803
+ console.log(`[MediaService] Getting media metadata by URL: ${url}`);
804
+ const q = (0, import_firestore2.query)(
805
+ (0, import_firestore2.collection)(this.db, MEDIA_METADATA_COLLECTION),
806
+ (0, import_firestore2.where)("url", "==", url),
807
+ (0, import_firestore2.limit)(1)
1091
808
  );
1092
- if (!appointmentDoc.exists()) {
809
+ try {
810
+ const querySnapshot = await (0, import_firestore2.getDocs)(q);
811
+ if (!querySnapshot.empty) {
812
+ const metadata = querySnapshot.docs[0].data();
813
+ console.log("[MediaService] Metadata found by URL:", metadata);
814
+ return metadata;
815
+ }
816
+ console.log("[MediaService] No metadata found for URL:", url);
1093
817
  return null;
818
+ } catch (error) {
819
+ console.error("[MediaService] Error fetching metadata by URL:", error);
820
+ throw error;
1094
821
  }
1095
- return appointmentDoc.data();
1096
- } catch (error) {
1097
- console.error(`Error getting appointment ${appointmentId}:`, error);
1098
- throw error;
1099
822
  }
1100
- }
1101
- async function searchAppointmentsUtil(db, params) {
1102
- try {
1103
- const constraints = [];
1104
- if (params.patientId) {
1105
- constraints.push((0, import_firestore.where)("patientId", "==", params.patientId));
1106
- }
1107
- if (params.practitionerId) {
1108
- constraints.push((0, import_firestore.where)("practitionerId", "==", params.practitionerId));
1109
- }
1110
- if (params.clinicBranchId) {
1111
- constraints.push((0, import_firestore.where)("clinicBranchId", "==", params.clinicBranchId));
1112
- }
1113
- if (params.startDate) {
1114
- constraints.push(
1115
- (0, import_firestore.where)(
1116
- "appointmentStartTime",
1117
- ">=",
1118
- import_firestore.Timestamp.fromDate(params.startDate)
1119
- )
823
+ /**
824
+ * Delete media from storage and remove metadata from Firestore.
825
+ * @param mediaId - ID of the media to delete.
826
+ */
827
+ async deleteMedia(mediaId) {
828
+ console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
829
+ const metadata = await this.getMediaMetadata(mediaId);
830
+ if (!metadata) {
831
+ console.warn(
832
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
1120
833
  );
834
+ return;
1121
835
  }
1122
- if (params.endDate) {
1123
- constraints.push(
1124
- (0, import_firestore.where)("appointmentStartTime", "<=", import_firestore.Timestamp.fromDate(params.endDate))
836
+ const storageFileRef = (0, import_storage2.ref)(this.storage, metadata.path);
837
+ try {
838
+ await (0, import_storage2.deleteObject)(storageFileRef);
839
+ console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
840
+ const metadataDocRef = (0, import_firestore2.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
841
+ await (0, import_firestore2.deleteDoc)(metadataDocRef);
842
+ console.log(
843
+ `[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
1125
844
  );
845
+ } catch (error) {
846
+ console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
847
+ throw error;
1126
848
  }
1127
- if (params.status) {
1128
- if (Array.isArray(params.status)) {
1129
- constraints.push((0, import_firestore.where)("status", "in", params.status));
1130
- } else {
1131
- constraints.push((0, import_firestore.where)("status", "==", params.status));
1132
- }
1133
- }
1134
- constraints.push((0, import_firestore.orderBy)("appointmentStartTime", "asc"));
1135
- if (params.limit) {
1136
- constraints.push((0, import_firestore.limit)(params.limit));
1137
- }
1138
- if (params.startAfter) {
1139
- constraints.push((0, import_firestore.startAfter)(params.startAfter));
1140
- }
1141
- const q = (0, import_firestore.query)((0, import_firestore.collection)(db, APPOINTMENTS_COLLECTION), ...constraints);
1142
- const querySnapshot = await (0, import_firestore.getDocs)(q);
1143
- const appointments = querySnapshot.docs.map(
1144
- (doc38) => doc38.data()
1145
- );
1146
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1147
- return { appointments, lastDoc };
1148
- } catch (error) {
1149
- console.error("Error searching appointments:", error);
1150
- throw error;
1151
- }
1152
- }
1153
-
1154
- // src/services/appointment/appointment.service.ts
1155
- var AppointmentService = class extends BaseService {
1156
- /**
1157
- * Creates a new AppointmentService instance.
1158
- *
1159
- * @param db Firestore instance
1160
- * @param auth Firebase Auth instance
1161
- * @param app Firebase App instance
1162
- * @param calendarService Calendar service instance
1163
- * @param patientService Patient service instance
1164
- * @param practitionerService Practitioner service instance
1165
- * @param clinicService Clinic service instance
1166
- */
1167
- constructor(db, auth, app, calendarService, patientService, practitionerService, clinicService, filledDocumentService) {
1168
- super(db, auth, app);
1169
- this.calendarService = calendarService;
1170
- this.patientService = patientService;
1171
- this.practitionerService = practitionerService;
1172
- this.clinicService = clinicService;
1173
- this.filledDocumentService = filledDocumentService;
1174
- this.functions = (0, import_functions.getFunctions)(app, "europe-west6");
1175
849
  }
1176
850
  /**
1177
- * Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
1178
- * This is an alternative implementation using direct HTTP request instead of callable function.
1179
- *
1180
- * @param clinicId ID of the clinic
1181
- * @param practitionerId ID of the practitioner
1182
- * @param procedureId ID of the procedure
1183
- * @param startDate Start date of the time range to check
1184
- * @param endDate End date of the time range to check
1185
- * @returns Array of available booking slots
851
+ * Update media access level. This involves moving the file in Firebase Storage
852
+ * to a new path reflecting the new access level, and updating its metadata.
853
+ * @param mediaId - ID of the media to update.
854
+ * @param newAccessLevel - New access level.
855
+ * @returns Promise with the updated media metadata, or null if metadata not found.
1186
856
  */
1187
- async getAvailableBookingSlotsHttp(clinicId, practitionerId, procedureId, startDate, endDate) {
1188
- try {
1189
- console.log(
1190
- `[APPOINTMENT_SERVICE] Getting available booking slots via HTTP for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
857
+ async updateMediaAccessLevel(mediaId, newAccessLevel) {
858
+ var _a;
859
+ console.log(
860
+ `[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
861
+ );
862
+ const metadata = await this.getMediaMetadata(mediaId);
863
+ if (!metadata) {
864
+ console.warn(
865
+ `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
1191
866
  );
1192
- if (!clinicId || !practitionerId || !procedureId || !startDate || !endDate) {
1193
- throw new Error("Missing required parameters for booking slots calculation");
1194
- }
1195
- if (endDate <= startDate) {
1196
- throw new Error("End date must be after start date");
1197
- }
1198
- const currentUser = this.auth.currentUser;
1199
- if (!currentUser) {
1200
- throw new Error("User must be authenticated to get available booking slots");
1201
- }
1202
- const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/getAvailableBookingSlots`;
1203
- const idToken = await currentUser.getIdToken();
1204
- console.log(`[APPOINTMENT_SERVICE] Got user token, user ID: ${currentUser.uid}`);
1205
- const requestData = {
1206
- clinicId,
1207
- practitionerId,
1208
- procedureId,
1209
- timeframe: {
1210
- start: startDate.getTime(),
1211
- // Convert to timestamp
1212
- end: endDate.getTime()
1213
- }
1214
- };
1215
- console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
1216
- const response = await fetch(functionUrl, {
1217
- method: "POST",
1218
- mode: "cors",
1219
- // Important for cross-origin requests
1220
- cache: "no-cache",
1221
- // Don't cache this request
1222
- credentials: "omit",
1223
- // Don't send cookies since we're using token auth
1224
- headers: {
1225
- "Content-Type": "application/json",
1226
- Authorization: `Bearer ${idToken}`
1227
- },
1228
- redirect: "follow",
1229
- referrerPolicy: "no-referrer",
1230
- body: JSON.stringify(requestData)
1231
- });
867
+ return null;
868
+ }
869
+ if (metadata.accessLevel === newAccessLevel) {
1232
870
  console.log(
1233
- `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
871
+ `[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
1234
872
  );
1235
- if (!response.ok) {
1236
- const errorText = await response.text();
1237
- console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
1238
- throw new Error(
1239
- `Failed to get available booking slots: ${response.status} ${response.statusText} - ${errorText}`
873
+ const metadataDocRef = (0, import_firestore2.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
874
+ try {
875
+ await (0, import_firestore2.updateDoc)(metadataDocRef, { updatedAt: import_firestore.Timestamp.now() });
876
+ return { ...metadata, updatedAt: import_firestore.Timestamp.now() };
877
+ } catch (error) {
878
+ console.error(
879
+ `[MediaService] Error updating timestamp for media ID ${mediaId}:`,
880
+ error
1240
881
  );
882
+ throw error;
1241
883
  }
1242
- const result = await response.json();
1243
- console.log(`[APPOINTMENT_SERVICE] Response parsed successfully`, result);
1244
- if (!result.success) {
1245
- throw new Error(result.error || "Failed to get available booking slots");
1246
- }
1247
- const slots = result.availableSlots.map((slot) => ({
1248
- start: new Date(slot.start)
1249
- }));
1250
- console.log(`[APPOINTMENT_SERVICE] Found ${slots.length} available booking slots via HTTP`);
1251
- return slots;
1252
- } catch (error) {
1253
- console.error("[APPOINTMENT_SERVICE] Error getting available booking slots via HTTP:", error);
1254
- throw error;
1255
884
  }
1256
- }
1257
- /**
1258
- * Creates an appointment via the Cloud Function orchestrateAppointmentCreation
1259
- *
1260
- * @param data - CreateAppointmentData object
1261
- * @returns The created appointment
1262
- */
1263
- async createAppointmentHttp(data) {
885
+ const oldStoragePath = metadata.path;
886
+ const fileNamePart = `${metadata.id}-${metadata.name}`;
887
+ const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
888
+ console.log(
889
+ `[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
890
+ );
891
+ const oldStorageFileRef = (0, import_storage2.ref)(this.storage, oldStoragePath);
892
+ const newStorageFileRef = (0, import_storage2.ref)(this.storage, newStoragePath);
1264
893
  try {
1265
- console.log("[APPOINTMENT_SERVICE] Creating appointment via cloud function");
1266
- const currentUser = this.auth.currentUser;
1267
- if (!currentUser) {
1268
- throw new Error("User must be authenticated to create an appointment");
1269
- }
1270
- const idToken = await currentUser.getIdToken();
1271
- const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
1272
- const requestData = {
1273
- patientId: data.patientId,
1274
- procedureId: data.procedureId,
1275
- appointmentStartTime: data.appointmentStartTime.toMillis ? data.appointmentStartTime.toMillis() : new Date(data.appointmentStartTime).getTime(),
1276
- appointmentEndTime: data.appointmentEndTime.toMillis ? data.appointmentEndTime.toMillis() : new Date(data.appointmentEndTime).getTime(),
1277
- patientNotes: (data == null ? void 0 : data.patientNotes) || null
1278
- };
1279
- console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
1280
- const response = await fetch(functionUrl, {
1281
- method: "POST",
1282
- mode: "cors",
1283
- cache: "no-cache",
1284
- credentials: "omit",
1285
- headers: {
1286
- "Content-Type": "application/json",
1287
- Authorization: `Bearer ${idToken}`
1288
- },
1289
- redirect: "follow",
1290
- referrerPolicy: "no-referrer",
1291
- body: JSON.stringify(requestData)
894
+ console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
895
+ const fileBytes = await (0, import_storage2.getBytes)(oldStorageFileRef);
896
+ console.log(
897
+ `[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
898
+ );
899
+ console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
900
+ await (0, import_storage2.uploadBytes)(newStorageFileRef, fileBytes, {
901
+ contentType: metadata.contentType
1292
902
  });
1293
903
  console.log(
1294
- `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
904
+ `[MediaService] Successfully uploaded bytes to ${newStoragePath}`
1295
905
  );
1296
- if (!response.ok) {
1297
- const errorText = await response.text();
1298
- console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
1299
- throw new Error(
1300
- `Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
906
+ const newDownloadURL = await (0, import_storage2.getDownloadURL)(newStorageFileRef);
907
+ console.log(
908
+ `[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
909
+ );
910
+ const updateData = {
911
+ accessLevel: newAccessLevel,
912
+ path: newStoragePath,
913
+ url: newDownloadURL,
914
+ updatedAt: import_firestore.Timestamp.now()
915
+ };
916
+ const metadataDocRef = (0, import_firestore2.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
917
+ console.log(
918
+ `[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
919
+ updateData
920
+ );
921
+ await (0, import_firestore2.updateDoc)(metadataDocRef, updateData);
922
+ console.log(
923
+ `[MediaService] Successfully updated Firestore metadata for ${mediaId}`
924
+ );
925
+ try {
926
+ console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
927
+ await (0, import_storage2.deleteObject)(oldStorageFileRef);
928
+ console.log(
929
+ `[MediaService] Successfully deleted old file from ${oldStoragePath}`
930
+ );
931
+ } catch (deleteError) {
932
+ console.error(
933
+ `[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
934
+ deleteError
1301
935
  );
1302
936
  }
1303
- const result = await response.json();
1304
- if (!result.success) {
1305
- throw new Error(result.error || "Failed to create appointment");
1306
- }
1307
- if (result.appointmentData) {
1308
- console.log(`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`);
1309
- return result.appointmentData;
1310
- }
1311
- const createdAppointment = await this.getAppointmentById(result.appointmentId);
1312
- if (!createdAppointment) {
1313
- throw new Error(`Failed to retrieve created appointment with ID: ${result.appointmentId}`);
1314
- }
1315
- return createdAppointment;
937
+ return { ...metadata, ...updateData };
1316
938
  } catch (error) {
1317
- console.error("[APPOINTMENT_SERVICE] Error creating appointment via cloud function:", error);
1318
- throw error;
1319
- }
1320
- }
1321
- /**
1322
- * Gets an appointment by ID.
1323
- *
1324
- * @param appointmentId ID of the appointment to retrieve
1325
- * @returns The appointment or null if not found
1326
- */
1327
- async getAppointmentById(appointmentId) {
1328
- try {
1329
- console.log(`[APPOINTMENT_SERVICE] Getting appointment with ID: ${appointmentId}`);
1330
- const appointment = await getAppointmentByIdUtil(this.db, appointmentId);
1331
- console.log(
1332
- `[APPOINTMENT_SERVICE] Appointment ${appointmentId} ${appointment ? "found" : "not found"}`
939
+ console.error(
940
+ `[MediaService] Error updating media access level and moving file for ${mediaId}:`,
941
+ error
1333
942
  );
1334
- return appointment;
1335
- } catch (error) {
1336
- console.error(`[APPOINTMENT_SERVICE] Error getting appointment ${appointmentId}:`, error);
943
+ if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
944
+ console.warn(
945
+ `[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
946
+ );
947
+ try {
948
+ await (0, import_storage2.deleteObject)(newStorageFileRef);
949
+ console.warn(
950
+ `[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
951
+ );
952
+ } catch (cleanupError) {
953
+ console.error(
954
+ `[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
955
+ cleanupError
956
+ );
957
+ }
958
+ }
1337
959
  throw error;
1338
960
  }
1339
961
  }
1340
962
  /**
1341
- * Updates an existing appointment.
1342
- *
1343
- * @param appointmentId ID of the appointment to update
1344
- * @param data Update data for the appointment
1345
- * @returns The updated appointment
1346
- */
1347
- async updateAppointment(appointmentId, data) {
1348
- try {
1349
- console.log(`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`);
1350
- const validatedData = await updateAppointmentSchema.parseAsync(data);
1351
- const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
1352
- console.log(`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`);
1353
- return updatedAppointment;
1354
- } catch (error) {
1355
- console.error(`[APPOINTMENT_SERVICE] Error updating appointment ${appointmentId}:`, error);
1356
- throw error;
1357
- }
1358
- }
1359
- /**
1360
- * Searches for appointments based on various criteria.
1361
- *
1362
- * @param params Search parameters
1363
- * @returns Found appointments and the last document for pagination
1364
- */
1365
- async searchAppointments(params) {
1366
- try {
1367
- console.log("[APPOINTMENT_SERVICE] Searching appointments with params:", params);
1368
- await searchAppointmentsSchema.parseAsync(params);
1369
- const result = await searchAppointmentsUtil(this.db, params);
1370
- console.log(`[APPOINTMENT_SERVICE] Found ${result.appointments.length} appointments`);
1371
- return result;
1372
- } catch (error) {
1373
- console.error("[APPOINTMENT_SERVICE] Error searching appointments:", error);
1374
- throw error;
1375
- }
1376
- }
1377
- /**
1378
- * Gets appointments for a specific patient.
1379
- *
1380
- * @param patientId ID of the patient
1381
- * @param options Optional parameters for filtering and pagination
1382
- * @returns Found appointments and the last document for pagination
1383
- */
1384
- async getPatientAppointments(patientId, options) {
1385
- console.log(`[APPOINTMENT_SERVICE] Getting appointments for patient: ${patientId}`);
1386
- const searchParams = {
1387
- patientId,
1388
- startDate: options == null ? void 0 : options.startDate,
1389
- endDate: options == null ? void 0 : options.endDate,
1390
- status: options == null ? void 0 : options.status,
1391
- limit: options == null ? void 0 : options.limit,
1392
- startAfter: options == null ? void 0 : options.startAfter
1393
- };
1394
- return this.searchAppointments(searchParams);
1395
- }
1396
- /**
1397
- * Gets appointments for a specific practitioner.
1398
- *
1399
- * @param practitionerId ID of the practitioner
1400
- * @param options Optional parameters for filtering and pagination
1401
- * @returns Found appointments and the last document for pagination
1402
- */
1403
- async getPractitionerAppointments(practitionerId, options) {
1404
- console.log(`[APPOINTMENT_SERVICE] Getting appointments for practitioner: ${practitionerId}`);
1405
- const searchParams = {
1406
- practitionerId,
1407
- startDate: options == null ? void 0 : options.startDate,
1408
- endDate: options == null ? void 0 : options.endDate,
1409
- status: options == null ? void 0 : options.status,
1410
- limit: options == null ? void 0 : options.limit,
1411
- startAfter: options == null ? void 0 : options.startAfter
1412
- };
1413
- return this.searchAppointments(searchParams);
1414
- }
1415
- /**
1416
- * Gets appointments for a specific clinic.
1417
- *
1418
- * @param clinicBranchId ID of the clinic branch
1419
- * @param options Optional parameters for filtering and pagination
1420
- * @returns Found appointments and the last document for pagination
1421
- */
1422
- async getClinicAppointments(clinicBranchId, options) {
1423
- console.log(`[APPOINTMENT_SERVICE] Getting appointments for clinic: ${clinicBranchId}`);
1424
- const searchParams = {
1425
- clinicBranchId,
1426
- practitionerId: options == null ? void 0 : options.practitionerId,
1427
- startDate: options == null ? void 0 : options.startDate,
1428
- endDate: options == null ? void 0 : options.endDate,
1429
- status: options == null ? void 0 : options.status,
1430
- limit: options == null ? void 0 : options.limit,
1431
- startAfter: options == null ? void 0 : options.startAfter
1432
- };
1433
- return this.searchAppointments(searchParams);
1434
- }
1435
- /**
1436
- * Updates the status of an appointment.
1437
- *
1438
- * @param appointmentId ID of the appointment
1439
- * @param newStatus New status to set
1440
- * @param details Optional details for the status change
1441
- * @returns The updated appointment
1442
- */
1443
- async updateAppointmentStatus(appointmentId, newStatus, details) {
1444
- console.log(
1445
- `[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
1446
- );
1447
- const updateData = {
1448
- status: newStatus,
1449
- updatedAt: (0, import_firestore2.serverTimestamp)()
1450
- };
1451
- if (newStatus === "canceled_clinic" /* CANCELED_CLINIC */ || newStatus === "canceled_patient" /* CANCELED_PATIENT */ || newStatus === "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */) {
1452
- if (!(details == null ? void 0 : details.cancellationReason)) {
1453
- throw new Error("Cancellation reason is required when canceling.");
1454
- }
1455
- if (!(details == null ? void 0 : details.canceledBy)) {
1456
- throw new Error("Canceled by is required when canceling.");
1457
- }
1458
- updateData.cancellationReason = details.cancellationReason;
1459
- updateData.canceledBy = details.canceledBy;
1460
- updateData.cancellationTime = import_firestore2.Timestamp.now();
1461
- }
1462
- if (newStatus === "confirmed" /* CONFIRMED */) {
1463
- updateData.confirmationTime = import_firestore2.Timestamp.now();
1464
- }
1465
- if (newStatus === "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1466
- updateData.rescheduleTime = import_firestore2.Timestamp.now();
1467
- }
1468
- return this.updateAppointment(appointmentId, updateData);
1469
- }
1470
- /**
1471
- * Confirms a PENDING appointment by an Admin/Clinic.
1472
- */
1473
- async confirmAppointmentAdmin(appointmentId) {
1474
- console.log(`[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`);
1475
- const appointment = await this.getAppointmentById(appointmentId);
1476
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1477
- if (appointment.status !== "pending" /* PENDING */) {
1478
- throw new Error(`Appointment ${appointmentId} is not in PENDING state to be confirmed.`);
1479
- }
1480
- return this.updateAppointmentStatus(appointmentId, "confirmed" /* CONFIRMED */);
1481
- }
1482
- /**
1483
- * Cancels an appointment by the User (Patient).
1484
- */
1485
- async cancelAppointmentUser(appointmentId, reason) {
1486
- console.log(`[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`);
1487
- return this.updateAppointmentStatus(appointmentId, "canceled_patient" /* CANCELED_PATIENT */, {
1488
- cancellationReason: reason,
1489
- canceledBy: "patient"
1490
- });
1491
- }
1492
- /**
1493
- * Cancels an appointment by an Admin/Clinic.
1494
- */
1495
- async cancelAppointmentAdmin(appointmentId, reason) {
1496
- console.log(`[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`);
1497
- return this.updateAppointmentStatus(appointmentId, "canceled_clinic" /* CANCELED_CLINIC */, {
1498
- cancellationReason: reason,
1499
- canceledBy: "clinic"
1500
- });
1501
- }
1502
- /**
1503
- * Admin proposes to reschedule an appointment.
1504
- * Sets status to RESCHEDULED_BY_CLINIC and updates times.
1505
- */
1506
- async rescheduleAppointmentAdmin(params) {
1507
- console.log(`[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${params.appointmentId}`);
1508
- const validatedParams = await rescheduleAppointmentSchema.parseAsync(params);
1509
- const startTimestamp = this.convertToTimestamp(validatedParams.newStartTime);
1510
- const endTimestamp = this.convertToTimestamp(validatedParams.newEndTime);
1511
- if (endTimestamp.toMillis() <= startTimestamp.toMillis()) {
1512
- throw new Error("New end time must be after new start time.");
1513
- }
1514
- const updateData = {
1515
- status: "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */,
1516
- appointmentStartTime: startTimestamp,
1517
- appointmentEndTime: endTimestamp,
1518
- rescheduleTime: import_firestore2.Timestamp.now(),
1519
- confirmationTime: null,
1520
- updatedAt: (0, import_firestore2.serverTimestamp)()
1521
- };
1522
- return this.updateAppointment(validatedParams.appointmentId, updateData);
1523
- }
1524
- /**
1525
- * Helper method to convert various timestamp formats to Firestore Timestamp
1526
- * @param value - Any timestamp format (Timestamp, number, string, Date, serialized Timestamp)
1527
- * @returns Firestore Timestamp object
963
+ * List all media for an owner, optionally filtered by collection and access level.
964
+ * @param ownerId - ID of the owner.
965
+ * @param collectionName - Optional: Filter by collection name.
966
+ * @param accessLevel - Optional: Filter by access level.
967
+ * @param count - Optional: Number of items to fetch.
968
+ * @param startAfterId - Optional: ID of the document to start after (for pagination).
1528
969
  */
1529
- convertToTimestamp(value) {
1530
- console.log(`[APPOINTMENT_SERVICE] Converting timestamp:`, {
1531
- value,
1532
- type: typeof value
1533
- });
1534
- if (value && typeof value.toMillis === "function") {
1535
- return value;
1536
- }
1537
- if (typeof value === "number") {
1538
- return import_firestore2.Timestamp.fromMillis(value);
970
+ async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
971
+ console.log(`[MediaService] Listing media for owner: ${ownerId}`);
972
+ let qConstraints = [(0, import_firestore2.where)("ownerId", "==", ownerId)];
973
+ if (collectionName) {
974
+ qConstraints.push((0, import_firestore2.where)("collectionName", "==", collectionName));
1539
975
  }
1540
- if (typeof value === "string") {
1541
- return import_firestore2.Timestamp.fromDate(new Date(value));
976
+ if (accessLevel) {
977
+ qConstraints.push((0, import_firestore2.where)("accessLevel", "==", accessLevel));
1542
978
  }
1543
- if (value instanceof Date) {
1544
- return import_firestore2.Timestamp.fromDate(value);
979
+ qConstraints.push((0, import_firestore2.orderBy)("createdAt", "desc"));
980
+ if (count) {
981
+ qConstraints.push((0, import_firestore2.limit)(count));
1545
982
  }
1546
- if (value && typeof value._seconds === "number") {
1547
- return new import_firestore2.Timestamp(value._seconds, value._nanoseconds || 0);
983
+ if (startAfterId) {
984
+ const startAfterDoc = await this.getMediaMetadata(startAfterId);
985
+ if (startAfterDoc) {
986
+ }
1548
987
  }
1549
- if (value && typeof value.seconds === "number") {
1550
- return new import_firestore2.Timestamp(value.seconds, value.nanoseconds || 0);
988
+ const finalQuery = (0, import_firestore2.query)(
989
+ (0, import_firestore2.collection)(this.db, MEDIA_METADATA_COLLECTION),
990
+ ...qConstraints
991
+ );
992
+ try {
993
+ const querySnapshot = await (0, import_firestore2.getDocs)(finalQuery);
994
+ const mediaList = querySnapshot.docs.map(
995
+ (doc38) => doc38.data()
996
+ );
997
+ console.log(`[MediaService] Found ${mediaList.length} media items.`);
998
+ return mediaList;
999
+ } catch (error) {
1000
+ console.error("[MediaService] Error listing media:", error);
1001
+ throw error;
1551
1002
  }
1552
- throw new Error(`Invalid timestamp format: ${typeof value}, value: ${JSON.stringify(value)}`);
1553
1003
  }
1554
1004
  /**
1555
- * User confirms a reschedule proposed by the clinic.
1556
- * Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
1005
+ * Get download URL for media. (Convenience, as URL is in metadata)
1006
+ * @param mediaId - ID of the media.
1557
1007
  */
1558
- async rescheduleAppointmentConfirmUser(appointmentId) {
1559
- console.log(`[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`);
1560
- const appointment = await this.getAppointmentById(appointmentId);
1561
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1562
- if (appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1563
- throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
1008
+ async getMediaDownloadUrl(mediaId) {
1009
+ console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
1010
+ const metadata = await this.getMediaMetadata(mediaId);
1011
+ if (metadata && metadata.url) {
1012
+ console.log(`[MediaService] URL found: ${metadata.url}`);
1013
+ return metadata.url;
1564
1014
  }
1565
- return this.updateAppointmentStatus(appointmentId, "confirmed" /* CONFIRMED */);
1015
+ console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
1016
+ return null;
1566
1017
  }
1567
- /**
1568
- * User rejects a reschedule proposed by the clinic.
1569
- * Status changes from RESCHEDULED_BY_CLINIC to CANCELED_PATIENT_RESCHEDULED.
1570
- */
1571
- async rescheduleAppointmentRejectUser(appointmentId, reason) {
1572
- console.log(`[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`);
1573
- const appointment = await this.getAppointmentById(appointmentId);
1574
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1575
- if (appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1576
- throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
1577
- }
1578
- return this.updateAppointmentStatus(
1579
- appointmentId,
1580
- "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */,
1581
- {
1582
- cancellationReason: reason,
1583
- canceledBy: "patient"
1584
- }
1585
- );
1586
- }
1587
- /**
1588
- * Admin checks in a patient for their appointment.
1589
- * Requires all pending user forms to be completed.
1590
- */
1591
- async checkInPatientAdmin(appointmentId) {
1592
- console.log(`[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`);
1593
- const appointment = await this.getAppointmentById(appointmentId);
1594
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1595
- if (appointment.pendingUserFormsIds && appointment.pendingUserFormsIds.length > 0) {
1596
- throw new Error(
1597
- `Cannot check in: Patient has ${appointment.pendingUserFormsIds.length} pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(", ")}`
1598
- );
1599
- }
1600
- if (appointment.status !== "confirmed" /* CONFIRMED */ && appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1601
- console.warn(
1602
- `Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`
1603
- );
1604
- }
1605
- return this.updateAppointmentStatus(appointmentId, "checked_in" /* CHECKED_IN */);
1606
- }
1607
- /**
1608
- * Doctor starts the appointment procedure.
1609
- */
1610
- async startAppointmentDoctor(appointmentId) {
1611
- console.log(`[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`);
1612
- const appointment = await this.getAppointmentById(appointmentId);
1613
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1614
- if (appointment.status !== "checked_in" /* CHECKED_IN */) {
1615
- throw new Error(`Appointment ${appointmentId} must be CHECKED_IN to start.`);
1616
- }
1617
- const updateData = {
1618
- status: "in_progress" /* IN_PROGRESS */,
1619
- procedureActualStartTime: import_firestore2.Timestamp.now(),
1620
- // Set actual start time
1621
- updatedAt: (0, import_firestore2.serverTimestamp)()
1622
- };
1623
- return this.updateAppointment(appointmentId, updateData);
1624
- }
1625
- /**
1626
- * Doctor completes and finalizes the appointment.
1627
- */
1628
- async completeAppointmentDoctor(appointmentId, finalizationNotes, actualDurationMinutesInput) {
1629
- console.log(`[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`);
1630
- const currentUser = this.auth.currentUser;
1631
- if (!currentUser) throw new Error("Authentication required to complete appointment.");
1632
- const appointment = await this.getAppointmentById(appointmentId);
1633
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1634
- let calculatedDurationMinutes = actualDurationMinutesInput;
1635
- const procedureCompletionTime = import_firestore2.Timestamp.now();
1636
- if (calculatedDurationMinutes === void 0 && appointment.procedureActualStartTime) {
1637
- const startTimeMillis = appointment.procedureActualStartTime.toMillis();
1638
- const endTimeMillis = procedureCompletionTime.toMillis();
1639
- if (endTimeMillis > startTimeMillis) {
1640
- calculatedDurationMinutes = Math.round((endTimeMillis - startTimeMillis) / 6e4);
1641
- }
1642
- }
1643
- const updateData = {
1644
- status: "completed" /* COMPLETED */,
1645
- actualDurationMinutes: calculatedDurationMinutes,
1646
- // Use calculated or provided duration
1647
- finalizedDetails: {
1648
- by: currentUser.uid,
1649
- // This is used ID, not practitioner's profile ID (just so we know who completed the appointment)
1650
- at: procedureCompletionTime,
1651
- // Use consistent completion timestamp
1652
- notes: finalizationNotes
1653
- },
1654
- // Optionally update appointmentEndTime to the actual completion time
1655
- // appointmentEndTime: procedureCompletionTime,
1656
- updatedAt: (0, import_firestore2.serverTimestamp)()
1657
- };
1658
- return this.updateAppointment(appointmentId, updateData);
1659
- }
1660
- /**
1661
- * Admin marks an appointment as No-Show.
1662
- */
1663
- async markNoShowAdmin(appointmentId) {
1664
- console.log(`[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`);
1665
- const appointment = await this.getAppointmentById(appointmentId);
1666
- if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1667
- if (import_firestore2.Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()) {
1668
- throw new Error("Cannot mark no-show before appointment start time.");
1669
- }
1670
- return this.updateAppointmentStatus(appointmentId, "no_show" /* NO_SHOW */, {
1671
- cancellationReason: "Patient did not show up for the appointment.",
1672
- canceledBy: "clinic"
1673
- });
1674
- }
1675
- /**
1676
- * Adds a media item to an appointment.
1677
- */
1678
- async addMediaToAppointment(appointmentId, mediaItemData) {
1679
- console.log(`[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`);
1680
- const currentUser = this.auth.currentUser;
1681
- if (!currentUser) throw new Error("Authentication required.");
1682
- const newMediaItem = {
1683
- ...mediaItemData,
1684
- id: this.generateId(),
1685
- uploadedAt: import_firestore2.Timestamp.now(),
1686
- uploadedBy: currentUser.uid
1687
- };
1688
- const updateData = {
1689
- media: (0, import_firestore2.arrayUnion)(newMediaItem),
1690
- updatedAt: (0, import_firestore2.serverTimestamp)()
1691
- };
1692
- return this.updateAppointment(appointmentId, updateData);
1693
- }
1694
- /**
1695
- * Removes a media item from an appointment.
1696
- */
1697
- async removeMediaFromAppointment(appointmentId, mediaItemId) {
1698
- console.log(
1699
- `[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`
1700
- );
1701
- const appointment = await this.getAppointmentById(appointmentId);
1702
- if (!appointment || !appointment.media) {
1703
- throw new Error("Appointment or media list not found.");
1704
- }
1705
- const mediaToRemove = appointment.media.find((m) => m.id === mediaItemId);
1706
- if (!mediaToRemove) {
1707
- throw new Error(`Media item ${mediaItemId} not found in appointment.`);
1708
- }
1709
- const updateData = {
1710
- media: (0, import_firestore2.arrayRemove)(mediaToRemove),
1711
- updatedAt: (0, import_firestore2.serverTimestamp)()
1712
- };
1713
- return this.updateAppointment(appointmentId, updateData);
1714
- }
1715
- /**
1716
- * Adds or updates review information for an appointment.
1717
- */
1718
- async addReviewToAppointment(appointmentId, reviewData) {
1719
- console.log(`[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`);
1720
- const newReviewInfo = {
1721
- ...reviewData,
1722
- reviewId: this.generateId(),
1723
- reviewedAt: import_firestore2.Timestamp.now()
1724
- };
1725
- const updateData = {
1726
- reviewInfo: newReviewInfo,
1727
- updatedAt: (0, import_firestore2.serverTimestamp)()
1728
- };
1729
- return this.updateAppointment(appointmentId, updateData);
1730
- }
1731
- /**
1732
- * Updates the payment status of an appointment.
1733
- */
1734
- async updatePaymentStatus(appointmentId, paymentStatus, paymentTransactionId) {
1735
- console.log(
1736
- `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
1737
- );
1738
- const updateData = {
1739
- paymentStatus,
1740
- paymentTransactionId: paymentTransactionId || null,
1741
- updatedAt: (0, import_firestore2.serverTimestamp)()
1742
- };
1743
- return this.updateAppointment(appointmentId, updateData);
1744
- }
1745
- /**
1746
- * Updates the internal notes of an appointment.
1747
- *
1748
- * @param appointmentId ID of the appointment
1749
- * @param notes Updated internal notes
1750
- * @returns The updated appointment
1751
- */
1752
- async updateInternalNotes(appointmentId, notes) {
1753
- console.log(`[APPOINTMENT_SERVICE] Updating internal notes for appointment: ${appointmentId}`);
1754
- const updateData = {
1755
- internalNotes: notes
1756
- };
1757
- return this.updateAppointment(appointmentId, updateData);
1758
- }
1759
- /**
1760
- * Gets upcoming appointments for a specific patient.
1761
- * These include appointments with statuses: PENDING, CONFIRMED, CHECKED_IN, IN_PROGRESS
1762
- *
1763
- * @param patientId ID of the patient
1764
- * @param options Optional parameters for filtering and pagination
1765
- * @returns Found appointments and the last document for pagination
1766
- */
1767
- async getUpcomingPatientAppointments(patientId, options) {
1768
- try {
1769
- console.log(`[APPOINTMENT_SERVICE] Getting upcoming appointments for patient: ${patientId}`);
1770
- const effectiveStartDate = (options == null ? void 0 : options.startDate) || /* @__PURE__ */ new Date();
1771
- const upcomingStatuses = [
1772
- "pending" /* PENDING */,
1773
- "confirmed" /* CONFIRMED */,
1774
- "checked_in" /* CHECKED_IN */,
1775
- "in_progress" /* IN_PROGRESS */,
1776
- "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */
1777
- ];
1778
- const constraints = [];
1779
- constraints.push((0, import_firestore2.where)("patientId", "==", patientId));
1780
- constraints.push((0, import_firestore2.where)("status", "in", upcomingStatuses));
1781
- constraints.push((0, import_firestore2.where)("appointmentStartTime", ">=", import_firestore2.Timestamp.fromDate(effectiveStartDate)));
1782
- if (options == null ? void 0 : options.endDate) {
1783
- constraints.push((0, import_firestore2.where)("appointmentStartTime", "<=", import_firestore2.Timestamp.fromDate(options.endDate)));
1784
- }
1785
- constraints.push((0, import_firestore2.orderBy)("appointmentStartTime", "asc"));
1786
- if (options == null ? void 0 : options.limit) {
1787
- constraints.push((0, import_firestore2.limit)(options.limit));
1788
- }
1789
- if (options == null ? void 0 : options.startAfter) {
1790
- constraints.push((0, import_firestore2.startAfter)(options.startAfter));
1791
- }
1792
- const q = (0, import_firestore2.query)((0, import_firestore2.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
1793
- const querySnapshot = await (0, import_firestore2.getDocs)(q);
1794
- const appointments = querySnapshot.docs.map((doc38) => doc38.data());
1795
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1796
- console.log(
1797
- `[APPOINTMENT_SERVICE] Found ${appointments.length} upcoming appointments for patient ${patientId}`
1798
- );
1799
- return { appointments, lastDoc };
1800
- } catch (error) {
1801
- console.error(
1802
- `[APPOINTMENT_SERVICE] Error getting upcoming appointments for patient ${patientId}:`,
1803
- error
1804
- );
1805
- throw error;
1806
- }
1807
- }
1808
- /**
1809
- * Gets past appointments for a specific patient.
1810
- * These include appointments with statuses: COMPLETED, CANCELED_PATIENT,
1811
- * CANCELED_PATIENT_RESCHEDULED, CANCELED_CLINIC, NO_SHOW
1812
- *
1813
- * @param patientId ID of the patient
1814
- * @param options Optional parameters for filtering and pagination
1815
- * @returns Found appointments and the last document for pagination
1816
- */
1817
- async getPastPatientAppointments(patientId, options) {
1818
- try {
1819
- console.log(`[APPOINTMENT_SERVICE] Getting past appointments for patient: ${patientId}`);
1820
- const effectiveEndDate = (options == null ? void 0 : options.endDate) || /* @__PURE__ */ new Date();
1821
- const pastStatuses = ["completed" /* COMPLETED */];
1822
- if (options == null ? void 0 : options.showCanceled) {
1823
- pastStatuses.push(
1824
- "canceled_patient" /* CANCELED_PATIENT */,
1825
- "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */,
1826
- "canceled_clinic" /* CANCELED_CLINIC */
1827
- );
1828
- }
1829
- if (options == null ? void 0 : options.showNoShow) {
1830
- pastStatuses.push("no_show" /* NO_SHOW */);
1831
- }
1832
- const constraints = [];
1833
- constraints.push((0, import_firestore2.where)("patientId", "==", patientId));
1834
- constraints.push((0, import_firestore2.where)("status", "in", pastStatuses));
1835
- if (options == null ? void 0 : options.startDate) {
1836
- constraints.push(
1837
- (0, import_firestore2.where)("appointmentStartTime", ">=", import_firestore2.Timestamp.fromDate(options.startDate))
1838
- );
1839
- }
1840
- constraints.push((0, import_firestore2.where)("appointmentStartTime", "<=", import_firestore2.Timestamp.fromDate(effectiveEndDate)));
1841
- constraints.push((0, import_firestore2.orderBy)("appointmentStartTime", "desc"));
1842
- if (options == null ? void 0 : options.limit) {
1843
- constraints.push((0, import_firestore2.limit)(options.limit));
1844
- }
1845
- if (options == null ? void 0 : options.startAfter) {
1846
- constraints.push((0, import_firestore2.startAfter)(options.startAfter));
1847
- }
1848
- const q = (0, import_firestore2.query)((0, import_firestore2.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
1849
- const querySnapshot = await (0, import_firestore2.getDocs)(q);
1850
- const appointments = querySnapshot.docs.map((doc38) => doc38.data());
1851
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1852
- console.log(
1853
- `[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}`
1854
- );
1855
- return { appointments, lastDoc };
1856
- } catch (error) {
1857
- console.error(
1858
- `[APPOINTMENT_SERVICE] Error getting past appointments for patient ${patientId}:`,
1859
- error
1860
- );
1861
- throw error;
1862
- }
1863
- }
1864
- /**
1865
- * Counts completed appointments for a patient with optional clinic filtering.
1866
- *
1867
- * @param patientId ID of the patient.
1868
- * @param clinicBranchId Optional ID of the clinic branch to either include or exclude.
1869
- * @param excludeClinic Optional boolean. If true (default), excludes the specified clinic. If false, includes only that clinic.
1870
- * @returns The count of completed appointments.
1871
- */
1872
- async countCompletedAppointments(patientId, clinicBranchId, excludeClinic = true) {
1873
- try {
1874
- console.log(
1875
- `[APPOINTMENT_SERVICE] Counting completed appointments for patient: ${patientId}`,
1876
- { clinicBranchId, excludeClinic }
1877
- );
1878
- const constraints = [
1879
- (0, import_firestore2.where)("patientId", "==", patientId),
1880
- (0, import_firestore2.where)("status", "==", "completed" /* COMPLETED */)
1881
- ];
1882
- if (clinicBranchId) {
1883
- if (excludeClinic) {
1884
- constraints.push((0, import_firestore2.where)("clinicBranchId", "!=", clinicBranchId));
1885
- } else {
1886
- constraints.push((0, import_firestore2.where)("clinicBranchId", "==", clinicBranchId));
1887
- }
1888
- }
1889
- const q = (0, import_firestore2.query)((0, import_firestore2.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
1890
- const snapshot = await (0, import_firestore2.getCountFromServer)(q);
1891
- const count = snapshot.data().count;
1892
- console.log(
1893
- `[APPOINTMENT_SERVICE] Found ${count} completed appointments for patient ${patientId}`
1894
- );
1895
- return count;
1896
- } catch (error) {
1897
- console.error(
1898
- `[APPOINTMENT_SERVICE] Error counting completed appointments for patient ${patientId}:`,
1899
- error
1900
- );
1901
- throw error;
1902
- }
1903
- }
1904
- };
1905
-
1906
- // src/services/auth/auth.service.ts
1907
- var import_auth7 = require("firebase/auth");
1908
- var import_firestore29 = require("firebase/firestore");
1909
-
1910
- // src/types/user/index.ts
1911
- var UserRole = /* @__PURE__ */ ((UserRole2) => {
1912
- UserRole2["PATIENT"] = "patient";
1913
- UserRole2["PRACTITIONER"] = "practitioner";
1914
- UserRole2["APP_ADMIN"] = "app_admin";
1915
- UserRole2["CLINIC_ADMIN"] = "clinic_admin";
1916
- return UserRole2;
1917
- })(UserRole || {});
1918
- var USERS_COLLECTION = "users";
1919
-
1920
- // src/types/calendar/synced-calendar.types.ts
1921
- var SyncedCalendarProvider = /* @__PURE__ */ ((SyncedCalendarProvider3) => {
1922
- SyncedCalendarProvider3["GOOGLE"] = "google";
1923
- SyncedCalendarProvider3["OUTLOOK"] = "outlook";
1924
- SyncedCalendarProvider3["APPLE"] = "apple";
1925
- return SyncedCalendarProvider3;
1926
- })(SyncedCalendarProvider || {});
1927
- var SYNCED_CALENDARS_COLLECTION = "syncedCalendars";
1928
-
1929
- // src/types/notifications/index.ts
1930
- var NotificationType = /* @__PURE__ */ ((NotificationType3) => {
1931
- NotificationType3["APPOINTMENT_REMINDER"] = "appointmentReminder";
1932
- NotificationType3["APPOINTMENT_STATUS_CHANGE"] = "appointmentStatusChange";
1933
- NotificationType3["APPOINTMENT_RESCHEDULED_PROPOSAL"] = "appointmentRescheduledProposal";
1934
- NotificationType3["APPOINTMENT_CANCELLED"] = "appointmentCancelled";
1935
- NotificationType3["PRE_REQUIREMENT_INSTRUCTION_DUE"] = "preRequirementInstructionDue";
1936
- NotificationType3["POST_REQUIREMENT_INSTRUCTION_DUE"] = "postRequirementInstructionDue";
1937
- NotificationType3["REQUIREMENT_INSTRUCTION_DUE"] = "requirementInstructionDue";
1938
- NotificationType3["FORM_REMINDER"] = "formReminder";
1939
- NotificationType3["FORM_SUBMISSION_CONFIRMATION"] = "formSubmissionConfirmation";
1940
- NotificationType3["REVIEW_REQUEST"] = "reviewRequest";
1941
- NotificationType3["PAYMENT_DUE"] = "paymentDue";
1942
- NotificationType3["PAYMENT_CONFIRMATION"] = "paymentConfirmation";
1943
- NotificationType3["PAYMENT_FAILED"] = "paymentFailed";
1944
- NotificationType3["GENERAL_MESSAGE"] = "generalMessage";
1945
- NotificationType3["ACCOUNT_NOTIFICATION"] = "accountNotification";
1946
- return NotificationType3;
1947
- })(NotificationType || {});
1948
- var NOTIFICATIONS_COLLECTION = "notifications";
1949
- var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
1950
- NotificationStatus2["PENDING"] = "pending";
1951
- NotificationStatus2["PROCESSING"] = "processing";
1952
- NotificationStatus2["SENT"] = "sent";
1953
- NotificationStatus2["FAILED"] = "failed";
1954
- NotificationStatus2["DELIVERED"] = "delivered";
1955
- NotificationStatus2["CANCELLED"] = "cancelled";
1956
- NotificationStatus2["PARTIAL_SUCCESS"] = "partialSuccess";
1957
- return NotificationStatus2;
1958
- })(NotificationStatus || {});
1959
-
1960
- // src/types/patient/allergies.ts
1961
- var AllergyType = /* @__PURE__ */ ((AllergyType2) => {
1962
- AllergyType2["MEDICATION"] = "medication";
1963
- AllergyType2["FOOD"] = "food";
1964
- AllergyType2["ENVIRONMENTAL"] = "environmental";
1965
- AllergyType2["LATEX"] = "latex";
1966
- AllergyType2["COSMETIC"] = "cosmetic";
1967
- AllergyType2["OTHER"] = "other";
1968
- return AllergyType2;
1969
- })(AllergyType || {});
1970
- var MedicationAllergySubtype = /* @__PURE__ */ ((MedicationAllergySubtype2) => {
1971
- MedicationAllergySubtype2["ANTIBIOTICS"] = "antibiotics";
1972
- MedicationAllergySubtype2["NSAIDS"] = "nsaids";
1973
- MedicationAllergySubtype2["OPIOIDS"] = "opioids";
1974
- MedicationAllergySubtype2["ANESTHETICS"] = "anesthetics";
1975
- MedicationAllergySubtype2["VACCINES"] = "vaccines";
1976
- MedicationAllergySubtype2["OTHER"] = "other";
1977
- return MedicationAllergySubtype2;
1978
- })(MedicationAllergySubtype || {});
1979
- var FoodAllergySubtype = /* @__PURE__ */ ((FoodAllergySubtype2) => {
1980
- FoodAllergySubtype2["NUTS"] = "nuts";
1981
- FoodAllergySubtype2["SHELLFISH"] = "shellfish";
1982
- FoodAllergySubtype2["DAIRY"] = "dairy";
1983
- FoodAllergySubtype2["EGGS"] = "eggs";
1984
- FoodAllergySubtype2["WHEAT"] = "wheat";
1985
- FoodAllergySubtype2["SOY"] = "soy";
1986
- FoodAllergySubtype2["FISH"] = "fish";
1987
- FoodAllergySubtype2["FRUITS"] = "fruits";
1988
- FoodAllergySubtype2["OTHER"] = "other";
1989
- return FoodAllergySubtype2;
1990
- })(FoodAllergySubtype || {});
1991
- var EnvironmentalAllergySubtype = /* @__PURE__ */ ((EnvironmentalAllergySubtype2) => {
1992
- EnvironmentalAllergySubtype2["POLLEN"] = "pollen";
1993
- EnvironmentalAllergySubtype2["DUST"] = "dust";
1994
- EnvironmentalAllergySubtype2["MOLD"] = "mold";
1995
- EnvironmentalAllergySubtype2["PET_DANDER"] = "pet_dander";
1996
- EnvironmentalAllergySubtype2["INSECTS"] = "insects";
1997
- EnvironmentalAllergySubtype2["OTHER"] = "other";
1998
- return EnvironmentalAllergySubtype2;
1999
- })(EnvironmentalAllergySubtype || {});
2000
- var CosmeticAllergySubtype = /* @__PURE__ */ ((CosmeticAllergySubtype2) => {
2001
- CosmeticAllergySubtype2["FRAGRANCES"] = "fragrances";
2002
- CosmeticAllergySubtype2["PRESERVATIVES"] = "preservatives";
2003
- CosmeticAllergySubtype2["DYES"] = "dyes";
2004
- CosmeticAllergySubtype2["METALS"] = "metals";
2005
- CosmeticAllergySubtype2["OTHER"] = "other";
2006
- return CosmeticAllergySubtype2;
2007
- })(CosmeticAllergySubtype || {});
2008
-
2009
- // src/types/patient/patient-requirements.ts
2010
- var PatientInstructionStatus = /* @__PURE__ */ ((PatientInstructionStatus2) => {
2011
- PatientInstructionStatus2["PENDING_NOTIFICATION"] = "pendingNotification";
2012
- PatientInstructionStatus2["ACTION_DUE"] = "actionDue";
2013
- PatientInstructionStatus2["ACTION_TAKEN"] = "actionTaken";
2014
- PatientInstructionStatus2["MISSED"] = "missed";
2015
- PatientInstructionStatus2["CANCELLED"] = "cancelled";
2016
- PatientInstructionStatus2["SKIPPED"] = "skipped";
2017
- return PatientInstructionStatus2;
2018
- })(PatientInstructionStatus || {});
2019
- var PatientRequirementOverallStatus = /* @__PURE__ */ ((PatientRequirementOverallStatus2) => {
2020
- PatientRequirementOverallStatus2["ACTIVE"] = "active";
2021
- PatientRequirementOverallStatus2["ALL_INSTRUCTIONS_MET"] = "allInstructionsMet";
2022
- PatientRequirementOverallStatus2["PARTIALLY_COMPLETED"] = "partiallyCompleted";
2023
- PatientRequirementOverallStatus2["FAILED"] = "failed";
2024
- PatientRequirementOverallStatus2["CANCELLED_APPOINTMENT"] = "cancelledAppointment";
2025
- PatientRequirementOverallStatus2["SUPERSEDED_RESCHEDULE"] = "supersededReschedule";
2026
- PatientRequirementOverallStatus2["FAILED_TO_PROCESS"] = "failedToProcess";
2027
- return PatientRequirementOverallStatus2;
2028
- })(PatientRequirementOverallStatus || {});
2029
- var PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME = "patientRequirements";
2030
-
2031
- // src/types/patient/token.types.ts
2032
- var INVITE_TOKENS_COLLECTION = "inviteTokens";
2033
- var PatientTokenStatus = /* @__PURE__ */ ((PatientTokenStatus2) => {
2034
- PatientTokenStatus2["ACTIVE"] = "active";
2035
- PatientTokenStatus2["USED"] = "used";
2036
- PatientTokenStatus2["EXPIRED"] = "expired";
2037
- return PatientTokenStatus2;
2038
- })(PatientTokenStatus || {});
2039
-
2040
- // src/types/reviews/index.ts
2041
- var REVIEWS_COLLECTION = "reviews";
2042
-
2043
- // src/services/auth/auth.service.ts
2044
- var import_zod23 = require("zod");
2045
-
2046
- // src/validations/schemas.ts
2047
- var import_zod4 = require("zod");
2048
- var emailSchema = import_zod4.z.string().email("Invalid email format").min(5, "Email must be at least 5 characters").max(255, "Email must be less than 255 characters");
2049
- var passwordSchema = import_zod4.z.string().min(8, "Password must be at least 8 characters").max(100, "Password must be less than 100 characters").regex(
2050
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/,
2051
- "Password must contain at least one uppercase letter, one lowercase letter, and one number"
2052
- );
2053
- var userRoleSchema = import_zod4.z.nativeEnum(UserRole);
2054
- var userRolesSchema = import_zod4.z.array(userRoleSchema).min(1, "User must have at least one role").max(3, "User cannot have more than 3 roles");
2055
- var clinicAdminOptionsSchema = import_zod4.z.object({
2056
- isGroupOwner: import_zod4.z.boolean(),
2057
- groupToken: import_zod4.z.string().optional(),
2058
- groupId: import_zod4.z.string().optional()
2059
- }).refine(
2060
- (data) => {
2061
- if (!data.isGroupOwner && (!data.groupToken || !data.groupId)) {
2062
- return false;
2063
- }
2064
- if (data.isGroupOwner && (data.groupToken || data.groupId)) {
2065
- return false;
2066
- }
2067
- return true;
2068
- },
2069
- {
2070
- message: "Invalid clinic admin options configuration"
2071
- }
2072
- );
2073
- var createUserOptionsSchema = import_zod4.z.object({
2074
- clinicAdminData: clinicAdminOptionsSchema.optional()
2075
- });
2076
- var userSchema = import_zod4.z.object({
2077
- uid: import_zod4.z.string(),
2078
- email: import_zod4.z.string().email().nullable(),
2079
- roles: import_zod4.z.array(userRoleSchema),
2080
- isAnonymous: import_zod4.z.boolean(),
2081
- createdAt: import_zod4.z.any(),
2082
- updatedAt: import_zod4.z.any(),
2083
- lastLoginAt: import_zod4.z.any(),
2084
- patientProfile: import_zod4.z.string().optional(),
2085
- practitionerProfile: import_zod4.z.string().optional(),
2086
- adminProfile: import_zod4.z.string().optional()
2087
- });
2088
-
2089
- // src/errors/auth.errors.ts
2090
- var AuthError = class extends Error {
2091
- constructor(message, code, status = 400) {
2092
- super(message);
2093
- this.code = code;
2094
- this.status = status;
2095
- this.name = "AuthError";
2096
- }
2097
- };
2098
- var AUTH_ERRORS = {
2099
- // Basic validation errors
2100
- INVALID_EMAIL: new AuthError(
2101
- "Email address is not in a valid format",
2102
- "AUTH/INVALID_EMAIL",
2103
- 400
2104
- ),
2105
- INVALID_PASSWORD: new AuthError(
2106
- "Password must contain at least 8 characters, one uppercase letter, one number, and one special character",
2107
- "AUTH/INVALID_PASSWORD",
2108
- 400
2109
- ),
2110
- INVALID_ROLE: new AuthError(
2111
- "Specified user role is not valid",
2112
- "AUTH/INVALID_ROLE",
2113
- 400
2114
- ),
2115
- // Authentication errors
2116
- NOT_AUTHENTICATED: new AuthError(
2117
- "User is not authenticated",
2118
- "AUTH/NOT_AUTHENTICATED",
2119
- 401
2120
- ),
2121
- SESSION_EXPIRED: new AuthError(
2122
- "Your session has expired. Please sign in again",
2123
- "AUTH/SESSION_EXPIRED",
2124
- 401
2125
- ),
2126
- INVALID_TOKEN: new AuthError(
2127
- "Invalid authentication token",
2128
- "AUTH/INVALID_TOKEN",
2129
- 401
2130
- ),
2131
- // User state errors
2132
- USER_NOT_FOUND: new AuthError(
2133
- "User not found in the system",
2134
- "AUTH/USER_NOT_FOUND",
2135
- 404
2136
- ),
2137
- EMAIL_ALREADY_EXISTS: new AuthError(
2138
- "An account with this email already exists",
2139
- "AUTH/EMAIL_EXISTS",
2140
- 409
2141
- ),
2142
- USER_DISABLED: new AuthError(
2143
- "This account has been disabled",
2144
- "AUTH/USER_DISABLED",
2145
- 403
2146
- ),
2147
- // Rate limiting and security
2148
- TOO_MANY_REQUESTS: new AuthError(
2149
- "Too many login attempts. Please try again later",
2150
- "AUTH/TOO_MANY_REQUESTS",
2151
- 429
2152
- ),
2153
- ACCOUNT_LOCKED: new AuthError(
2154
- "Account temporarily locked due to too many failed login attempts",
2155
- "AUTH/ACCOUNT_LOCKED",
2156
- 403
2157
- ),
2158
- // Social auth specific
2159
- POPUP_CLOSED: new AuthError(
2160
- "Authentication popup was closed before completion",
2161
- "AUTH/POPUP_CLOSED",
2162
- 400
2163
- ),
2164
- POPUP_BLOCKED: new AuthError(
2165
- "Authentication popup was blocked by the browser",
2166
- "AUTH/POPUP_BLOCKED",
2167
- 400
2168
- ),
2169
- ACCOUNT_EXISTS: new AuthError(
2170
- "An account already exists with different credentials",
2171
- "AUTH/ACCOUNT_EXISTS",
2172
- 409
2173
- ),
2174
- // Anonymous auth specific
2175
- NOT_ANONYMOUS: new AuthError(
2176
- "Current user is not anonymous",
2177
- "AUTH/NOT_ANONYMOUS_USER",
2178
- 400
2179
- ),
2180
- ANONYMOUS_UPGRADE_FAILED: new AuthError(
2181
- "Failed to upgrade anonymous account",
2182
- "AUTH/ANONYMOUS_UPGRADE_FAILED",
2183
- 400
2184
- ),
2185
- // General errors
2186
- VALIDATION_ERROR: new AuthError(
2187
- "Data validation error occurred",
2188
- "AUTH/VALIDATION_ERROR",
2189
- 400
2190
- ),
2191
- OPERATION_NOT_ALLOWED: new AuthError(
2192
- "This operation is not allowed",
2193
- "AUTH/OPERATION_NOT_ALLOWED",
2194
- 403
2195
- ),
2196
- NETWORK_ERROR: new AuthError(
2197
- "Network error occurred. Please check your connection",
2198
- "AUTH/NETWORK_ERROR",
2199
- 503
2200
- ),
2201
- REQUIRES_RECENT_LOGIN: new AuthError(
2202
- "This operation requires recent authentication. Please sign in again",
2203
- "AUTH/REQUIRES_RECENT_LOGIN",
2204
- 401
2205
- ),
2206
- INVALID_PROVIDER: new AuthError(
2207
- "Invalid authentication provider",
2208
- "AUTH/INVALID_PROVIDER",
2209
- 400
2210
- ),
2211
- INVALID_CREDENTIAL: new AuthError(
2212
- "The provided credentials are invalid or expired",
2213
- "AUTH/INVALID_CREDENTIAL",
2214
- 401
2215
- ),
2216
- // Resource not found
2217
- NOT_FOUND: new AuthError(
2218
- "The requested resource was not found",
2219
- "AUTH/NOT_FOUND",
2220
- 404
2221
- ),
2222
- // Detailed password validation errors
2223
- PASSWORD_LENGTH_ERROR: new AuthError(
2224
- "Password must be at least 8 characters long",
2225
- "AUTH/PASSWORD_LENGTH_ERROR",
2226
- 400
2227
- ),
2228
- PASSWORD_UPPERCASE_ERROR: new AuthError(
2229
- "Password must contain at least one uppercase letter",
2230
- "AUTH/PASSWORD_UPPERCASE_ERROR",
2231
- 400
2232
- ),
2233
- PASSWORD_NUMBER_ERROR: new AuthError(
2234
- "Password must contain at least one number",
2235
- "AUTH/PASSWORD_NUMBER_ERROR",
2236
- 400
2237
- ),
2238
- PASSWORD_SPECIAL_CHAR_ERROR: new AuthError(
2239
- "Password must contain at least one special character",
2240
- "AUTH/PASSWORD_SPECIAL_CHAR_ERROR",
2241
- 400
2242
- ),
2243
- // Detailed email validation errors
2244
- EMAIL_FORMAT_ERROR: new AuthError(
2245
- "Invalid email format. Please enter a valid email address",
2246
- "AUTH/EMAIL_FORMAT_ERROR",
2247
- 400
2248
- ),
2249
- PASSWORD_VALIDATION_ERROR: new AuthError(
2250
- "Password validation failed. Please check all requirements",
2251
- "AUTH/PASSWORD_VALIDATION_ERROR",
2252
- 400
2253
- ),
2254
- // Password reset specific errors
2255
- EXPIRED_ACTION_CODE: new AuthError(
2256
- "Kod za resetovanje lozinke je istekao. Molimo zatra\u017Eite novi link za resetovanje.",
2257
- "AUTH/EXPIRED_ACTION_CODE",
2258
- 400
2259
- ),
2260
- INVALID_ACTION_CODE: new AuthError(
2261
- "Kod za resetovanje lozinke je neva\u017Ee\u0107i ili je ve\u0107 iskori\u0161\u0107en. Molimo zatra\u017Eite novi link za resetovanje.",
2262
- "AUTH/INVALID_ACTION_CODE",
2263
- 400
2264
- ),
2265
- WEAK_PASSWORD: new AuthError(
2266
- "Lozinka je previ\u0161e slaba. Molimo koristite ja\u010Du lozinku.",
2267
- "AUTH/WEAK_PASSWORD",
2268
- 400
2269
- )
2270
1018
  };
2271
1019
 
2272
- // src/services/user/user.service.ts
2273
- var import_firestore22 = require("firebase/firestore");
1020
+ // src/services/appointment/utils/appointment.utils.ts
1021
+ var import_firestore3 = require("firebase/firestore");
1022
+
1023
+ // src/types/calendar/index.ts
1024
+ var CalendarEventStatus = /* @__PURE__ */ ((CalendarEventStatus4) => {
1025
+ CalendarEventStatus4["PENDING"] = "pending";
1026
+ CalendarEventStatus4["CONFIRMED"] = "confirmed";
1027
+ CalendarEventStatus4["REJECTED"] = "rejected";
1028
+ CalendarEventStatus4["CANCELED"] = "canceled";
1029
+ CalendarEventStatus4["RESCHEDULED"] = "rescheduled";
1030
+ CalendarEventStatus4["COMPLETED"] = "completed";
1031
+ CalendarEventStatus4["NO_SHOW"] = "no_show";
1032
+ return CalendarEventStatus4;
1033
+ })(CalendarEventStatus || {});
1034
+ var CalendarSyncStatus = /* @__PURE__ */ ((CalendarSyncStatus4) => {
1035
+ CalendarSyncStatus4["INTERNAL"] = "internal";
1036
+ CalendarSyncStatus4["EXTERNAL"] = "external";
1037
+ return CalendarSyncStatus4;
1038
+ })(CalendarSyncStatus || {});
1039
+ var CalendarEventType = /* @__PURE__ */ ((CalendarEventType3) => {
1040
+ CalendarEventType3["APPOINTMENT"] = "appointment";
1041
+ CalendarEventType3["BLOCKING"] = "blocking";
1042
+ CalendarEventType3["BREAK"] = "break";
1043
+ CalendarEventType3["FREE_DAY"] = "free_day";
1044
+ CalendarEventType3["OTHER"] = "other";
1045
+ return CalendarEventType3;
1046
+ })(CalendarEventType || {});
1047
+ var CALENDAR_COLLECTION = "calendar";
1048
+ var SearchLocationEnum = /* @__PURE__ */ ((SearchLocationEnum2) => {
1049
+ SearchLocationEnum2["PRACTITIONER"] = "practitioner";
1050
+ SearchLocationEnum2["PATIENT"] = "patient";
1051
+ SearchLocationEnum2["CLINIC"] = "clinic";
1052
+ return SearchLocationEnum2;
1053
+ })(SearchLocationEnum || {});
1054
+
1055
+ // src/types/practitioner/index.ts
1056
+ var PRACTITIONERS_COLLECTION = "practitioners";
1057
+ var REGISTER_TOKENS_COLLECTION = "register_tokens";
1058
+ var PractitionerStatus = /* @__PURE__ */ ((PractitionerStatus2) => {
1059
+ PractitionerStatus2["DRAFT"] = "draft";
1060
+ PractitionerStatus2["ACTIVE"] = "active";
1061
+ return PractitionerStatus2;
1062
+ })(PractitionerStatus || {});
1063
+ var PractitionerTokenStatus = /* @__PURE__ */ ((PractitionerTokenStatus2) => {
1064
+ PractitionerTokenStatus2["ACTIVE"] = "active";
1065
+ PractitionerTokenStatus2["USED"] = "used";
1066
+ PractitionerTokenStatus2["EXPIRED"] = "expired";
1067
+ PractitionerTokenStatus2["REVOKED"] = "revoked";
1068
+ return PractitionerTokenStatus2;
1069
+ })(PractitionerTokenStatus || {});
1070
+
1071
+ // src/types/clinic/preferences.types.ts
1072
+ var PracticeType = /* @__PURE__ */ ((PracticeType2) => {
1073
+ PracticeType2["GENERAL_PRACTICE"] = "general_practice";
1074
+ PracticeType2["DENTAL"] = "dental";
1075
+ PracticeType2["DERMATOLOGY"] = "dermatology";
1076
+ PracticeType2["CARDIOLOGY"] = "cardiology";
1077
+ PracticeType2["ORTHOPEDICS"] = "orthopedics";
1078
+ PracticeType2["GYNECOLOGY"] = "gynecology";
1079
+ PracticeType2["PEDIATRICS"] = "pediatrics";
1080
+ PracticeType2["OPHTHALMOLOGY"] = "ophthalmology";
1081
+ PracticeType2["NEUROLOGY"] = "neurology";
1082
+ PracticeType2["PSYCHIATRY"] = "psychiatry";
1083
+ PracticeType2["UROLOGY"] = "urology";
1084
+ PracticeType2["ONCOLOGY"] = "oncology";
1085
+ PracticeType2["ENDOCRINOLOGY"] = "endocrinology";
1086
+ PracticeType2["GASTROENTEROLOGY"] = "gastroenterology";
1087
+ PracticeType2["PULMONOLOGY"] = "pulmonology";
1088
+ PracticeType2["RHEUMATOLOGY"] = "rheumatology";
1089
+ PracticeType2["PHYSICAL_THERAPY"] = "physical_therapy";
1090
+ PracticeType2["NUTRITION"] = "nutrition";
1091
+ PracticeType2["ALTERNATIVE_MEDICINE"] = "alternative_medicine";
1092
+ PracticeType2["OTHER"] = "other";
1093
+ return PracticeType2;
1094
+ })(PracticeType || {});
1095
+ var Language = /* @__PURE__ */ ((Language2) => {
1096
+ Language2["ENGLISH"] = "english";
1097
+ Language2["GERMAN"] = "german";
1098
+ Language2["ITALIAN"] = "italian";
1099
+ Language2["FRENCH"] = "french";
1100
+ Language2["SPANISH"] = "spanish";
1101
+ return Language2;
1102
+ })(Language || {});
1103
+ var ClinicTag = /* @__PURE__ */ ((ClinicTag6) => {
1104
+ ClinicTag6["PARKING"] = "parking";
1105
+ ClinicTag6["WIFI"] = "wifi";
1106
+ ClinicTag6["LUXURY_WAITING"] = "luxury_waiting";
1107
+ ClinicTag6["REFRESHMENTS"] = "refreshments";
1108
+ ClinicTag6["PRIVATE_ROOMS"] = "private_rooms";
1109
+ ClinicTag6["RECOVERY_AREA"] = "recovery_area";
1110
+ ClinicTag6["CARD_PAYMENT"] = "card_payment";
1111
+ ClinicTag6["FINANCING"] = "financing";
1112
+ ClinicTag6["FREE_CONSULTATION"] = "free_consultation";
1113
+ ClinicTag6["VIRTUAL_CONSULTATION"] = "virtual_consultation";
1114
+ ClinicTag6["BEFORE_AFTER_PHOTOS"] = "before_after_photos";
1115
+ ClinicTag6["AFTERCARE_SUPPORT"] = "aftercare_support";
1116
+ ClinicTag6["BOTOX"] = "botox";
1117
+ ClinicTag6["DERMAL_FILLERS"] = "dermal_fillers";
1118
+ ClinicTag6["LASER_HAIR_REMOVAL"] = "laser_hair_removal";
1119
+ ClinicTag6["LASER_SKIN_RESURFACING"] = "laser_skin_resurfacing";
1120
+ ClinicTag6["CHEMICAL_PEELS"] = "chemical_peels";
1121
+ ClinicTag6["MICRONEEDLING"] = "microneedling";
1122
+ ClinicTag6["COOLSCULPTING"] = "coolsculpting";
1123
+ ClinicTag6["THREAD_LIFT"] = "thread_lift";
1124
+ ClinicTag6["LIP_ENHANCEMENT"] = "lip_enhancement";
1125
+ ClinicTag6["RHINOPLASTY"] = "rhinoplasty";
1126
+ ClinicTag6["SKIN_TIGHTENING"] = "skin_tightening";
1127
+ ClinicTag6["FAT_DISSOLVING"] = "fat_dissolving";
1128
+ ClinicTag6["PRP_TREATMENT"] = "prp_treatment";
1129
+ ClinicTag6["HYDRAFACIAL"] = "hydrafacial";
1130
+ ClinicTag6["IPL_PHOTOFACIAL"] = "ipl_photofacial";
1131
+ ClinicTag6["BODY_CONTOURING"] = "body_contouring";
1132
+ ClinicTag6["FACELIFT"] = "facelift";
1133
+ ClinicTag6["RHINOPLASTY_SURGICAL"] = "rhinoplasty_surgical";
1134
+ ClinicTag6["BREAST_AUGMENTATION"] = "breast_augmentation";
1135
+ ClinicTag6["BREAST_REDUCTION"] = "breast_reduction";
1136
+ ClinicTag6["BREAST_LIFT"] = "breast_lift";
1137
+ ClinicTag6["TUMMY_TUCK"] = "tummy_tuck";
1138
+ ClinicTag6["LIPOSUCTION"] = "liposuction";
1139
+ ClinicTag6["BBL"] = "bbl";
1140
+ ClinicTag6["MOMMY_MAKEOVER"] = "mommy_makeover";
1141
+ ClinicTag6["ARM_LIFT"] = "arm_lift";
1142
+ ClinicTag6["THIGH_LIFT"] = "thigh_lift";
1143
+ ClinicTag6["EYELID_SURGERY"] = "eyelid_surgery";
1144
+ ClinicTag6["BROW_LIFT"] = "brow_lift";
1145
+ ClinicTag6["NECK_LIFT"] = "neck_lift";
1146
+ ClinicTag6["OTOPLASTY"] = "otoplasty";
1147
+ ClinicTag6["LABIAPLASTY"] = "labiaplasty";
1148
+ ClinicTag6["ONLINE_BOOKING"] = "online_booking";
1149
+ ClinicTag6["MOBILE_APP"] = "mobile_app";
1150
+ ClinicTag6["SMS_NOTIFICATIONS"] = "sms_notifications";
1151
+ ClinicTag6["EMAIL_NOTIFICATIONS"] = "email_notifications";
1152
+ ClinicTag6["VIRTUAL_TRY_ON"] = "virtual_try_on";
1153
+ ClinicTag6["SKIN_ANALYSIS"] = "skin_analysis";
1154
+ ClinicTag6["TREATMENT_TRACKING"] = "treatment_tracking";
1155
+ ClinicTag6["LOYALTY_PROGRAM"] = "loyalty_program";
1156
+ ClinicTag6["ENGLISH"] = "english";
1157
+ ClinicTag6["GERMAN"] = "german";
1158
+ ClinicTag6["FRENCH"] = "french";
1159
+ ClinicTag6["SPANISH"] = "spanish";
1160
+ ClinicTag6["ITALIAN"] = "italian";
1161
+ ClinicTag6["DUTCH"] = "dutch";
1162
+ ClinicTag6["RUSSIAN"] = "russian";
1163
+ ClinicTag6["PORTUGUESE"] = "portuguese";
1164
+ ClinicTag6["OPEN_24_7"] = "open_24_7";
1165
+ ClinicTag6["WEEKEND_HOURS"] = "weekend_hours";
1166
+ ClinicTag6["EXTENDED_HOURS"] = "extended_hours";
1167
+ ClinicTag6["HOLIDAY_HOURS"] = "holiday_hours";
1168
+ return ClinicTag6;
1169
+ })(ClinicTag || {});
1170
+ var ClinicPhotoTag = /* @__PURE__ */ ((ClinicPhotoTag2) => {
1171
+ ClinicPhotoTag2["BUILDING_EXTERIOR"] = "building_exterior";
1172
+ ClinicPhotoTag2["ENTRANCE"] = "entrance";
1173
+ ClinicPhotoTag2["PARKING"] = "parking";
1174
+ ClinicPhotoTag2["RECEPTION"] = "reception";
1175
+ ClinicPhotoTag2["WAITING_ROOM"] = "waiting_room";
1176
+ ClinicPhotoTag2["HALLWAY"] = "hallway";
1177
+ ClinicPhotoTag2["EXAM_ROOM"] = "exam_room";
1178
+ ClinicPhotoTag2["TREATMENT_ROOM"] = "treatment_room";
1179
+ ClinicPhotoTag2["LABORATORY"] = "laboratory";
1180
+ ClinicPhotoTag2["XRAY_ROOM"] = "xray_room";
1181
+ ClinicPhotoTag2["ULTRASOUND_ROOM"] = "ultrasound_room";
1182
+ ClinicPhotoTag2["DENTAL_OFFICE"] = "dental_office";
1183
+ ClinicPhotoTag2["OPERATING_ROOM"] = "operating_room";
1184
+ ClinicPhotoTag2["RECOVERY_ROOM"] = "recovery_room";
1185
+ ClinicPhotoTag2["MEDICAL_EQUIPMENT"] = "medical_equipment";
1186
+ ClinicPhotoTag2["PHARMACY"] = "pharmacy";
1187
+ ClinicPhotoTag2["CAFETERIA"] = "cafeteria";
1188
+ ClinicPhotoTag2["CHILDREN_AREA"] = "children_area";
1189
+ ClinicPhotoTag2["STAFF"] = "staff";
1190
+ ClinicPhotoTag2["OTHER"] = "other";
1191
+ return ClinicPhotoTag2;
1192
+ })(ClinicPhotoTag || {});
1193
+
1194
+ // src/types/clinic/practitioner-invite.types.ts
1195
+ var PRACTITIONER_INVITES_COLLECTION = "practitioner-invites";
1196
+ var PractitionerInviteStatus = /* @__PURE__ */ ((PractitionerInviteStatus2) => {
1197
+ PractitionerInviteStatus2["PENDING"] = "pending";
1198
+ PractitionerInviteStatus2["ACCEPTED"] = "accepted";
1199
+ PractitionerInviteStatus2["REJECTED"] = "rejected";
1200
+ PractitionerInviteStatus2["CANCELLED"] = "cancelled";
1201
+ return PractitionerInviteStatus2;
1202
+ })(PractitionerInviteStatus || {});
2274
1203
 
2275
- // src/errors/user.errors.ts
2276
- var USER_ERRORS = {
2277
- // Basic user errors
2278
- NOT_FOUND: new AuthError(
2279
- "User not found in the system",
2280
- "USER/NOT_FOUND",
2281
- 404
2282
- ),
2283
- // Role management errors
2284
- ROLE_EXISTS: new AuthError(
2285
- "User already has this role assigned",
2286
- "USER/ROLE_EXISTS",
2287
- 400
2288
- ),
2289
- ROLE_NOT_FOUND: new AuthError(
2290
- "User does not have this role assigned",
2291
- "USER/ROLE_NOT_FOUND",
2292
- 404
2293
- ),
2294
- MAX_ROLES_EXCEEDED: new AuthError(
2295
- "User cannot have more than 3 roles",
2296
- "USER/MAX_ROLES_EXCEEDED",
2297
- 400
2298
- ),
2299
- MIN_ROLES_REQUIRED: new AuthError(
2300
- "User must have at least one role",
2301
- "USER/MIN_ROLES_REQUIRED",
2302
- 400
2303
- ),
2304
- // Validation errors
2305
- VALIDATION_ERROR: new AuthError(
2306
- "Data validation error occurred",
2307
- "USER/VALIDATION_ERROR",
2308
- 400
2309
- ),
2310
- PASSWORD_VALIDATION_ERROR: new AuthError(
2311
- "Password must contain at least 8 characters, one uppercase letter, one number, and one special character",
2312
- "USER/PASSWORD_VALIDATION_ERROR",
2313
- 400
2314
- ),
2315
- EMAIL_VALIDATION_ERROR: new AuthError(
2316
- "Email address is not in a valid format",
2317
- "USER/EMAIL_VALIDATION_ERROR",
2318
- 400
2319
- ),
2320
- // Profile management errors
2321
- PROFILE_UPDATE_ERROR: new AuthError(
2322
- "Unable to update user profile",
2323
- "USER/PROFILE_UPDATE_ERROR",
2324
- 400
2325
- ),
2326
- PROFILE_DELETE_ERROR: new AuthError(
2327
- "Unable to delete user profile",
2328
- "USER/PROFILE_DELETE_ERROR",
2329
- 400
2330
- ),
2331
- // Permission errors
2332
- INSUFFICIENT_PERMISSIONS: new AuthError(
2333
- "You don't have sufficient permissions for this action",
2334
- "USER/INSUFFICIENT_PERMISSIONS",
2335
- 403
2336
- ),
2337
- // Session errors
2338
- SESSION_EXPIRED: new AuthError(
2339
- "Your session has expired. Please sign in again",
2340
- "USER/SESSION_EXPIRED",
2341
- 401
2342
- ),
2343
- INVALID_TOKEN: new AuthError(
2344
- "Invalid authentication token",
2345
- "USER/INVALID_TOKEN",
2346
- 401
2347
- ),
2348
- // Rate limiting
2349
- TOO_MANY_REQUESTS: new AuthError(
2350
- "Too many requests. Please try again later",
2351
- "USER/TOO_MANY_REQUESTS",
2352
- 429
2353
- ),
2354
- // Account state errors
2355
- ACCOUNT_LOCKED: new AuthError(
2356
- "Account is temporarily locked due to too many failed login attempts",
2357
- "USER/ACCOUNT_LOCKED",
2358
- 403
2359
- ),
2360
- ACCOUNT_DISABLED: new AuthError(
2361
- "This account has been disabled",
2362
- "USER/ACCOUNT_DISABLED",
2363
- 403
2364
- )
1204
+ // src/types/clinic/index.ts
1205
+ var CLINIC_GROUPS_COLLECTION = "clinic_groups";
1206
+ var CLINIC_ADMINS_COLLECTION = "clinic_admins";
1207
+ var CLINICS_COLLECTION = "clinics";
1208
+ var AdminTokenStatus = /* @__PURE__ */ ((AdminTokenStatus2) => {
1209
+ AdminTokenStatus2["ACTIVE"] = "active";
1210
+ AdminTokenStatus2["USED"] = "used";
1211
+ AdminTokenStatus2["EXPIRED"] = "expired";
1212
+ return AdminTokenStatus2;
1213
+ })(AdminTokenStatus || {});
1214
+ var SubscriptionModel = /* @__PURE__ */ ((SubscriptionModel2) => {
1215
+ SubscriptionModel2["NO_SUBSCRIPTION"] = "no_subscription";
1216
+ SubscriptionModel2["BASIC"] = "basic";
1217
+ SubscriptionModel2["PREMIUM"] = "premium";
1218
+ SubscriptionModel2["ENTERPRISE"] = "enterprise";
1219
+ return SubscriptionModel2;
1220
+ })(SubscriptionModel || {});
1221
+ var SubscriptionStatus = /* @__PURE__ */ ((SubscriptionStatus2) => {
1222
+ SubscriptionStatus2["PENDING"] = "pending";
1223
+ SubscriptionStatus2["ACTIVE"] = "active";
1224
+ SubscriptionStatus2["PENDING_CANCELLATION"] = "pending_cancellation";
1225
+ SubscriptionStatus2["CANCELED"] = "canceled";
1226
+ SubscriptionStatus2["PAST_DUE"] = "past_due";
1227
+ SubscriptionStatus2["TRIALING"] = "trialing";
1228
+ return SubscriptionStatus2;
1229
+ })(SubscriptionStatus || {});
1230
+ var BillingTransactionType = /* @__PURE__ */ ((BillingTransactionType2) => {
1231
+ BillingTransactionType2["SUBSCRIPTION_CREATED"] = "subscription_created";
1232
+ BillingTransactionType2["SUBSCRIPTION_ACTIVATED"] = "subscription_activated";
1233
+ BillingTransactionType2["SUBSCRIPTION_RENEWED"] = "subscription_renewed";
1234
+ BillingTransactionType2["SUBSCRIPTION_UPDATED"] = "subscription_updated";
1235
+ BillingTransactionType2["SUBSCRIPTION_CANCELED"] = "subscription_canceled";
1236
+ BillingTransactionType2["SUBSCRIPTION_REACTIVATED"] = "subscription_reactivated";
1237
+ BillingTransactionType2["SUBSCRIPTION_DELETED"] = "subscription_deleted";
1238
+ return BillingTransactionType2;
1239
+ })(BillingTransactionType || {});
1240
+
1241
+ // src/types/patient/medical-info.types.ts
1242
+ var PATIENT_MEDICAL_INFO_COLLECTION = "medical_info";
1243
+ var DEFAULT_MEDICAL_INFO = {
1244
+ vitalStats: {},
1245
+ blockingConditions: [],
1246
+ contraindications: [],
1247
+ allergies: [],
1248
+ currentMedications: []
2365
1249
  };
2366
1250
 
2367
- // src/services/user/user.service.ts
2368
- var import_zod17 = require("zod");
1251
+ // src/types/patient/index.ts
1252
+ var PATIENTS_COLLECTION = "patients";
1253
+ var PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
1254
+ var PATIENT_MEDICAL_HISTORY_COLLECTION = "medical-history";
1255
+ var PATIENT_APPOINTMENTS_COLLECTION = "appointments";
1256
+ var PATIENT_LOCATION_INFO_COLLECTION = "location-info";
1257
+ var Gender = /* @__PURE__ */ ((Gender2) => {
1258
+ Gender2["MALE"] = "male";
1259
+ Gender2["FEMALE"] = "female";
1260
+ Gender2["TRANSGENDER_MALE"] = "transgender_male";
1261
+ Gender2["TRANSGENDER_FEMALE"] = "transgender_female";
1262
+ Gender2["PREFER_NOT_TO_SAY"] = "prefer_not_to_say";
1263
+ Gender2["OTHER"] = "other";
1264
+ return Gender2;
1265
+ })(Gender || {});
2369
1266
 
2370
- // src/services/patient/patient.service.ts
2371
- var import_firestore18 = require("firebase/firestore");
1267
+ // src/types/procedure/index.ts
1268
+ var PROCEDURES_COLLECTION = "procedures";
1269
+
1270
+ // src/backoffice/types/technology.types.ts
1271
+ var TECHNOLOGIES_COLLECTION = "technologies";
1272
+
1273
+ // src/services/appointment/utils/appointment.utils.ts
1274
+ async function updateAppointmentUtil(db, appointmentId, data) {
1275
+ try {
1276
+ const appointmentRef = (0, import_firestore3.doc)(db, APPOINTMENTS_COLLECTION, appointmentId);
1277
+ const appointmentDoc = await (0, import_firestore3.getDoc)(appointmentRef);
1278
+ if (!appointmentDoc.exists()) {
1279
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
1280
+ }
1281
+ const currentAppointment = appointmentDoc.data();
1282
+ let completedPreRequirements = currentAppointment.completedPreRequirements || [];
1283
+ let completedPostRequirements = currentAppointment.completedPostRequirements || [];
1284
+ if (data.completedPreRequirements) {
1285
+ const validPreReqIds = currentAppointment.preProcedureRequirements.map(
1286
+ (req) => req.id
1287
+ );
1288
+ if (Array.isArray(data.completedPreRequirements)) {
1289
+ const invalidPreReqIds = data.completedPreRequirements.filter(
1290
+ (id) => !validPreReqIds.includes(id)
1291
+ );
1292
+ if (invalidPreReqIds.length > 0) {
1293
+ throw new Error(
1294
+ `Invalid pre-requirement IDs: ${invalidPreReqIds.join(", ")}`
1295
+ );
1296
+ }
1297
+ completedPreRequirements = [
1298
+ .../* @__PURE__ */ new Set([
1299
+ ...completedPreRequirements,
1300
+ ...data.completedPreRequirements
1301
+ ])
1302
+ ];
1303
+ }
1304
+ }
1305
+ if (data.completedPostRequirements) {
1306
+ const validPostReqIds = currentAppointment.postProcedureRequirements.map(
1307
+ (req) => req.id
1308
+ );
1309
+ if (Array.isArray(data.completedPostRequirements)) {
1310
+ const invalidPostReqIds = data.completedPostRequirements.filter(
1311
+ (id) => !validPostReqIds.includes(id)
1312
+ );
1313
+ if (invalidPostReqIds.length > 0) {
1314
+ throw new Error(
1315
+ `Invalid post-requirement IDs: ${invalidPostReqIds.join(", ")}`
1316
+ );
1317
+ }
1318
+ completedPostRequirements = [
1319
+ .../* @__PURE__ */ new Set([
1320
+ ...completedPostRequirements,
1321
+ ...data.completedPostRequirements
1322
+ ])
1323
+ ];
1324
+ }
1325
+ }
1326
+ const updateData = {
1327
+ ...data,
1328
+ completedPreRequirements: Array.isArray(data.completedPreRequirements) ? completedPreRequirements : data.completedPreRequirements,
1329
+ completedPostRequirements: Array.isArray(data.completedPostRequirements) ? completedPostRequirements : data.completedPostRequirements,
1330
+ updatedAt: (0, import_firestore3.serverTimestamp)()
1331
+ };
1332
+ Object.keys(updateData).forEach((key) => {
1333
+ if (updateData[key] === void 0) {
1334
+ delete updateData[key];
1335
+ }
1336
+ });
1337
+ if (data.status && data.status !== currentAppointment.status) {
1338
+ if (data.status === "confirmed" /* CONFIRMED */ && !updateData.confirmationTime) {
1339
+ updateData.confirmationTime = import_firestore3.Timestamp.now();
1340
+ }
1341
+ if (currentAppointment.calendarEventId) {
1342
+ await updateCalendarEventStatus(
1343
+ db,
1344
+ currentAppointment.calendarEventId,
1345
+ data.status
1346
+ );
1347
+ }
1348
+ }
1349
+ await (0, import_firestore3.updateDoc)(appointmentRef, updateData);
1350
+ const updatedAppointmentDoc = await (0, import_firestore3.getDoc)(appointmentRef);
1351
+ if (!updatedAppointmentDoc.exists()) {
1352
+ throw new Error(
1353
+ `Failed to retrieve updated appointment ${appointmentId}`
1354
+ );
1355
+ }
1356
+ return updatedAppointmentDoc.data();
1357
+ } catch (error) {
1358
+ console.error(`Error updating appointment ${appointmentId}:`, error);
1359
+ throw error;
1360
+ }
1361
+ }
1362
+ async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus) {
1363
+ try {
1364
+ const calendarEventRef = (0, import_firestore3.doc)(db, CALENDAR_COLLECTION, calendarEventId);
1365
+ const calendarEventDoc = await (0, import_firestore3.getDoc)(calendarEventRef);
1366
+ if (!calendarEventDoc.exists()) {
1367
+ console.warn(`Calendar event with ID ${calendarEventId} not found`);
1368
+ return;
1369
+ }
1370
+ let calendarStatus;
1371
+ switch (appointmentStatus) {
1372
+ case "confirmed" /* CONFIRMED */:
1373
+ calendarStatus = "confirmed";
1374
+ break;
1375
+ case "canceled_patient" /* CANCELED_PATIENT */:
1376
+ case "canceled_clinic" /* CANCELED_CLINIC */:
1377
+ calendarStatus = "canceled";
1378
+ break;
1379
+ case "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */:
1380
+ calendarStatus = "rescheduled";
1381
+ break;
1382
+ case "completed" /* COMPLETED */:
1383
+ calendarStatus = "completed";
1384
+ break;
1385
+ default:
1386
+ return;
1387
+ }
1388
+ await (0, import_firestore3.updateDoc)(calendarEventRef, {
1389
+ status: calendarStatus,
1390
+ updatedAt: (0, import_firestore3.serverTimestamp)()
1391
+ });
1392
+ } catch (error) {
1393
+ console.error(`Error updating calendar event ${calendarEventId}:`, error);
1394
+ }
1395
+ }
1396
+ async function getAppointmentByIdUtil(db, appointmentId) {
1397
+ try {
1398
+ const appointmentDoc = await (0, import_firestore3.getDoc)(
1399
+ (0, import_firestore3.doc)(db, APPOINTMENTS_COLLECTION, appointmentId)
1400
+ );
1401
+ if (!appointmentDoc.exists()) {
1402
+ return null;
1403
+ }
1404
+ return appointmentDoc.data();
1405
+ } catch (error) {
1406
+ console.error(`Error getting appointment ${appointmentId}:`, error);
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ async function searchAppointmentsUtil(db, params) {
1411
+ try {
1412
+ const constraints = [];
1413
+ if (params.patientId) {
1414
+ constraints.push((0, import_firestore3.where)("patientId", "==", params.patientId));
1415
+ }
1416
+ if (params.practitionerId) {
1417
+ constraints.push((0, import_firestore3.where)("practitionerId", "==", params.practitionerId));
1418
+ }
1419
+ if (params.clinicBranchId) {
1420
+ constraints.push((0, import_firestore3.where)("clinicBranchId", "==", params.clinicBranchId));
1421
+ }
1422
+ if (params.startDate) {
1423
+ constraints.push(
1424
+ (0, import_firestore3.where)(
1425
+ "appointmentStartTime",
1426
+ ">=",
1427
+ import_firestore3.Timestamp.fromDate(params.startDate)
1428
+ )
1429
+ );
1430
+ }
1431
+ if (params.endDate) {
1432
+ constraints.push(
1433
+ (0, import_firestore3.where)("appointmentStartTime", "<=", import_firestore3.Timestamp.fromDate(params.endDate))
1434
+ );
1435
+ }
1436
+ if (params.status) {
1437
+ if (Array.isArray(params.status)) {
1438
+ constraints.push((0, import_firestore3.where)("status", "in", params.status));
1439
+ } else {
1440
+ constraints.push((0, import_firestore3.where)("status", "==", params.status));
1441
+ }
1442
+ }
1443
+ constraints.push((0, import_firestore3.orderBy)("appointmentStartTime", "asc"));
1444
+ if (params.limit) {
1445
+ constraints.push((0, import_firestore3.limit)(params.limit));
1446
+ }
1447
+ if (params.startAfter) {
1448
+ constraints.push((0, import_firestore3.startAfter)(params.startAfter));
1449
+ }
1450
+ const q = (0, import_firestore3.query)((0, import_firestore3.collection)(db, APPOINTMENTS_COLLECTION), ...constraints);
1451
+ const querySnapshot = await (0, import_firestore3.getDocs)(q);
1452
+ const appointments = querySnapshot.docs.map(
1453
+ (doc38) => doc38.data()
1454
+ );
1455
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1456
+ return { appointments, lastDoc };
1457
+ } catch (error) {
1458
+ console.error("Error searching appointments:", error);
1459
+ throw error;
1460
+ }
1461
+ }
2372
1462
 
2373
- // src/services/media/media.service.ts
2374
- var import_firestore3 = require("firebase/firestore");
2375
- var import_storage2 = require("firebase/storage");
2376
- var import_firestore4 = require("firebase/firestore");
2377
- var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
2378
- MediaAccessLevel2["PUBLIC"] = "public";
2379
- MediaAccessLevel2["PRIVATE"] = "private";
2380
- MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
2381
- return MediaAccessLevel2;
2382
- })(MediaAccessLevel || {});
2383
- var MEDIA_METADATA_COLLECTION = "media_metadata";
2384
- var MediaService = class extends BaseService {
2385
- constructor(...args) {
2386
- super(...args);
1463
+ // src/services/appointment/appointment.service.ts
1464
+ var AppointmentService = class extends BaseService {
1465
+ /**
1466
+ * Creates a new AppointmentService instance.
1467
+ *
1468
+ * @param db Firestore instance
1469
+ * @param auth Firebase Auth instance
1470
+ * @param app Firebase App instance
1471
+ * @param calendarService Calendar service instance
1472
+ * @param patientService Patient service instance
1473
+ * @param practitionerService Practitioner service instance
1474
+ * @param clinicService Clinic service instance
1475
+ * @param filledDocumentService Filled document service instance
1476
+ */
1477
+ constructor(db, auth, app, calendarService, patientService, practitionerService, clinicService, filledDocumentService) {
1478
+ super(db, auth, app);
1479
+ this.calendarService = calendarService;
1480
+ this.patientService = patientService;
1481
+ this.practitionerService = practitionerService;
1482
+ this.clinicService = clinicService;
1483
+ this.filledDocumentService = filledDocumentService;
1484
+ this.mediaService = new MediaService(db, auth, app);
1485
+ this.functions = (0, import_functions.getFunctions)(app, "europe-west6");
1486
+ }
1487
+ /**
1488
+ * Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
1489
+ * This is an alternative implementation using direct HTTP request instead of callable function.
1490
+ *
1491
+ * @param clinicId ID of the clinic
1492
+ * @param practitionerId ID of the practitioner
1493
+ * @param procedureId ID of the procedure
1494
+ * @param startDate Start date of the time range to check
1495
+ * @param endDate End date of the time range to check
1496
+ * @returns Array of available booking slots
1497
+ */
1498
+ async getAvailableBookingSlotsHttp(clinicId, practitionerId, procedureId, startDate, endDate) {
1499
+ try {
1500
+ console.log(
1501
+ `[APPOINTMENT_SERVICE] Getting available booking slots via HTTP for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
1502
+ );
1503
+ if (!clinicId || !practitionerId || !procedureId || !startDate || !endDate) {
1504
+ throw new Error("Missing required parameters for booking slots calculation");
1505
+ }
1506
+ if (endDate <= startDate) {
1507
+ throw new Error("End date must be after start date");
1508
+ }
1509
+ const currentUser = this.auth.currentUser;
1510
+ if (!currentUser) {
1511
+ throw new Error("User must be authenticated to get available booking slots");
1512
+ }
1513
+ const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/getAvailableBookingSlots`;
1514
+ const idToken = await currentUser.getIdToken();
1515
+ console.log(`[APPOINTMENT_SERVICE] Got user token, user ID: ${currentUser.uid}`);
1516
+ const requestData = {
1517
+ clinicId,
1518
+ practitionerId,
1519
+ procedureId,
1520
+ timeframe: {
1521
+ start: startDate.getTime(),
1522
+ // Convert to timestamp
1523
+ end: endDate.getTime()
1524
+ }
1525
+ };
1526
+ console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
1527
+ const response = await fetch(functionUrl, {
1528
+ method: "POST",
1529
+ mode: "cors",
1530
+ // Important for cross-origin requests
1531
+ cache: "no-cache",
1532
+ // Don't cache this request
1533
+ credentials: "omit",
1534
+ // Don't send cookies since we're using token auth
1535
+ headers: {
1536
+ "Content-Type": "application/json",
1537
+ Authorization: `Bearer ${idToken}`
1538
+ },
1539
+ redirect: "follow",
1540
+ referrerPolicy: "no-referrer",
1541
+ body: JSON.stringify(requestData)
1542
+ });
1543
+ console.log(
1544
+ `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
1545
+ );
1546
+ if (!response.ok) {
1547
+ const errorText = await response.text();
1548
+ console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
1549
+ throw new Error(
1550
+ `Failed to get available booking slots: ${response.status} ${response.statusText} - ${errorText}`
1551
+ );
1552
+ }
1553
+ const result = await response.json();
1554
+ console.log(`[APPOINTMENT_SERVICE] Response parsed successfully`, result);
1555
+ if (!result.success) {
1556
+ throw new Error(result.error || "Failed to get available booking slots");
1557
+ }
1558
+ const slots = result.availableSlots.map((slot) => ({
1559
+ start: new Date(slot.start)
1560
+ }));
1561
+ console.log(`[APPOINTMENT_SERVICE] Found ${slots.length} available booking slots via HTTP`);
1562
+ return slots;
1563
+ } catch (error) {
1564
+ console.error("[APPOINTMENT_SERVICE] Error getting available booking slots via HTTP:", error);
1565
+ throw error;
1566
+ }
1567
+ }
1568
+ /**
1569
+ * Creates an appointment via the Cloud Function orchestrateAppointmentCreation
1570
+ *
1571
+ * @param data - CreateAppointmentData object
1572
+ * @returns The created appointment
1573
+ */
1574
+ async createAppointmentHttp(data) {
1575
+ try {
1576
+ console.log("[APPOINTMENT_SERVICE] Creating appointment via cloud function");
1577
+ const currentUser = this.auth.currentUser;
1578
+ if (!currentUser) {
1579
+ throw new Error("User must be authenticated to create an appointment");
1580
+ }
1581
+ const idToken = await currentUser.getIdToken();
1582
+ const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
1583
+ const requestData = {
1584
+ patientId: data.patientId,
1585
+ procedureId: data.procedureId,
1586
+ appointmentStartTime: data.appointmentStartTime.toMillis ? data.appointmentStartTime.toMillis() : new Date(data.appointmentStartTime).getTime(),
1587
+ appointmentEndTime: data.appointmentEndTime.toMillis ? data.appointmentEndTime.toMillis() : new Date(data.appointmentEndTime).getTime(),
1588
+ patientNotes: (data == null ? void 0 : data.patientNotes) || null
1589
+ };
1590
+ console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
1591
+ const response = await fetch(functionUrl, {
1592
+ method: "POST",
1593
+ mode: "cors",
1594
+ cache: "no-cache",
1595
+ credentials: "omit",
1596
+ headers: {
1597
+ "Content-Type": "application/json",
1598
+ Authorization: `Bearer ${idToken}`
1599
+ },
1600
+ redirect: "follow",
1601
+ referrerPolicy: "no-referrer",
1602
+ body: JSON.stringify(requestData)
1603
+ });
1604
+ console.log(
1605
+ `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
1606
+ );
1607
+ if (!response.ok) {
1608
+ const errorText = await response.text();
1609
+ console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
1610
+ throw new Error(
1611
+ `Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
1612
+ );
1613
+ }
1614
+ const result = await response.json();
1615
+ if (!result.success) {
1616
+ throw new Error(result.error || "Failed to create appointment");
1617
+ }
1618
+ if (result.appointmentData) {
1619
+ console.log(`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`);
1620
+ return result.appointmentData;
1621
+ }
1622
+ const createdAppointment = await this.getAppointmentById(result.appointmentId);
1623
+ if (!createdAppointment) {
1624
+ throw new Error(`Failed to retrieve created appointment with ID: ${result.appointmentId}`);
1625
+ }
1626
+ return createdAppointment;
1627
+ } catch (error) {
1628
+ console.error("[APPOINTMENT_SERVICE] Error creating appointment via cloud function:", error);
1629
+ throw error;
1630
+ }
1631
+ }
1632
+ /**
1633
+ * Gets an appointment by ID.
1634
+ *
1635
+ * @param appointmentId ID of the appointment to retrieve
1636
+ * @returns The appointment or null if not found
1637
+ */
1638
+ async getAppointmentById(appointmentId) {
1639
+ try {
1640
+ console.log(`[APPOINTMENT_SERVICE] Getting appointment with ID: ${appointmentId}`);
1641
+ const appointment = await getAppointmentByIdUtil(this.db, appointmentId);
1642
+ console.log(
1643
+ `[APPOINTMENT_SERVICE] Appointment ${appointmentId} ${appointment ? "found" : "not found"}`
1644
+ );
1645
+ return appointment;
1646
+ } catch (error) {
1647
+ console.error(`[APPOINTMENT_SERVICE] Error getting appointment ${appointmentId}:`, error);
1648
+ throw error;
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Updates an existing appointment.
1653
+ *
1654
+ * @param appointmentId ID of the appointment to update
1655
+ * @param data Update data for the appointment
1656
+ * @returns The updated appointment
1657
+ */
1658
+ async updateAppointment(appointmentId, data) {
1659
+ try {
1660
+ console.log(`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`);
1661
+ const validatedData = await updateAppointmentSchema.parseAsync(data);
1662
+ const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
1663
+ console.log(`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`);
1664
+ return updatedAppointment;
1665
+ } catch (error) {
1666
+ console.error(`[APPOINTMENT_SERVICE] Error updating appointment ${appointmentId}:`, error);
1667
+ throw error;
1668
+ }
1669
+ }
1670
+ /**
1671
+ * Searches for appointments based on various criteria.
1672
+ *
1673
+ * @param params Search parameters
1674
+ * @returns Found appointments and the last document for pagination
1675
+ */
1676
+ async searchAppointments(params) {
1677
+ try {
1678
+ console.log("[APPOINTMENT_SERVICE] Searching appointments with params:", params);
1679
+ await searchAppointmentsSchema.parseAsync(params);
1680
+ const result = await searchAppointmentsUtil(this.db, params);
1681
+ console.log(`[APPOINTMENT_SERVICE] Found ${result.appointments.length} appointments`);
1682
+ return result;
1683
+ } catch (error) {
1684
+ console.error("[APPOINTMENT_SERVICE] Error searching appointments:", error);
1685
+ throw error;
1686
+ }
1687
+ }
1688
+ /**
1689
+ * Gets appointments for a specific patient.
1690
+ *
1691
+ * @param patientId ID of the patient
1692
+ * @param options Optional parameters for filtering and pagination
1693
+ * @returns Found appointments and the last document for pagination
1694
+ */
1695
+ async getPatientAppointments(patientId, options) {
1696
+ console.log(`[APPOINTMENT_SERVICE] Getting appointments for patient: ${patientId}`);
1697
+ const searchParams = {
1698
+ patientId,
1699
+ startDate: options == null ? void 0 : options.startDate,
1700
+ endDate: options == null ? void 0 : options.endDate,
1701
+ status: options == null ? void 0 : options.status,
1702
+ limit: options == null ? void 0 : options.limit,
1703
+ startAfter: options == null ? void 0 : options.startAfter
1704
+ };
1705
+ return this.searchAppointments(searchParams);
1706
+ }
1707
+ /**
1708
+ * Gets appointments for a specific practitioner.
1709
+ *
1710
+ * @param practitionerId ID of the practitioner
1711
+ * @param options Optional parameters for filtering and pagination
1712
+ * @returns Found appointments and the last document for pagination
1713
+ */
1714
+ async getPractitionerAppointments(practitionerId, options) {
1715
+ console.log(`[APPOINTMENT_SERVICE] Getting appointments for practitioner: ${practitionerId}`);
1716
+ const searchParams = {
1717
+ practitionerId,
1718
+ startDate: options == null ? void 0 : options.startDate,
1719
+ endDate: options == null ? void 0 : options.endDate,
1720
+ status: options == null ? void 0 : options.status,
1721
+ limit: options == null ? void 0 : options.limit,
1722
+ startAfter: options == null ? void 0 : options.startAfter
1723
+ };
1724
+ return this.searchAppointments(searchParams);
1725
+ }
1726
+ /**
1727
+ * Gets appointments for a specific clinic.
1728
+ *
1729
+ * @param clinicBranchId ID of the clinic branch
1730
+ * @param options Optional parameters for filtering and pagination
1731
+ * @returns Found appointments and the last document for pagination
1732
+ */
1733
+ async getClinicAppointments(clinicBranchId, options) {
1734
+ console.log(`[APPOINTMENT_SERVICE] Getting appointments for clinic: ${clinicBranchId}`);
1735
+ const searchParams = {
1736
+ clinicBranchId,
1737
+ practitionerId: options == null ? void 0 : options.practitionerId,
1738
+ startDate: options == null ? void 0 : options.startDate,
1739
+ endDate: options == null ? void 0 : options.endDate,
1740
+ status: options == null ? void 0 : options.status,
1741
+ limit: options == null ? void 0 : options.limit,
1742
+ startAfter: options == null ? void 0 : options.startAfter
1743
+ };
1744
+ return this.searchAppointments(searchParams);
1745
+ }
1746
+ /**
1747
+ * Updates the status of an appointment.
1748
+ *
1749
+ * @param appointmentId ID of the appointment
1750
+ * @param newStatus New status to set
1751
+ * @param details Optional details for the status change
1752
+ * @returns The updated appointment
1753
+ */
1754
+ async updateAppointmentStatus(appointmentId, newStatus, details) {
1755
+ console.log(
1756
+ `[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
1757
+ );
1758
+ const updateData = {
1759
+ status: newStatus,
1760
+ updatedAt: (0, import_firestore4.serverTimestamp)()
1761
+ };
1762
+ if (newStatus === "canceled_clinic" /* CANCELED_CLINIC */ || newStatus === "canceled_patient" /* CANCELED_PATIENT */ || newStatus === "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */) {
1763
+ if (!(details == null ? void 0 : details.cancellationReason)) {
1764
+ throw new Error("Cancellation reason is required when canceling.");
1765
+ }
1766
+ if (!(details == null ? void 0 : details.canceledBy)) {
1767
+ throw new Error("Canceled by is required when canceling.");
1768
+ }
1769
+ updateData.cancellationReason = details.cancellationReason;
1770
+ updateData.canceledBy = details.canceledBy;
1771
+ updateData.cancellationTime = import_firestore4.Timestamp.now();
1772
+ }
1773
+ if (newStatus === "confirmed" /* CONFIRMED */) {
1774
+ updateData.confirmationTime = import_firestore4.Timestamp.now();
1775
+ }
1776
+ if (newStatus === "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1777
+ updateData.rescheduleTime = import_firestore4.Timestamp.now();
1778
+ }
1779
+ return this.updateAppointment(appointmentId, updateData);
1780
+ }
1781
+ /**
1782
+ * Confirms a PENDING appointment by an Admin/Clinic.
1783
+ */
1784
+ async confirmAppointmentAdmin(appointmentId) {
1785
+ console.log(`[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`);
1786
+ const appointment = await this.getAppointmentById(appointmentId);
1787
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1788
+ if (appointment.status !== "pending" /* PENDING */) {
1789
+ throw new Error(`Appointment ${appointmentId} is not in PENDING state to be confirmed.`);
1790
+ }
1791
+ return this.updateAppointmentStatus(appointmentId, "confirmed" /* CONFIRMED */);
1792
+ }
1793
+ /**
1794
+ * Cancels an appointment by the User (Patient).
1795
+ */
1796
+ async cancelAppointmentUser(appointmentId, reason) {
1797
+ console.log(`[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`);
1798
+ return this.updateAppointmentStatus(appointmentId, "canceled_patient" /* CANCELED_PATIENT */, {
1799
+ cancellationReason: reason,
1800
+ canceledBy: "patient"
1801
+ });
1802
+ }
1803
+ /**
1804
+ * Cancels an appointment by an Admin/Clinic.
1805
+ */
1806
+ async cancelAppointmentAdmin(appointmentId, reason) {
1807
+ console.log(`[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`);
1808
+ return this.updateAppointmentStatus(appointmentId, "canceled_clinic" /* CANCELED_CLINIC */, {
1809
+ cancellationReason: reason,
1810
+ canceledBy: "clinic"
1811
+ });
1812
+ }
1813
+ /**
1814
+ * Admin proposes to reschedule an appointment.
1815
+ * Sets status to RESCHEDULED_BY_CLINIC and updates times.
1816
+ */
1817
+ async rescheduleAppointmentAdmin(params) {
1818
+ console.log(`[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${params.appointmentId}`);
1819
+ const validatedParams = await rescheduleAppointmentSchema.parseAsync(params);
1820
+ const startTimestamp = this.convertToTimestamp(validatedParams.newStartTime);
1821
+ const endTimestamp = this.convertToTimestamp(validatedParams.newEndTime);
1822
+ if (endTimestamp.toMillis() <= startTimestamp.toMillis()) {
1823
+ throw new Error("New end time must be after new start time.");
1824
+ }
1825
+ const updateData = {
1826
+ status: "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */,
1827
+ appointmentStartTime: startTimestamp,
1828
+ appointmentEndTime: endTimestamp,
1829
+ rescheduleTime: import_firestore4.Timestamp.now(),
1830
+ confirmationTime: null,
1831
+ updatedAt: (0, import_firestore4.serverTimestamp)()
1832
+ };
1833
+ return this.updateAppointment(validatedParams.appointmentId, updateData);
1834
+ }
1835
+ /**
1836
+ * Helper method to convert various timestamp formats to Firestore Timestamp
1837
+ * @param value - Any timestamp format (Timestamp, number, string, Date, serialized Timestamp)
1838
+ * @returns Firestore Timestamp object
1839
+ */
1840
+ convertToTimestamp(value) {
1841
+ console.log(`[APPOINTMENT_SERVICE] Converting timestamp:`, {
1842
+ value,
1843
+ type: typeof value
1844
+ });
1845
+ if (value && typeof value.toMillis === "function") {
1846
+ return value;
1847
+ }
1848
+ if (typeof value === "number") {
1849
+ return import_firestore4.Timestamp.fromMillis(value);
1850
+ }
1851
+ if (typeof value === "string") {
1852
+ return import_firestore4.Timestamp.fromDate(new Date(value));
1853
+ }
1854
+ if (value instanceof Date) {
1855
+ return import_firestore4.Timestamp.fromDate(value);
1856
+ }
1857
+ if (value && typeof value._seconds === "number") {
1858
+ return new import_firestore4.Timestamp(value._seconds, value._nanoseconds || 0);
1859
+ }
1860
+ if (value && typeof value.seconds === "number") {
1861
+ return new import_firestore4.Timestamp(value.seconds, value.nanoseconds || 0);
1862
+ }
1863
+ throw new Error(`Invalid timestamp format: ${typeof value}, value: ${JSON.stringify(value)}`);
1864
+ }
1865
+ /**
1866
+ * User confirms a reschedule proposed by the clinic.
1867
+ * Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
1868
+ */
1869
+ async rescheduleAppointmentConfirmUser(appointmentId) {
1870
+ console.log(`[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`);
1871
+ const appointment = await this.getAppointmentById(appointmentId);
1872
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1873
+ if (appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1874
+ throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
1875
+ }
1876
+ return this.updateAppointmentStatus(appointmentId, "confirmed" /* CONFIRMED */);
2387
1877
  }
2388
1878
  /**
2389
- * Upload a media file, store its metadata, and return the metadata including the URL.
2390
- * @param file - The file to upload.
2391
- * @param ownerId - ID of the owner (user, patient, clinic, etc.).
2392
- * @param accessLevel - Access level (public, private, confidential).
2393
- * @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
2394
- * @param originalFileName - Optional: the original name of the file, if not using file.name.
2395
- * @returns Promise with the media metadata.
1879
+ * User rejects a reschedule proposed by the clinic.
1880
+ * Status changes from RESCHEDULED_BY_CLINIC to CANCELED_PATIENT_RESCHEDULED.
2396
1881
  */
2397
- async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
2398
- const mediaId = this.generateId();
2399
- const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
2400
- const uniqueFileName = `${mediaId}-${fileNameToUse}`;
2401
- const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
2402
- console.log(`[MediaService] Uploading file to: ${filePath}`);
2403
- const storageRef = (0, import_storage2.ref)(this.storage, filePath);
2404
- try {
2405
- const uploadResult = await (0, import_storage2.uploadBytes)(storageRef, file, {
2406
- contentType: file.type
2407
- });
2408
- console.log("[MediaService] File uploaded successfully", uploadResult);
2409
- const downloadURL = await (0, import_storage2.getDownloadURL)(uploadResult.ref);
2410
- console.log("[MediaService] Got download URL:", downloadURL);
2411
- const metadata = {
2412
- id: mediaId,
2413
- name: fileNameToUse,
2414
- url: downloadURL,
2415
- contentType: file.type,
2416
- size: file.size,
2417
- createdAt: import_firestore3.Timestamp.now(),
2418
- accessLevel,
2419
- ownerId,
2420
- collectionName,
2421
- path: filePath
2422
- };
2423
- const metadataDocRef = (0, import_firestore4.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
2424
- await (0, import_firestore4.setDoc)(metadataDocRef, metadata);
2425
- console.log("[MediaService] Metadata stored in Firestore:", mediaId);
2426
- return metadata;
2427
- } catch (error) {
2428
- console.error("[MediaService] Error during media upload:", error);
2429
- throw error;
1882
+ async rescheduleAppointmentRejectUser(appointmentId, reason) {
1883
+ console.log(`[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`);
1884
+ const appointment = await this.getAppointmentById(appointmentId);
1885
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1886
+ if (appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1887
+ throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
2430
1888
  }
1889
+ return this.updateAppointmentStatus(
1890
+ appointmentId,
1891
+ "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */,
1892
+ {
1893
+ cancellationReason: reason,
1894
+ canceledBy: "patient"
1895
+ }
1896
+ );
2431
1897
  }
2432
1898
  /**
2433
- * Get media metadata from Firestore by its ID.
2434
- * @param mediaId - ID of the media.
2435
- * @returns Promise with the media metadata or null if not found.
1899
+ * Admin checks in a patient for their appointment.
1900
+ * Requires all pending user forms to be completed.
2436
1901
  */
2437
- async getMediaMetadata(mediaId) {
2438
- console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
2439
- const docRef = (0, import_firestore4.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
2440
- const docSnap = await (0, import_firestore4.getDoc)(docRef);
2441
- if (docSnap.exists()) {
2442
- console.log("[MediaService] Metadata found:", docSnap.data());
2443
- return docSnap.data();
1902
+ async checkInPatientAdmin(appointmentId) {
1903
+ console.log(`[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`);
1904
+ const appointment = await this.getAppointmentById(appointmentId);
1905
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1906
+ if (appointment.pendingUserFormsIds && appointment.pendingUserFormsIds.length > 0) {
1907
+ throw new Error(
1908
+ `Cannot check in: Patient has ${appointment.pendingUserFormsIds.length} pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(", ")}`
1909
+ );
1910
+ }
1911
+ if (appointment.status !== "confirmed" /* CONFIRMED */ && appointment.status !== "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */) {
1912
+ console.warn(
1913
+ `Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`
1914
+ );
1915
+ }
1916
+ return this.updateAppointmentStatus(appointmentId, "checked_in" /* CHECKED_IN */);
1917
+ }
1918
+ /**
1919
+ * Doctor starts the appointment procedure.
1920
+ */
1921
+ async startAppointmentDoctor(appointmentId) {
1922
+ console.log(`[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`);
1923
+ const appointment = await this.getAppointmentById(appointmentId);
1924
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1925
+ if (appointment.status !== "checked_in" /* CHECKED_IN */) {
1926
+ throw new Error(`Appointment ${appointmentId} must be CHECKED_IN to start.`);
1927
+ }
1928
+ const updateData = {
1929
+ status: "in_progress" /* IN_PROGRESS */,
1930
+ procedureActualStartTime: import_firestore4.Timestamp.now(),
1931
+ // Set actual start time
1932
+ updatedAt: (0, import_firestore4.serverTimestamp)()
1933
+ };
1934
+ return this.updateAppointment(appointmentId, updateData);
1935
+ }
1936
+ /**
1937
+ * Doctor completes and finalizes the appointment.
1938
+ */
1939
+ async completeAppointmentDoctor(appointmentId, finalizationNotes, actualDurationMinutesInput) {
1940
+ console.log(`[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`);
1941
+ const currentUser = this.auth.currentUser;
1942
+ if (!currentUser) throw new Error("Authentication required to complete appointment.");
1943
+ const appointment = await this.getAppointmentById(appointmentId);
1944
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1945
+ let calculatedDurationMinutes = actualDurationMinutesInput;
1946
+ const procedureCompletionTime = import_firestore4.Timestamp.now();
1947
+ if (calculatedDurationMinutes === void 0 && appointment.procedureActualStartTime) {
1948
+ const startTimeMillis = appointment.procedureActualStartTime.toMillis();
1949
+ const endTimeMillis = procedureCompletionTime.toMillis();
1950
+ if (endTimeMillis > startTimeMillis) {
1951
+ calculatedDurationMinutes = Math.round((endTimeMillis - startTimeMillis) / 6e4);
1952
+ }
1953
+ }
1954
+ const updateData = {
1955
+ status: "completed" /* COMPLETED */,
1956
+ actualDurationMinutes: calculatedDurationMinutes,
1957
+ // Use calculated or provided duration
1958
+ finalizedDetails: {
1959
+ by: currentUser.uid,
1960
+ // This is used ID, not practitioner's profile ID (just so we know who completed the appointment)
1961
+ at: procedureCompletionTime,
1962
+ // Use consistent completion timestamp
1963
+ notes: finalizationNotes
1964
+ },
1965
+ // Optionally update appointmentEndTime to the actual completion time
1966
+ // appointmentEndTime: procedureCompletionTime,
1967
+ updatedAt: (0, import_firestore4.serverTimestamp)()
1968
+ };
1969
+ return this.updateAppointment(appointmentId, updateData);
1970
+ }
1971
+ /**
1972
+ * Admin marks an appointment as No-Show.
1973
+ */
1974
+ async markNoShowAdmin(appointmentId) {
1975
+ console.log(`[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`);
1976
+ const appointment = await this.getAppointmentById(appointmentId);
1977
+ if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
1978
+ if (import_firestore4.Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()) {
1979
+ throw new Error("Cannot mark no-show before appointment start time.");
1980
+ }
1981
+ return this.updateAppointmentStatus(appointmentId, "no_show" /* NO_SHOW */, {
1982
+ cancellationReason: "Patient did not show up for the appointment.",
1983
+ canceledBy: "clinic"
1984
+ });
1985
+ }
1986
+ /**
1987
+ * Adds a media item to an appointment.
1988
+ */
1989
+ async addMediaToAppointment(appointmentId, mediaItemData) {
1990
+ console.log(`[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`);
1991
+ const currentUser = this.auth.currentUser;
1992
+ if (!currentUser) throw new Error("Authentication required.");
1993
+ const newMediaItem = {
1994
+ ...mediaItemData,
1995
+ id: this.generateId(),
1996
+ uploadedAt: import_firestore4.Timestamp.now(),
1997
+ uploadedBy: currentUser.uid
1998
+ };
1999
+ const updateData = {
2000
+ media: (0, import_firestore4.arrayUnion)(newMediaItem),
2001
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2002
+ };
2003
+ return this.updateAppointment(appointmentId, updateData);
2004
+ }
2005
+ /**
2006
+ * Removes a media item from an appointment.
2007
+ */
2008
+ async removeMediaFromAppointment(appointmentId, mediaItemId) {
2009
+ console.log(
2010
+ `[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`
2011
+ );
2012
+ const appointment = await this.getAppointmentById(appointmentId);
2013
+ if (!appointment || !appointment.media) {
2014
+ throw new Error("Appointment or media list not found.");
2015
+ }
2016
+ const mediaToRemove = appointment.media.find((m) => m.id === mediaItemId);
2017
+ if (!mediaToRemove) {
2018
+ throw new Error(`Media item ${mediaItemId} not found in appointment.`);
2019
+ }
2020
+ const updateData = {
2021
+ media: (0, import_firestore4.arrayRemove)(mediaToRemove),
2022
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2023
+ };
2024
+ return this.updateAppointment(appointmentId, updateData);
2025
+ }
2026
+ /**
2027
+ * Adds or updates review information for an appointment.
2028
+ */
2029
+ async addReviewToAppointment(appointmentId, reviewData) {
2030
+ console.log(`[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`);
2031
+ const newReviewInfo = {
2032
+ ...reviewData,
2033
+ reviewId: this.generateId(),
2034
+ reviewedAt: import_firestore4.Timestamp.now()
2035
+ };
2036
+ const updateData = {
2037
+ reviewInfo: newReviewInfo,
2038
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2039
+ };
2040
+ return this.updateAppointment(appointmentId, updateData);
2041
+ }
2042
+ /**
2043
+ * Updates the payment status of an appointment.
2044
+ */
2045
+ async updatePaymentStatus(appointmentId, paymentStatus, paymentTransactionId) {
2046
+ console.log(
2047
+ `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
2048
+ );
2049
+ const updateData = {
2050
+ paymentStatus,
2051
+ paymentTransactionId: paymentTransactionId || null,
2052
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2053
+ };
2054
+ return this.updateAppointment(appointmentId, updateData);
2055
+ }
2056
+ /**
2057
+ * Updates the internal notes of an appointment.
2058
+ *
2059
+ * @param appointmentId ID of the appointment
2060
+ * @param notes Updated internal notes
2061
+ * @returns The updated appointment
2062
+ */
2063
+ async updateInternalNotes(appointmentId, notes) {
2064
+ console.log(`[APPOINTMENT_SERVICE] Updating internal notes for appointment: ${appointmentId}`);
2065
+ const updateData = {
2066
+ internalNotes: notes
2067
+ };
2068
+ return this.updateAppointment(appointmentId, updateData);
2069
+ }
2070
+ /**
2071
+ * Gets upcoming appointments for a specific patient.
2072
+ * These include appointments with statuses: PENDING, CONFIRMED, CHECKED_IN, IN_PROGRESS
2073
+ *
2074
+ * @param patientId ID of the patient
2075
+ * @param options Optional parameters for filtering and pagination
2076
+ * @returns Found appointments and the last document for pagination
2077
+ */
2078
+ async getUpcomingPatientAppointments(patientId, options) {
2079
+ try {
2080
+ console.log(`[APPOINTMENT_SERVICE] Getting upcoming appointments for patient: ${patientId}`);
2081
+ const effectiveStartDate = (options == null ? void 0 : options.startDate) || /* @__PURE__ */ new Date();
2082
+ const upcomingStatuses = [
2083
+ "pending" /* PENDING */,
2084
+ "confirmed" /* CONFIRMED */,
2085
+ "checked_in" /* CHECKED_IN */,
2086
+ "in_progress" /* IN_PROGRESS */,
2087
+ "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */
2088
+ ];
2089
+ const constraints = [];
2090
+ constraints.push((0, import_firestore4.where)("patientId", "==", patientId));
2091
+ constraints.push((0, import_firestore4.where)("status", "in", upcomingStatuses));
2092
+ constraints.push((0, import_firestore4.where)("appointmentStartTime", ">=", import_firestore4.Timestamp.fromDate(effectiveStartDate)));
2093
+ if (options == null ? void 0 : options.endDate) {
2094
+ constraints.push((0, import_firestore4.where)("appointmentStartTime", "<=", import_firestore4.Timestamp.fromDate(options.endDate)));
2095
+ }
2096
+ constraints.push((0, import_firestore4.orderBy)("appointmentStartTime", "asc"));
2097
+ if (options == null ? void 0 : options.limit) {
2098
+ constraints.push((0, import_firestore4.limit)(options.limit));
2099
+ }
2100
+ if (options == null ? void 0 : options.startAfter) {
2101
+ constraints.push((0, import_firestore4.startAfter)(options.startAfter));
2102
+ }
2103
+ const q = (0, import_firestore4.query)((0, import_firestore4.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
2104
+ const querySnapshot = await (0, import_firestore4.getDocs)(q);
2105
+ const appointments = querySnapshot.docs.map((doc38) => doc38.data());
2106
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
2107
+ console.log(
2108
+ `[APPOINTMENT_SERVICE] Found ${appointments.length} upcoming appointments for patient ${patientId}`
2109
+ );
2110
+ return { appointments, lastDoc };
2111
+ } catch (error) {
2112
+ console.error(
2113
+ `[APPOINTMENT_SERVICE] Error getting upcoming appointments for patient ${patientId}:`,
2114
+ error
2115
+ );
2116
+ throw error;
2444
2117
  }
2445
- console.log("[MediaService] No metadata found for ID:", mediaId);
2446
- return null;
2447
2118
  }
2448
2119
  /**
2449
- * Get media metadata from Firestore by its public URL.
2450
- * @param url - The public URL of the media file.
2451
- * @returns Promise with the media metadata or null if not found.
2120
+ * Gets past appointments for a specific patient.
2121
+ * These include appointments with statuses: COMPLETED, CANCELED_PATIENT,
2122
+ * CANCELED_PATIENT_RESCHEDULED, CANCELED_CLINIC, NO_SHOW
2123
+ *
2124
+ * @param patientId ID of the patient
2125
+ * @param options Optional parameters for filtering and pagination
2126
+ * @returns Found appointments and the last document for pagination
2452
2127
  */
2453
- async getMediaMetadataByUrl(url) {
2454
- console.log(`[MediaService] Getting media metadata by URL: ${url}`);
2455
- const q = (0, import_firestore4.query)(
2456
- (0, import_firestore4.collection)(this.db, MEDIA_METADATA_COLLECTION),
2457
- (0, import_firestore4.where)("url", "==", url),
2458
- (0, import_firestore4.limit)(1)
2459
- );
2128
+ async getPastPatientAppointments(patientId, options) {
2460
2129
  try {
2461
- const querySnapshot = await (0, import_firestore4.getDocs)(q);
2462
- if (!querySnapshot.empty) {
2463
- const metadata = querySnapshot.docs[0].data();
2464
- console.log("[MediaService] Metadata found by URL:", metadata);
2465
- return metadata;
2130
+ console.log(`[APPOINTMENT_SERVICE] Getting past appointments for patient: ${patientId}`);
2131
+ const effectiveEndDate = (options == null ? void 0 : options.endDate) || /* @__PURE__ */ new Date();
2132
+ const pastStatuses = ["completed" /* COMPLETED */];
2133
+ if (options == null ? void 0 : options.showCanceled) {
2134
+ pastStatuses.push(
2135
+ "canceled_patient" /* CANCELED_PATIENT */,
2136
+ "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */,
2137
+ "canceled_clinic" /* CANCELED_CLINIC */
2138
+ );
2466
2139
  }
2467
- console.log("[MediaService] No metadata found for URL:", url);
2468
- return null;
2140
+ if (options == null ? void 0 : options.showNoShow) {
2141
+ pastStatuses.push("no_show" /* NO_SHOW */);
2142
+ }
2143
+ const constraints = [];
2144
+ constraints.push((0, import_firestore4.where)("patientId", "==", patientId));
2145
+ constraints.push((0, import_firestore4.where)("status", "in", pastStatuses));
2146
+ if (options == null ? void 0 : options.startDate) {
2147
+ constraints.push(
2148
+ (0, import_firestore4.where)("appointmentStartTime", ">=", import_firestore4.Timestamp.fromDate(options.startDate))
2149
+ );
2150
+ }
2151
+ constraints.push((0, import_firestore4.where)("appointmentStartTime", "<=", import_firestore4.Timestamp.fromDate(effectiveEndDate)));
2152
+ constraints.push((0, import_firestore4.orderBy)("appointmentStartTime", "desc"));
2153
+ if (options == null ? void 0 : options.limit) {
2154
+ constraints.push((0, import_firestore4.limit)(options.limit));
2155
+ }
2156
+ if (options == null ? void 0 : options.startAfter) {
2157
+ constraints.push((0, import_firestore4.startAfter)(options.startAfter));
2158
+ }
2159
+ const q = (0, import_firestore4.query)((0, import_firestore4.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
2160
+ const querySnapshot = await (0, import_firestore4.getDocs)(q);
2161
+ const appointments = querySnapshot.docs.map((doc38) => doc38.data());
2162
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
2163
+ console.log(
2164
+ `[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}`
2165
+ );
2166
+ return { appointments, lastDoc };
2469
2167
  } catch (error) {
2470
- console.error("[MediaService] Error fetching metadata by URL:", error);
2168
+ console.error(
2169
+ `[APPOINTMENT_SERVICE] Error getting past appointments for patient ${patientId}:`,
2170
+ error
2171
+ );
2471
2172
  throw error;
2472
2173
  }
2473
2174
  }
2474
2175
  /**
2475
- * Delete media from storage and remove metadata from Firestore.
2476
- * @param mediaId - ID of the media to delete.
2176
+ * Counts completed appointments for a patient with optional clinic filtering.
2177
+ *
2178
+ * @param patientId ID of the patient.
2179
+ * @param clinicBranchId Optional ID of the clinic branch to either include or exclude.
2180
+ * @param excludeClinic Optional boolean. If true (default), excludes the specified clinic. If false, includes only that clinic.
2181
+ * @returns The count of completed appointments.
2477
2182
  */
2478
- async deleteMedia(mediaId) {
2479
- console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
2480
- const metadata = await this.getMediaMetadata(mediaId);
2481
- if (!metadata) {
2482
- console.warn(
2483
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
2484
- );
2485
- return;
2486
- }
2487
- const storageFileRef = (0, import_storage2.ref)(this.storage, metadata.path);
2183
+ async countCompletedAppointments(patientId, clinicBranchId, excludeClinic = true) {
2488
2184
  try {
2489
- await (0, import_storage2.deleteObject)(storageFileRef);
2490
- console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
2491
- const metadataDocRef = (0, import_firestore4.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
2492
- await (0, import_firestore4.deleteDoc)(metadataDocRef);
2493
2185
  console.log(
2494
- `[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
2186
+ `[APPOINTMENT_SERVICE] Counting completed appointments for patient: ${patientId}`,
2187
+ { clinicBranchId, excludeClinic }
2188
+ );
2189
+ const constraints = [
2190
+ (0, import_firestore4.where)("patientId", "==", patientId),
2191
+ (0, import_firestore4.where)("status", "==", "completed" /* COMPLETED */)
2192
+ ];
2193
+ if (clinicBranchId) {
2194
+ if (excludeClinic) {
2195
+ constraints.push((0, import_firestore4.where)("clinicBranchId", "!=", clinicBranchId));
2196
+ } else {
2197
+ constraints.push((0, import_firestore4.where)("clinicBranchId", "==", clinicBranchId));
2198
+ }
2199
+ }
2200
+ const q = (0, import_firestore4.query)((0, import_firestore4.collection)(this.db, APPOINTMENTS_COLLECTION), ...constraints);
2201
+ const snapshot = await (0, import_firestore4.getCountFromServer)(q);
2202
+ const count = snapshot.data().count;
2203
+ console.log(
2204
+ `[APPOINTMENT_SERVICE] Found ${count} completed appointments for patient ${patientId}`
2495
2205
  );
2206
+ return count;
2496
2207
  } catch (error) {
2497
- console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
2208
+ console.error(
2209
+ `[APPOINTMENT_SERVICE] Error counting completed appointments for patient ${patientId}:`,
2210
+ error
2211
+ );
2498
2212
  throw error;
2499
2213
  }
2500
2214
  }
2501
2215
  /**
2502
- * Update media access level. This involves moving the file in Firebase Storage
2503
- * to a new path reflecting the new access level, and updating its metadata.
2504
- * @param mediaId - ID of the media to update.
2505
- * @param newAccessLevel - New access level.
2506
- * @returns Promise with the updated media metadata, or null if metadata not found.
2216
+ * Uploads a zone photo and updates appointment metadata
2217
+ *
2218
+ * @param uploadData Zone photo upload data containing appointment ID, zone ID, photo type, file, and optional notes
2219
+ * @returns The uploaded media metadata
2507
2220
  */
2508
- async updateMediaAccessLevel(mediaId, newAccessLevel) {
2221
+ async uploadZonePhoto(uploadData) {
2509
2222
  var _a;
2510
- console.log(
2511
- `[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
2512
- );
2513
- const metadata = await this.getMediaMetadata(mediaId);
2514
- if (!metadata) {
2515
- console.warn(
2516
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
2517
- );
2518
- return null;
2519
- }
2520
- if (metadata.accessLevel === newAccessLevel) {
2223
+ try {
2521
2224
  console.log(
2522
- `[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
2225
+ `[APPOINTMENT_SERVICE] Uploading ${uploadData.photoType} photo for zone ${uploadData.zoneId} in appointment ${uploadData.appointmentId}`
2523
2226
  );
2524
- const metadataDocRef = (0, import_firestore4.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
2525
- try {
2526
- await (0, import_firestore4.updateDoc)(metadataDocRef, { updatedAt: import_firestore3.Timestamp.now() });
2527
- return { ...metadata, updatedAt: import_firestore3.Timestamp.now() };
2528
- } catch (error) {
2529
- console.error(
2530
- `[MediaService] Error updating timestamp for media ID ${mediaId}:`,
2531
- error
2532
- );
2533
- throw error;
2227
+ const validatedData = await zonePhotoUploadSchema.parseAsync(uploadData);
2228
+ const currentUser = this.auth.currentUser;
2229
+ if (!currentUser) {
2230
+ throw new Error("User must be authenticated to upload zone photos");
2534
2231
  }
2535
- }
2536
- const oldStoragePath = metadata.path;
2537
- const fileNamePart = `${metadata.id}-${metadata.name}`;
2538
- const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
2539
- console.log(
2540
- `[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
2541
- );
2542
- const oldStorageFileRef = (0, import_storage2.ref)(this.storage, oldStoragePath);
2543
- const newStorageFileRef = (0, import_storage2.ref)(this.storage, newStoragePath);
2544
- try {
2545
- console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
2546
- const fileBytes = await (0, import_storage2.getBytes)(oldStorageFileRef);
2232
+ const appointment = await this.getAppointmentById(validatedData.appointmentId);
2233
+ if (!appointment) {
2234
+ throw new Error(`Appointment with ID ${validatedData.appointmentId} not found`);
2235
+ }
2236
+ const collectionName = `appointment_${validatedData.appointmentId}_zone_photos`;
2237
+ const timestamp = Date.now();
2238
+ const fileExtension = ((_a = validatedData.file.type) == null ? void 0 : _a.split("/")[1]) || "jpg";
2239
+ const fileName = `${validatedData.photoType}_${validatedData.zoneId}_${timestamp}.${fileExtension}`;
2547
2240
  console.log(
2548
- `[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
2241
+ `[APPOINTMENT_SERVICE] Uploading file: ${fileName} to collection: ${collectionName}`
2242
+ );
2243
+ const uploadedMedia = await this.mediaService.uploadMedia(
2244
+ validatedData.file,
2245
+ validatedData.appointmentId,
2246
+ // ownerId is the appointment ID
2247
+ "private" /* PRIVATE */,
2248
+ // Zone photos are private
2249
+ collectionName,
2250
+ fileName
2251
+ );
2252
+ console.log(`[APPOINTMENT_SERVICE] Media uploaded successfully with ID: ${uploadedMedia.id}`);
2253
+ await this.updateAppointmentZonePhoto(
2254
+ validatedData.appointmentId,
2255
+ validatedData.zoneId,
2256
+ validatedData.photoType,
2257
+ uploadedMedia,
2258
+ validatedData.notes
2549
2259
  );
2550
- console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
2551
- await (0, import_storage2.uploadBytes)(newStorageFileRef, fileBytes, {
2552
- contentType: metadata.contentType
2553
- });
2554
2260
  console.log(
2555
- `[MediaService] Successfully uploaded bytes to ${newStoragePath}`
2261
+ `[APPOINTMENT_SERVICE] Successfully uploaded and linked ${validatedData.photoType} photo for zone ${validatedData.zoneId}`
2556
2262
  );
2557
- const newDownloadURL = await (0, import_storage2.getDownloadURL)(newStorageFileRef);
2263
+ return uploadedMedia;
2264
+ } catch (error) {
2265
+ console.error("[APPOINTMENT_SERVICE] Error uploading zone photo:", error);
2266
+ throw error;
2267
+ }
2268
+ }
2269
+ /**
2270
+ * Updates appointment metadata with zone photo information
2271
+ *
2272
+ * @param appointmentId ID of the appointment
2273
+ * @param zoneId ID of the zone
2274
+ * @param photoType Type of photo ('before' or 'after')
2275
+ * @param mediaMetadata Uploaded media metadata
2276
+ * @param notes Optional notes for the photo
2277
+ * @returns The updated appointment
2278
+ */
2279
+ async updateAppointmentZonePhoto(appointmentId, zoneId, photoType, mediaMetadata, notes) {
2280
+ try {
2558
2281
  console.log(
2559
- `[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
2282
+ `[APPOINTMENT_SERVICE] Updating appointment metadata for ${photoType} photo in zone ${zoneId}`
2560
2283
  );
2284
+ const appointment = await this.getAppointmentById(appointmentId);
2285
+ if (!appointment) {
2286
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
2287
+ }
2288
+ const currentMetadata = appointment.metadata || {
2289
+ selectedZones: null,
2290
+ zonePhotos: null,
2291
+ zoneBilling: null,
2292
+ finalbilling: null,
2293
+ finalizationNotes: null
2294
+ };
2295
+ const currentZonePhotos = currentMetadata.zonePhotos || {};
2296
+ if (!currentZonePhotos[zoneId]) {
2297
+ currentZonePhotos[zoneId] = {
2298
+ before: null,
2299
+ after: null,
2300
+ beforeNote: null,
2301
+ afterNote: null
2302
+ };
2303
+ }
2304
+ if (photoType === "before") {
2305
+ currentZonePhotos[zoneId].before = mediaMetadata.url;
2306
+ if (notes) {
2307
+ currentZonePhotos[zoneId].beforeNote = notes;
2308
+ }
2309
+ } else {
2310
+ currentZonePhotos[zoneId].after = mediaMetadata.url;
2311
+ if (notes) {
2312
+ currentZonePhotos[zoneId].afterNote = notes;
2313
+ }
2314
+ }
2561
2315
  const updateData = {
2562
- accessLevel: newAccessLevel,
2563
- path: newStoragePath,
2564
- url: newDownloadURL,
2565
- updatedAt: import_firestore3.Timestamp.now()
2316
+ metadata: {
2317
+ selectedZones: currentMetadata.selectedZones,
2318
+ zonePhotos: currentZonePhotos,
2319
+ zoneBilling: currentMetadata.zoneBilling,
2320
+ finalbilling: currentMetadata.finalbilling,
2321
+ finalizationNotes: currentMetadata.finalizationNotes
2322
+ },
2323
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2566
2324
  };
2567
- const metadataDocRef = (0, import_firestore4.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
2568
- console.log(
2569
- `[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
2570
- updateData
2571
- );
2572
- await (0, import_firestore4.updateDoc)(metadataDocRef, updateData);
2325
+ const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
2573
2326
  console.log(
2574
- `[MediaService] Successfully updated Firestore metadata for ${mediaId}`
2327
+ `[APPOINTMENT_SERVICE] Successfully updated appointment metadata for ${photoType} photo in zone ${zoneId}`
2575
2328
  );
2576
- try {
2577
- console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
2578
- await (0, import_storage2.deleteObject)(oldStorageFileRef);
2579
- console.log(
2580
- `[MediaService] Successfully deleted old file from ${oldStoragePath}`
2581
- );
2582
- } catch (deleteError) {
2583
- console.error(
2584
- `[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
2585
- deleteError
2586
- );
2587
- }
2588
- return { ...metadata, ...updateData };
2329
+ return updatedAppointment;
2589
2330
  } catch (error) {
2590
2331
  console.error(
2591
- `[MediaService] Error updating media access level and moving file for ${mediaId}:`,
2332
+ `[APPOINTMENT_SERVICE] Error updating appointment metadata for zone photo:`,
2592
2333
  error
2593
2334
  );
2594
- if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
2595
- console.warn(
2596
- `[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
2597
- );
2598
- try {
2599
- await (0, import_storage2.deleteObject)(newStorageFileRef);
2600
- console.warn(
2601
- `[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
2602
- );
2603
- } catch (cleanupError) {
2604
- console.error(
2605
- `[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
2606
- cleanupError
2607
- );
2608
- }
2609
- }
2610
2335
  throw error;
2611
2336
  }
2612
2337
  }
2613
2338
  /**
2614
- * List all media for an owner, optionally filtered by collection and access level.
2615
- * @param ownerId - ID of the owner.
2616
- * @param collectionName - Optional: Filter by collection name.
2617
- * @param accessLevel - Optional: Filter by access level.
2618
- * @param count - Optional: Number of items to fetch.
2619
- * @param startAfterId - Optional: ID of the document to start after (for pagination).
2339
+ * Gets zone photos for a specific appointment and zone
2340
+ *
2341
+ * @param appointmentId ID of the appointment
2342
+ * @param zoneId ID of the zone (optional - if not provided, returns all zones)
2343
+ * @returns Zone photos data
2620
2344
  */
2621
- async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
2622
- console.log(`[MediaService] Listing media for owner: ${ownerId}`);
2623
- let qConstraints = [(0, import_firestore4.where)("ownerId", "==", ownerId)];
2624
- if (collectionName) {
2625
- qConstraints.push((0, import_firestore4.where)("collectionName", "==", collectionName));
2626
- }
2627
- if (accessLevel) {
2628
- qConstraints.push((0, import_firestore4.where)("accessLevel", "==", accessLevel));
2629
- }
2630
- qConstraints.push((0, import_firestore4.orderBy)("createdAt", "desc"));
2631
- if (count) {
2632
- qConstraints.push((0, import_firestore4.limit)(count));
2345
+ async getZonePhotos(appointmentId, zoneId) {
2346
+ var _a;
2347
+ try {
2348
+ console.log(`[APPOINTMENT_SERVICE] Getting zone photos for appointment ${appointmentId}`);
2349
+ const appointment = await this.getAppointmentById(appointmentId);
2350
+ if (!appointment) {
2351
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
2352
+ }
2353
+ const zonePhotos = (_a = appointment.metadata) == null ? void 0 : _a.zonePhotos;
2354
+ if (!zonePhotos) {
2355
+ return null;
2356
+ }
2357
+ if (zoneId) {
2358
+ return zonePhotos[zoneId] || null;
2359
+ }
2360
+ return zonePhotos;
2361
+ } catch (error) {
2362
+ console.error(`[APPOINTMENT_SERVICE] Error getting zone photos:`, error);
2363
+ throw error;
2633
2364
  }
2634
- if (startAfterId) {
2635
- const startAfterDoc = await this.getMediaMetadata(startAfterId);
2636
- if (startAfterDoc) {
2365
+ }
2366
+ /**
2367
+ * Deletes a zone photo and updates appointment metadata
2368
+ *
2369
+ * @param appointmentId ID of the appointment
2370
+ * @param zoneId ID of the zone
2371
+ * @param photoType Type of photo to delete ('before' or 'after')
2372
+ * @returns The updated appointment
2373
+ */
2374
+ async deleteZonePhoto(appointmentId, zoneId, photoType) {
2375
+ var _a, _b, _c, _d, _e;
2376
+ try {
2377
+ console.log(
2378
+ `[APPOINTMENT_SERVICE] Deleting ${photoType} photo for zone ${zoneId} in appointment ${appointmentId}`
2379
+ );
2380
+ const appointment = await this.getAppointmentById(appointmentId);
2381
+ if (!appointment) {
2382
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
2383
+ }
2384
+ const zonePhotos = (_a = appointment.metadata) == null ? void 0 : _a.zonePhotos;
2385
+ if (!zonePhotos || !zonePhotos[zoneId]) {
2386
+ throw new Error(`No photos found for zone ${zoneId} in appointment ${appointmentId}`);
2387
+ }
2388
+ const photoUrl = photoType === "before" ? zonePhotos[zoneId].before : zonePhotos[zoneId].after;
2389
+ if (!photoUrl) {
2390
+ throw new Error(`No ${photoType} photo found for zone ${zoneId}`);
2637
2391
  }
2638
- }
2639
- const finalQuery = (0, import_firestore4.query)(
2640
- (0, import_firestore4.collection)(this.db, MEDIA_METADATA_COLLECTION),
2641
- ...qConstraints
2642
- );
2643
- try {
2644
- const querySnapshot = await (0, import_firestore4.getDocs)(finalQuery);
2645
- const mediaList = querySnapshot.docs.map(
2646
- (doc38) => doc38.data()
2392
+ try {
2393
+ if (typeof photoUrl === "string") {
2394
+ const mediaMetadata = await this.mediaService.getMediaMetadataByUrl(photoUrl);
2395
+ if (mediaMetadata) {
2396
+ await this.mediaService.deleteMedia(mediaMetadata.id);
2397
+ console.log(`[APPOINTMENT_SERVICE] Deleted media file with ID: ${mediaMetadata.id}`);
2398
+ }
2399
+ }
2400
+ } catch (mediaError) {
2401
+ console.warn(
2402
+ `[APPOINTMENT_SERVICE] Could not delete media file for URL ${photoUrl}:`,
2403
+ mediaError
2404
+ );
2405
+ }
2406
+ const updatedZonePhotos = { ...zonePhotos };
2407
+ if (photoType === "before") {
2408
+ updatedZonePhotos[zoneId].before = null;
2409
+ updatedZonePhotos[zoneId].beforeNote = null;
2410
+ } else {
2411
+ updatedZonePhotos[zoneId].after = null;
2412
+ updatedZonePhotos[zoneId].afterNote = null;
2413
+ }
2414
+ if (!updatedZonePhotos[zoneId].before && !updatedZonePhotos[zoneId].after) {
2415
+ delete updatedZonePhotos[zoneId];
2416
+ }
2417
+ const updateData = {
2418
+ metadata: {
2419
+ selectedZones: ((_b = appointment.metadata) == null ? void 0 : _b.selectedZones) || null,
2420
+ zonePhotos: updatedZonePhotos,
2421
+ zoneBilling: ((_c = appointment.metadata) == null ? void 0 : _c.zoneBilling) || null,
2422
+ finalbilling: ((_d = appointment.metadata) == null ? void 0 : _d.finalbilling) || null,
2423
+ finalizationNotes: ((_e = appointment.metadata) == null ? void 0 : _e.finalizationNotes) || null
2424
+ },
2425
+ updatedAt: (0, import_firestore4.serverTimestamp)()
2426
+ };
2427
+ const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
2428
+ console.log(
2429
+ `[APPOINTMENT_SERVICE] Successfully deleted ${photoType} photo for zone ${zoneId}`
2647
2430
  );
2648
- console.log(`[MediaService] Found ${mediaList.length} media items.`);
2649
- return mediaList;
2431
+ return updatedAppointment;
2650
2432
  } catch (error) {
2651
- console.error("[MediaService] Error listing media:", error);
2433
+ console.error(`[APPOINTMENT_SERVICE] Error deleting zone photo:`, error);
2652
2434
  throw error;
2653
2435
  }
2654
2436
  }
2655
- /**
2656
- * Get download URL for media. (Convenience, as URL is in metadata)
2657
- * @param mediaId - ID of the media.
2658
- */
2659
- async getMediaDownloadUrl(mediaId) {
2660
- console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
2661
- const metadata = await this.getMediaMetadata(mediaId);
2662
- if (metadata && metadata.url) {
2663
- console.log(`[MediaService] URL found: ${metadata.url}`);
2664
- return metadata.url;
2437
+ };
2438
+
2439
+ // src/services/auth/auth.service.ts
2440
+ var import_auth7 = require("firebase/auth");
2441
+ var import_firestore29 = require("firebase/firestore");
2442
+
2443
+ // src/types/user/index.ts
2444
+ var UserRole = /* @__PURE__ */ ((UserRole2) => {
2445
+ UserRole2["PATIENT"] = "patient";
2446
+ UserRole2["PRACTITIONER"] = "practitioner";
2447
+ UserRole2["APP_ADMIN"] = "app_admin";
2448
+ UserRole2["CLINIC_ADMIN"] = "clinic_admin";
2449
+ return UserRole2;
2450
+ })(UserRole || {});
2451
+ var USERS_COLLECTION = "users";
2452
+
2453
+ // src/types/calendar/synced-calendar.types.ts
2454
+ var SyncedCalendarProvider = /* @__PURE__ */ ((SyncedCalendarProvider3) => {
2455
+ SyncedCalendarProvider3["GOOGLE"] = "google";
2456
+ SyncedCalendarProvider3["OUTLOOK"] = "outlook";
2457
+ SyncedCalendarProvider3["APPLE"] = "apple";
2458
+ return SyncedCalendarProvider3;
2459
+ })(SyncedCalendarProvider || {});
2460
+ var SYNCED_CALENDARS_COLLECTION = "syncedCalendars";
2461
+
2462
+ // src/types/notifications/index.ts
2463
+ var NotificationType = /* @__PURE__ */ ((NotificationType3) => {
2464
+ NotificationType3["APPOINTMENT_REMINDER"] = "appointmentReminder";
2465
+ NotificationType3["APPOINTMENT_STATUS_CHANGE"] = "appointmentStatusChange";
2466
+ NotificationType3["APPOINTMENT_RESCHEDULED_PROPOSAL"] = "appointmentRescheduledProposal";
2467
+ NotificationType3["APPOINTMENT_CANCELLED"] = "appointmentCancelled";
2468
+ NotificationType3["PRE_REQUIREMENT_INSTRUCTION_DUE"] = "preRequirementInstructionDue";
2469
+ NotificationType3["POST_REQUIREMENT_INSTRUCTION_DUE"] = "postRequirementInstructionDue";
2470
+ NotificationType3["REQUIREMENT_INSTRUCTION_DUE"] = "requirementInstructionDue";
2471
+ NotificationType3["FORM_REMINDER"] = "formReminder";
2472
+ NotificationType3["FORM_SUBMISSION_CONFIRMATION"] = "formSubmissionConfirmation";
2473
+ NotificationType3["REVIEW_REQUEST"] = "reviewRequest";
2474
+ NotificationType3["PAYMENT_DUE"] = "paymentDue";
2475
+ NotificationType3["PAYMENT_CONFIRMATION"] = "paymentConfirmation";
2476
+ NotificationType3["PAYMENT_FAILED"] = "paymentFailed";
2477
+ NotificationType3["GENERAL_MESSAGE"] = "generalMessage";
2478
+ NotificationType3["ACCOUNT_NOTIFICATION"] = "accountNotification";
2479
+ return NotificationType3;
2480
+ })(NotificationType || {});
2481
+ var NOTIFICATIONS_COLLECTION = "notifications";
2482
+ var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
2483
+ NotificationStatus2["PENDING"] = "pending";
2484
+ NotificationStatus2["PROCESSING"] = "processing";
2485
+ NotificationStatus2["SENT"] = "sent";
2486
+ NotificationStatus2["FAILED"] = "failed";
2487
+ NotificationStatus2["DELIVERED"] = "delivered";
2488
+ NotificationStatus2["CANCELLED"] = "cancelled";
2489
+ NotificationStatus2["PARTIAL_SUCCESS"] = "partialSuccess";
2490
+ return NotificationStatus2;
2491
+ })(NotificationStatus || {});
2492
+
2493
+ // src/types/patient/allergies.ts
2494
+ var AllergyType = /* @__PURE__ */ ((AllergyType2) => {
2495
+ AllergyType2["MEDICATION"] = "medication";
2496
+ AllergyType2["FOOD"] = "food";
2497
+ AllergyType2["ENVIRONMENTAL"] = "environmental";
2498
+ AllergyType2["LATEX"] = "latex";
2499
+ AllergyType2["COSMETIC"] = "cosmetic";
2500
+ AllergyType2["OTHER"] = "other";
2501
+ return AllergyType2;
2502
+ })(AllergyType || {});
2503
+ var MedicationAllergySubtype = /* @__PURE__ */ ((MedicationAllergySubtype2) => {
2504
+ MedicationAllergySubtype2["ANTIBIOTICS"] = "antibiotics";
2505
+ MedicationAllergySubtype2["NSAIDS"] = "nsaids";
2506
+ MedicationAllergySubtype2["OPIOIDS"] = "opioids";
2507
+ MedicationAllergySubtype2["ANESTHETICS"] = "anesthetics";
2508
+ MedicationAllergySubtype2["VACCINES"] = "vaccines";
2509
+ MedicationAllergySubtype2["OTHER"] = "other";
2510
+ return MedicationAllergySubtype2;
2511
+ })(MedicationAllergySubtype || {});
2512
+ var FoodAllergySubtype = /* @__PURE__ */ ((FoodAllergySubtype2) => {
2513
+ FoodAllergySubtype2["NUTS"] = "nuts";
2514
+ FoodAllergySubtype2["SHELLFISH"] = "shellfish";
2515
+ FoodAllergySubtype2["DAIRY"] = "dairy";
2516
+ FoodAllergySubtype2["EGGS"] = "eggs";
2517
+ FoodAllergySubtype2["WHEAT"] = "wheat";
2518
+ FoodAllergySubtype2["SOY"] = "soy";
2519
+ FoodAllergySubtype2["FISH"] = "fish";
2520
+ FoodAllergySubtype2["FRUITS"] = "fruits";
2521
+ FoodAllergySubtype2["OTHER"] = "other";
2522
+ return FoodAllergySubtype2;
2523
+ })(FoodAllergySubtype || {});
2524
+ var EnvironmentalAllergySubtype = /* @__PURE__ */ ((EnvironmentalAllergySubtype2) => {
2525
+ EnvironmentalAllergySubtype2["POLLEN"] = "pollen";
2526
+ EnvironmentalAllergySubtype2["DUST"] = "dust";
2527
+ EnvironmentalAllergySubtype2["MOLD"] = "mold";
2528
+ EnvironmentalAllergySubtype2["PET_DANDER"] = "pet_dander";
2529
+ EnvironmentalAllergySubtype2["INSECTS"] = "insects";
2530
+ EnvironmentalAllergySubtype2["OTHER"] = "other";
2531
+ return EnvironmentalAllergySubtype2;
2532
+ })(EnvironmentalAllergySubtype || {});
2533
+ var CosmeticAllergySubtype = /* @__PURE__ */ ((CosmeticAllergySubtype2) => {
2534
+ CosmeticAllergySubtype2["FRAGRANCES"] = "fragrances";
2535
+ CosmeticAllergySubtype2["PRESERVATIVES"] = "preservatives";
2536
+ CosmeticAllergySubtype2["DYES"] = "dyes";
2537
+ CosmeticAllergySubtype2["METALS"] = "metals";
2538
+ CosmeticAllergySubtype2["OTHER"] = "other";
2539
+ return CosmeticAllergySubtype2;
2540
+ })(CosmeticAllergySubtype || {});
2541
+
2542
+ // src/types/patient/patient-requirements.ts
2543
+ var PatientInstructionStatus = /* @__PURE__ */ ((PatientInstructionStatus2) => {
2544
+ PatientInstructionStatus2["PENDING_NOTIFICATION"] = "pendingNotification";
2545
+ PatientInstructionStatus2["ACTION_DUE"] = "actionDue";
2546
+ PatientInstructionStatus2["ACTION_TAKEN"] = "actionTaken";
2547
+ PatientInstructionStatus2["MISSED"] = "missed";
2548
+ PatientInstructionStatus2["CANCELLED"] = "cancelled";
2549
+ PatientInstructionStatus2["SKIPPED"] = "skipped";
2550
+ return PatientInstructionStatus2;
2551
+ })(PatientInstructionStatus || {});
2552
+ var PatientRequirementOverallStatus = /* @__PURE__ */ ((PatientRequirementOverallStatus2) => {
2553
+ PatientRequirementOverallStatus2["ACTIVE"] = "active";
2554
+ PatientRequirementOverallStatus2["ALL_INSTRUCTIONS_MET"] = "allInstructionsMet";
2555
+ PatientRequirementOverallStatus2["PARTIALLY_COMPLETED"] = "partiallyCompleted";
2556
+ PatientRequirementOverallStatus2["FAILED"] = "failed";
2557
+ PatientRequirementOverallStatus2["CANCELLED_APPOINTMENT"] = "cancelledAppointment";
2558
+ PatientRequirementOverallStatus2["SUPERSEDED_RESCHEDULE"] = "supersededReschedule";
2559
+ PatientRequirementOverallStatus2["FAILED_TO_PROCESS"] = "failedToProcess";
2560
+ return PatientRequirementOverallStatus2;
2561
+ })(PatientRequirementOverallStatus || {});
2562
+ var PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME = "patientRequirements";
2563
+
2564
+ // src/types/patient/token.types.ts
2565
+ var INVITE_TOKENS_COLLECTION = "inviteTokens";
2566
+ var PatientTokenStatus = /* @__PURE__ */ ((PatientTokenStatus2) => {
2567
+ PatientTokenStatus2["ACTIVE"] = "active";
2568
+ PatientTokenStatus2["USED"] = "used";
2569
+ PatientTokenStatus2["EXPIRED"] = "expired";
2570
+ return PatientTokenStatus2;
2571
+ })(PatientTokenStatus || {});
2572
+
2573
+ // src/types/reviews/index.ts
2574
+ var REVIEWS_COLLECTION = "reviews";
2575
+
2576
+ // src/services/auth/auth.service.ts
2577
+ var import_zod23 = require("zod");
2578
+
2579
+ // src/validations/schemas.ts
2580
+ var import_zod4 = require("zod");
2581
+ var emailSchema = import_zod4.z.string().email("Invalid email format").min(5, "Email must be at least 5 characters").max(255, "Email must be less than 255 characters");
2582
+ var passwordSchema = import_zod4.z.string().min(8, "Password must be at least 8 characters").max(100, "Password must be less than 100 characters").regex(
2583
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/,
2584
+ "Password must contain at least one uppercase letter, one lowercase letter, and one number"
2585
+ );
2586
+ var userRoleSchema = import_zod4.z.nativeEnum(UserRole);
2587
+ var userRolesSchema = import_zod4.z.array(userRoleSchema).min(1, "User must have at least one role").max(3, "User cannot have more than 3 roles");
2588
+ var clinicAdminOptionsSchema = import_zod4.z.object({
2589
+ isGroupOwner: import_zod4.z.boolean(),
2590
+ groupToken: import_zod4.z.string().optional(),
2591
+ groupId: import_zod4.z.string().optional()
2592
+ }).refine(
2593
+ (data) => {
2594
+ if (!data.isGroupOwner && (!data.groupToken || !data.groupId)) {
2595
+ return false;
2665
2596
  }
2666
- console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
2667
- return null;
2597
+ if (data.isGroupOwner && (data.groupToken || data.groupId)) {
2598
+ return false;
2599
+ }
2600
+ return true;
2601
+ },
2602
+ {
2603
+ message: "Invalid clinic admin options configuration"
2604
+ }
2605
+ );
2606
+ var createUserOptionsSchema = import_zod4.z.object({
2607
+ clinicAdminData: clinicAdminOptionsSchema.optional()
2608
+ });
2609
+ var userSchema = import_zod4.z.object({
2610
+ uid: import_zod4.z.string(),
2611
+ email: import_zod4.z.string().email().nullable(),
2612
+ roles: import_zod4.z.array(userRoleSchema),
2613
+ isAnonymous: import_zod4.z.boolean(),
2614
+ createdAt: import_zod4.z.any(),
2615
+ updatedAt: import_zod4.z.any(),
2616
+ lastLoginAt: import_zod4.z.any(),
2617
+ patientProfile: import_zod4.z.string().optional(),
2618
+ practitionerProfile: import_zod4.z.string().optional(),
2619
+ adminProfile: import_zod4.z.string().optional()
2620
+ });
2621
+
2622
+ // src/errors/auth.errors.ts
2623
+ var AuthError = class extends Error {
2624
+ constructor(message, code, status = 400) {
2625
+ super(message);
2626
+ this.code = code;
2627
+ this.status = status;
2628
+ this.name = "AuthError";
2668
2629
  }
2669
2630
  };
2631
+ var AUTH_ERRORS = {
2632
+ // Basic validation errors
2633
+ INVALID_EMAIL: new AuthError(
2634
+ "Email address is not in a valid format",
2635
+ "AUTH/INVALID_EMAIL",
2636
+ 400
2637
+ ),
2638
+ INVALID_PASSWORD: new AuthError(
2639
+ "Password must contain at least 8 characters, one uppercase letter, one number, and one special character",
2640
+ "AUTH/INVALID_PASSWORD",
2641
+ 400
2642
+ ),
2643
+ INVALID_ROLE: new AuthError(
2644
+ "Specified user role is not valid",
2645
+ "AUTH/INVALID_ROLE",
2646
+ 400
2647
+ ),
2648
+ // Authentication errors
2649
+ NOT_AUTHENTICATED: new AuthError(
2650
+ "User is not authenticated",
2651
+ "AUTH/NOT_AUTHENTICATED",
2652
+ 401
2653
+ ),
2654
+ SESSION_EXPIRED: new AuthError(
2655
+ "Your session has expired. Please sign in again",
2656
+ "AUTH/SESSION_EXPIRED",
2657
+ 401
2658
+ ),
2659
+ INVALID_TOKEN: new AuthError(
2660
+ "Invalid authentication token",
2661
+ "AUTH/INVALID_TOKEN",
2662
+ 401
2663
+ ),
2664
+ // User state errors
2665
+ USER_NOT_FOUND: new AuthError(
2666
+ "User not found in the system",
2667
+ "AUTH/USER_NOT_FOUND",
2668
+ 404
2669
+ ),
2670
+ EMAIL_ALREADY_EXISTS: new AuthError(
2671
+ "An account with this email already exists",
2672
+ "AUTH/EMAIL_EXISTS",
2673
+ 409
2674
+ ),
2675
+ USER_DISABLED: new AuthError(
2676
+ "This account has been disabled",
2677
+ "AUTH/USER_DISABLED",
2678
+ 403
2679
+ ),
2680
+ // Rate limiting and security
2681
+ TOO_MANY_REQUESTS: new AuthError(
2682
+ "Too many login attempts. Please try again later",
2683
+ "AUTH/TOO_MANY_REQUESTS",
2684
+ 429
2685
+ ),
2686
+ ACCOUNT_LOCKED: new AuthError(
2687
+ "Account temporarily locked due to too many failed login attempts",
2688
+ "AUTH/ACCOUNT_LOCKED",
2689
+ 403
2690
+ ),
2691
+ // Social auth specific
2692
+ POPUP_CLOSED: new AuthError(
2693
+ "Authentication popup was closed before completion",
2694
+ "AUTH/POPUP_CLOSED",
2695
+ 400
2696
+ ),
2697
+ POPUP_BLOCKED: new AuthError(
2698
+ "Authentication popup was blocked by the browser",
2699
+ "AUTH/POPUP_BLOCKED",
2700
+ 400
2701
+ ),
2702
+ ACCOUNT_EXISTS: new AuthError(
2703
+ "An account already exists with different credentials",
2704
+ "AUTH/ACCOUNT_EXISTS",
2705
+ 409
2706
+ ),
2707
+ // Anonymous auth specific
2708
+ NOT_ANONYMOUS: new AuthError(
2709
+ "Current user is not anonymous",
2710
+ "AUTH/NOT_ANONYMOUS_USER",
2711
+ 400
2712
+ ),
2713
+ ANONYMOUS_UPGRADE_FAILED: new AuthError(
2714
+ "Failed to upgrade anonymous account",
2715
+ "AUTH/ANONYMOUS_UPGRADE_FAILED",
2716
+ 400
2717
+ ),
2718
+ // General errors
2719
+ VALIDATION_ERROR: new AuthError(
2720
+ "Data validation error occurred",
2721
+ "AUTH/VALIDATION_ERROR",
2722
+ 400
2723
+ ),
2724
+ OPERATION_NOT_ALLOWED: new AuthError(
2725
+ "This operation is not allowed",
2726
+ "AUTH/OPERATION_NOT_ALLOWED",
2727
+ 403
2728
+ ),
2729
+ NETWORK_ERROR: new AuthError(
2730
+ "Network error occurred. Please check your connection",
2731
+ "AUTH/NETWORK_ERROR",
2732
+ 503
2733
+ ),
2734
+ REQUIRES_RECENT_LOGIN: new AuthError(
2735
+ "This operation requires recent authentication. Please sign in again",
2736
+ "AUTH/REQUIRES_RECENT_LOGIN",
2737
+ 401
2738
+ ),
2739
+ INVALID_PROVIDER: new AuthError(
2740
+ "Invalid authentication provider",
2741
+ "AUTH/INVALID_PROVIDER",
2742
+ 400
2743
+ ),
2744
+ INVALID_CREDENTIAL: new AuthError(
2745
+ "The provided credentials are invalid or expired",
2746
+ "AUTH/INVALID_CREDENTIAL",
2747
+ 401
2748
+ ),
2749
+ // Resource not found
2750
+ NOT_FOUND: new AuthError(
2751
+ "The requested resource was not found",
2752
+ "AUTH/NOT_FOUND",
2753
+ 404
2754
+ ),
2755
+ // Detailed password validation errors
2756
+ PASSWORD_LENGTH_ERROR: new AuthError(
2757
+ "Password must be at least 8 characters long",
2758
+ "AUTH/PASSWORD_LENGTH_ERROR",
2759
+ 400
2760
+ ),
2761
+ PASSWORD_UPPERCASE_ERROR: new AuthError(
2762
+ "Password must contain at least one uppercase letter",
2763
+ "AUTH/PASSWORD_UPPERCASE_ERROR",
2764
+ 400
2765
+ ),
2766
+ PASSWORD_NUMBER_ERROR: new AuthError(
2767
+ "Password must contain at least one number",
2768
+ "AUTH/PASSWORD_NUMBER_ERROR",
2769
+ 400
2770
+ ),
2771
+ PASSWORD_SPECIAL_CHAR_ERROR: new AuthError(
2772
+ "Password must contain at least one special character",
2773
+ "AUTH/PASSWORD_SPECIAL_CHAR_ERROR",
2774
+ 400
2775
+ ),
2776
+ // Detailed email validation errors
2777
+ EMAIL_FORMAT_ERROR: new AuthError(
2778
+ "Invalid email format. Please enter a valid email address",
2779
+ "AUTH/EMAIL_FORMAT_ERROR",
2780
+ 400
2781
+ ),
2782
+ PASSWORD_VALIDATION_ERROR: new AuthError(
2783
+ "Password validation failed. Please check all requirements",
2784
+ "AUTH/PASSWORD_VALIDATION_ERROR",
2785
+ 400
2786
+ ),
2787
+ // Password reset specific errors
2788
+ EXPIRED_ACTION_CODE: new AuthError(
2789
+ "Kod za resetovanje lozinke je istekao. Molimo zatra\u017Eite novi link za resetovanje.",
2790
+ "AUTH/EXPIRED_ACTION_CODE",
2791
+ 400
2792
+ ),
2793
+ INVALID_ACTION_CODE: new AuthError(
2794
+ "Kod za resetovanje lozinke je neva\u017Ee\u0107i ili je ve\u0107 iskori\u0161\u0107en. Molimo zatra\u017Eite novi link za resetovanje.",
2795
+ "AUTH/INVALID_ACTION_CODE",
2796
+ 400
2797
+ ),
2798
+ WEAK_PASSWORD: new AuthError(
2799
+ "Lozinka je previ\u0161e slaba. Molimo koristite ja\u010Du lozinku.",
2800
+ "AUTH/WEAK_PASSWORD",
2801
+ 400
2802
+ )
2803
+ };
2804
+
2805
+ // src/services/user/user.service.ts
2806
+ var import_firestore22 = require("firebase/firestore");
2807
+
2808
+ // src/errors/user.errors.ts
2809
+ var USER_ERRORS = {
2810
+ // Basic user errors
2811
+ NOT_FOUND: new AuthError(
2812
+ "User not found in the system",
2813
+ "USER/NOT_FOUND",
2814
+ 404
2815
+ ),
2816
+ // Role management errors
2817
+ ROLE_EXISTS: new AuthError(
2818
+ "User already has this role assigned",
2819
+ "USER/ROLE_EXISTS",
2820
+ 400
2821
+ ),
2822
+ ROLE_NOT_FOUND: new AuthError(
2823
+ "User does not have this role assigned",
2824
+ "USER/ROLE_NOT_FOUND",
2825
+ 404
2826
+ ),
2827
+ MAX_ROLES_EXCEEDED: new AuthError(
2828
+ "User cannot have more than 3 roles",
2829
+ "USER/MAX_ROLES_EXCEEDED",
2830
+ 400
2831
+ ),
2832
+ MIN_ROLES_REQUIRED: new AuthError(
2833
+ "User must have at least one role",
2834
+ "USER/MIN_ROLES_REQUIRED",
2835
+ 400
2836
+ ),
2837
+ // Validation errors
2838
+ VALIDATION_ERROR: new AuthError(
2839
+ "Data validation error occurred",
2840
+ "USER/VALIDATION_ERROR",
2841
+ 400
2842
+ ),
2843
+ PASSWORD_VALIDATION_ERROR: new AuthError(
2844
+ "Password must contain at least 8 characters, one uppercase letter, one number, and one special character",
2845
+ "USER/PASSWORD_VALIDATION_ERROR",
2846
+ 400
2847
+ ),
2848
+ EMAIL_VALIDATION_ERROR: new AuthError(
2849
+ "Email address is not in a valid format",
2850
+ "USER/EMAIL_VALIDATION_ERROR",
2851
+ 400
2852
+ ),
2853
+ // Profile management errors
2854
+ PROFILE_UPDATE_ERROR: new AuthError(
2855
+ "Unable to update user profile",
2856
+ "USER/PROFILE_UPDATE_ERROR",
2857
+ 400
2858
+ ),
2859
+ PROFILE_DELETE_ERROR: new AuthError(
2860
+ "Unable to delete user profile",
2861
+ "USER/PROFILE_DELETE_ERROR",
2862
+ 400
2863
+ ),
2864
+ // Permission errors
2865
+ INSUFFICIENT_PERMISSIONS: new AuthError(
2866
+ "You don't have sufficient permissions for this action",
2867
+ "USER/INSUFFICIENT_PERMISSIONS",
2868
+ 403
2869
+ ),
2870
+ // Session errors
2871
+ SESSION_EXPIRED: new AuthError(
2872
+ "Your session has expired. Please sign in again",
2873
+ "USER/SESSION_EXPIRED",
2874
+ 401
2875
+ ),
2876
+ INVALID_TOKEN: new AuthError(
2877
+ "Invalid authentication token",
2878
+ "USER/INVALID_TOKEN",
2879
+ 401
2880
+ ),
2881
+ // Rate limiting
2882
+ TOO_MANY_REQUESTS: new AuthError(
2883
+ "Too many requests. Please try again later",
2884
+ "USER/TOO_MANY_REQUESTS",
2885
+ 429
2886
+ ),
2887
+ // Account state errors
2888
+ ACCOUNT_LOCKED: new AuthError(
2889
+ "Account is temporarily locked due to too many failed login attempts",
2890
+ "USER/ACCOUNT_LOCKED",
2891
+ 403
2892
+ ),
2893
+ ACCOUNT_DISABLED: new AuthError(
2894
+ "This account has been disabled",
2895
+ "USER/ACCOUNT_DISABLED",
2896
+ 403
2897
+ )
2898
+ };
2899
+
2900
+ // src/services/user/user.service.ts
2901
+ var import_zod17 = require("zod");
2670
2902
 
2671
2903
  // src/services/patient/patient.service.ts
2904
+ var import_firestore18 = require("firebase/firestore");
2672
2905
  var import_firestore19 = require("firebase/firestore");
2673
2906
 
2674
2907
  // src/services/patient/utils/clinic.utils.ts
@@ -16089,51 +16322,17 @@ var ReviewService = class extends BaseService {
16089
16322
  return docSnap.data();
16090
16323
  }
16091
16324
  /**
16092
- * Gets all reviews for a specific patient with enhanced entity names
16325
+ * Gets all reviews for a specific patient
16093
16326
  * @param patientId The ID of the patient
16094
- * @returns Array of reviews for the patient with clinic, practitioner, and procedure names
16327
+ * @returns Array of reviews for the patient
16095
16328
  */
16096
16329
  async getReviewsByPatient(patientId) {
16097
- const q = (0, import_firestore48.query)((0, import_firestore48.collection)(this.db, REVIEWS_COLLECTION), (0, import_firestore48.where)("patientId", "==", patientId));
16098
- const snapshot = await (0, import_firestore48.getDocs)(q);
16099
- const reviews = snapshot.docs.map((doc38) => doc38.data());
16100
- const enhancedReviews = await Promise.all(
16101
- reviews.map(async (review) => {
16102
- try {
16103
- const appointmentDoc = await (0, import_firestore48.getDoc)(
16104
- (0, import_firestore48.doc)(this.db, APPOINTMENTS_COLLECTION, review.appointmentId)
16105
- );
16106
- if (appointmentDoc.exists()) {
16107
- const appointment = appointmentDoc.data();
16108
- const enhancedReview = { ...review };
16109
- if (enhancedReview.clinicReview && appointment.clinicInfo) {
16110
- enhancedReview.clinicReview = {
16111
- ...enhancedReview.clinicReview,
16112
- clinicName: appointment.clinicInfo.name
16113
- };
16114
- }
16115
- if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
16116
- enhancedReview.practitionerReview = {
16117
- ...enhancedReview.practitionerReview,
16118
- practitionerName: appointment.practitionerInfo.name
16119
- };
16120
- }
16121
- if (enhancedReview.procedureReview && appointment.procedureInfo) {
16122
- enhancedReview.procedureReview = {
16123
- ...enhancedReview.procedureReview,
16124
- procedureName: appointment.procedureInfo.name
16125
- };
16126
- }
16127
- return enhancedReview;
16128
- }
16129
- return review;
16130
- } catch (error) {
16131
- console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
16132
- return review;
16133
- }
16134
- })
16330
+ const q = (0, import_firestore48.query)(
16331
+ (0, import_firestore48.collection)(this.db, REVIEWS_COLLECTION),
16332
+ (0, import_firestore48.where)("patientId", "==", patientId)
16135
16333
  );
16136
- return enhancedReviews;
16334
+ const snapshot = await (0, import_firestore48.getDocs)(q);
16335
+ return snapshot.docs.map((doc38) => doc38.data());
16137
16336
  }
16138
16337
  /**
16139
16338
  * Gets all reviews for a specific clinic