@blackcode_sa/metaestetics-api 1.13.21 → 1.14.0

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.
@@ -714,6 +714,324 @@ export class PractitionerService extends BaseService {
714
714
  }
715
715
  }
716
716
 
717
+ /**
718
+ * Finds all draft practitioner profiles by email address
719
+ * Used when a doctor signs in with Google to show all clinic invitations
720
+ *
721
+ * @param email - Email address to search for
722
+ * @returns Array of draft practitioner profiles with clinic information
723
+ *
724
+ * @remarks
725
+ * Requires Firestore composite index on:
726
+ * - Collection: practitioners
727
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
728
+ */
729
+ async getDraftProfilesByEmail(
730
+ email: string
731
+ ): Promise<Practitioner[]> {
732
+ try {
733
+ const normalizedEmail = email.toLowerCase().trim();
734
+
735
+ console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
736
+ email: normalizedEmail,
737
+ });
738
+
739
+ const q = query(
740
+ collection(this.db, PRACTITIONERS_COLLECTION),
741
+ where("basicInfo.email", "==", normalizedEmail),
742
+ where("status", "==", PractitionerStatus.DRAFT),
743
+ where("userRef", "==", "")
744
+ );
745
+
746
+ const querySnapshot = await getDocs(q);
747
+
748
+ if (querySnapshot.empty) {
749
+ console.log("[PRACTITIONER] No draft practitioners found for email", {
750
+ email: normalizedEmail,
751
+ });
752
+ return [];
753
+ }
754
+
755
+ const draftPractitioners = querySnapshot.docs.map(
756
+ (doc) => doc.data() as Practitioner
757
+ );
758
+
759
+ console.log("[PRACTITIONER] Found draft practitioners", {
760
+ email: normalizedEmail,
761
+ count: draftPractitioners.length,
762
+ practitionerIds: draftPractitioners.map((p) => p.id),
763
+ });
764
+
765
+ return draftPractitioners;
766
+ } catch (error) {
767
+ console.error(
768
+ "[PRACTITIONER] Error finding draft practitioners by email:",
769
+ error
770
+ );
771
+ // If query fails (e.g., index not created), return empty array
772
+ return [];
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Claims a draft practitioner profile and links it to a user account
778
+ * Used when a doctor selects which clinic(s) to join after Google Sign-In
779
+ *
780
+ * @param practitionerId - ID of the draft practitioner profile to claim
781
+ * @param userId - ID of the user account to link the profile to
782
+ * @returns The claimed practitioner profile
783
+ */
784
+ async claimDraftProfileWithGoogle(
785
+ practitionerId: string,
786
+ userId: string
787
+ ): Promise<Practitioner> {
788
+ try {
789
+ console.log("[PRACTITIONER] Claiming draft profile with Google", {
790
+ practitionerId,
791
+ userId,
792
+ });
793
+
794
+ // Get the draft practitioner profile
795
+ const practitioner = await this.getPractitioner(practitionerId);
796
+ if (!practitioner) {
797
+ throw new Error(`Practitioner ${practitionerId} not found`);
798
+ }
799
+
800
+ // Ensure practitioner is in DRAFT status
801
+ if (practitioner.status !== PractitionerStatus.DRAFT) {
802
+ throw new Error("This practitioner profile has already been claimed");
803
+ }
804
+
805
+ // Check if user already has a practitioner profile
806
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
807
+ if (existingPractitioner) {
808
+ // User already has a profile - merge clinics from draft profile into existing profile
809
+ console.log("[PRACTITIONER] User already has profile, merging clinics");
810
+
811
+ // Merge clinics (avoid duplicates)
812
+ const mergedClinics = Array.from(new Set([
813
+ ...existingPractitioner.clinics,
814
+ ...practitioner.clinics,
815
+ ]));
816
+
817
+ // Merge clinic working hours
818
+ const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
819
+ for (const workingHours of practitioner.clinicWorkingHours) {
820
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
821
+ mergedWorkingHours.push(workingHours);
822
+ }
823
+ }
824
+
825
+ // Merge clinics info (avoid duplicates)
826
+ const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
827
+ for (const clinicInfo of practitioner.clinicsInfo) {
828
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
829
+ mergedClinicsInfo.push(clinicInfo);
830
+ }
831
+ }
832
+
833
+ // Update existing practitioner with merged data
834
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
835
+ clinics: mergedClinics,
836
+ clinicWorkingHours: mergedWorkingHours,
837
+ clinicsInfo: mergedClinicsInfo,
838
+ });
839
+
840
+ // Delete the draft profile since we've merged it
841
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId));
842
+
843
+ // Mark all active tokens for the draft practitioner as used
844
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
845
+ for (const token of activeTokens) {
846
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
847
+ }
848
+
849
+ return updatedPractitioner;
850
+ }
851
+
852
+ // Claim the profile by linking it to the user
853
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
854
+ userRef: userId,
855
+ status: PractitionerStatus.ACTIVE,
856
+ });
857
+
858
+ // Mark all active tokens for this practitioner as used
859
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
860
+ for (const token of activeTokens) {
861
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
862
+ }
863
+
864
+ console.log("[PRACTITIONER] Draft profile claimed successfully", {
865
+ practitionerId: updatedPractitioner.id,
866
+ userId,
867
+ });
868
+
869
+ return updatedPractitioner;
870
+ } catch (error) {
871
+ console.error(
872
+ "[PRACTITIONER] Error claiming draft profile with Google:",
873
+ error
874
+ );
875
+ throw error;
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Claims multiple draft practitioner profiles and merges them into one profile
881
+ * Used when a doctor selects multiple clinics to join after Google Sign-In
882
+ *
883
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
884
+ * @param userId - ID of the user account to link the profiles to
885
+ * @returns The claimed practitioner profile (first one becomes main, others merged)
886
+ */
887
+ async claimMultipleDraftProfilesWithGoogle(
888
+ practitionerIds: string[],
889
+ userId: string
890
+ ): Promise<Practitioner> {
891
+ try {
892
+ if (practitionerIds.length === 0) {
893
+ throw new Error("No practitioner IDs provided");
894
+ }
895
+
896
+ console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
897
+ practitionerIds,
898
+ userId,
899
+ count: practitionerIds.length,
900
+ });
901
+
902
+ // Get all draft profiles
903
+ const draftProfiles = await Promise.all(
904
+ practitionerIds.map(id => this.getPractitioner(id))
905
+ );
906
+
907
+ // Filter out nulls and ensure all are drafts
908
+ const validDrafts = draftProfiles.filter((p): p is Practitioner => {
909
+ if (!p) return false;
910
+ if (p.status !== PractitionerStatus.DRAFT) {
911
+ throw new Error(`Practitioner ${p.id} has already been claimed`);
912
+ }
913
+ return true;
914
+ });
915
+
916
+ if (validDrafts.length === 0) {
917
+ throw new Error("No valid draft profiles found");
918
+ }
919
+
920
+ // Check if user already has a practitioner profile
921
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
922
+
923
+ if (existingPractitioner) {
924
+ // Merge all draft profiles into existing profile
925
+ let mergedClinics = new Set(existingPractitioner.clinics);
926
+ let mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
927
+ let mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
928
+
929
+ for (const draft of validDrafts) {
930
+ // Merge clinics
931
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
932
+
933
+ // Merge working hours
934
+ for (const workingHours of draft.clinicWorkingHours) {
935
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
936
+ mergedWorkingHours.push(workingHours);
937
+ }
938
+ }
939
+
940
+ // Merge clinics info
941
+ for (const clinicInfo of draft.clinicsInfo) {
942
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
943
+ mergedClinicsInfo.push(clinicInfo);
944
+ }
945
+ }
946
+ }
947
+
948
+ // Update existing practitioner
949
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
950
+ clinics: Array.from(mergedClinics),
951
+ clinicWorkingHours: mergedWorkingHours,
952
+ clinicsInfo: mergedClinicsInfo,
953
+ });
954
+
955
+ // Delete all draft profiles
956
+ for (const draft of validDrafts) {
957
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
958
+
959
+ // Mark all active tokens as used
960
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
961
+ for (const token of activeTokens) {
962
+ await this.markTokenAsUsed(token.id, draft.id, userId);
963
+ }
964
+ }
965
+
966
+ return updatedPractitioner;
967
+ }
968
+
969
+ // Use first draft as the main profile, merge others into it
970
+ const mainDraft = validDrafts[0];
971
+ const otherDrafts = validDrafts.slice(1);
972
+
973
+ // Merge clinics from other drafts
974
+ let mergedClinics = new Set(mainDraft.clinics);
975
+ let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
976
+ let mergedClinicsInfo = [...mainDraft.clinicsInfo];
977
+
978
+ for (const draft of otherDrafts) {
979
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
980
+
981
+ for (const workingHours of draft.clinicWorkingHours) {
982
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
983
+ mergedWorkingHours.push(workingHours);
984
+ }
985
+ }
986
+
987
+ for (const clinicInfo of draft.clinicsInfo) {
988
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
989
+ mergedClinicsInfo.push(clinicInfo);
990
+ }
991
+ }
992
+ }
993
+
994
+ // Claim the main profile
995
+ const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
996
+ userRef: userId,
997
+ status: PractitionerStatus.ACTIVE,
998
+ clinics: Array.from(mergedClinics),
999
+ clinicWorkingHours: mergedWorkingHours,
1000
+ clinicsInfo: mergedClinicsInfo,
1001
+ });
1002
+
1003
+ // Mark all active tokens for main profile as used
1004
+ const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
1005
+ for (const token of mainActiveTokens) {
1006
+ await this.markTokenAsUsed(token.id, mainDraft.id, userId);
1007
+ }
1008
+
1009
+ // Delete other draft profiles
1010
+ for (const draft of otherDrafts) {
1011
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
1012
+
1013
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
1014
+ for (const token of activeTokens) {
1015
+ await this.markTokenAsUsed(token.id, draft.id, userId);
1016
+ }
1017
+ }
1018
+
1019
+ console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
1020
+ practitionerId: updatedPractitioner.id,
1021
+ userId,
1022
+ mergedCount: validDrafts.length,
1023
+ });
1024
+
1025
+ return updatedPractitioner;
1026
+ } catch (error) {
1027
+ console.error(
1028
+ "[PRACTITIONER] Error claiming multiple draft profiles with Google:",
1029
+ error
1030
+ );
1031
+ throw error;
1032
+ }
1033
+ }
1034
+
717
1035
  /**
718
1036
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
719
1037
  */