@blackcode_sa/metaestetics-api 1.5.27 → 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.
- package/dist/admin/index.d.mts +1199 -1
- package/dist/admin/index.d.ts +1199 -1
- package/dist/admin/index.js +1337 -2
- package/dist/admin/index.mjs +1333 -2
- package/dist/backoffice/index.d.mts +99 -7
- package/dist/backoffice/index.d.ts +99 -7
- package/dist/index.d.mts +4184 -2426
- package/dist/index.d.ts +4184 -2426
- package/dist/index.js +2692 -1546
- package/dist/index.mjs +2663 -1502
- package/package.json +1 -1
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
- package/src/admin/index.ts +53 -4
- package/src/index.ts +28 -4
- package/src/services/calendar/calendar-refactored.service.ts +1 -1
- package/src/services/clinic/clinic.service.ts +344 -77
- package/src/services/clinic/utils/clinic.utils.ts +187 -8
- 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/practitioner.service.ts +616 -5
- package/src/services/procedure/procedure.service.ts +678 -52
- 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 +39 -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 +14 -3
- package/src/validations/reviews.schema.ts +189 -0
- package/src/services/clinic/utils/review.utils.ts +0 -93
|
@@ -18,17 +18,19 @@ import {
|
|
|
18
18
|
Clinic,
|
|
19
19
|
CreateClinicData,
|
|
20
20
|
CLINICS_COLLECTION,
|
|
21
|
-
ClinicReview,
|
|
22
21
|
ClinicTag,
|
|
23
22
|
ClinicGroup,
|
|
24
23
|
ClinicBranchSetupData,
|
|
25
24
|
ClinicLocation,
|
|
26
25
|
} from "../../../types/clinic";
|
|
27
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
geohashForLocation,
|
|
28
|
+
distanceBetween,
|
|
29
|
+
geohashQueryBounds,
|
|
30
|
+
} from "geofire-common";
|
|
28
31
|
import {
|
|
29
32
|
clinicSchema,
|
|
30
33
|
createClinicSchema,
|
|
31
|
-
clinicReviewSchema,
|
|
32
34
|
} from "../../../validations/clinic.schema";
|
|
33
35
|
import { z } from "zod";
|
|
34
36
|
import { FirebaseApp } from "firebase/app";
|
|
@@ -273,11 +275,18 @@ export async function createClinic(
|
|
|
273
275
|
photosWithTags: processedPhotosWithTags,
|
|
274
276
|
doctors: [],
|
|
275
277
|
doctorsInfo: [],
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
procedures: [],
|
|
279
|
+
proceduresInfo: [],
|
|
280
|
+
reviewInfo: {
|
|
281
|
+
totalReviews: 0,
|
|
282
|
+
averageRating: 0,
|
|
283
|
+
cleanliness: 0,
|
|
284
|
+
facilities: 0,
|
|
285
|
+
staffFriendliness: 0,
|
|
286
|
+
waitingTime: 0,
|
|
287
|
+
accessibility: 0,
|
|
288
|
+
recommendationPercentage: 0,
|
|
289
|
+
},
|
|
281
290
|
admins: [creatorAdminId],
|
|
282
291
|
createdAt: now,
|
|
283
292
|
updatedAt: now,
|
|
@@ -768,3 +777,173 @@ export async function getActiveClinicsByAdmin(
|
|
|
768
777
|
clinicGroupService
|
|
769
778
|
);
|
|
770
779
|
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Retrieves a clinic by its ID
|
|
783
|
+
*
|
|
784
|
+
* @param db - Firestore database instance
|
|
785
|
+
* @param clinicId - ID of the clinic to retrieve
|
|
786
|
+
* @returns The clinic if found, null otherwise
|
|
787
|
+
*/
|
|
788
|
+
export async function getClinicById(
|
|
789
|
+
db: Firestore,
|
|
790
|
+
clinicId: string
|
|
791
|
+
): Promise<Clinic | null> {
|
|
792
|
+
try {
|
|
793
|
+
const clinicRef = doc(db, CLINICS_COLLECTION, clinicId);
|
|
794
|
+
const clinicSnapshot = await getDoc(clinicRef);
|
|
795
|
+
|
|
796
|
+
if (!clinicSnapshot.exists()) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const clinicData = clinicSnapshot.data() as Clinic;
|
|
801
|
+
return {
|
|
802
|
+
...clinicData,
|
|
803
|
+
id: clinicSnapshot.id,
|
|
804
|
+
};
|
|
805
|
+
} catch (error) {
|
|
806
|
+
console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Retrieves all clinics with optional pagination
|
|
813
|
+
*
|
|
814
|
+
* @param db - Firestore database instance
|
|
815
|
+
* @param pagination - Optional number of clinics per page (0 or undefined returns all)
|
|
816
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
817
|
+
* @returns Array of clinics and the last document for pagination
|
|
818
|
+
*/
|
|
819
|
+
export async function getAllClinics(
|
|
820
|
+
db: Firestore,
|
|
821
|
+
pagination?: number,
|
|
822
|
+
lastDoc?: any
|
|
823
|
+
): Promise<{ clinics: Clinic[]; lastDoc: any }> {
|
|
824
|
+
try {
|
|
825
|
+
const clinicsCollection = collection(db, CLINICS_COLLECTION);
|
|
826
|
+
let clinicsQuery = query(clinicsCollection);
|
|
827
|
+
|
|
828
|
+
// If pagination is specified and greater than 0, limit the query
|
|
829
|
+
if (pagination && pagination > 0) {
|
|
830
|
+
const { limit, startAfter } = require("firebase/firestore");
|
|
831
|
+
|
|
832
|
+
if (lastDoc) {
|
|
833
|
+
clinicsQuery = query(
|
|
834
|
+
clinicsCollection,
|
|
835
|
+
startAfter(lastDoc),
|
|
836
|
+
limit(pagination)
|
|
837
|
+
);
|
|
838
|
+
} else {
|
|
839
|
+
clinicsQuery = query(clinicsCollection, limit(pagination));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const clinicsSnapshot = await getDocs(clinicsQuery);
|
|
844
|
+
const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
|
|
845
|
+
|
|
846
|
+
const clinics = clinicsSnapshot.docs.map((doc) => {
|
|
847
|
+
const data = doc.data() as Clinic;
|
|
848
|
+
return {
|
|
849
|
+
...data,
|
|
850
|
+
id: doc.id,
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
clinics,
|
|
856
|
+
lastDoc: lastVisible,
|
|
857
|
+
};
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.error("[CLINIC_UTILS] Error getting all clinics:", error);
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Retrieves all clinics within a specified range from a location with optional pagination
|
|
866
|
+
*
|
|
867
|
+
* @param db - Firestore database instance
|
|
868
|
+
* @param center - The center location coordinates {latitude, longitude}
|
|
869
|
+
* @param rangeInKm - The range in kilometers to search within
|
|
870
|
+
* @param pagination - Optional number of clinics per page (0 or undefined returns all)
|
|
871
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
872
|
+
* @returns Array of clinics with distance information and the last document for pagination
|
|
873
|
+
*/
|
|
874
|
+
export async function getAllClinicsInRange(
|
|
875
|
+
db: Firestore,
|
|
876
|
+
center: { latitude: number; longitude: number },
|
|
877
|
+
rangeInKm: number,
|
|
878
|
+
pagination?: number,
|
|
879
|
+
lastDoc?: any
|
|
880
|
+
): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
|
|
881
|
+
const bounds = geohashQueryBounds(
|
|
882
|
+
[center.latitude, center.longitude],
|
|
883
|
+
rangeInKm * 1000
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const matchingClinics: (Clinic & { distance: number })[] = [];
|
|
887
|
+
let lastDocSnapshot = null;
|
|
888
|
+
|
|
889
|
+
for (const b of bounds) {
|
|
890
|
+
const constraints: QueryConstraint[] = [
|
|
891
|
+
where("location.geohash", ">=", b[0]),
|
|
892
|
+
where("location.geohash", "<=", b[1]),
|
|
893
|
+
where("isActive", "==", true),
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
897
|
+
const querySnapshot = await getDocs(q);
|
|
898
|
+
|
|
899
|
+
for (const doc of querySnapshot.docs) {
|
|
900
|
+
const clinic = doc.data() as Clinic;
|
|
901
|
+
const distance = distanceBetween(
|
|
902
|
+
[center.latitude, center.longitude],
|
|
903
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// Convert to kilometers
|
|
907
|
+
const distanceInKm = distance / 1000;
|
|
908
|
+
|
|
909
|
+
if (distanceInKm <= rangeInKm) {
|
|
910
|
+
matchingClinics.push({
|
|
911
|
+
...clinic,
|
|
912
|
+
distance: distanceInKm,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Sort by distance
|
|
919
|
+
matchingClinics.sort((a, b) => a.distance - b.distance);
|
|
920
|
+
|
|
921
|
+
if (pagination && pagination > 0) {
|
|
922
|
+
// Paginate results
|
|
923
|
+
let result = matchingClinics;
|
|
924
|
+
if (lastDoc && matchingClinics.length > 0) {
|
|
925
|
+
const lastIndex = matchingClinics.findIndex(
|
|
926
|
+
(clinic) => clinic.id === lastDoc.id
|
|
927
|
+
);
|
|
928
|
+
if (lastIndex !== -1) {
|
|
929
|
+
result = matchingClinics.slice(lastIndex + 1);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const paginatedClinics = result.slice(0, pagination);
|
|
934
|
+
const newLastDoc =
|
|
935
|
+
paginatedClinics.length > 0
|
|
936
|
+
? paginatedClinics[paginatedClinics.length - 1]
|
|
937
|
+
: null;
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
clinics: paginatedClinics,
|
|
941
|
+
lastDoc: newLastDoc,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
clinics: matchingClinics,
|
|
947
|
+
lastDoc: null,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Firestore } from "firebase/firestore";
|
|
2
|
+
import { Clinic, ClinicTag } from "../../../types/clinic";
|
|
3
|
+
|
|
4
|
+
export function getClinicsByFilters(
|
|
5
|
+
db: Firestore,
|
|
6
|
+
filters: {
|
|
7
|
+
center?: { latitude: number; longitude: number };
|
|
8
|
+
radiusInKm?: number;
|
|
9
|
+
tags?: ClinicTag[];
|
|
10
|
+
procedureFamily?: string;
|
|
11
|
+
procedureCategory?: string;
|
|
12
|
+
procedureSubcategory?: string;
|
|
13
|
+
procedureTechnology?: string;
|
|
14
|
+
minRating?: number;
|
|
15
|
+
maxRating?: number;
|
|
16
|
+
pagination?: number;
|
|
17
|
+
lastDoc?: any;
|
|
18
|
+
isActive?: boolean;
|
|
19
|
+
}
|
|
20
|
+
): Promise<{
|
|
21
|
+
clinics: (Clinic & { distance?: number })[];
|
|
22
|
+
lastDoc: any;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
query,
|
|
4
|
+
where,
|
|
5
|
+
getDocs,
|
|
6
|
+
Firestore,
|
|
7
|
+
QueryConstraint,
|
|
8
|
+
startAfter,
|
|
9
|
+
limit,
|
|
10
|
+
documentId,
|
|
11
|
+
orderBy,
|
|
12
|
+
} from "firebase/firestore";
|
|
13
|
+
import { Clinic, ClinicTag, CLINICS_COLLECTION } from "../../../types/clinic";
|
|
14
|
+
import { geohashQueryBounds, distanceBetween } from "geofire-common";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get clinics based on multiple filtering criteria
|
|
18
|
+
*
|
|
19
|
+
* @param db - Firestore database instance
|
|
20
|
+
* @param filters - Various filters to apply
|
|
21
|
+
* @returns Filtered clinics and the last document for pagination
|
|
22
|
+
*/
|
|
23
|
+
export async function getClinicsByFilters(
|
|
24
|
+
db: Firestore,
|
|
25
|
+
filters: {
|
|
26
|
+
center?: { latitude: number; longitude: number };
|
|
27
|
+
radiusInKm?: number;
|
|
28
|
+
tags?: ClinicTag[];
|
|
29
|
+
procedureFamily?: string;
|
|
30
|
+
procedureCategory?: string;
|
|
31
|
+
procedureSubcategory?: string;
|
|
32
|
+
procedureTechnology?: string;
|
|
33
|
+
minRating?: number;
|
|
34
|
+
maxRating?: number;
|
|
35
|
+
pagination?: number;
|
|
36
|
+
lastDoc?: any;
|
|
37
|
+
isActive?: boolean;
|
|
38
|
+
}
|
|
39
|
+
): Promise<{
|
|
40
|
+
clinics: (Clinic & { distance?: number })[];
|
|
41
|
+
lastDoc: any;
|
|
42
|
+
}> {
|
|
43
|
+
console.log(
|
|
44
|
+
"[FILTER_UTILS] Starting clinic filtering with criteria:",
|
|
45
|
+
filters
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Determine if we're doing a geo query or a regular query
|
|
49
|
+
const isGeoQuery =
|
|
50
|
+
filters.center && filters.radiusInKm && filters.radiusInKm > 0;
|
|
51
|
+
|
|
52
|
+
// Initialize base constraints
|
|
53
|
+
const constraints: QueryConstraint[] = [];
|
|
54
|
+
|
|
55
|
+
// Add active status filter (default to active if not specified)
|
|
56
|
+
if (filters.isActive !== undefined) {
|
|
57
|
+
constraints.push(where("isActive", "==", filters.isActive));
|
|
58
|
+
} else {
|
|
59
|
+
constraints.push(where("isActive", "==", true));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add tag filtering if specified
|
|
63
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
64
|
+
// Note: We can only use array-contains-any once per query, so we're selecting one tag
|
|
65
|
+
// for the query and we'll filter the remaining tags in memory
|
|
66
|
+
constraints.push(where("tags", "array-contains", filters.tags[0]));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Hierarchical procedure filter - only apply the most specific one provided
|
|
70
|
+
// Order of specificity: technology > subcategory > category > family
|
|
71
|
+
if (filters.procedureTechnology) {
|
|
72
|
+
constraints.push(
|
|
73
|
+
where("servicesInfo.technology", "==", filters.procedureTechnology)
|
|
74
|
+
);
|
|
75
|
+
} else if (filters.procedureSubcategory) {
|
|
76
|
+
constraints.push(
|
|
77
|
+
where("servicesInfo.subCategory", "==", filters.procedureSubcategory)
|
|
78
|
+
);
|
|
79
|
+
} else if (filters.procedureCategory) {
|
|
80
|
+
constraints.push(
|
|
81
|
+
where("servicesInfo.category", "==", filters.procedureCategory)
|
|
82
|
+
);
|
|
83
|
+
} else if (filters.procedureFamily) {
|
|
84
|
+
constraints.push(
|
|
85
|
+
where("servicesInfo.procedureFamily", "==", filters.procedureFamily)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add pagination if specified
|
|
90
|
+
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
91
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
92
|
+
constraints.push(limit(filters.pagination));
|
|
93
|
+
} else if (filters.pagination && filters.pagination > 0) {
|
|
94
|
+
constraints.push(limit(filters.pagination));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add ordering to make pagination consistent
|
|
98
|
+
constraints.push(orderBy(documentId()));
|
|
99
|
+
|
|
100
|
+
let clinicsResult: (Clinic & { distance?: number })[] = [];
|
|
101
|
+
let lastVisibleDoc = null;
|
|
102
|
+
|
|
103
|
+
// For geo queries, we need a different approach
|
|
104
|
+
if (isGeoQuery) {
|
|
105
|
+
const center = filters.center!;
|
|
106
|
+
const radiusInKm = filters.radiusInKm!;
|
|
107
|
+
|
|
108
|
+
// Get the geohash query bounds
|
|
109
|
+
const bounds = geohashQueryBounds(
|
|
110
|
+
[center.latitude, center.longitude],
|
|
111
|
+
radiusInKm * 1000 // Convert to meters
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Collect matching clinics from all bounds
|
|
115
|
+
const matchingClinics: (Clinic & { distance: number })[] = [];
|
|
116
|
+
|
|
117
|
+
// Execute queries for each bound
|
|
118
|
+
for (const bound of bounds) {
|
|
119
|
+
// Create a geo query for this bound
|
|
120
|
+
const geoConstraints = [
|
|
121
|
+
...constraints,
|
|
122
|
+
where("location.geohash", ">=", bound[0]),
|
|
123
|
+
where("location.geohash", "<=", bound[1]),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...geoConstraints);
|
|
127
|
+
const querySnapshot = await getDocs(q);
|
|
128
|
+
|
|
129
|
+
console.log(
|
|
130
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Process results and filter by actual distance
|
|
134
|
+
for (const doc of querySnapshot.docs) {
|
|
135
|
+
const clinic = { ...doc.data(), id: doc.id } as Clinic;
|
|
136
|
+
|
|
137
|
+
// Calculate actual distance
|
|
138
|
+
const distance = distanceBetween(
|
|
139
|
+
[center.latitude, center.longitude],
|
|
140
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Convert to kilometers
|
|
144
|
+
const distanceInKm = distance / 1000;
|
|
145
|
+
|
|
146
|
+
// Check if within radius
|
|
147
|
+
if (distanceInKm <= radiusInKm) {
|
|
148
|
+
// Add distance to clinic object
|
|
149
|
+
matchingClinics.push({
|
|
150
|
+
...clinic,
|
|
151
|
+
distance: distanceInKm,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply additional filters that couldn't be applied in the query
|
|
158
|
+
let filteredClinics = matchingClinics;
|
|
159
|
+
|
|
160
|
+
// Filter by multiple tags if more than one tag was specified
|
|
161
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
162
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
163
|
+
// Check if clinic has all specified tags
|
|
164
|
+
return filters.tags!.every((tag) => clinic.tags.includes(tag));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Filter by rating
|
|
169
|
+
if (filters.minRating !== undefined) {
|
|
170
|
+
filteredClinics = filteredClinics.filter(
|
|
171
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (filters.maxRating !== undefined) {
|
|
176
|
+
filteredClinics = filteredClinics.filter(
|
|
177
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sort by distance
|
|
182
|
+
filteredClinics.sort((a, b) => a.distance - b.distance);
|
|
183
|
+
|
|
184
|
+
// Apply pagination after all filters have been applied
|
|
185
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
186
|
+
// If we have a lastDoc, find its index
|
|
187
|
+
let startIndex = 0;
|
|
188
|
+
if (filters.lastDoc) {
|
|
189
|
+
const lastDocIndex = filteredClinics.findIndex(
|
|
190
|
+
(clinic) => clinic.id === filters.lastDoc.id
|
|
191
|
+
);
|
|
192
|
+
if (lastDocIndex !== -1) {
|
|
193
|
+
startIndex = lastDocIndex + 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Get paginated subset
|
|
198
|
+
const paginatedClinics = filteredClinics.slice(
|
|
199
|
+
startIndex,
|
|
200
|
+
startIndex + filters.pagination
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Set last document for next pagination
|
|
204
|
+
lastVisibleDoc =
|
|
205
|
+
paginatedClinics.length > 0
|
|
206
|
+
? paginatedClinics[paginatedClinics.length - 1]
|
|
207
|
+
: null;
|
|
208
|
+
|
|
209
|
+
clinicsResult = paginatedClinics;
|
|
210
|
+
} else {
|
|
211
|
+
clinicsResult = filteredClinics;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// For non-geo queries, execute a single query with all constraints
|
|
215
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
216
|
+
const querySnapshot = await getDocs(q);
|
|
217
|
+
|
|
218
|
+
console.log(
|
|
219
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Convert docs to clinics
|
|
223
|
+
const clinics = querySnapshot.docs.map((doc) => {
|
|
224
|
+
return { ...doc.data(), id: doc.id } as Clinic;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Apply filters that couldn't be applied in the query
|
|
228
|
+
let filteredClinics = clinics;
|
|
229
|
+
|
|
230
|
+
// Filter by multiple tags if more than one tag was specified
|
|
231
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
232
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
233
|
+
// Check if clinic has all specified tags
|
|
234
|
+
return filters.tags!.every((tag) => clinic.tags.includes(tag));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Filter by rating
|
|
239
|
+
if (filters.minRating !== undefined) {
|
|
240
|
+
filteredClinics = filteredClinics.filter(
|
|
241
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (filters.maxRating !== undefined) {
|
|
246
|
+
filteredClinics = filteredClinics.filter(
|
|
247
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Set last document for pagination
|
|
252
|
+
lastVisibleDoc =
|
|
253
|
+
querySnapshot.docs.length > 0
|
|
254
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
255
|
+
: null;
|
|
256
|
+
|
|
257
|
+
clinicsResult = filteredClinics;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
clinics: clinicsResult,
|
|
262
|
+
lastDoc: lastVisibleDoc,
|
|
263
|
+
};
|
|
264
|
+
}
|