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