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