@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.
- package/dist/admin/index.js +13 -2
- package/dist/admin/index.mjs +13 -2
- package/dist/index.d.mts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +363 -0
- package/dist/index.mjs +363 -0
- package/package.json +1 -1
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +13 -2
- package/src/services/auth/auth.service.ts +173 -0
- package/src/services/practitioner/practitioner.service.ts +318 -0
|
@@ -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
|
*/
|