@blackcode_sa/metaestetics-api 1.8.14 → 1.8.15
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/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +535 -213
- package/dist/index.mjs +536 -214
- package/package.json +1 -1
- package/src/services/clinic/utils/filter.utils.ts +242 -57
- package/src/services/practitioner/practitioner.service.ts +191 -80
- package/src/services/procedure/procedure.service.ts +279 -123
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ import { Clinic, ClinicTag, CLINICS_COLLECTION } from "../../../types/clinic";
|
|
|
14
14
|
import { geohashQueryBounds, distanceBetween } from "geofire-common";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Get clinics based on multiple filtering criteria
|
|
17
|
+
* Get clinics based on multiple filtering criteria with fallback strategies
|
|
18
18
|
*
|
|
19
19
|
* @param db - Firestore database instance
|
|
20
20
|
* @param filters - Various filters to apply
|
|
@@ -41,79 +41,264 @@ export async function getClinicsByFilters(
|
|
|
41
41
|
clinics: (Clinic & { distance?: number })[];
|
|
42
42
|
lastDoc: any;
|
|
43
43
|
}> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
try {
|
|
45
|
+
console.log("[CLINIC_SERVICE] Starting clinic filtering with multiple strategies");
|
|
46
|
+
|
|
47
|
+
// Geo query debug i validacija
|
|
48
|
+
if (filters.center && filters.radiusInKm) {
|
|
49
|
+
console.log('[CLINIC_SERVICE] Executing geo query:', {
|
|
50
|
+
center: filters.center,
|
|
51
|
+
radius: filters.radiusInKm,
|
|
52
|
+
serviceName: 'ClinicService'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Validacija location podataka
|
|
56
|
+
if (!filters.center.latitude || !filters.center.longitude) {
|
|
57
|
+
console.warn('[CLINIC_SERVICE] Invalid location data:', filters.center);
|
|
58
|
+
filters.center = undefined;
|
|
59
|
+
filters.radiusInKm = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
// Base constraints function (used in all strategies)
|
|
64
|
+
const getBaseConstraints = () => {
|
|
65
|
+
const constraints: QueryConstraint[] = [];
|
|
66
|
+
constraints.push(where("isActive", "==", filters.isActive ?? true));
|
|
67
|
+
|
|
68
|
+
// Tag (only first, due to Firestore limitation)
|
|
69
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
70
|
+
constraints.push(where("tags", "array-contains", filters.tags[0]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Procedure filters (most specific first)
|
|
74
|
+
if (filters.procedureTechnology) {
|
|
75
|
+
constraints.push(where("servicesInfo.technology", "==", filters.procedureTechnology));
|
|
76
|
+
} else if (filters.procedureSubcategory) {
|
|
77
|
+
constraints.push(where("servicesInfo.subCategory", "==", filters.procedureSubcategory));
|
|
78
|
+
} else if (filters.procedureCategory) {
|
|
79
|
+
constraints.push(where("servicesInfo.category", "==", filters.procedureCategory));
|
|
80
|
+
} else if (filters.procedureFamily) {
|
|
81
|
+
constraints.push(where("servicesInfo.procedureFamily", "==", filters.procedureFamily));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Rating filters
|
|
85
|
+
if (filters.minRating !== undefined) {
|
|
86
|
+
constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
|
|
87
|
+
}
|
|
88
|
+
if (filters.maxRating !== undefined) {
|
|
89
|
+
constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return constraints;
|
|
93
|
+
};
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
95
|
+
// Strategy 1: Try nameLower search if nameSearch exists
|
|
96
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
97
|
+
try {
|
|
98
|
+
console.log("[CLINIC_SERVICE] Strategy 1: Trying nameLower search");
|
|
99
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
100
|
+
const constraints = getBaseConstraints();
|
|
101
|
+
constraints.push(where("nameLower", ">=", searchTerm));
|
|
102
|
+
constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
|
|
103
|
+
constraints.push(orderBy("nameLower"));
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
if (filters.lastDoc) {
|
|
106
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
107
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
108
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
109
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
110
|
+
} else {
|
|
111
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
constraints.push(limit(filters.pagination || 5));
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
117
|
+
const querySnapshot = await getDocs(q);
|
|
118
|
+
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
119
|
+
|
|
120
|
+
// Apply in-memory filters
|
|
121
|
+
clinics = applyInMemoryFilters(clinics, filters);
|
|
122
|
+
|
|
123
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
124
|
+
|
|
125
|
+
console.log(`[CLINIC_SERVICE] Strategy 1 success: ${clinics.length} clinics`);
|
|
126
|
+
|
|
127
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
128
|
+
if (clinics.length < (filters.pagination || 5)) {
|
|
129
|
+
return { clinics, lastDoc: null };
|
|
130
|
+
}
|
|
131
|
+
return { clinics, lastDoc };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.log("[CLINIC_SERVICE] Strategy 1 failed:", error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
81
136
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
137
|
+
// Strategy 2: Try name field search as fallback
|
|
138
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
139
|
+
try {
|
|
140
|
+
console.log("[CLINIC_SERVICE] Strategy 2: Trying name field search");
|
|
141
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
142
|
+
const constraints = getBaseConstraints();
|
|
143
|
+
constraints.push(where("name", ">=", searchTerm));
|
|
144
|
+
constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
|
|
145
|
+
constraints.push(orderBy("name"));
|
|
146
|
+
|
|
147
|
+
if (filters.lastDoc) {
|
|
148
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
149
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
150
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
151
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
152
|
+
} else {
|
|
153
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
constraints.push(limit(filters.pagination || 5));
|
|
157
|
+
|
|
158
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
159
|
+
const querySnapshot = await getDocs(q);
|
|
160
|
+
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
161
|
+
|
|
162
|
+
// Apply in-memory filters
|
|
163
|
+
clinics = applyInMemoryFilters(clinics, filters);
|
|
164
|
+
|
|
165
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
166
|
+
|
|
167
|
+
console.log(`[CLINIC_SERVICE] Strategy 2 success: ${clinics.length} clinics`);
|
|
168
|
+
|
|
169
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
170
|
+
if (clinics.length < (filters.pagination || 5)) {
|
|
171
|
+
return { clinics, lastDoc: null };
|
|
172
|
+
}
|
|
173
|
+
return { clinics, lastDoc };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.log("[CLINIC_SERVICE] Strategy 2 failed:", error);
|
|
176
|
+
}
|
|
91
177
|
}
|
|
92
|
-
}
|
|
93
|
-
constraints.push(limit(filters.pagination || 5));
|
|
94
178
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
179
|
+
// Strategy 3: createdAt ordering with client-side name filtering
|
|
180
|
+
try {
|
|
181
|
+
console.log("[CLINIC_SERVICE] Strategy 3: Using createdAt ordering with client-side filtering");
|
|
182
|
+
const constraints = getBaseConstraints();
|
|
183
|
+
constraints.push(orderBy("createdAt", "desc"));
|
|
184
|
+
|
|
185
|
+
if (filters.lastDoc) {
|
|
186
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
187
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
188
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
189
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
190
|
+
} else {
|
|
191
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
constraints.push(limit(filters.pagination || 5));
|
|
99
195
|
|
|
100
|
-
|
|
196
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
197
|
+
const querySnapshot = await getDocs(q);
|
|
198
|
+
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
199
|
+
|
|
200
|
+
// Client-side name filtering
|
|
201
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
202
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
203
|
+
clinics = clinics.filter(clinic => {
|
|
204
|
+
const name = (clinic.name || '').toLowerCase();
|
|
205
|
+
const nameLower = clinic.nameLower || '';
|
|
206
|
+
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
207
|
+
});
|
|
208
|
+
console.log(`[CLINIC_SERVICE] Applied name filter, results: ${clinics.length}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Apply in-memory filters
|
|
212
|
+
clinics = applyInMemoryFilters(clinics, filters);
|
|
213
|
+
|
|
214
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
215
|
+
|
|
216
|
+
console.log(`[CLINIC_SERVICE] Strategy 3 success: ${clinics.length} clinics`);
|
|
217
|
+
|
|
218
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
219
|
+
if (clinics.length < (filters.pagination || 5)) {
|
|
220
|
+
return { clinics, lastDoc: null };
|
|
221
|
+
}
|
|
222
|
+
return { clinics, lastDoc };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.log("[CLINIC_SERVICE] Strategy 3 failed:", error);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Strategy 4: Minimal fallback query
|
|
228
|
+
try {
|
|
229
|
+
console.log("[CLINIC_SERVICE] Strategy 4: Minimal fallback");
|
|
230
|
+
const constraints: QueryConstraint[] = [
|
|
231
|
+
where("isActive", "==", true),
|
|
232
|
+
orderBy("createdAt", "desc"),
|
|
233
|
+
limit(filters.pagination || 5)
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
237
|
+
const querySnapshot = await getDocs(q);
|
|
238
|
+
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
239
|
+
|
|
240
|
+
// Client-side name filtering
|
|
241
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
242
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
243
|
+
clinics = clinics.filter(clinic => {
|
|
244
|
+
const name = (clinic.name || '').toLowerCase();
|
|
245
|
+
const nameLower = clinic.nameLower || '';
|
|
246
|
+
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
247
|
+
});
|
|
248
|
+
console.log(`[CLINIC_SERVICE] Applied name filter, results: ${clinics.length}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Apply in-memory filters
|
|
252
|
+
clinics = applyInMemoryFilters(clinics, filters);
|
|
253
|
+
|
|
254
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
255
|
+
|
|
256
|
+
console.log(`[CLINIC_SERVICE] Strategy 4 success: ${clinics.length} clinics`);
|
|
257
|
+
|
|
258
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
259
|
+
if (clinics.length < (filters.pagination || 5)) {
|
|
260
|
+
return { clinics, lastDoc: null };
|
|
261
|
+
}
|
|
262
|
+
return { clinics, lastDoc };
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.log("[CLINIC_SERVICE] Strategy 4 failed:", error);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// All strategies failed
|
|
268
|
+
console.log("[CLINIC_SERVICE] All strategies failed, returning empty result");
|
|
269
|
+
return { clinics: [], lastDoc: null };
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error("[CLINIC_SERVICE] Error filtering clinics:", error);
|
|
273
|
+
return { clinics: [], lastDoc: null };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Helper function to apply in-memory filters that Firestore doesn't support well
|
|
279
|
+
*/
|
|
280
|
+
function applyInMemoryFilters(clinics: Clinic[], filters: any): (Clinic & { distance?: number })[] {
|
|
281
|
+
// Multi-tag filter (Firestore only supports single array-contains)
|
|
101
282
|
if (filters.tags && filters.tags.length > 1) {
|
|
102
|
-
clinics = clinics.filter(clinic => filters.tags!.every(tag => clinic.tags.includes(tag)));
|
|
283
|
+
clinics = clinics.filter(clinic => filters.tags!.every((tag: ClinicTag) => clinic.tags.includes(tag)));
|
|
103
284
|
}
|
|
285
|
+
|
|
286
|
+
// Geo-radius filter
|
|
104
287
|
if (filters.center && filters.radiusInKm) {
|
|
105
288
|
clinics = clinics.filter(clinic => {
|
|
106
289
|
const distance = distanceBetween(
|
|
107
290
|
[filters.center!.latitude, filters.center!.longitude],
|
|
108
291
|
[clinic.location.latitude, clinic.location.longitude]
|
|
109
|
-
) / 1000;
|
|
110
|
-
|
|
292
|
+
) / 1000; // Convert to km
|
|
293
|
+
|
|
294
|
+
// Attach distance for frontend sorting/display
|
|
111
295
|
(clinic as any).distance = distance;
|
|
112
296
|
return distance <= filters.radiusInKm!;
|
|
113
297
|
});
|
|
298
|
+
|
|
299
|
+
// Sort by distance when geo filtering is applied
|
|
300
|
+
clinics.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
|
|
114
301
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
118
|
-
return { clinics, lastDoc: lastVisibleDoc };
|
|
302
|
+
|
|
303
|
+
return clinics as (Clinic & { distance?: number })[];
|
|
119
304
|
}
|
|
@@ -1043,101 +1043,212 @@ export class PractitionerService extends BaseService {
|
|
|
1043
1043
|
includeDraftPractitioners?: boolean;
|
|
1044
1044
|
}): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
|
|
1045
1045
|
try {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1046
|
+
console.log("[PRACTITIONER_SERVICE] Starting practitioner filtering with fallback strategies");
|
|
1047
|
+
|
|
1048
|
+
// Geo query debug i validacija
|
|
1049
|
+
if (filters.location && filters.radiusInKm) {
|
|
1050
|
+
console.log('[PRACTITIONER_SERVICE] Executing geo query:', {
|
|
1051
|
+
location: filters.location,
|
|
1052
|
+
radius: filters.radiusInKm,
|
|
1053
|
+
serviceName: 'PractitionerService'
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Validacija location podataka
|
|
1057
|
+
if (!filters.location.latitude || !filters.location.longitude) {
|
|
1058
|
+
console.warn('[PRACTITIONER_SERVICE] Invalid location data:', filters.location);
|
|
1059
|
+
filters.location = undefined;
|
|
1060
|
+
filters.radiusInKm = undefined;
|
|
1061
|
+
}
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
-
//
|
|
1064
|
+
// Strategy 1: Try fullNameLower search if nameSearch exists
|
|
1065
1065
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1066
|
+
try {
|
|
1067
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 1: Trying fullNameLower search");
|
|
1068
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1069
|
+
const constraints: any[] = [];
|
|
1070
|
+
|
|
1071
|
+
if (!filters.includeDraftPractitioners) {
|
|
1072
|
+
constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
|
|
1073
|
+
}
|
|
1074
|
+
constraints.push(where("isActive", "==", true));
|
|
1075
|
+
constraints.push(where("fullNameLower", ">=", searchTerm));
|
|
1076
|
+
constraints.push(where("fullNameLower", "<=", searchTerm + "\uf8ff"));
|
|
1077
|
+
constraints.push(orderBy("fullNameLower"));
|
|
1078
|
+
|
|
1079
|
+
if (filters.lastDoc) {
|
|
1080
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
1081
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
1082
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
1083
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
1084
|
+
} else {
|
|
1085
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
constraints.push(limit(filters.pagination || 10));
|
|
1089
|
+
|
|
1090
|
+
const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
|
|
1091
|
+
const querySnapshot = await getDocs(q);
|
|
1092
|
+
const practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
|
|
1093
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1094
|
+
|
|
1095
|
+
console.log(`[PRACTITIONER_SERVICE] Strategy 1 success: ${practitioners.length} practitioners`);
|
|
1096
|
+
|
|
1097
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1098
|
+
if (practitioners.length < (filters.pagination || 10)) {
|
|
1099
|
+
return { practitioners, lastDoc: null };
|
|
1100
|
+
}
|
|
1101
|
+
return { practitioners, lastDoc };
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 1 failed:", error);
|
|
1104
|
+
}
|
|
1069
1105
|
}
|
|
1070
1106
|
|
|
1071
|
-
//
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
constraints.push(where("
|
|
1080
|
-
}
|
|
1107
|
+
// Strategy 2: Basic query with createdAt ordering (no name search)
|
|
1108
|
+
try {
|
|
1109
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 2: Basic query with createdAt ordering");
|
|
1110
|
+
const constraints: any[] = [];
|
|
1111
|
+
|
|
1112
|
+
if (!filters.includeDraftPractitioners) {
|
|
1113
|
+
constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
|
|
1114
|
+
}
|
|
1115
|
+
constraints.push(where("isActive", "==", true));
|
|
1081
1116
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1117
|
+
// Add other filters that work well with Firestore
|
|
1118
|
+
if (filters.certifications && filters.certifications.length > 0) {
|
|
1119
|
+
constraints.push(
|
|
1120
|
+
where("certification.specialties", "array-contains-any", filters.certifications)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (filters.minRating !== undefined) {
|
|
1125
|
+
constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
|
|
1126
|
+
}
|
|
1127
|
+
if (filters.maxRating !== undefined) {
|
|
1128
|
+
constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
constraints.push(orderBy("createdAt", "desc"));
|
|
1089
1132
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
constraints.push(startAfter(filters.lastDoc));
|
|
1095
|
-
} else if (Array.isArray(filters.lastDoc)) {
|
|
1096
|
-
constraints.push(startAfter(...filters.lastDoc));
|
|
1133
|
+
// Pagination sa createdAt - poboljšano za geo queries
|
|
1134
|
+
if (filters.location && filters.radiusInKm) {
|
|
1135
|
+
// Ne koristiti lastDoc za geo queries, već preuzmi više rezultata
|
|
1136
|
+
constraints.push(limit((filters.pagination || 10) * 2)); // Dvostruko više za geo filter
|
|
1097
1137
|
} else {
|
|
1098
|
-
|
|
1138
|
+
if (filters.lastDoc) {
|
|
1139
|
+
if (typeof filters.lastDoc.data === "function") {
|
|
1140
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
1141
|
+
} else if (Array.isArray(filters.lastDoc)) {
|
|
1142
|
+
constraints.push(startAfter(...filters.lastDoc));
|
|
1143
|
+
} else {
|
|
1144
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
constraints.push(limit(filters.pagination || 10));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
|
|
1151
|
+
const querySnapshot = await getDocs(q);
|
|
1152
|
+
let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
|
|
1153
|
+
|
|
1154
|
+
// Apply geo filter if needed (this is the only in-memory filter we keep)
|
|
1155
|
+
if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
|
|
1156
|
+
const location = filters.location;
|
|
1157
|
+
const radiusInKm = filters.radiusInKm;
|
|
1158
|
+
practitioners = practitioners.filter((practitioner) => {
|
|
1159
|
+
const clinics = practitioner.clinicsInfo || [];
|
|
1160
|
+
return clinics.some((clinic) => {
|
|
1161
|
+
const distance = distanceBetween(
|
|
1162
|
+
[location.latitude, location.longitude],
|
|
1163
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
1164
|
+
);
|
|
1165
|
+
const distanceInKm = distance / 1000;
|
|
1166
|
+
return distanceInKm <= radiusInKm;
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Ograniči na pagination broj nakon geo filtera
|
|
1171
|
+
practitioners = practitioners.slice(0, filters.pagination || 10);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Apply client-side name filtering if needed
|
|
1175
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1176
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1177
|
+
practitioners = practitioners.filter(practitioner => {
|
|
1178
|
+
const firstName = (practitioner.basicInfo?.firstName || '').toLowerCase();
|
|
1179
|
+
const lastName = (practitioner.basicInfo?.lastName || '').toLowerCase();
|
|
1180
|
+
const fullName = `${firstName} ${lastName}`.trim();
|
|
1181
|
+
const fullNameLower = practitioner.fullNameLower || '';
|
|
1182
|
+
|
|
1183
|
+
return firstName.includes(searchTerm) ||
|
|
1184
|
+
lastName.includes(searchTerm) ||
|
|
1185
|
+
fullName.includes(searchTerm) ||
|
|
1186
|
+
fullNameLower.includes(searchTerm);
|
|
1187
|
+
});
|
|
1188
|
+
console.log(`[PRACTITIONER_SERVICE] Applied name filter, results: ${practitioners.length}`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1192
|
+
console.log(`[PRACTITIONER_SERVICE] Strategy 2 success: ${practitioners.length} practitioners`);
|
|
1193
|
+
|
|
1194
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1195
|
+
if (practitioners.length < (filters.pagination || 10)) {
|
|
1196
|
+
return { practitioners, lastDoc: null };
|
|
1099
1197
|
}
|
|
1198
|
+
return { practitioners, lastDoc };
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 2 failed:", error);
|
|
1100
1201
|
}
|
|
1101
|
-
constraints.push(limit(filters.pagination || 5));
|
|
1102
1202
|
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
);
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1203
|
+
// Strategy 3: Minimal query fallback
|
|
1204
|
+
try {
|
|
1205
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 3: Minimal query fallback");
|
|
1206
|
+
const constraints: any[] = [
|
|
1207
|
+
where("isActive", "==", true),
|
|
1208
|
+
orderBy("createdAt", "desc"),
|
|
1209
|
+
limit(filters.pagination || 10)
|
|
1210
|
+
];
|
|
1211
|
+
|
|
1212
|
+
const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
|
|
1213
|
+
const querySnapshot = await getDocs(q);
|
|
1214
|
+
let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
|
|
1215
|
+
|
|
1216
|
+
// Apply client-side name filtering if needed
|
|
1217
|
+
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1218
|
+
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
1219
|
+
practitioners = practitioners.filter(practitioner => {
|
|
1220
|
+
const firstName = (practitioner.basicInfo?.firstName || '').toLowerCase();
|
|
1221
|
+
const lastName = (practitioner.basicInfo?.lastName || '').toLowerCase();
|
|
1222
|
+
const fullName = `${firstName} ${lastName}`.trim();
|
|
1223
|
+
const fullNameLower = practitioner.fullNameLower || '';
|
|
1224
|
+
|
|
1225
|
+
return firstName.includes(searchTerm) ||
|
|
1226
|
+
lastName.includes(searchTerm) ||
|
|
1227
|
+
fullName.includes(searchTerm) ||
|
|
1228
|
+
fullNameLower.includes(searchTerm);
|
|
1125
1229
|
});
|
|
1126
|
-
|
|
1230
|
+
console.log(`[PRACTITIONER_SERVICE] Applied name filter, results: ${practitioners.length}`);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1234
|
+
console.log(`[PRACTITIONER_SERVICE] Strategy 3 success: ${practitioners.length} practitioners`);
|
|
1235
|
+
|
|
1236
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1237
|
+
if (practitioners.length < (filters.pagination || 10)) {
|
|
1238
|
+
return { practitioners, lastDoc: null };
|
|
1239
|
+
}
|
|
1240
|
+
return { practitioners, lastDoc };
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 3 failed:", error);
|
|
1127
1243
|
}
|
|
1128
1244
|
|
|
1129
|
-
//
|
|
1130
|
-
|
|
1131
|
-
return {
|
|
1132
|
-
|
|
1133
|
-
lastDoc,
|
|
1134
|
-
};
|
|
1245
|
+
// All strategies failed
|
|
1246
|
+
console.log("[PRACTITIONER_SERVICE] All strategies failed, returning empty result");
|
|
1247
|
+
return { practitioners: [], lastDoc: null };
|
|
1248
|
+
|
|
1135
1249
|
} catch (error) {
|
|
1136
|
-
console.error(
|
|
1137
|
-
|
|
1138
|
-
error
|
|
1139
|
-
);
|
|
1140
|
-
throw error;
|
|
1250
|
+
console.error("[PRACTITIONER_SERVICE] Error filtering practitioners:", error);
|
|
1251
|
+
return { practitioners: [], lastDoc: null };
|
|
1141
1252
|
}
|
|
1142
1253
|
}
|
|
1143
1254
|
|