@blackcode_sa/metaestetics-api 1.5.28 → 1.5.29

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.
Files changed (34) hide show
  1. package/dist/admin/index.d.mts +1199 -1
  2. package/dist/admin/index.d.ts +1199 -1
  3. package/dist/admin/index.js +1337 -2
  4. package/dist/admin/index.mjs +1333 -2
  5. package/dist/backoffice/index.d.mts +99 -7
  6. package/dist/backoffice/index.d.ts +99 -7
  7. package/dist/index.d.mts +4035 -2364
  8. package/dist/index.d.ts +4035 -2364
  9. package/dist/index.js +2616 -1929
  10. package/dist/index.mjs +2646 -1952
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
  13. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
  14. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
  15. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
  16. package/src/admin/index.ts +53 -4
  17. package/src/index.ts +28 -4
  18. package/src/services/clinic/clinic.service.ts +320 -107
  19. package/src/services/clinic/utils/clinic.utils.ts +66 -117
  20. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  21. package/src/services/clinic/utils/filter.utils.ts +264 -0
  22. package/src/services/practitioner/practitioner.service.ts +616 -5
  23. package/src/services/procedure/procedure.service.ts +599 -352
  24. package/src/services/reviews/reviews.service.ts +842 -0
  25. package/src/types/clinic/index.ts +24 -56
  26. package/src/types/practitioner/index.ts +34 -33
  27. package/src/types/procedure/index.ts +32 -0
  28. package/src/types/profile/index.ts +1 -1
  29. package/src/types/reviews/index.ts +126 -0
  30. package/src/validations/clinic.schema.ts +37 -64
  31. package/src/validations/practitioner.schema.ts +42 -32
  32. package/src/validations/procedure.schema.ts +11 -3
  33. package/src/validations/reviews.schema.ts +189 -0
  34. package/src/services/clinic/utils/review.utils.ts +0 -93
@@ -10,6 +10,13 @@ import {
10
10
  deleteDoc,
11
11
  Timestamp,
12
12
  serverTimestamp,
13
+ limit,
14
+ startAfter,
15
+ orderBy,
16
+ writeBatch,
17
+ arrayUnion,
18
+ arrayRemove,
19
+ FieldValue,
13
20
  } from "firebase/firestore";
14
21
  import { BaseService } from "../base.service";
15
22
  import {
@@ -23,7 +30,9 @@ import {
23
30
  PractitionerToken,
24
31
  CreatePractitionerTokenData,
25
32
  PractitionerTokenStatus,
33
+ PractitionerBasicInfo,
26
34
  } from "../../types/practitioner";
35
+ import { ProcedureSummaryInfo } from "../../types/procedure";
27
36
  import { ClinicService } from "../clinic/clinic.service";
28
37
  import {
29
38
  practitionerSchema,
@@ -36,6 +45,11 @@ import { z } from "zod";
36
45
  import { Auth } from "firebase/auth";
37
46
  import { Firestore } from "firebase/firestore";
38
47
  import { FirebaseApp } from "firebase/app";
48
+ import { PractitionerReviewInfo } from "../../types/reviews";
49
+ import { distanceBetween } from "geofire-common";
50
+ import { CertificationSpecialty } from "../../backoffice/types/static/certification.types";
51
+ import { Clinic, DoctorInfo, CLINICS_COLLECTION } from "../../types/clinic";
52
+ import { ClinicInfo } from "../../types/profile";
39
53
 
40
54
  export class PractitionerService extends BaseService {
41
55
  private clinicService?: ClinicService;
@@ -64,6 +78,78 @@ export class PractitionerService extends BaseService {
64
78
  this.clinicService = clinicService;
65
79
  }
66
80
 
81
+ /**
82
+ * Aggregates clinic information for a practitioner
83
+ * @param clinicIds Array of clinic IDs the practitioner works at
84
+ * @returns Array of ClinicInfo objects
85
+ */
86
+ private async aggregateClinicInfo(
87
+ clinicIds: string[]
88
+ ): Promise<ClinicInfo[]> {
89
+ const clinicsInfo: ClinicInfo[] = [];
90
+
91
+ for (const clinicId of clinicIds) {
92
+ const clinic = await this.getClinicService().getClinic(clinicId);
93
+ if (!clinic) continue;
94
+
95
+ clinicsInfo.push({
96
+ id: clinic.id,
97
+ featuredPhoto:
98
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
99
+ ? clinic.featuredPhotos[0]
100
+ : clinic.coverPhoto || "",
101
+ name: clinic.name,
102
+ description: clinic.description || "",
103
+ location: clinic.location,
104
+ contactInfo: clinic.contactInfo,
105
+ });
106
+ }
107
+
108
+ return clinicsInfo;
109
+ }
110
+
111
+ /**
112
+ * @deprecated Aggregation of procedure info is now handled by ProcedureService.
113
+ */
114
+ private async aggregateProcedureInfo(
115
+ clinicIds: string[],
116
+ practitionerId: string
117
+ ): Promise<ProcedureSummaryInfo[]> {
118
+ console.warn("PractitionerService.aggregateProcedureInfo is deprecated.");
119
+ return [];
120
+ }
121
+
122
+ /**
123
+ * Updates aggregated data (clinics and procedures) for a practitioner
124
+ * @param practitionerId ID of the practitioner to update
125
+ * @returns Updated practitioner
126
+ */
127
+ async updateAggregatedData(
128
+ practitionerId: string
129
+ ): Promise<Practitioner | null> {
130
+ const practitioner = await this.getPractitioner(practitionerId);
131
+ if (!practitioner) {
132
+ return null;
133
+ }
134
+
135
+ // Aggregate clinic info
136
+ const clinicsInfo = await this.aggregateClinicInfo(practitioner.clinics);
137
+
138
+ // Aggregate procedure info
139
+ const proceduresInfo = await this.aggregateProcedureInfo(
140
+ practitioner.clinics,
141
+ practitionerId
142
+ );
143
+
144
+ // Update the practitioner with aggregated data
145
+ const updatedPractitioner = await this.updatePractitioner(practitionerId, {
146
+ clinicsInfo: clinicsInfo,
147
+ proceduresInfo: proceduresInfo,
148
+ });
149
+
150
+ return updatedPractitioner;
151
+ }
152
+
67
153
  /**
68
154
  * Kreira novog zdravstvenog radnika
69
155
  */
@@ -92,16 +178,49 @@ export class PractitionerService extends BaseService {
92
178
  }
93
179
  }
94
180
 
181
+ // Initialize default review info for new practitioners
182
+ const defaultReviewInfo: PractitionerReviewInfo = {
183
+ totalReviews: 0,
184
+ averageRating: 0,
185
+ knowledgeAndExpertise: 0,
186
+ communicationSkills: 0,
187
+ bedSideManner: 0,
188
+ thoroughness: 0,
189
+ trustworthiness: 0,
190
+ recommendationPercentage: 0,
191
+ };
192
+
193
+ // Generate ID for the new practitioner
194
+ const practitionerId = this.generateId();
195
+
196
+ // Aggregate clinic info if not provided
197
+ let clinicsInfo = validatedData.clinicsInfo || [];
198
+ if (
199
+ clinicsInfo.length === 0 &&
200
+ validatedData.clinics &&
201
+ validatedData.clinics.length > 0
202
+ ) {
203
+ clinicsInfo = await this.aggregateClinicInfo(validatedData.clinics);
204
+ }
205
+
206
+ // Can't aggregate procedures at creation time since the practitioner ID doesn't exist yet
207
+ // We'll initialize with an empty array and update after creation if needed
208
+ const proceduresInfo: ProcedureSummaryInfo[] = [];
209
+
95
210
  const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
96
211
  createdAt: ReturnType<typeof serverTimestamp>;
97
212
  updatedAt: ReturnType<typeof serverTimestamp>;
98
213
  } = {
99
- id: this.generateId(),
214
+ id: practitionerId,
100
215
  userRef: validatedData.userRef,
101
216
  basicInfo: validatedData.basicInfo,
102
217
  certification: validatedData.certification,
103
218
  clinics: validatedData.clinics || [],
104
219
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
220
+ clinicsInfo: clinicsInfo,
221
+ procedures: [],
222
+ proceduresInfo: proceduresInfo,
223
+ reviewInfo: defaultReviewInfo,
105
224
  isActive: validatedData.isActive,
106
225
  isVerified: validatedData.isVerified,
107
226
  status: validatedData.status || PractitionerStatus.ACTIVE,
@@ -122,11 +241,24 @@ export class PractitionerService extends BaseService {
122
241
  practitionerData
123
242
  );
124
243
 
125
- const savedPractitioner = await this.getPractitioner(practitionerData.id);
244
+ // Get the saved practitioner
245
+ let savedPractitioner = await this.getPractitioner(practitionerData.id);
126
246
  if (!savedPractitioner) {
127
247
  throw new Error("Failed to create practitioner profile");
128
248
  }
129
249
 
250
+ // If procedures weren't provided and the practitioner is associated with clinics,
251
+ // update the aggregated procedure info
252
+ if (
253
+ proceduresInfo.length === 0 &&
254
+ validatedData.clinics &&
255
+ validatedData.clinics.length > 0
256
+ ) {
257
+ savedPractitioner =
258
+ (await this.updateAggregatedData(savedPractitioner.id)) ||
259
+ savedPractitioner;
260
+ }
261
+
130
262
  return savedPractitioner;
131
263
  } catch (error) {
132
264
  if (error instanceof z.ZodError) {
@@ -174,7 +306,30 @@ export class PractitionerService extends BaseService {
174
306
  }
175
307
  }
176
308
 
309
+ // Initialize default review info for new practitioners
310
+ const defaultReviewInfo: PractitionerReviewInfo = {
311
+ totalReviews: 0,
312
+ averageRating: 0,
313
+ knowledgeAndExpertise: 0,
314
+ communicationSkills: 0,
315
+ bedSideManner: 0,
316
+ thoroughness: 0,
317
+ trustworthiness: 0,
318
+ recommendationPercentage: 0,
319
+ };
320
+
321
+ // Generate ID for the new practitioner
177
322
  const practitionerId = this.generateId();
323
+
324
+ // Aggregate clinic info if not provided
325
+ let clinicsInfo = validatedData.clinicsInfo || [];
326
+ if (clinicsInfo.length === 0 && clinics.length > 0) {
327
+ clinicsInfo = await this.aggregateClinicInfo(clinics);
328
+ }
329
+
330
+ // Can't aggregate procedures for draft practitioners yet
331
+ const proceduresInfo: ProcedureSummaryInfo[] = [];
332
+
178
333
  const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
179
334
  createdAt: ReturnType<typeof serverTimestamp>;
180
335
  updatedAt: ReturnType<typeof serverTimestamp>;
@@ -185,6 +340,10 @@ export class PractitionerService extends BaseService {
185
340
  certification: validatedData.certification,
186
341
  clinics: clinics,
187
342
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
343
+ clinicsInfo: clinicsInfo,
344
+ procedures: [],
345
+ proceduresInfo: proceduresInfo,
346
+ reviewInfo: defaultReviewInfo,
188
347
  isActive:
189
348
  validatedData.isActive !== undefined ? validatedData.isActive : false,
190
349
  isVerified:
@@ -506,7 +665,10 @@ export class PractitionerService extends BaseService {
506
665
  }
507
666
 
508
667
  try {
668
+ const currentPractitioner = practitionerDoc.data() as Practitioner;
669
+
509
670
  // Ako se ažurira lista klinika, proveravamo da li sve postoje
671
+ // i agregiramo informacije o klinikama
510
672
  if (data.clinics) {
511
673
  for (const clinicId of data.clinics) {
512
674
  const clinic = await this.getClinicService().getClinic(clinicId);
@@ -514,6 +676,19 @@ export class PractitionerService extends BaseService {
514
676
  throw new Error(`Clinic ${clinicId} not found`);
515
677
  }
516
678
  }
679
+
680
+ // If clinics changed and clinicsInfo wasn't explicitly provided, update clinicsInfo
681
+ if (!data.clinicsInfo) {
682
+ data.clinicsInfo = await this.aggregateClinicInfo(data.clinics);
683
+ }
684
+
685
+ // If clinics changed, update procedures info
686
+ if (!data.proceduresInfo) {
687
+ data.proceduresInfo = await this.aggregateProcedureInfo(
688
+ data.clinics,
689
+ practitionerId
690
+ );
691
+ }
517
692
  }
518
693
 
519
694
  const updateData = {
@@ -523,7 +698,7 @@ export class PractitionerService extends BaseService {
523
698
 
524
699
  // Validiramo kompletan objekat
525
700
  practitionerSchema.parse({
526
- ...practitionerDoc.data(),
701
+ ...currentPractitioner,
527
702
  ...data,
528
703
  updatedAt: Timestamp.now(),
529
704
  });
@@ -562,9 +737,32 @@ export class PractitionerService extends BaseService {
562
737
  throw new Error("Practitioner is already associated with this clinic");
563
738
  }
564
739
 
740
+ // Add clinic to the list
741
+ const updatedClinics = [...practitioner.clinics, clinicId];
742
+
743
+ // Update clinicsInfo array
744
+ const clinicInfo: ClinicInfo = {
745
+ id: clinic.id,
746
+ name: clinic.name,
747
+ description: clinic.description || "",
748
+ featuredPhoto:
749
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
750
+ ? clinic.featuredPhotos[0]
751
+ : clinic.coverPhoto || "",
752
+ location: clinic.location,
753
+ contactInfo: clinic.contactInfo,
754
+ };
755
+
756
+ const updatedClinicsInfo = [...practitioner.clinicsInfo, clinicInfo];
757
+
758
+ // Update practitioner with new clinic information
565
759
  await this.updatePractitioner(practitionerId, {
566
- clinics: [...practitioner.clinics, clinicId],
760
+ clinics: updatedClinics,
761
+ clinicsInfo: updatedClinicsInfo,
567
762
  });
763
+
764
+ // After adding clinic, update aggregated procedure info
765
+ await this.updateAggregatedData(practitionerId);
568
766
  }
569
767
 
570
768
  /**
@@ -580,8 +778,24 @@ export class PractitionerService extends BaseService {
580
778
  throw new Error("Practitioner is not associated with this clinic");
581
779
  }
582
780
 
781
+ // Remove clinic from the list
782
+ const updatedClinics = practitioner.clinics.filter((id) => id !== clinicId);
783
+
784
+ // Update clinicsInfo array
785
+ const updatedClinicsInfo = practitioner.clinicsInfo.filter(
786
+ (clinic) => clinic.id !== clinicId
787
+ );
788
+
789
+ // Update proceduresInfo array - remove procedures from this clinic
790
+ const updatedProceduresInfo = practitioner.proceduresInfo.filter(
791
+ (procedure) => procedure.clinicId !== clinicId
792
+ );
793
+
794
+ // Update practitioner with updated information
583
795
  await this.updatePractitioner(practitionerId, {
584
- clinics: practitioner.clinics.filter((id) => id !== clinicId),
796
+ clinics: updatedClinics,
797
+ clinicsInfo: updatedClinicsInfo,
798
+ proceduresInfo: updatedProceduresInfo,
585
799
  });
586
800
  }
587
801
 
@@ -661,4 +875,401 @@ export class PractitionerService extends BaseService {
661
875
 
662
876
  return updatedPractitioner;
663
877
  }
878
+
879
+ /**
880
+ * Retrieves all practitioners with optional pagination and draft inclusion
881
+ *
882
+ * @param options - Search options
883
+ * @param options.pagination - Optional limit for number of results per page
884
+ * @param options.lastDoc - Optional last document for pagination
885
+ * @param options.includeDraftPractitioners - Whether to include draft practitioners
886
+ * @returns Array of practitioners and the last document for pagination
887
+ */
888
+ async getAllPractitioners(options?: {
889
+ pagination?: number;
890
+ lastDoc?: any;
891
+ includeDraftPractitioners?: boolean;
892
+ }): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
893
+ try {
894
+ const constraints = [];
895
+
896
+ // Filter by status if not including drafts
897
+ if (!options?.includeDraftPractitioners) {
898
+ constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
899
+ }
900
+
901
+ // Add ordering for consistent pagination
902
+ constraints.push(orderBy("basicInfo.lastName", "asc"));
903
+ constraints.push(orderBy("basicInfo.firstName", "asc"));
904
+
905
+ // Add pagination if specified
906
+ if (options?.pagination && options.pagination > 0) {
907
+ if (options.lastDoc) {
908
+ constraints.push(startAfter(options.lastDoc));
909
+ }
910
+ constraints.push(limit(options.pagination));
911
+ }
912
+
913
+ const q = query(
914
+ collection(this.db, PRACTITIONERS_COLLECTION),
915
+ ...constraints
916
+ );
917
+
918
+ const querySnapshot = await getDocs(q);
919
+
920
+ const practitioners = querySnapshot.docs.map(
921
+ (doc) => doc.data() as Practitioner
922
+ );
923
+
924
+ // Get last document for pagination
925
+ const lastDoc =
926
+ querySnapshot.docs.length > 0
927
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
928
+ : null;
929
+
930
+ return {
931
+ practitioners,
932
+ lastDoc,
933
+ };
934
+ } catch (error) {
935
+ console.error(
936
+ "[PRACTITIONER_SERVICE] Error getting all practitioners:",
937
+ error
938
+ );
939
+ throw error;
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Searches and filters practitioners based on multiple criteria
945
+ *
946
+ * @param filters - Various filters to apply
947
+ * @param filters.nameSearch - Optional search text for first/last name
948
+ * @param filters.certifications - Optional array of certifications to filter by
949
+ * @param filters.specialties - Optional array of specialties to filter by
950
+ * @param filters.procedureFamily - Optional procedure family practitioners provide
951
+ * @param filters.procedureCategory - Optional procedure category practitioners provide
952
+ * @param filters.procedureSubcategory - Optional procedure subcategory practitioners provide
953
+ * @param filters.procedureTechnology - Optional procedure technology practitioners provide
954
+ * @param filters.location - Optional location for distance-based search
955
+ * @param filters.radiusInKm - Optional radius in kilometers (required if location is provided)
956
+ * @param filters.minRating - Optional minimum rating (0-5)
957
+ * @param filters.maxRating - Optional maximum rating (0-5)
958
+ * @param filters.pagination - Optional number of results per page
959
+ * @param filters.lastDoc - Optional last document for pagination
960
+ * @param filters.includeDraftPractitioners - Whether to include draft practitioners
961
+ * @returns Filtered practitioners and the last document for pagination
962
+ */
963
+ async getPractitionersByFilters(filters: {
964
+ nameSearch?: string;
965
+ certifications?: string[];
966
+ specialties?: CertificationSpecialty[];
967
+ procedureFamily?: string;
968
+ procedureCategory?: string;
969
+ procedureSubcategory?: string;
970
+ procedureTechnology?: string;
971
+ location?: { latitude: number; longitude: number };
972
+ radiusInKm?: number;
973
+ minRating?: number;
974
+ maxRating?: number;
975
+ pagination?: number;
976
+ lastDoc?: any;
977
+ includeDraftPractitioners?: boolean;
978
+ }): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
979
+ try {
980
+ console.log(
981
+ "[PRACTITIONER_SERVICE] Starting practitioner filtering with criteria:",
982
+ filters
983
+ );
984
+
985
+ const constraints = [];
986
+
987
+ // Filter by status if not including drafts
988
+ if (!filters.includeDraftPractitioners) {
989
+ constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
990
+ }
991
+
992
+ // Filter by active status
993
+ constraints.push(where("isActive", "==", true));
994
+
995
+ // Add certifications filter if specified
996
+ if (filters.certifications && filters.certifications.length > 0) {
997
+ constraints.push(
998
+ where(
999
+ "certification.certifications",
1000
+ "array-contains-any",
1001
+ filters.certifications
1002
+ )
1003
+ );
1004
+ }
1005
+
1006
+ // Add ordering for consistent pagination
1007
+ constraints.push(orderBy("basicInfo.lastName", "asc"));
1008
+ constraints.push(orderBy("basicInfo.firstName", "asc"));
1009
+
1010
+ // Add pagination if specified
1011
+ if (filters.pagination && filters.pagination > 0) {
1012
+ if (filters.lastDoc) {
1013
+ constraints.push(startAfter(filters.lastDoc));
1014
+ }
1015
+ constraints.push(limit(filters.pagination));
1016
+ }
1017
+
1018
+ // Execute the query
1019
+ const q = query(
1020
+ collection(this.db, PRACTITIONERS_COLLECTION),
1021
+ ...constraints
1022
+ );
1023
+ const querySnapshot = await getDocs(q);
1024
+
1025
+ console.log(
1026
+ `[PRACTITIONER_SERVICE] Found ${querySnapshot.docs.length} practitioners with base query`
1027
+ );
1028
+
1029
+ // Convert docs to practitioners
1030
+ let practitioners = querySnapshot.docs.map((doc) => {
1031
+ return { ...doc.data(), id: doc.id } as Practitioner;
1032
+ });
1033
+
1034
+ // Get last document for pagination
1035
+ const lastDoc =
1036
+ querySnapshot.docs.length > 0
1037
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
1038
+ : null;
1039
+
1040
+ // Further filter results in memory
1041
+
1042
+ // Filter by name search if specified
1043
+ if (filters.nameSearch && filters.nameSearch.trim() !== "") {
1044
+ const searchTerm = filters.nameSearch.toLowerCase().trim();
1045
+ practitioners = practitioners.filter((practitioner) => {
1046
+ const fullName =
1047
+ `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`.toLowerCase();
1048
+ return fullName.includes(searchTerm);
1049
+ });
1050
+ }
1051
+
1052
+ // Filter by specialties
1053
+ if (filters.specialties && filters.specialties.length > 0) {
1054
+ practitioners = practitioners.filter((practitioner) => {
1055
+ return filters.specialties!.every((specialty) =>
1056
+ practitioner.certification.specialties.includes(specialty)
1057
+ );
1058
+ });
1059
+ }
1060
+
1061
+ // Filter by procedure attributes using the aggregated proceduresInfo
1062
+ if (
1063
+ filters.procedureTechnology ||
1064
+ filters.procedureSubcategory ||
1065
+ filters.procedureCategory ||
1066
+ filters.procedureFamily
1067
+ ) {
1068
+ practitioners = practitioners.filter((practitioner) => {
1069
+ const procedures = practitioner.proceduresInfo || [];
1070
+ return procedures.some((procedure: ProcedureSummaryInfo) => {
1071
+ // Apply hierarchical filter - most specific first
1072
+ if (filters.procedureTechnology) {
1073
+ return procedure.technologyName === filters.procedureTechnology;
1074
+ }
1075
+ if (filters.procedureSubcategory) {
1076
+ return procedure.subcategoryName === filters.procedureSubcategory;
1077
+ }
1078
+ if (filters.procedureCategory) {
1079
+ return procedure.categoryName === filters.procedureCategory;
1080
+ }
1081
+ if (filters.procedureFamily) {
1082
+ return procedure.family === filters.procedureFamily;
1083
+ }
1084
+ return false;
1085
+ });
1086
+ });
1087
+ }
1088
+
1089
+ // Filter by location/distance if specified
1090
+ if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1091
+ const location = filters.location;
1092
+ const radiusInKm = filters.radiusInKm;
1093
+
1094
+ practitioners = practitioners.filter((practitioner) => {
1095
+ // Use the aggregated clinicsInfo to check if any clinic is within range
1096
+ const clinics = practitioner.clinicsInfo || [];
1097
+
1098
+ // Check if any clinic is within the specified radius
1099
+ return clinics.some((clinic) => {
1100
+ // Calculate distance
1101
+ const distance = distanceBetween(
1102
+ [location.latitude, location.longitude],
1103
+ [clinic.location.latitude, clinic.location.longitude]
1104
+ );
1105
+
1106
+ // Convert to kilometers
1107
+ const distanceInKm = distance / 1000;
1108
+
1109
+ // Check if within radius
1110
+ return distanceInKm <= radiusInKm;
1111
+ });
1112
+ });
1113
+ }
1114
+
1115
+ // Filter by rating
1116
+ if (filters.minRating !== undefined) {
1117
+ practitioners = practitioners.filter(
1118
+ (p) => p.reviewInfo.averageRating >= filters.minRating!
1119
+ );
1120
+ }
1121
+
1122
+ if (filters.maxRating !== undefined) {
1123
+ practitioners = practitioners.filter(
1124
+ (p) => p.reviewInfo.averageRating <= filters.maxRating!
1125
+ );
1126
+ }
1127
+
1128
+ console.log(
1129
+ `[PRACTITIONER_SERVICE] Filtered to ${practitioners.length} practitioners`
1130
+ );
1131
+
1132
+ // Apply pagination after all filters have been applied
1133
+ // This is a secondary pagination for in-memory filtered results
1134
+ if (filters.pagination && filters.pagination > 0) {
1135
+ practitioners = practitioners.slice(0, filters.pagination);
1136
+ }
1137
+
1138
+ return {
1139
+ practitioners,
1140
+ lastDoc,
1141
+ };
1142
+ } catch (error) {
1143
+ console.error(
1144
+ "[PRACTITIONER_SERVICE] Error filtering practitioners:",
1145
+ error
1146
+ );
1147
+ throw error;
1148
+ }
1149
+ }
1150
+
1151
+ // --- Helper Functions ---
1152
+
1153
+ /**
1154
+ * Aggregates essential clinic information for embedding in Practitioner.
1155
+ * @param clinicIds Array of clinic IDs the practitioner works at
1156
+ * @returns Array of ClinicInfo objects
1157
+ */
1158
+ private async _aggregateClinicInfoForPractitioner(
1159
+ clinicIds: string[]
1160
+ ): Promise<ClinicInfo[]> {
1161
+ const clinicsInfo: ClinicInfo[] = [];
1162
+ const clinicService = this.getClinicService(); // Get service instance
1163
+
1164
+ for (const clinicId of clinicIds) {
1165
+ try {
1166
+ const clinic = await clinicService.getClinic(clinicId);
1167
+ if (!clinic) {
1168
+ console.warn(
1169
+ `Clinic ${clinicId} not found during practitioner aggregation.`
1170
+ );
1171
+ continue;
1172
+ }
1173
+
1174
+ clinicsInfo.push({
1175
+ id: clinic.id,
1176
+ featuredPhoto:
1177
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
1178
+ ? clinic.featuredPhotos[0]
1179
+ : clinic.coverPhoto || "",
1180
+ name: clinic.name,
1181
+ description: clinic.description || "",
1182
+ location: clinic.location,
1183
+ contactInfo: clinic.contactInfo,
1184
+ });
1185
+ } catch (error) {
1186
+ console.error(
1187
+ `Error fetching clinic ${clinicId} for practitioner aggregation:`,
1188
+ error
1189
+ );
1190
+ }
1191
+ }
1192
+ return clinicsInfo;
1193
+ }
1194
+
1195
+ /**
1196
+ * Creates an aggregated DoctorInfo object from Practitioner data.
1197
+ * @param practitioner The practitioner object
1198
+ * @returns DoctorInfo object
1199
+ */
1200
+ private _createDoctorInfoForClinic(practitioner: Practitioner): DoctorInfo {
1201
+ return {
1202
+ id: practitioner.id,
1203
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
1204
+ description: practitioner.basicInfo.bio || "",
1205
+ photo: practitioner.basicInfo.profileImageUrl || "",
1206
+ rating: practitioner.reviewInfo?.averageRating || 0,
1207
+ services: practitioner.procedures || [], // List of procedure IDs
1208
+ };
1209
+ }
1210
+
1211
+ /**
1212
+ * Updates the DoctorInfo within the doctorsInfo array for multiple clinics.
1213
+ * @param clinicIds IDs of clinics to update
1214
+ * @param doctorInfo The updated DoctorInfo object
1215
+ */
1216
+ private async _updateDoctorInfoInClinics(
1217
+ clinicIds: string[],
1218
+ doctorInfo: DoctorInfo
1219
+ ): Promise<void> {
1220
+ const batch = writeBatch(this.db);
1221
+ const practitionerId = doctorInfo.id;
1222
+
1223
+ for (const clinicId of clinicIds) {
1224
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
1225
+ // First, remove the old doctor info based on ID
1226
+ batch.update(clinicRef, {
1227
+ doctorsInfo: arrayRemove(...[{ id: practitionerId }]),
1228
+ updatedAt: serverTimestamp(),
1229
+ });
1230
+ // Then, add the updated doctor info
1231
+ batch.update(clinicRef, {
1232
+ doctorsInfo: arrayUnion(doctorInfo),
1233
+ updatedAt: serverTimestamp(),
1234
+ });
1235
+ }
1236
+ try {
1237
+ await batch.commit();
1238
+ } catch (error) {
1239
+ console.error(
1240
+ `Error updating doctor info in clinics for practitioner ${practitionerId}:`,
1241
+ error
1242
+ );
1243
+ // Decide on error handling: throw, retry, log?
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Removes DoctorInfo from the doctorsInfo array for multiple clinics.
1249
+ * @param clinicIds IDs of clinics to update
1250
+ * @param practitionerId ID of the practitioner whose info should be removed
1251
+ */
1252
+ private async _removeDoctorInfoFromClinics(
1253
+ clinicIds: string[],
1254
+ practitionerId: string
1255
+ ): Promise<void> {
1256
+ const batch = writeBatch(this.db);
1257
+
1258
+ for (const clinicId of clinicIds) {
1259
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
1260
+ batch.update(clinicRef, {
1261
+ doctors: arrayRemove(practitionerId), // Also remove from simple ID list
1262
+ doctorsInfo: arrayRemove(...[{ id: practitionerId }]), // Remove by ID matcher
1263
+ updatedAt: serverTimestamp(),
1264
+ });
1265
+ }
1266
+ try {
1267
+ await batch.commit();
1268
+ } catch (error) {
1269
+ console.error(
1270
+ `Error removing doctor info from clinics for practitioner ${practitionerId}:`,
1271
+ error
1272
+ );
1273
+ }
1274
+ }
664
1275
  }