@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.8.14",
4
+ "version": "1.8.15",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -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
- // 1. Prepare Firestore constraints
45
- const constraints: QueryConstraint[] = [];
46
- constraints.push(where("isActive", "==", filters.isActive ?? true));
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
- // Tag (only first, Firestore limitation)
49
- if (filters.tags && filters.tags.length > 0) {
50
- constraints.push(where("tags", "array-contains", filters.tags[0]));
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
- // Procedure filters (most specific)
54
- if (filters.procedureTechnology) {
55
- constraints.push(where("servicesInfo.technology", "==", filters.procedureTechnology));
56
- } else if (filters.procedureSubcategory) {
57
- constraints.push(where("servicesInfo.subCategory", "==", filters.procedureSubcategory));
58
- } else if (filters.procedureCategory) {
59
- constraints.push(where("servicesInfo.category", "==", filters.procedureCategory));
60
- } else if (filters.procedureFamily) {
61
- constraints.push(where("servicesInfo.procedureFamily", "==", filters.procedureFamily));
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
- // Text search by nameLower
65
- let useNameLower = false;
66
- let searchTerm = "";
67
- if (filters.nameSearch && filters.nameSearch.trim()) {
68
- searchTerm = filters.nameSearch.trim().toLowerCase();
69
- constraints.push(where("nameLower", ">=", searchTerm));
70
- constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
71
- useNameLower = true;
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
- // Rating filters
75
- if (filters.minRating !== undefined) {
76
- constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
77
- }
78
- if (filters.maxRating !== undefined) {
79
- constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
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
- // Pagination and ordering
83
- constraints.push(orderBy("nameLower"));
84
- if (filters.lastDoc) {
85
- if (typeof filters.lastDoc.data === "function") {
86
- constraints.push(startAfter(filters.lastDoc));
87
- } else if (Array.isArray(filters.lastDoc)) {
88
- constraints.push(startAfter(...filters.lastDoc));
89
- } else {
90
- constraints.push(startAfter(filters.lastDoc));
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
- // 2. Firestore query
96
- const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
97
- const querySnapshot = await getDocs(q);
98
- let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
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
- // 3. In-memory filters for multi-tag and geo-radius
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
- // Optionally attach distance for frontend
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
- // 4. Return results and lastDoc
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
- // 1. Prepare Firestore constraints
1047
- const constraints: any[] = [];
1048
- if (!filters.includeDraftPractitioners) {
1049
- constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
1050
- }
1051
- constraints.push(where("isActive", "==", true));
1052
-
1053
- // Certifications
1054
- if (filters.certifications && filters.certifications.length > 0) {
1055
- constraints.push(
1056
- where(
1057
- "certification.certifications",
1058
- "array-contains-any",
1059
- filters.certifications
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
- // Text search by fullNameLower
1064
+ // Strategy 1: Try fullNameLower search if nameSearch exists
1065
1065
  if (filters.nameSearch && filters.nameSearch.trim()) {
1066
- const searchTerm = filters.nameSearch.trim().toLowerCase();
1067
- constraints.push(where("fullNameLower", ">=", searchTerm));
1068
- constraints.push(where("fullNameLower", "<=", searchTerm + "\uf8ff"));
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
- // Procedure filters (if mapped to fields)
1072
- if (filters.procedureTechnology) {
1073
- constraints.push(where("proceduresInfo.technologyName", "==", filters.procedureTechnology));
1074
- } else if (filters.procedureSubcategory) {
1075
- constraints.push(where("proceduresInfo.subcategoryName", "==", filters.procedureSubcategory));
1076
- } else if (filters.procedureCategory) {
1077
- constraints.push(where("proceduresInfo.categoryName", "==", filters.procedureCategory));
1078
- } else if (filters.procedureFamily) {
1079
- constraints.push(where("proceduresInfo.family", "==", filters.procedureFamily));
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
- // Rating filters
1083
- if (filters.minRating !== undefined) {
1084
- constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
1085
- }
1086
- if (filters.maxRating !== undefined) {
1087
- constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
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
- // Pagination and ordering
1091
- constraints.push(orderBy("fullNameLower"));
1092
- if (filters.lastDoc) {
1093
- if (typeof filters.lastDoc.data === "function") {
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
- constraints.push(startAfter(filters.lastDoc));
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
- // 2. Firestore query
1104
- const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
1105
- const querySnapshot = await getDocs(q);
1106
- let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
1107
-
1108
- // 3. In-memory filter ONLY for geo-radius (if needed)
1109
- if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1110
- const location = filters.location;
1111
- const radiusInKm = filters.radiusInKm;
1112
- practitioners = practitioners.filter((practitioner) => {
1113
- // Use the aggregated clinicsInfo to check if any clinic is within range
1114
- const clinics = practitioner.clinicsInfo || [];
1115
- return clinics.some((clinic) => {
1116
- // Calculate distance
1117
- const distance = distanceBetween(
1118
- [location.latitude, location.longitude],
1119
- [clinic.location.latitude, clinic.location.longitude]
1120
- );
1121
- // Convert to kilometers
1122
- const distanceInKm = distance / 1000;
1123
- // Check if within radius
1124
- return distanceInKm <= radiusInKm;
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
- // 4. Return results and lastDoc
1130
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1131
- return {
1132
- practitioners,
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
- "[PRACTITIONER_SERVICE] Error filtering practitioners:",
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