@blackcode_sa/metaestetics-api 1.5.28 → 1.5.30

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 (49) hide show
  1. package/dist/admin/index.d.mts +1324 -1
  2. package/dist/admin/index.d.ts +1324 -1
  3. package/dist/admin/index.js +1674 -2
  4. package/dist/admin/index.mjs +1668 -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 +4036 -2372
  8. package/dist/index.d.ts +4036 -2372
  9. package/dist/index.js +2331 -2009
  10. package/dist/index.mjs +2279 -1954
  11. package/package.json +2 -1
  12. package/src/admin/aggregation/README.md +79 -0
  13. package/src/admin/aggregation/clinic/README.md +52 -0
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
  15. package/src/admin/aggregation/patient/README.md +27 -0
  16. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
  17. package/src/admin/aggregation/practitioner/README.md +42 -0
  18. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
  19. package/src/admin/aggregation/procedure/README.md +43 -0
  20. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
  21. package/src/admin/index.ts +60 -4
  22. package/src/admin/mailing/README.md +95 -0
  23. package/src/admin/mailing/base.mailing.service.ts +131 -0
  24. package/src/admin/mailing/index.ts +2 -0
  25. package/src/admin/mailing/practitionerInvite/index.ts +1 -0
  26. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
  27. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
  28. package/src/index.ts +28 -4
  29. package/src/services/README.md +106 -0
  30. package/src/services/clinic/README.md +87 -0
  31. package/src/services/clinic/clinic.service.ts +197 -107
  32. package/src/services/clinic/utils/clinic.utils.ts +68 -119
  33. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  34. package/src/services/clinic/utils/filter.utils.ts +264 -0
  35. package/src/services/practitioner/README.md +145 -0
  36. package/src/services/practitioner/practitioner.service.ts +439 -104
  37. package/src/services/procedure/README.md +88 -0
  38. package/src/services/procedure/procedure.service.ts +521 -311
  39. package/src/services/reviews/reviews.service.ts +842 -0
  40. package/src/types/clinic/index.ts +24 -56
  41. package/src/types/practitioner/index.ts +34 -33
  42. package/src/types/procedure/index.ts +32 -0
  43. package/src/types/profile/index.ts +1 -1
  44. package/src/types/reviews/index.ts +126 -0
  45. package/src/validations/clinic.schema.ts +37 -64
  46. package/src/validations/practitioner.schema.ts +42 -32
  47. package/src/validations/procedure.schema.ts +11 -3
  48. package/src/validations/reviews.schema.ts +189 -0
  49. 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;
@@ -52,86 +66,89 @@ export class PractitionerService extends BaseService {
52
66
 
53
67
  private getClinicService(): ClinicService {
54
68
  if (!this.clinicService) {
55
- throw new Error("ClinicService nije inicijalizovan");
69
+ throw new Error("Clinic service not initialized!");
56
70
  }
57
71
  return this.clinicService;
58
72
  }
59
73
 
60
- /**
61
- * Postavlja referencu na ClinicService nakon inicijalizacije
62
- */
63
74
  setClinicService(clinicService: ClinicService): void {
64
75
  this.clinicService = clinicService;
65
76
  }
66
77
 
67
78
  /**
68
- * Kreira novog zdravstvenog radnika
79
+ * Creates a new practitioner
69
80
  */
70
81
  async createPractitioner(
71
82
  data: CreatePractitionerData
72
83
  ): Promise<Practitioner> {
73
84
  try {
74
- // Validacija ulaznih podataka
75
- const validatedData = createPractitionerSchema.parse(data);
76
-
77
- // Provera da li već postoji profil za ovog korisnika
78
- const existingPractitioner = await this.getPractitionerByUserRef(
79
- validatedData.userRef
80
- );
81
- if (existingPractitioner) {
82
- throw new Error("User already has a practitioner profile");
83
- }
85
+ const validData = createPractitionerSchema.parse(data);
86
+ const practitionerId = this.generateId();
84
87
 
85
- // Provera da li sve klinike postoje
86
- if (validatedData.clinics) {
87
- for (const clinicId of validatedData.clinics) {
88
- const clinic = await this.getClinicService().getClinic(clinicId);
89
- if (!clinic) {
90
- throw new Error(`Clinic ${clinicId} not found`);
91
- }
92
- }
93
- }
88
+ // Default review info
89
+ const reviewInfo: PractitionerReviewInfo = {
90
+ totalReviews: 0,
91
+ averageRating: 0,
92
+ knowledgeAndExpertise: 0,
93
+ communicationSkills: 0,
94
+ bedSideManner: 0,
95
+ thoroughness: 0,
96
+ trustworthiness: 0,
97
+ recommendationPercentage: 0,
98
+ };
94
99
 
95
- const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
96
- createdAt: ReturnType<typeof serverTimestamp>;
97
- updatedAt: ReturnType<typeof serverTimestamp>;
100
+ // Create practitioner object
101
+ const practitioner: Omit<Practitioner, "createdAt" | "updatedAt"> & {
102
+ createdAt: FieldValue;
103
+ updatedAt: FieldValue;
98
104
  } = {
99
- id: this.generateId(),
100
- userRef: validatedData.userRef,
101
- basicInfo: validatedData.basicInfo,
102
- certification: validatedData.certification,
103
- clinics: validatedData.clinics || [],
104
- clinicWorkingHours: validatedData.clinicWorkingHours || [],
105
- isActive: validatedData.isActive,
106
- isVerified: validatedData.isVerified,
107
- status: validatedData.status || PractitionerStatus.ACTIVE,
105
+ id: practitionerId,
106
+ userRef: validData.userRef,
107
+ basicInfo: validData.basicInfo,
108
+ certification: validData.certification,
109
+ clinics: validData.clinics || [],
110
+ clinicWorkingHours: validData.clinicWorkingHours || [],
111
+ clinicsInfo: [],
112
+ procedures: [],
113
+ proceduresInfo: [],
114
+ reviewInfo,
115
+ isActive: validData.isActive !== undefined ? validData.isActive : true,
116
+ isVerified:
117
+ validData.isVerified !== undefined ? validData.isVerified : false,
118
+ status: validData.status || PractitionerStatus.ACTIVE,
108
119
  createdAt: serverTimestamp(),
109
120
  updatedAt: serverTimestamp(),
110
121
  };
111
122
 
112
- // Validacija kompletnog objekta
123
+ // Validate the entire object
113
124
  practitionerSchema.parse({
114
- ...practitionerData,
125
+ ...practitioner,
115
126
  createdAt: Timestamp.now(),
116
127
  updatedAt: Timestamp.now(),
117
128
  });
118
129
 
119
- // Čuvamo u Firestore
120
- await setDoc(
121
- doc(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
122
- practitionerData
130
+ // Create practitioner document
131
+ const practitionerRef = doc(
132
+ this.db,
133
+ PRACTITIONERS_COLLECTION,
134
+ practitionerId
123
135
  );
124
136
 
125
- const savedPractitioner = await this.getPractitioner(practitionerData.id);
126
- if (!savedPractitioner) {
127
- throw new Error("Failed to create practitioner profile");
128
- }
137
+ await setDoc(practitionerRef, practitioner);
129
138
 
130
- return savedPractitioner;
139
+ // Return the created practitioner
140
+ const createdPractitioner = await this.getPractitioner(practitionerId);
141
+ if (!createdPractitioner) {
142
+ throw new Error(
143
+ `Failed to retrieve created practitioner ${practitionerId}`
144
+ );
145
+ }
146
+ return createdPractitioner;
131
147
  } catch (error) {
132
148
  if (error instanceof z.ZodError) {
133
- throw new Error("Invalid practitioner data: " + error.message);
149
+ throw new Error(`Invalid practitioner data: ${error.message}`);
134
150
  }
151
+ console.error("Error creating practitioner:", error);
135
152
  throw error;
136
153
  }
137
154
  }
@@ -174,7 +191,24 @@ export class PractitionerService extends BaseService {
174
191
  }
175
192
  }
176
193
 
194
+ // Initialize default review info for new practitioners
195
+ const defaultReviewInfo: PractitionerReviewInfo = {
196
+ totalReviews: 0,
197
+ averageRating: 0,
198
+ knowledgeAndExpertise: 0,
199
+ communicationSkills: 0,
200
+ bedSideManner: 0,
201
+ thoroughness: 0,
202
+ trustworthiness: 0,
203
+ recommendationPercentage: 0,
204
+ };
205
+
206
+ // Generate ID for the new practitioner
177
207
  const practitionerId = this.generateId();
208
+
209
+ const clinicsInfo = validatedData.clinicsInfo || [];
210
+ const proceduresInfo: ProcedureSummaryInfo[] = [];
211
+
178
212
  const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
179
213
  createdAt: ReturnType<typeof serverTimestamp>;
180
214
  updatedAt: ReturnType<typeof serverTimestamp>;
@@ -185,6 +219,10 @@ export class PractitionerService extends BaseService {
185
219
  certification: validatedData.certification,
186
220
  clinics: clinics,
187
221
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
222
+ clinicsInfo: clinicsInfo,
223
+ procedures: [],
224
+ proceduresInfo: proceduresInfo,
225
+ reviewInfo: defaultReviewInfo,
188
226
  isActive:
189
227
  validatedData.isActive !== undefined ? validatedData.isActive : false,
190
228
  isVerified:
@@ -488,101 +526,126 @@ export class PractitionerService extends BaseService {
488
526
  }
489
527
 
490
528
  /**
491
- * Ažurira profil zdravstvenog radnika
529
+ * Updates a practitioner
492
530
  */
493
531
  async updatePractitioner(
494
532
  practitionerId: string,
495
533
  data: UpdatePractitionerData
496
534
  ): Promise<Practitioner> {
497
- const practitionerRef = doc(
498
- this.db,
499
- PRACTITIONERS_COLLECTION,
500
- practitionerId
501
- );
502
- const practitionerDoc = await getDoc(practitionerRef);
535
+ try {
536
+ // Validate update data
537
+ const validData = data; // Using the passed data directly as it's already validated by the schema type
503
538
 
504
- if (!practitionerDoc.exists()) {
505
- throw new Error("Practitioner not found");
506
- }
539
+ // Get current practitioner data
540
+ const practitionerRef = doc(
541
+ this.db,
542
+ PRACTITIONERS_COLLECTION,
543
+ practitionerId
544
+ );
545
+ const practitionerDoc = await getDoc(practitionerRef);
507
546
 
508
- try {
509
- // Ako se ažurira lista klinika, proveravamo da li sve postoje
510
- if (data.clinics) {
511
- for (const clinicId of data.clinics) {
512
- const clinic = await this.getClinicService().getClinic(clinicId);
513
- if (!clinic) {
514
- throw new Error(`Clinic ${clinicId} not found`);
515
- }
516
- }
547
+ if (!practitionerDoc.exists()) {
548
+ throw new Error(`Practitioner ${practitionerId} not found`);
517
549
  }
518
550
 
551
+ const currentPractitioner = practitionerDoc.data() as Practitioner;
552
+
553
+ // Prepare update data
519
554
  const updateData = {
520
- ...data,
555
+ ...validData,
521
556
  updatedAt: serverTimestamp(),
522
557
  };
523
558
 
524
- // Validiramo kompletan objekat
525
- practitionerSchema.parse({
526
- ...practitionerDoc.data(),
527
- ...data,
528
- updatedAt: Timestamp.now(),
529
- });
530
-
559
+ // Update practitioner
531
560
  await updateDoc(practitionerRef, updateData);
532
561
 
562
+ // Return updated practitioner
533
563
  const updatedPractitioner = await this.getPractitioner(practitionerId);
534
564
  if (!updatedPractitioner) {
535
- throw new Error("Failed to retrieve updated practitioner profile");
565
+ throw new Error(
566
+ `Failed to retrieve updated practitioner ${practitionerId}`
567
+ );
536
568
  }
537
-
538
569
  return updatedPractitioner;
539
570
  } catch (error) {
540
571
  if (error instanceof z.ZodError) {
541
- throw new Error("Invalid practitioner data: " + error.message);
572
+ throw new Error(`Invalid practitioner update data: ${error.message}`);
542
573
  }
574
+ console.error(`Error updating practitioner ${practitionerId}:`, error);
543
575
  throw error;
544
576
  }
545
577
  }
546
578
 
547
579
  /**
548
- * Dodaje kliniku zdravstvenom radniku
580
+ * Adds a clinic to a practitioner
549
581
  */
550
582
  async addClinic(practitionerId: string, clinicId: string): Promise<void> {
551
- const practitioner = await this.getPractitioner(practitionerId);
552
- if (!practitioner) {
553
- throw new Error("Practitioner not found");
554
- }
583
+ try {
584
+ // Get practitioner
585
+ const practitionerRef = doc(
586
+ this.db,
587
+ PRACTITIONERS_COLLECTION,
588
+ practitionerId
589
+ );
590
+ const practitionerDoc = await getDoc(practitionerRef);
555
591
 
556
- const clinic = await this.getClinicService().getClinic(clinicId);
557
- if (!clinic) {
558
- throw new Error("Clinic not found");
559
- }
592
+ if (!practitionerDoc.exists()) {
593
+ throw new Error(`Practitioner ${practitionerId} not found`);
594
+ }
560
595
 
561
- if (practitioner.clinics.includes(clinicId)) {
562
- throw new Error("Practitioner is already associated with this clinic");
563
- }
596
+ const practitioner = practitionerDoc.data() as Practitioner;
564
597
 
565
- await this.updatePractitioner(practitionerId, {
566
- clinics: [...practitioner.clinics, clinicId],
567
- });
598
+ // Check if clinic already added
599
+ if (practitioner.clinics?.includes(clinicId)) {
600
+ console.log(
601
+ `Clinic ${clinicId} already added to practitioner ${practitionerId}`
602
+ );
603
+ return;
604
+ }
605
+
606
+ // Add clinic to clinics array
607
+ await updateDoc(practitionerRef, {
608
+ clinics: arrayUnion(clinicId),
609
+ updatedAt: serverTimestamp(),
610
+ });
611
+ } catch (error) {
612
+ console.error(
613
+ `Error adding clinic ${clinicId} to practitioner ${practitionerId}:`,
614
+ error
615
+ );
616
+ throw error;
617
+ }
568
618
  }
569
619
 
570
620
  /**
571
- * Uklanja kliniku iz liste klinika zdravstvenog radnika
621
+ * Removes a clinic from a practitioner
572
622
  */
573
623
  async removeClinic(practitionerId: string, clinicId: string): Promise<void> {
574
- const practitioner = await this.getPractitioner(practitionerId);
575
- if (!practitioner) {
576
- throw new Error("Practitioner not found");
577
- }
624
+ try {
625
+ // Get practitioner
626
+ const practitionerRef = doc(
627
+ this.db,
628
+ PRACTITIONERS_COLLECTION,
629
+ practitionerId
630
+ );
631
+ const practitionerDoc = await getDoc(practitionerRef);
578
632
 
579
- if (!practitioner.clinics.includes(clinicId)) {
580
- throw new Error("Practitioner is not associated with this clinic");
581
- }
633
+ if (!practitionerDoc.exists()) {
634
+ throw new Error(`Practitioner ${practitionerId} not found`);
635
+ }
582
636
 
583
- await this.updatePractitioner(practitionerId, {
584
- clinics: practitioner.clinics.filter((id) => id !== clinicId),
585
- });
637
+ // Remove clinic from clinics array
638
+ await updateDoc(practitionerRef, {
639
+ clinics: arrayRemove(clinicId),
640
+ updatedAt: serverTimestamp(),
641
+ });
642
+ } catch (error) {
643
+ console.error(
644
+ `Error removing clinic ${clinicId} from practitioner ${practitionerId}:`,
645
+ error
646
+ );
647
+ throw error;
648
+ }
586
649
  }
587
650
 
588
651
  /**
@@ -661,4 +724,276 @@ export class PractitionerService extends BaseService {
661
724
 
662
725
  return updatedPractitioner;
663
726
  }
727
+
728
+ /**
729
+ * Retrieves all practitioners with optional pagination and draft inclusion
730
+ *
731
+ * @param options - Search options
732
+ * @param options.pagination - Optional limit for number of results per page
733
+ * @param options.lastDoc - Optional last document for pagination
734
+ * @param options.includeDraftPractitioners - Whether to include draft practitioners
735
+ * @returns Array of practitioners and the last document for pagination
736
+ */
737
+ async getAllPractitioners(options?: {
738
+ pagination?: number;
739
+ lastDoc?: any;
740
+ includeDraftPractitioners?: boolean;
741
+ }): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
742
+ try {
743
+ const constraints = [];
744
+
745
+ // Filter by status if not including drafts
746
+ if (!options?.includeDraftPractitioners) {
747
+ constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
748
+ }
749
+
750
+ // Add ordering for consistent pagination
751
+ constraints.push(orderBy("basicInfo.lastName", "asc"));
752
+ constraints.push(orderBy("basicInfo.firstName", "asc"));
753
+
754
+ // Add pagination if specified
755
+ if (options?.pagination && options.pagination > 0) {
756
+ if (options.lastDoc) {
757
+ constraints.push(startAfter(options.lastDoc));
758
+ }
759
+ constraints.push(limit(options.pagination));
760
+ }
761
+
762
+ const q = query(
763
+ collection(this.db, PRACTITIONERS_COLLECTION),
764
+ ...constraints
765
+ );
766
+
767
+ const querySnapshot = await getDocs(q);
768
+
769
+ const practitioners = querySnapshot.docs.map(
770
+ (doc) => doc.data() as Practitioner
771
+ );
772
+
773
+ // Get last document for pagination
774
+ const lastDoc =
775
+ querySnapshot.docs.length > 0
776
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
777
+ : null;
778
+
779
+ return {
780
+ practitioners,
781
+ lastDoc,
782
+ };
783
+ } catch (error) {
784
+ console.error(
785
+ "[PRACTITIONER_SERVICE] Error getting all practitioners:",
786
+ error
787
+ );
788
+ throw error;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Searches and filters practitioners based on multiple criteria
794
+ *
795
+ * @param filters - Various filters to apply
796
+ * @param filters.nameSearch - Optional search text for first/last name
797
+ * @param filters.certifications - Optional array of certifications to filter by
798
+ * @param filters.specialties - Optional array of specialties to filter by
799
+ * @param filters.procedureFamily - Optional procedure family practitioners provide
800
+ * @param filters.procedureCategory - Optional procedure category practitioners provide
801
+ * @param filters.procedureSubcategory - Optional procedure subcategory practitioners provide
802
+ * @param filters.procedureTechnology - Optional procedure technology practitioners provide
803
+ * @param filters.location - Optional location for distance-based search
804
+ * @param filters.radiusInKm - Optional radius in kilometers (required if location is provided)
805
+ * @param filters.minRating - Optional minimum rating (0-5)
806
+ * @param filters.maxRating - Optional maximum rating (0-5)
807
+ * @param filters.pagination - Optional number of results per page
808
+ * @param filters.lastDoc - Optional last document for pagination
809
+ * @param filters.includeDraftPractitioners - Whether to include draft practitioners
810
+ * @returns Filtered practitioners and the last document for pagination
811
+ */
812
+ async getPractitionersByFilters(filters: {
813
+ nameSearch?: string;
814
+ certifications?: string[];
815
+ specialties?: CertificationSpecialty[];
816
+ procedureFamily?: string;
817
+ procedureCategory?: string;
818
+ procedureSubcategory?: string;
819
+ procedureTechnology?: string;
820
+ location?: { latitude: number; longitude: number };
821
+ radiusInKm?: number;
822
+ minRating?: number;
823
+ maxRating?: number;
824
+ pagination?: number;
825
+ lastDoc?: any;
826
+ includeDraftPractitioners?: boolean;
827
+ }): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
828
+ try {
829
+ console.log(
830
+ "[PRACTITIONER_SERVICE] Starting practitioner filtering with criteria:",
831
+ filters
832
+ );
833
+
834
+ const constraints = [];
835
+
836
+ // Filter by status if not including drafts
837
+ if (!filters.includeDraftPractitioners) {
838
+ constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
839
+ }
840
+
841
+ // Filter by active status
842
+ constraints.push(where("isActive", "==", true));
843
+
844
+ // Add certifications filter if specified
845
+ if (filters.certifications && filters.certifications.length > 0) {
846
+ constraints.push(
847
+ where(
848
+ "certification.certifications",
849
+ "array-contains-any",
850
+ filters.certifications
851
+ )
852
+ );
853
+ }
854
+
855
+ // Add ordering for consistent pagination
856
+ constraints.push(orderBy("basicInfo.lastName", "asc"));
857
+ constraints.push(orderBy("basicInfo.firstName", "asc"));
858
+
859
+ // Add pagination if specified
860
+ if (filters.pagination && filters.pagination > 0) {
861
+ if (filters.lastDoc) {
862
+ constraints.push(startAfter(filters.lastDoc));
863
+ }
864
+ constraints.push(limit(filters.pagination));
865
+ }
866
+
867
+ // Execute the query
868
+ const q = query(
869
+ collection(this.db, PRACTITIONERS_COLLECTION),
870
+ ...constraints
871
+ );
872
+ const querySnapshot = await getDocs(q);
873
+
874
+ console.log(
875
+ `[PRACTITIONER_SERVICE] Found ${querySnapshot.docs.length} practitioners with base query`
876
+ );
877
+
878
+ // Convert docs to practitioners
879
+ let practitioners = querySnapshot.docs.map((doc) => {
880
+ return { ...doc.data(), id: doc.id } as Practitioner;
881
+ });
882
+
883
+ // Get last document for pagination
884
+ const lastDoc =
885
+ querySnapshot.docs.length > 0
886
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
887
+ : null;
888
+
889
+ // Further filter results in memory
890
+
891
+ // Filter by name search if specified
892
+ if (filters.nameSearch && filters.nameSearch.trim() !== "") {
893
+ const searchTerm = filters.nameSearch.toLowerCase().trim();
894
+ practitioners = practitioners.filter((practitioner) => {
895
+ const fullName =
896
+ `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`.toLowerCase();
897
+ return fullName.includes(searchTerm);
898
+ });
899
+ }
900
+
901
+ // Filter by specialties
902
+ if (filters.specialties && filters.specialties.length > 0) {
903
+ practitioners = practitioners.filter((practitioner) => {
904
+ return filters.specialties!.every((specialty) =>
905
+ practitioner.certification.specialties.includes(specialty)
906
+ );
907
+ });
908
+ }
909
+
910
+ // Filter by procedure attributes using the aggregated proceduresInfo
911
+ if (
912
+ filters.procedureTechnology ||
913
+ filters.procedureSubcategory ||
914
+ filters.procedureCategory ||
915
+ filters.procedureFamily
916
+ ) {
917
+ practitioners = practitioners.filter((practitioner) => {
918
+ const procedures = practitioner.proceduresInfo || [];
919
+ return procedures.some((procedure: ProcedureSummaryInfo) => {
920
+ // Apply hierarchical filter - most specific first
921
+ if (filters.procedureTechnology) {
922
+ return procedure.technologyName === filters.procedureTechnology;
923
+ }
924
+ if (filters.procedureSubcategory) {
925
+ return procedure.subcategoryName === filters.procedureSubcategory;
926
+ }
927
+ if (filters.procedureCategory) {
928
+ return procedure.categoryName === filters.procedureCategory;
929
+ }
930
+ if (filters.procedureFamily) {
931
+ return procedure.family === filters.procedureFamily;
932
+ }
933
+ return false;
934
+ });
935
+ });
936
+ }
937
+
938
+ // Filter by location/distance if specified
939
+ if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
940
+ const location = filters.location;
941
+ const radiusInKm = filters.radiusInKm;
942
+
943
+ practitioners = practitioners.filter((practitioner) => {
944
+ // Use the aggregated clinicsInfo to check if any clinic is within range
945
+ const clinics = practitioner.clinicsInfo || [];
946
+
947
+ // Check if any clinic is within the specified radius
948
+ return clinics.some((clinic) => {
949
+ // Calculate distance
950
+ const distance = distanceBetween(
951
+ [location.latitude, location.longitude],
952
+ [clinic.location.latitude, clinic.location.longitude]
953
+ );
954
+
955
+ // Convert to kilometers
956
+ const distanceInKm = distance / 1000;
957
+
958
+ // Check if within radius
959
+ return distanceInKm <= radiusInKm;
960
+ });
961
+ });
962
+ }
963
+
964
+ // Filter by rating
965
+ if (filters.minRating !== undefined) {
966
+ practitioners = practitioners.filter(
967
+ (p) => p.reviewInfo.averageRating >= filters.minRating!
968
+ );
969
+ }
970
+
971
+ if (filters.maxRating !== undefined) {
972
+ practitioners = practitioners.filter(
973
+ (p) => p.reviewInfo.averageRating <= filters.maxRating!
974
+ );
975
+ }
976
+
977
+ console.log(
978
+ `[PRACTITIONER_SERVICE] Filtered to ${practitioners.length} practitioners`
979
+ );
980
+
981
+ // Apply pagination after all filters have been applied
982
+ // This is a secondary pagination for in-memory filtered results
983
+ if (filters.pagination && filters.pagination > 0) {
984
+ practitioners = practitioners.slice(0, filters.pagination);
985
+ }
986
+
987
+ return {
988
+ practitioners,
989
+ lastDoc,
990
+ };
991
+ } catch (error) {
992
+ console.error(
993
+ "[PRACTITIONER_SERVICE] Error filtering practitioners:",
994
+ error
995
+ );
996
+ throw error;
997
+ }
998
+ }
664
999
  }