@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.
- package/dist/admin/index.d.mts +1324 -1
- package/dist/admin/index.d.ts +1324 -1
- package/dist/admin/index.js +1674 -2
- package/dist/admin/index.mjs +1668 -2
- package/dist/backoffice/index.d.mts +99 -7
- package/dist/backoffice/index.d.ts +99 -7
- package/dist/index.d.mts +4036 -2372
- package/dist/index.d.ts +4036 -2372
- package/dist/index.js +2331 -2009
- package/dist/index.mjs +2279 -1954
- package/package.json +2 -1
- package/src/admin/aggregation/README.md +79 -0
- package/src/admin/aggregation/clinic/README.md +52 -0
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
- package/src/admin/aggregation/patient/README.md +27 -0
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
- package/src/admin/index.ts +60 -4
- package/src/admin/mailing/README.md +95 -0
- package/src/admin/mailing/base.mailing.service.ts +131 -0
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/mailing/practitionerInvite/index.ts +1 -0
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
- package/src/index.ts +28 -4
- package/src/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +197 -107
- package/src/services/clinic/utils/clinic.utils.ts +68 -119
- package/src/services/clinic/utils/filter.utils.d.ts +23 -0
- package/src/services/clinic/utils/filter.utils.ts +264 -0
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +439 -104
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +521 -311
- package/src/services/reviews/reviews.service.ts +842 -0
- package/src/types/clinic/index.ts +24 -56
- package/src/types/practitioner/index.ts +34 -33
- package/src/types/procedure/index.ts +32 -0
- package/src/types/profile/index.ts +1 -1
- package/src/types/reviews/index.ts +126 -0
- package/src/validations/clinic.schema.ts +37 -64
- package/src/validations/practitioner.schema.ts +42 -32
- package/src/validations/procedure.schema.ts +11 -3
- package/src/validations/reviews.schema.ts +189 -0
- 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("
|
|
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
|
-
*
|
|
79
|
+
* Creates a new practitioner
|
|
69
80
|
*/
|
|
70
81
|
async createPractitioner(
|
|
71
82
|
data: CreatePractitionerData
|
|
72
83
|
): Promise<Practitioner> {
|
|
73
84
|
try {
|
|
74
|
-
|
|
75
|
-
const
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
// Create practitioner object
|
|
101
|
+
const practitioner: Omit<Practitioner, "createdAt" | "updatedAt"> & {
|
|
102
|
+
createdAt: FieldValue;
|
|
103
|
+
updatedAt: FieldValue;
|
|
98
104
|
} = {
|
|
99
|
-
id:
|
|
100
|
-
userRef:
|
|
101
|
-
basicInfo:
|
|
102
|
-
certification:
|
|
103
|
-
clinics:
|
|
104
|
-
clinicWorkingHours:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
123
|
+
// Validate the entire object
|
|
113
124
|
practitionerSchema.parse({
|
|
114
|
-
...
|
|
125
|
+
...practitioner,
|
|
115
126
|
createdAt: Timestamp.now(),
|
|
116
127
|
updatedAt: Timestamp.now(),
|
|
117
128
|
});
|
|
118
129
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
// Create practitioner document
|
|
131
|
+
const practitionerRef = doc(
|
|
132
|
+
this.db,
|
|
133
|
+
PRACTITIONERS_COLLECTION,
|
|
134
|
+
practitionerId
|
|
123
135
|
);
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
if (!savedPractitioner) {
|
|
127
|
-
throw new Error("Failed to create practitioner profile");
|
|
128
|
-
}
|
|
137
|
+
await setDoc(practitionerRef, practitioner);
|
|
129
138
|
|
|
130
|
-
|
|
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(
|
|
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
|
-
*
|
|
529
|
+
* Updates a practitioner
|
|
492
530
|
*/
|
|
493
531
|
async updatePractitioner(
|
|
494
532
|
practitionerId: string,
|
|
495
533
|
data: UpdatePractitionerData
|
|
496
534
|
): Promise<Practitioner> {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
...
|
|
555
|
+
...validData,
|
|
521
556
|
updatedAt: serverTimestamp(),
|
|
522
557
|
};
|
|
523
558
|
|
|
524
|
-
//
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
580
|
+
* Adds a clinic to a practitioner
|
|
549
581
|
*/
|
|
550
582
|
async addClinic(practitionerId: string, clinicId: string): Promise<void> {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
592
|
+
if (!practitionerDoc.exists()) {
|
|
593
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
594
|
+
}
|
|
560
595
|
|
|
561
|
-
|
|
562
|
-
throw new Error("Practitioner is already associated with this clinic");
|
|
563
|
-
}
|
|
596
|
+
const practitioner = practitionerDoc.data() as Practitioner;
|
|
564
597
|
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
*
|
|
621
|
+
* Removes a clinic from a practitioner
|
|
572
622
|
*/
|
|
573
623
|
async removeClinic(practitionerId: string, clinicId: string): Promise<void> {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
633
|
+
if (!practitionerDoc.exists()) {
|
|
634
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
635
|
+
}
|
|
582
636
|
|
|
583
|
-
|
|
584
|
-
|
|
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
|
}
|