@blackcode_sa/metaestetics-api 1.12.20 → 1.12.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +14 -0
- package/dist/admin/index.d.ts +14 -0
- package/dist/admin/index.js +194 -180
- package/dist/admin/index.mjs +194 -180
- package/dist/index.d.mts +57 -9
- package/dist/index.d.ts +57 -9
- package/dist/index.js +2340 -1862
- package/dist/index.mjs +2412 -1934
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +459 -457
- package/src/services/appointment/appointment.service.ts +297 -0
- package/src/services/reviews/README.md +129 -0
- package/src/services/reviews/reviews.service.ts +331 -12
- package/src/types/appointment/index.ts +11 -0
- package/src/types/reviews/index.ts +2 -1
- package/src/validations/appointment.schema.ts +38 -18
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// src/services/appointment/appointment.service.ts
|
|
2
2
|
import {
|
|
3
|
-
Timestamp as
|
|
3
|
+
Timestamp as Timestamp3,
|
|
4
4
|
serverTimestamp as serverTimestamp2,
|
|
5
5
|
arrayUnion,
|
|
6
6
|
arrayRemove,
|
|
7
|
-
where as
|
|
8
|
-
orderBy as
|
|
9
|
-
collection as
|
|
10
|
-
query as
|
|
11
|
-
limit as
|
|
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
|
|
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/
|
|
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
|
-
|
|
603
|
+
setDoc,
|
|
604
|
+
updateDoc,
|
|
605
|
+
collection,
|
|
586
606
|
query,
|
|
587
607
|
where,
|
|
588
|
-
updateDoc,
|
|
589
|
-
serverTimestamp,
|
|
590
|
-
Timestamp,
|
|
591
|
-
orderBy,
|
|
592
608
|
limit,
|
|
593
|
-
|
|
609
|
+
getDocs,
|
|
610
|
+
deleteDoc,
|
|
611
|
+
orderBy
|
|
594
612
|
} from "firebase/firestore";
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (
|
|
990
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
* @param
|
|
1063
|
-
* @
|
|
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
|
|
1070
|
-
|
|
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
|
-
`[
|
|
758
|
+
`[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
|
|
1073
759
|
);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
`[
|
|
791
|
+
`[MediaService] Successfully uploaded bytes to ${newStoragePath}`
|
|
1116
792
|
);
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
* @param
|
|
1143
|
-
* @
|
|
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
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
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("[
|
|
887
|
+
console.error("[MediaService] Error listing media:", error);
|
|
1256
888
|
throw error;
|
|
1257
889
|
}
|
|
1258
890
|
}
|
|
1259
891
|
/**
|
|
1260
|
-
*
|
|
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
|
|
1389
|
-
console.log(`[
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
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";
|
|
1931
|
-
|
|
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 || {});
|
|
1940
|
-
|
|
1941
|
-
// src/types/reviews/index.ts
|
|
1942
|
-
var REVIEWS_COLLECTION = "reviews";
|
|
1943
|
-
|
|
1944
|
-
// src/services/auth/auth.service.ts
|
|
1945
|
-
import { z as z23 } from "zod";
|
|
1946
|
-
|
|
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
|
-
});
|
|
1989
|
-
|
|
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
|
-
)
|
|
2171
905
|
};
|
|
2172
906
|
|
|
2173
|
-
// src/services/
|
|
2174
|
-
import {
|
|
2175
|
-
collection as
|
|
2176
|
-
doc as
|
|
2177
|
-
getDoc as
|
|
2178
|
-
getDocs as
|
|
2179
|
-
query as
|
|
2180
|
-
where as
|
|
2181
|
-
updateDoc as
|
|
2182
|
-
|
|
2183
|
-
Timestamp as
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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";
|
|
922
|
+
|
|
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 || {});
|
|
954
|
+
|
|
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 || {});
|
|
970
|
+
|
|
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 || {});
|
|
1093
|
+
|
|
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 || {});
|
|
2187
1103
|
|
|
2188
|
-
// src/
|
|
2189
|
-
var
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
-
)
|
|
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: []
|
|
2278
1149
|
};
|
|
2279
1150
|
|
|
2280
|
-
// src/
|
|
2281
|
-
|
|
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 || {});
|
|
2282
1166
|
|
|
2283
|
-
// src/
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
1167
|
+
// src/types/procedure/index.ts
|
|
1168
|
+
var PROCEDURES_COLLECTION = "procedures";
|
|
1169
|
+
|
|
1170
|
+
// src/backoffice/types/technology.types.ts
|
|
1171
|
+
var TECHNOLOGIES_COLLECTION = "technologies";
|
|
1172
|
+
|
|
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/
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
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
|
+
});
|
|
2324
1712
|
}
|
|
2325
1713
|
/**
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
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.
|
|
1714
|
+
* Admin proposes to reschedule an appointment.
|
|
1715
|
+
* Sets status to RESCHEDULED_BY_CLINIC and updates times.
|
|
2333
1716
|
*/
|
|
2334
|
-
async
|
|
2335
|
-
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
return
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
|
|
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 */);
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Doctor starts the appointment procedure.
|
|
1820
|
+
*/
|
|
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
|
+
}
|
|
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);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Admin marks an appointment as No-Show.
|
|
1873
|
+
*/
|
|
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.");
|
|
1880
|
+
}
|
|
1881
|
+
return this.updateAppointmentStatus(appointmentId, "no_show" /* NO_SHOW */, {
|
|
1882
|
+
cancellationReason: "Patient did not show up for the appointment.",
|
|
1883
|
+
canceledBy: "clinic"
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Adds a media item to an appointment.
|
|
1888
|
+
*/
|
|
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.`);
|
|
2367
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}`
|
|
1948
|
+
);
|
|
1949
|
+
const updateData = {
|
|
1950
|
+
paymentStatus,
|
|
1951
|
+
paymentTransactionId: paymentTransactionId || null,
|
|
1952
|
+
updatedAt: serverTimestamp2()
|
|
1953
|
+
};
|
|
1954
|
+
return this.updateAppointment(appointmentId, updateData);
|
|
2368
1955
|
}
|
|
2369
1956
|
/**
|
|
2370
|
-
*
|
|
2371
|
-
*
|
|
2372
|
-
* @
|
|
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
|
|
2373
1962
|
*/
|
|
2374
|
-
async
|
|
2375
|
-
console.log(`[
|
|
2376
|
-
const
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
return docSnap.data();
|
|
2381
|
-
}
|
|
2382
|
-
console.log("[MediaService] No metadata found for ID:", mediaId);
|
|
2383
|
-
return null;
|
|
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);
|
|
2384
1969
|
}
|
|
2385
1970
|
/**
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
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
|
|
2389
1977
|
*/
|
|
2390
|
-
async
|
|
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)
|
|
2396
|
-
);
|
|
1978
|
+
async getUpcomingPatientAppointments(patientId, options) {
|
|
2397
1979
|
try {
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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)));
|
|
2403
1995
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
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 };
|
|
2406
2011
|
} catch (error) {
|
|
2407
|
-
console.error(
|
|
2012
|
+
console.error(
|
|
2013
|
+
`[APPOINTMENT_SERVICE] Error getting upcoming appointments for patient ${patientId}:`,
|
|
2014
|
+
error
|
|
2015
|
+
);
|
|
2408
2016
|
throw error;
|
|
2409
2017
|
}
|
|
2410
2018
|
}
|
|
2411
2019
|
/**
|
|
2412
|
-
*
|
|
2413
|
-
*
|
|
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
|
|
2414
2027
|
*/
|
|
2415
|
-
async
|
|
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);
|
|
2028
|
+
async getPastPatientAppointments(patientId, options) {
|
|
2425
2029
|
try {
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
const
|
|
2429
|
-
|
|
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
|
+
);
|
|
2039
|
+
}
|
|
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;
|
|
2430
2063
|
console.log(
|
|
2431
|
-
`[
|
|
2064
|
+
`[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}`
|
|
2432
2065
|
);
|
|
2066
|
+
return { appointments, lastDoc };
|
|
2433
2067
|
} catch (error) {
|
|
2434
|
-
console.error(
|
|
2068
|
+
console.error(
|
|
2069
|
+
`[APPOINTMENT_SERVICE] Error getting past appointments for patient ${patientId}:`,
|
|
2070
|
+
error
|
|
2071
|
+
);
|
|
2435
2072
|
throw error;
|
|
2436
2073
|
}
|
|
2437
2074
|
}
|
|
2438
2075
|
/**
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
* @param
|
|
2442
|
-
* @param
|
|
2443
|
-
* @
|
|
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.
|
|
2444
2082
|
*/
|
|
2445
|
-
async
|
|
2446
|
-
|
|
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) {
|
|
2083
|
+
async countCompletedAppointments(patientId, clinicBranchId, excludeClinic = true) {
|
|
2084
|
+
try {
|
|
2458
2085
|
console.log(
|
|
2459
|
-
`[
|
|
2086
|
+
`[APPOINTMENT_SERVICE] Counting completed appointments for patient: ${patientId}`,
|
|
2087
|
+
{ clinicBranchId, excludeClinic }
|
|
2460
2088
|
);
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
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
|
+
}
|
|
2471
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}`
|
|
2105
|
+
);
|
|
2106
|
+
return count;
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
console.error(
|
|
2109
|
+
`[APPOINTMENT_SERVICE] Error counting completed appointments for patient ${patientId}:`,
|
|
2110
|
+
error
|
|
2111
|
+
);
|
|
2112
|
+
throw error;
|
|
2472
2113
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
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
|
|
2120
|
+
*/
|
|
2121
|
+
async uploadZonePhoto(uploadData) {
|
|
2122
|
+
var _a;
|
|
2481
2123
|
try {
|
|
2482
|
-
console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
|
|
2483
|
-
const fileBytes = await getBytes(oldStorageFileRef);
|
|
2484
2124
|
console.log(
|
|
2485
|
-
`[
|
|
2125
|
+
`[APPOINTMENT_SERVICE] Uploading ${uploadData.photoType} photo for zone ${uploadData.zoneId} in appointment ${uploadData.appointmentId}`
|
|
2486
2126
|
);
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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");
|
|
2131
|
+
}
|
|
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}`;
|
|
2491
2140
|
console.log(
|
|
2492
|
-
`[
|
|
2141
|
+
`[APPOINTMENT_SERVICE] Uploading file: ${fileName} to collection: ${collectionName}`
|
|
2493
2142
|
);
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
|
2497
2159
|
);
|
|
2498
|
-
const updateData = {
|
|
2499
|
-
accessLevel: newAccessLevel,
|
|
2500
|
-
path: newStoragePath,
|
|
2501
|
-
url: newDownloadURL,
|
|
2502
|
-
updatedAt: Timestamp3.now()
|
|
2503
|
-
};
|
|
2504
|
-
const metadataDocRef = doc2(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
2505
2160
|
console.log(
|
|
2506
|
-
`[
|
|
2507
|
-
updateData
|
|
2161
|
+
`[APPOINTMENT_SERVICE] Successfully uploaded and linked ${validatedData.photoType} photo for zone ${validatedData.zoneId}`
|
|
2508
2162
|
);
|
|
2509
|
-
|
|
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 {
|
|
2510
2181
|
console.log(
|
|
2511
|
-
`[
|
|
2182
|
+
`[APPOINTMENT_SERVICE] Updating appointment metadata for ${photoType} photo in zone ${zoneId}`
|
|
2512
2183
|
);
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
+
}
|
|
2524
2214
|
}
|
|
2525
|
-
|
|
2215
|
+
const updateData = {
|
|
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()
|
|
2224
|
+
};
|
|
2225
|
+
const updatedAppointment = await this.updateAppointment(appointmentId, updateData);
|
|
2226
|
+
console.log(
|
|
2227
|
+
`[APPOINTMENT_SERVICE] Successfully updated appointment metadata for ${photoType} photo in zone ${zoneId}`
|
|
2228
|
+
);
|
|
2229
|
+
return updatedAppointment;
|
|
2526
2230
|
} catch (error) {
|
|
2527
2231
|
console.error(
|
|
2528
|
-
`[
|
|
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
|
-
*
|
|
2552
|
-
*
|
|
2553
|
-
* @param
|
|
2554
|
-
* @param
|
|
2555
|
-
* @
|
|
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
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
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;
|
|
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;
|
|
2570
2264
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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;
|
|
2276
|
+
try {
|
|
2277
|
+
console.log(
|
|
2278
|
+
`[APPOINTMENT_SERVICE] Deleting ${photoType} photo for zone ${zoneId} in appointment ${appointmentId}`
|
|
2279
|
+
);
|
|
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];
|
|
2574
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;
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
console.error(`[APPOINTMENT_SERVICE] Error deleting zone photo:`, error);
|
|
2334
|
+
throw error;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
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;
|
|
2575
2515
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
...qConstraints
|
|
2579
|
-
);
|
|
2580
|
-
try {
|
|
2581
|
-
const querySnapshot = await getDocs3(finalQuery);
|
|
2582
|
-
const mediaList = querySnapshot.docs.map(
|
|
2583
|
-
(doc38) => doc38.data()
|
|
2584
|
-
);
|
|
2585
|
-
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
2586
|
-
return mediaList;
|
|
2587
|
-
} catch (error) {
|
|
2588
|
-
console.error("[MediaService] Error listing media:", error);
|
|
2589
|
-
throw error;
|
|
2516
|
+
if (data.isGroupOwner && (data.groupToken || data.groupId)) {
|
|
2517
|
+
return false;
|
|
2590
2518
|
}
|
|
2519
|
+
return true;
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
message: "Invalid clinic admin options configuration"
|
|
2591
2523
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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
|
|
@@ -16258,7 +16491,17 @@ var ReviewService = class extends BaseService {
|
|
|
16258
16491
|
* @returns The created review
|
|
16259
16492
|
*/
|
|
16260
16493
|
async createReview(data, appointmentId) {
|
|
16494
|
+
var _a, _b, _c, _d, _e, _f;
|
|
16261
16495
|
try {
|
|
16496
|
+
console.log("\u{1F50D} ReviewService.createReview - Input data:", {
|
|
16497
|
+
appointmentId,
|
|
16498
|
+
hasClinicReview: !!data.clinicReview,
|
|
16499
|
+
hasPractitionerReview: !!data.practitionerReview,
|
|
16500
|
+
hasProcedureReview: !!data.procedureReview,
|
|
16501
|
+
practitionerId: (_a = data.practitionerReview) == null ? void 0 : _a.practitionerId,
|
|
16502
|
+
clinicId: (_b = data.clinicReview) == null ? void 0 : _b.clinicId,
|
|
16503
|
+
procedureId: (_c = data.procedureReview) == null ? void 0 : _c.procedureId
|
|
16504
|
+
});
|
|
16262
16505
|
const validatedData = createReviewSchema.parse(data);
|
|
16263
16506
|
const ratings = [];
|
|
16264
16507
|
if (data.clinicReview) {
|
|
@@ -16331,6 +16574,12 @@ var ReviewService = class extends BaseService {
|
|
|
16331
16574
|
createdAt: serverTimestamp26(),
|
|
16332
16575
|
updatedAt: serverTimestamp26()
|
|
16333
16576
|
});
|
|
16577
|
+
console.log("\u2705 ReviewService.createReview - Review saved to Firestore:", {
|
|
16578
|
+
reviewId,
|
|
16579
|
+
practitionerId: (_d = review.practitionerReview) == null ? void 0 : _d.practitionerId,
|
|
16580
|
+
clinicId: (_e = review.clinicReview) == null ? void 0 : _e.clinicId,
|
|
16581
|
+
procedureId: (_f = review.procedureReview) == null ? void 0 : _f.procedureId
|
|
16582
|
+
});
|
|
16334
16583
|
return review;
|
|
16335
16584
|
} catch (error) {
|
|
16336
16585
|
if (error instanceof z26.ZodError) {
|
|
@@ -16340,17 +16589,60 @@ var ReviewService = class extends BaseService {
|
|
|
16340
16589
|
}
|
|
16341
16590
|
}
|
|
16342
16591
|
/**
|
|
16343
|
-
* Gets a review by ID
|
|
16592
|
+
* Gets a review by ID with enhanced entity names
|
|
16344
16593
|
* @param reviewId The ID of the review to get
|
|
16345
|
-
* @returns The review if found, null otherwise
|
|
16594
|
+
* @returns The review with entity names if found, null otherwise
|
|
16346
16595
|
*/
|
|
16347
16596
|
async getReview(reviewId) {
|
|
16597
|
+
var _a, _b, _c;
|
|
16598
|
+
console.log("\u{1F50D} ReviewService.getReview - Getting review:", reviewId);
|
|
16348
16599
|
const docRef = doc31(this.db, REVIEWS_COLLECTION, reviewId);
|
|
16349
16600
|
const docSnap = await getDoc33(docRef);
|
|
16350
16601
|
if (!docSnap.exists()) {
|
|
16602
|
+
console.log("\u274C ReviewService.getReview - Review not found:", reviewId);
|
|
16351
16603
|
return null;
|
|
16352
16604
|
}
|
|
16353
|
-
|
|
16605
|
+
const review = { ...docSnap.data(), id: reviewId };
|
|
16606
|
+
try {
|
|
16607
|
+
const appointmentDoc = await getDoc33(
|
|
16608
|
+
doc31(this.db, APPOINTMENTS_COLLECTION, review.appointmentId)
|
|
16609
|
+
);
|
|
16610
|
+
if (appointmentDoc.exists()) {
|
|
16611
|
+
const appointment = appointmentDoc.data();
|
|
16612
|
+
const enhancedReview = { ...review };
|
|
16613
|
+
if (enhancedReview.clinicReview && appointment.clinicInfo) {
|
|
16614
|
+
enhancedReview.clinicReview = {
|
|
16615
|
+
...enhancedReview.clinicReview,
|
|
16616
|
+
clinicName: appointment.clinicInfo.name
|
|
16617
|
+
};
|
|
16618
|
+
}
|
|
16619
|
+
if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
|
|
16620
|
+
enhancedReview.practitionerReview = {
|
|
16621
|
+
...enhancedReview.practitionerReview,
|
|
16622
|
+
practitionerName: appointment.practitionerInfo.name
|
|
16623
|
+
};
|
|
16624
|
+
}
|
|
16625
|
+
if (enhancedReview.procedureReview && appointment.procedureInfo) {
|
|
16626
|
+
enhancedReview.procedureReview = {
|
|
16627
|
+
...enhancedReview.procedureReview,
|
|
16628
|
+
procedureName: appointment.procedureInfo.name
|
|
16629
|
+
};
|
|
16630
|
+
}
|
|
16631
|
+
if (appointment.patientInfo) {
|
|
16632
|
+
enhancedReview.patientName = appointment.patientInfo.fullName;
|
|
16633
|
+
}
|
|
16634
|
+
console.log("\u2705 ReviewService.getReview - Enhanced review:", {
|
|
16635
|
+
reviewId,
|
|
16636
|
+
hasEntityNames: !!(((_a = enhancedReview.clinicReview) == null ? void 0 : _a.clinicName) || ((_b = enhancedReview.practitionerReview) == null ? void 0 : _b.practitionerName) || ((_c = enhancedReview.procedureReview) == null ? void 0 : _c.procedureName) || enhancedReview.patientName)
|
|
16637
|
+
});
|
|
16638
|
+
return enhancedReview;
|
|
16639
|
+
}
|
|
16640
|
+
console.log("\u26A0\uFE0F ReviewService.getReview - Appointment not found for review:", reviewId);
|
|
16641
|
+
return review;
|
|
16642
|
+
} catch (error) {
|
|
16643
|
+
console.warn(`Failed to enhance review ${reviewId} with entity names:`, error);
|
|
16644
|
+
return review;
|
|
16645
|
+
}
|
|
16354
16646
|
}
|
|
16355
16647
|
/**
|
|
16356
16648
|
* Gets all reviews for a specific patient with enhanced entity names
|
|
@@ -16388,6 +16680,9 @@ var ReviewService = class extends BaseService {
|
|
|
16388
16680
|
procedureName: appointment.procedureInfo.name
|
|
16389
16681
|
};
|
|
16390
16682
|
}
|
|
16683
|
+
if (appointment.patientInfo) {
|
|
16684
|
+
enhancedReview.patientName = appointment.patientInfo.fullName;
|
|
16685
|
+
}
|
|
16391
16686
|
return enhancedReview;
|
|
16392
16687
|
}
|
|
16393
16688
|
return review;
|
|
@@ -16400,43 +16695,226 @@ var ReviewService = class extends BaseService {
|
|
|
16400
16695
|
return enhancedReviews;
|
|
16401
16696
|
}
|
|
16402
16697
|
/**
|
|
16403
|
-
* Gets all reviews for a specific clinic
|
|
16698
|
+
* Gets all reviews for a specific clinic with enhanced entity names
|
|
16404
16699
|
* @param clinicId The ID of the clinic
|
|
16405
|
-
* @returns Array of reviews containing clinic reviews
|
|
16700
|
+
* @returns Array of reviews containing clinic reviews with clinic, practitioner, and procedure names
|
|
16406
16701
|
*/
|
|
16407
16702
|
async getReviewsByClinic(clinicId) {
|
|
16703
|
+
console.log("\u{1F50D} ReviewService.getReviewsByClinic - Querying for clinic:", clinicId);
|
|
16408
16704
|
const q = query31(
|
|
16409
16705
|
collection31(this.db, REVIEWS_COLLECTION),
|
|
16410
16706
|
where31("clinicReview.clinicId", "==", clinicId)
|
|
16411
16707
|
);
|
|
16412
16708
|
const snapshot = await getDocs31(q);
|
|
16413
|
-
|
|
16709
|
+
const reviews = snapshot.docs.map((doc38) => {
|
|
16710
|
+
const data = doc38.data();
|
|
16711
|
+
return { ...data, id: doc38.id };
|
|
16712
|
+
});
|
|
16713
|
+
console.log("\u{1F50D} ReviewService.getReviewsByClinic - Found reviews before enhancement:", {
|
|
16714
|
+
clinicId,
|
|
16715
|
+
reviewCount: reviews.length,
|
|
16716
|
+
reviewIds: reviews.map((r) => r.id)
|
|
16717
|
+
});
|
|
16718
|
+
const enhancedReviews = await Promise.all(
|
|
16719
|
+
reviews.map(async (review) => {
|
|
16720
|
+
try {
|
|
16721
|
+
const appointmentDoc = await getDoc33(
|
|
16722
|
+
doc31(this.db, APPOINTMENTS_COLLECTION, review.appointmentId)
|
|
16723
|
+
);
|
|
16724
|
+
if (appointmentDoc.exists()) {
|
|
16725
|
+
const appointment = appointmentDoc.data();
|
|
16726
|
+
const enhancedReview = { ...review };
|
|
16727
|
+
if (enhancedReview.clinicReview && appointment.clinicInfo) {
|
|
16728
|
+
enhancedReview.clinicReview = {
|
|
16729
|
+
...enhancedReview.clinicReview,
|
|
16730
|
+
clinicName: appointment.clinicInfo.name
|
|
16731
|
+
};
|
|
16732
|
+
}
|
|
16733
|
+
if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
|
|
16734
|
+
enhancedReview.practitionerReview = {
|
|
16735
|
+
...enhancedReview.practitionerReview,
|
|
16736
|
+
practitionerName: appointment.practitionerInfo.name
|
|
16737
|
+
};
|
|
16738
|
+
}
|
|
16739
|
+
if (enhancedReview.procedureReview && appointment.procedureInfo) {
|
|
16740
|
+
enhancedReview.procedureReview = {
|
|
16741
|
+
...enhancedReview.procedureReview,
|
|
16742
|
+
procedureName: appointment.procedureInfo.name
|
|
16743
|
+
};
|
|
16744
|
+
}
|
|
16745
|
+
if (appointment.patientInfo) {
|
|
16746
|
+
enhancedReview.patientName = appointment.patientInfo.fullName;
|
|
16747
|
+
}
|
|
16748
|
+
return enhancedReview;
|
|
16749
|
+
}
|
|
16750
|
+
return review;
|
|
16751
|
+
} catch (error) {
|
|
16752
|
+
console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
|
|
16753
|
+
return review;
|
|
16754
|
+
}
|
|
16755
|
+
})
|
|
16756
|
+
);
|
|
16757
|
+
console.log("\u2705 ReviewService.getReviewsByClinic - Enhanced reviews:", {
|
|
16758
|
+
clinicId,
|
|
16759
|
+
reviewCount: enhancedReviews.length,
|
|
16760
|
+
reviewIds: enhancedReviews.map((r) => r.id),
|
|
16761
|
+
hasEntityNames: enhancedReviews.some(
|
|
16762
|
+
(r) => {
|
|
16763
|
+
var _a, _b, _c;
|
|
16764
|
+
return ((_a = r.clinicReview) == null ? void 0 : _a.clinicName) || ((_b = r.practitionerReview) == null ? void 0 : _b.practitionerName) || ((_c = r.procedureReview) == null ? void 0 : _c.procedureName) || r.patientName;
|
|
16765
|
+
}
|
|
16766
|
+
)
|
|
16767
|
+
});
|
|
16768
|
+
return enhancedReviews;
|
|
16414
16769
|
}
|
|
16415
16770
|
/**
|
|
16416
|
-
* Gets all reviews for a specific practitioner
|
|
16771
|
+
* Gets all reviews for a specific practitioner with enhanced entity names
|
|
16417
16772
|
* @param practitionerId The ID of the practitioner
|
|
16418
|
-
* @returns Array of reviews containing practitioner reviews
|
|
16773
|
+
* @returns Array of reviews containing practitioner reviews with clinic, practitioner, and procedure names
|
|
16419
16774
|
*/
|
|
16420
16775
|
async getReviewsByPractitioner(practitionerId) {
|
|
16776
|
+
console.log(
|
|
16777
|
+
"\u{1F50D} ReviewService.getReviewsByPractitioner - Querying for practitioner:",
|
|
16778
|
+
practitionerId
|
|
16779
|
+
);
|
|
16421
16780
|
const q = query31(
|
|
16422
16781
|
collection31(this.db, REVIEWS_COLLECTION),
|
|
16423
16782
|
where31("practitionerReview.practitionerId", "==", practitionerId)
|
|
16424
16783
|
);
|
|
16425
16784
|
const snapshot = await getDocs31(q);
|
|
16426
|
-
|
|
16785
|
+
const reviews = snapshot.docs.map((doc38) => {
|
|
16786
|
+
const data = doc38.data();
|
|
16787
|
+
return { ...data, id: doc38.id };
|
|
16788
|
+
});
|
|
16789
|
+
console.log("\u{1F50D} ReviewService.getReviewsByPractitioner - Found reviews before enhancement:", {
|
|
16790
|
+
practitionerId,
|
|
16791
|
+
reviewCount: reviews.length,
|
|
16792
|
+
reviewIds: reviews.map((r) => r.id)
|
|
16793
|
+
});
|
|
16794
|
+
const enhancedReviews = await Promise.all(
|
|
16795
|
+
reviews.map(async (review) => {
|
|
16796
|
+
try {
|
|
16797
|
+
const appointmentDoc = await getDoc33(
|
|
16798
|
+
doc31(this.db, APPOINTMENTS_COLLECTION, review.appointmentId)
|
|
16799
|
+
);
|
|
16800
|
+
if (appointmentDoc.exists()) {
|
|
16801
|
+
const appointment = appointmentDoc.data();
|
|
16802
|
+
const enhancedReview = { ...review };
|
|
16803
|
+
if (enhancedReview.clinicReview && appointment.clinicInfo) {
|
|
16804
|
+
enhancedReview.clinicReview = {
|
|
16805
|
+
...enhancedReview.clinicReview,
|
|
16806
|
+
clinicName: appointment.clinicInfo.name
|
|
16807
|
+
};
|
|
16808
|
+
}
|
|
16809
|
+
if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
|
|
16810
|
+
enhancedReview.practitionerReview = {
|
|
16811
|
+
...enhancedReview.practitionerReview,
|
|
16812
|
+
practitionerName: appointment.practitionerInfo.name
|
|
16813
|
+
};
|
|
16814
|
+
}
|
|
16815
|
+
if (enhancedReview.procedureReview && appointment.procedureInfo) {
|
|
16816
|
+
enhancedReview.procedureReview = {
|
|
16817
|
+
...enhancedReview.procedureReview,
|
|
16818
|
+
procedureName: appointment.procedureInfo.name
|
|
16819
|
+
};
|
|
16820
|
+
}
|
|
16821
|
+
if (appointment.patientInfo) {
|
|
16822
|
+
enhancedReview.patientName = appointment.patientInfo.fullName;
|
|
16823
|
+
}
|
|
16824
|
+
return enhancedReview;
|
|
16825
|
+
}
|
|
16826
|
+
return review;
|
|
16827
|
+
} catch (error) {
|
|
16828
|
+
console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
|
|
16829
|
+
return review;
|
|
16830
|
+
}
|
|
16831
|
+
})
|
|
16832
|
+
);
|
|
16833
|
+
console.log("\u2705 ReviewService.getReviewsByPractitioner - Enhanced reviews:", {
|
|
16834
|
+
practitionerId,
|
|
16835
|
+
reviewCount: enhancedReviews.length,
|
|
16836
|
+
reviewIds: enhancedReviews.map((r) => r.id),
|
|
16837
|
+
hasEntityNames: enhancedReviews.some(
|
|
16838
|
+
(r) => {
|
|
16839
|
+
var _a, _b, _c;
|
|
16840
|
+
return ((_a = r.clinicReview) == null ? void 0 : _a.clinicName) || ((_b = r.practitionerReview) == null ? void 0 : _b.practitionerName) || ((_c = r.procedureReview) == null ? void 0 : _c.procedureName);
|
|
16841
|
+
}
|
|
16842
|
+
)
|
|
16843
|
+
});
|
|
16844
|
+
return enhancedReviews;
|
|
16427
16845
|
}
|
|
16428
16846
|
/**
|
|
16429
|
-
* Gets all reviews for a specific procedure
|
|
16847
|
+
* Gets all reviews for a specific procedure with enhanced entity names
|
|
16430
16848
|
* @param procedureId The ID of the procedure
|
|
16431
|
-
* @returns Array of reviews containing procedure reviews
|
|
16849
|
+
* @returns Array of reviews containing procedure reviews with clinic, practitioner, and procedure names
|
|
16432
16850
|
*/
|
|
16433
16851
|
async getReviewsByProcedure(procedureId) {
|
|
16852
|
+
console.log("\u{1F50D} ReviewService.getReviewsByProcedure - Querying for procedure:", procedureId);
|
|
16434
16853
|
const q = query31(
|
|
16435
16854
|
collection31(this.db, REVIEWS_COLLECTION),
|
|
16436
16855
|
where31("procedureReview.procedureId", "==", procedureId)
|
|
16437
16856
|
);
|
|
16438
16857
|
const snapshot = await getDocs31(q);
|
|
16439
|
-
|
|
16858
|
+
const reviews = snapshot.docs.map((doc38) => {
|
|
16859
|
+
const data = doc38.data();
|
|
16860
|
+
return { ...data, id: doc38.id };
|
|
16861
|
+
});
|
|
16862
|
+
console.log("\u{1F50D} ReviewService.getReviewsByProcedure - Found reviews before enhancement:", {
|
|
16863
|
+
procedureId,
|
|
16864
|
+
reviewCount: reviews.length,
|
|
16865
|
+
reviewIds: reviews.map((r) => r.id)
|
|
16866
|
+
});
|
|
16867
|
+
const enhancedReviews = await Promise.all(
|
|
16868
|
+
reviews.map(async (review) => {
|
|
16869
|
+
try {
|
|
16870
|
+
const appointmentDoc = await getDoc33(
|
|
16871
|
+
doc31(this.db, APPOINTMENTS_COLLECTION, review.appointmentId)
|
|
16872
|
+
);
|
|
16873
|
+
if (appointmentDoc.exists()) {
|
|
16874
|
+
const appointment = appointmentDoc.data();
|
|
16875
|
+
const enhancedReview = { ...review };
|
|
16876
|
+
if (enhancedReview.clinicReview && appointment.clinicInfo) {
|
|
16877
|
+
enhancedReview.clinicReview = {
|
|
16878
|
+
...enhancedReview.clinicReview,
|
|
16879
|
+
clinicName: appointment.clinicInfo.name
|
|
16880
|
+
};
|
|
16881
|
+
}
|
|
16882
|
+
if (enhancedReview.practitionerReview && appointment.practitionerInfo) {
|
|
16883
|
+
enhancedReview.practitionerReview = {
|
|
16884
|
+
...enhancedReview.practitionerReview,
|
|
16885
|
+
practitionerName: appointment.practitionerInfo.name
|
|
16886
|
+
};
|
|
16887
|
+
}
|
|
16888
|
+
if (enhancedReview.procedureReview && appointment.procedureInfo) {
|
|
16889
|
+
enhancedReview.procedureReview = {
|
|
16890
|
+
...enhancedReview.procedureReview,
|
|
16891
|
+
procedureName: appointment.procedureInfo.name
|
|
16892
|
+
};
|
|
16893
|
+
}
|
|
16894
|
+
if (appointment.patientInfo) {
|
|
16895
|
+
enhancedReview.patientName = appointment.patientInfo.fullName;
|
|
16896
|
+
}
|
|
16897
|
+
return enhancedReview;
|
|
16898
|
+
}
|
|
16899
|
+
return review;
|
|
16900
|
+
} catch (error) {
|
|
16901
|
+
console.warn(`Failed to enhance review ${review.id} with entity names:`, error);
|
|
16902
|
+
return review;
|
|
16903
|
+
}
|
|
16904
|
+
})
|
|
16905
|
+
);
|
|
16906
|
+
console.log("\u2705 ReviewService.getReviewsByProcedure - Enhanced reviews:", {
|
|
16907
|
+
procedureId,
|
|
16908
|
+
reviewCount: enhancedReviews.length,
|
|
16909
|
+
reviewIds: enhancedReviews.map((r) => r.id),
|
|
16910
|
+
hasEntityNames: enhancedReviews.some(
|
|
16911
|
+
(r) => {
|
|
16912
|
+
var _a, _b, _c;
|
|
16913
|
+
return ((_a = r.clinicReview) == null ? void 0 : _a.clinicName) || ((_b = r.practitionerReview) == null ? void 0 : _b.practitionerName) || ((_c = r.procedureReview) == null ? void 0 : _c.procedureName) || r.patientName;
|
|
16914
|
+
}
|
|
16915
|
+
)
|
|
16916
|
+
});
|
|
16917
|
+
return enhancedReviews;
|
|
16440
16918
|
}
|
|
16441
16919
|
/**
|
|
16442
16920
|
* Gets all reviews for a specific appointment
|