@blackcode_sa/metaestetics-api 1.8.17 → 1.10.0
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 +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +233 -205
- package/dist/index.mjs +236 -210
- package/package.json +1 -1
- package/src/services/clinic/clinic.service.ts +91 -142
- package/src/services/clinic/utils/filter.utils.ts +180 -96
- package/src/services/practitioner/practitioner.service.ts +31 -0
- package/src/services/procedure/procedure.service.ts +326 -362
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
limit,
|
|
10
10
|
documentId,
|
|
11
11
|
orderBy,
|
|
12
|
-
} from
|
|
13
|
-
import { Clinic, ClinicTag, CLINICS_COLLECTION } from
|
|
14
|
-
import { geohashQueryBounds, distanceBetween } from
|
|
12
|
+
} from 'firebase/firestore';
|
|
13
|
+
import { Clinic, ClinicTag, CLINICS_COLLECTION } from '../../../types/clinic';
|
|
14
|
+
import { geohashQueryBounds, distanceBetween } from 'geofire-common';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Get clinics based on multiple filtering criteria with fallback strategies
|
|
@@ -36,22 +36,22 @@ export async function getClinicsByFilters(
|
|
|
36
36
|
pagination?: number;
|
|
37
37
|
lastDoc?: any;
|
|
38
38
|
isActive?: boolean;
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
40
|
): Promise<{
|
|
41
41
|
clinics: (Clinic & { distance?: number })[];
|
|
42
42
|
lastDoc: any;
|
|
43
43
|
}> {
|
|
44
44
|
try {
|
|
45
|
-
console.log(
|
|
46
|
-
|
|
45
|
+
console.log('[CLINIC_SERVICE] Starting clinic filtering with multiple strategies');
|
|
46
|
+
|
|
47
47
|
// Geo query debug i validacija
|
|
48
48
|
if (filters.center && filters.radiusInKm) {
|
|
49
49
|
console.log('[CLINIC_SERVICE] Executing geo query:', {
|
|
50
50
|
center: filters.center,
|
|
51
51
|
radius: filters.radiusInKm,
|
|
52
|
-
serviceName: 'ClinicService'
|
|
52
|
+
serviceName: 'ClinicService',
|
|
53
53
|
});
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
// Validacija location podataka
|
|
56
56
|
if (!filters.center.latitude || !filters.center.longitude) {
|
|
57
57
|
console.warn('[CLINIC_SERVICE] Invalid location data:', filters.center);
|
|
@@ -60,39 +60,95 @@ export async function getClinicsByFilters(
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Strategy 0: Geohash-bounds prefilter + in-memory refinement when geo radius is provided
|
|
64
|
+
if (filters.center && filters.radiusInKm) {
|
|
65
|
+
try {
|
|
66
|
+
console.log('[CLINIC_SERVICE] Strategy 0: Geohash bounds prefilter');
|
|
67
|
+
const bounds = geohashQueryBounds(
|
|
68
|
+
[filters.center.latitude, filters.center.longitude],
|
|
69
|
+
(filters.radiusInKm || 0) * 1000,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const collected: Clinic[] = [];
|
|
73
|
+
for (const b of bounds) {
|
|
74
|
+
const constraints: QueryConstraint[] = [
|
|
75
|
+
where('location.geohash', '>=', b[0]),
|
|
76
|
+
where('location.geohash', '<=', b[1]),
|
|
77
|
+
where('isActive', '==', filters.isActive ?? true),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Single tag in query if provided; remaining tag logic is applied in-memory
|
|
81
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
82
|
+
constraints.push(where('tags', 'array-contains', filters.tags[0]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const q0 = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
86
|
+
const snap = await getDocs(q0);
|
|
87
|
+
snap.docs.forEach((d) => collected.push({ ...(d.data() as Clinic), id: d.id }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Deduplicate
|
|
91
|
+
const uniqueMap = new Map<string, Clinic>();
|
|
92
|
+
for (const c of collected) {
|
|
93
|
+
uniqueMap.set(c.id, c);
|
|
94
|
+
}
|
|
95
|
+
let clinics = Array.from(uniqueMap.values());
|
|
96
|
+
|
|
97
|
+
// Apply all remaining filters and compute exact distance + sorting
|
|
98
|
+
clinics = applyInMemoryFilters(clinics, filters);
|
|
99
|
+
|
|
100
|
+
// Manual pagination over materialized results
|
|
101
|
+
const pageSize = filters.pagination || 5;
|
|
102
|
+
let startIndex = 0;
|
|
103
|
+
if (filters.lastDoc && typeof filters.lastDoc === 'object' && (filters.lastDoc as any).id) {
|
|
104
|
+
const idx = clinics.findIndex((c) => c.id === (filters.lastDoc as any).id);
|
|
105
|
+
if (idx >= 0) startIndex = idx + 1;
|
|
106
|
+
}
|
|
107
|
+
const page = clinics.slice(startIndex, startIndex + pageSize);
|
|
108
|
+
const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
|
|
109
|
+
|
|
110
|
+
console.log(
|
|
111
|
+
`[CLINIC_SERVICE] Strategy 0 success: ${page.length} clinics (of ${clinics.length})`,
|
|
112
|
+
);
|
|
113
|
+
return { clinics: page, lastDoc: newLastDoc };
|
|
114
|
+
} catch (geoErr) {
|
|
115
|
+
console.log('[CLINIC_SERVICE] Strategy 0 failed:', geoErr);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
63
119
|
// Base constraints function (used in all strategies)
|
|
64
120
|
const getBaseConstraints = () => {
|
|
65
121
|
const constraints: QueryConstraint[] = [];
|
|
66
|
-
constraints.push(where(
|
|
67
|
-
|
|
122
|
+
constraints.push(where('isActive', '==', filters.isActive ?? true));
|
|
123
|
+
|
|
68
124
|
// Tag (only first, due to Firestore limitation)
|
|
69
125
|
if (filters.tags && filters.tags.length > 0) {
|
|
70
|
-
constraints.push(where(
|
|
126
|
+
constraints.push(where('tags', 'array-contains', filters.tags[0]));
|
|
71
127
|
}
|
|
72
|
-
|
|
128
|
+
|
|
73
129
|
// Rating filters
|
|
74
130
|
if (filters.minRating !== undefined) {
|
|
75
|
-
constraints.push(where(
|
|
131
|
+
constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
|
|
76
132
|
}
|
|
77
133
|
if (filters.maxRating !== undefined) {
|
|
78
|
-
constraints.push(where(
|
|
134
|
+
constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
|
|
79
135
|
}
|
|
80
|
-
|
|
136
|
+
|
|
81
137
|
return constraints;
|
|
82
138
|
};
|
|
83
139
|
|
|
84
140
|
// Strategy 1: Try nameLower search if nameSearch exists
|
|
85
141
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
86
142
|
try {
|
|
87
|
-
console.log(
|
|
143
|
+
console.log('[CLINIC_SERVICE] Strategy 1: Trying nameLower search');
|
|
88
144
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
89
145
|
const constraints = getBaseConstraints();
|
|
90
|
-
constraints.push(where(
|
|
91
|
-
constraints.push(where(
|
|
92
|
-
constraints.push(orderBy(
|
|
146
|
+
constraints.push(where('nameLower', '>=', searchTerm));
|
|
147
|
+
constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
|
|
148
|
+
constraints.push(orderBy('nameLower'));
|
|
93
149
|
|
|
94
150
|
if (filters.lastDoc) {
|
|
95
|
-
if (typeof filters.lastDoc.data ===
|
|
151
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
96
152
|
constraints.push(startAfter(filters.lastDoc));
|
|
97
153
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
98
154
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -104,37 +160,38 @@ export async function getClinicsByFilters(
|
|
|
104
160
|
|
|
105
161
|
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
106
162
|
const querySnapshot = await getDocs(q);
|
|
107
|
-
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
108
|
-
|
|
163
|
+
let clinics = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
164
|
+
|
|
109
165
|
// Apply in-memory filters
|
|
110
166
|
clinics = applyInMemoryFilters(clinics, filters);
|
|
111
|
-
|
|
112
|
-
const lastDoc =
|
|
113
|
-
|
|
167
|
+
|
|
168
|
+
const lastDoc =
|
|
169
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
170
|
+
|
|
114
171
|
console.log(`[CLINIC_SERVICE] Strategy 1 success: ${clinics.length} clinics`);
|
|
115
|
-
|
|
172
|
+
|
|
116
173
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
117
174
|
if (clinics.length < (filters.pagination || 5)) {
|
|
118
175
|
return { clinics, lastDoc: null };
|
|
119
176
|
}
|
|
120
177
|
return { clinics, lastDoc };
|
|
121
178
|
} catch (error) {
|
|
122
|
-
console.log(
|
|
179
|
+
console.log('[CLINIC_SERVICE] Strategy 1 failed:', error);
|
|
123
180
|
}
|
|
124
181
|
}
|
|
125
182
|
|
|
126
183
|
// Strategy 2: Try name field search as fallback
|
|
127
184
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
128
185
|
try {
|
|
129
|
-
console.log(
|
|
186
|
+
console.log('[CLINIC_SERVICE] Strategy 2: Trying name field search');
|
|
130
187
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
131
188
|
const constraints = getBaseConstraints();
|
|
132
|
-
constraints.push(where(
|
|
133
|
-
constraints.push(where(
|
|
134
|
-
constraints.push(orderBy(
|
|
189
|
+
constraints.push(where('name', '>=', searchTerm));
|
|
190
|
+
constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
|
|
191
|
+
constraints.push(orderBy('name'));
|
|
135
192
|
|
|
136
193
|
if (filters.lastDoc) {
|
|
137
|
-
if (typeof filters.lastDoc.data ===
|
|
194
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
138
195
|
constraints.push(startAfter(filters.lastDoc));
|
|
139
196
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
140
197
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -146,33 +203,36 @@ export async function getClinicsByFilters(
|
|
|
146
203
|
|
|
147
204
|
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
148
205
|
const querySnapshot = await getDocs(q);
|
|
149
|
-
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
150
|
-
|
|
206
|
+
let clinics = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
207
|
+
|
|
151
208
|
// Apply in-memory filters
|
|
152
209
|
clinics = applyInMemoryFilters(clinics, filters);
|
|
153
|
-
|
|
154
|
-
const lastDoc =
|
|
155
|
-
|
|
210
|
+
|
|
211
|
+
const lastDoc =
|
|
212
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
213
|
+
|
|
156
214
|
console.log(`[CLINIC_SERVICE] Strategy 2 success: ${clinics.length} clinics`);
|
|
157
|
-
|
|
215
|
+
|
|
158
216
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
159
217
|
if (clinics.length < (filters.pagination || 5)) {
|
|
160
218
|
return { clinics, lastDoc: null };
|
|
161
219
|
}
|
|
162
220
|
return { clinics, lastDoc };
|
|
163
221
|
} catch (error) {
|
|
164
|
-
console.log(
|
|
222
|
+
console.log('[CLINIC_SERVICE] Strategy 2 failed:', error);
|
|
165
223
|
}
|
|
166
224
|
}
|
|
167
225
|
|
|
168
226
|
// Strategy 3: createdAt ordering with client-side name filtering
|
|
169
227
|
try {
|
|
170
|
-
console.log(
|
|
228
|
+
console.log(
|
|
229
|
+
'[CLINIC_SERVICE] Strategy 3: Using createdAt ordering with client-side filtering',
|
|
230
|
+
);
|
|
171
231
|
const constraints = getBaseConstraints();
|
|
172
|
-
constraints.push(orderBy(
|
|
232
|
+
constraints.push(orderBy('createdAt', 'desc'));
|
|
173
233
|
|
|
174
234
|
if (filters.lastDoc) {
|
|
175
|
-
if (typeof filters.lastDoc.data ===
|
|
235
|
+
if (typeof filters.lastDoc.data === 'function') {
|
|
176
236
|
constraints.push(startAfter(filters.lastDoc));
|
|
177
237
|
} else if (Array.isArray(filters.lastDoc)) {
|
|
178
238
|
constraints.push(startAfter(...filters.lastDoc));
|
|
@@ -184,59 +244,60 @@ export async function getClinicsByFilters(
|
|
|
184
244
|
|
|
185
245
|
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
186
246
|
const querySnapshot = await getDocs(q);
|
|
187
|
-
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
247
|
+
let clinics = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
188
248
|
|
|
189
249
|
// Apply in-memory filters (includes name search)
|
|
190
250
|
clinics = applyInMemoryFilters(clinics, filters);
|
|
191
|
-
|
|
192
|
-
const lastDoc =
|
|
193
|
-
|
|
251
|
+
|
|
252
|
+
const lastDoc =
|
|
253
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
254
|
+
|
|
194
255
|
console.log(`[CLINIC_SERVICE] Strategy 3 success: ${clinics.length} clinics`);
|
|
195
|
-
|
|
256
|
+
|
|
196
257
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
197
258
|
if (clinics.length < (filters.pagination || 5)) {
|
|
198
259
|
return { clinics, lastDoc: null };
|
|
199
260
|
}
|
|
200
261
|
return { clinics, lastDoc };
|
|
201
262
|
} catch (error) {
|
|
202
|
-
console.log(
|
|
263
|
+
console.log('[CLINIC_SERVICE] Strategy 3 failed:', error);
|
|
203
264
|
}
|
|
204
265
|
|
|
205
266
|
// Strategy 4: Minimal fallback query
|
|
206
267
|
try {
|
|
207
|
-
console.log(
|
|
268
|
+
console.log('[CLINIC_SERVICE] Strategy 4: Minimal fallback');
|
|
208
269
|
const constraints: QueryConstraint[] = [
|
|
209
|
-
where(
|
|
210
|
-
orderBy(
|
|
211
|
-
limit(filters.pagination || 5)
|
|
270
|
+
where('isActive', '==', true),
|
|
271
|
+
orderBy('createdAt', 'desc'),
|
|
272
|
+
limit(filters.pagination || 5),
|
|
212
273
|
];
|
|
213
274
|
|
|
214
275
|
const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
|
|
215
276
|
const querySnapshot = await getDocs(q);
|
|
216
|
-
let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
277
|
+
let clinics = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Clinic));
|
|
217
278
|
|
|
218
279
|
// Apply in-memory filters (includes name search)
|
|
219
280
|
clinics = applyInMemoryFilters(clinics, filters);
|
|
220
|
-
|
|
221
|
-
const lastDoc =
|
|
222
|
-
|
|
281
|
+
|
|
282
|
+
const lastDoc =
|
|
283
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
284
|
+
|
|
223
285
|
console.log(`[CLINIC_SERVICE] Strategy 4 success: ${clinics.length} clinics`);
|
|
224
|
-
|
|
286
|
+
|
|
225
287
|
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
226
288
|
if (clinics.length < (filters.pagination || 5)) {
|
|
227
289
|
return { clinics, lastDoc: null };
|
|
228
290
|
}
|
|
229
291
|
return { clinics, lastDoc };
|
|
230
292
|
} catch (error) {
|
|
231
|
-
console.log(
|
|
293
|
+
console.log('[CLINIC_SERVICE] Strategy 4 failed:', error);
|
|
232
294
|
}
|
|
233
295
|
|
|
234
296
|
// All strategies failed
|
|
235
|
-
console.log(
|
|
297
|
+
console.log('[CLINIC_SERVICE] All strategies failed, returning empty result');
|
|
236
298
|
return { clinics: [], lastDoc: null };
|
|
237
|
-
|
|
238
299
|
} catch (error) {
|
|
239
|
-
console.error(
|
|
300
|
+
console.error('[CLINIC_SERVICE] Error filtering clinics:', error);
|
|
240
301
|
return { clinics: [], lastDoc: null };
|
|
241
302
|
}
|
|
242
303
|
}
|
|
@@ -247,112 +308,135 @@ export async function getClinicsByFilters(
|
|
|
247
308
|
function applyInMemoryFilters(clinics: Clinic[], filters: any): (Clinic & { distance?: number })[] {
|
|
248
309
|
let filteredClinics = [...clinics]; // Kreiraj kopiju
|
|
249
310
|
|
|
250
|
-
console.log(
|
|
251
|
-
|
|
311
|
+
console.log(
|
|
312
|
+
`[CLINIC_SERVICE] Applying in-memory filters - input: ${filteredClinics.length} clinics`,
|
|
313
|
+
);
|
|
314
|
+
|
|
252
315
|
// ✅ Multi-tag filter (postojeći kod)
|
|
253
316
|
if (filters.tags && filters.tags.length > 1) {
|
|
254
317
|
const initialCount = filteredClinics.length;
|
|
255
|
-
filteredClinics = filteredClinics.filter(clinic =>
|
|
256
|
-
filters.tags.every((tag: ClinicTag) => clinic.tags.includes(tag))
|
|
318
|
+
filteredClinics = filteredClinics.filter((clinic) =>
|
|
319
|
+
filters.tags.every((tag: ClinicTag) => clinic.tags.includes(tag)),
|
|
320
|
+
);
|
|
321
|
+
console.log(
|
|
322
|
+
`[CLINIC_SERVICE] Applied multi-tag filter: ${initialCount} → ${filteredClinics.length}`,
|
|
257
323
|
);
|
|
258
|
-
console.log(`[CLINIC_SERVICE] Applied multi-tag filter: ${initialCount} → ${filteredClinics.length}`);
|
|
259
324
|
}
|
|
260
325
|
|
|
261
326
|
// 🆕 DODAJTE: Single tag filter (za Strategy 4 fallback)
|
|
262
327
|
if (filters.tags && filters.tags.length === 1) {
|
|
263
328
|
const initialCount = filteredClinics.length;
|
|
264
|
-
filteredClinics = filteredClinics.filter(clinic =>
|
|
265
|
-
|
|
329
|
+
filteredClinics = filteredClinics.filter((clinic) => clinic.tags.includes(filters.tags[0]));
|
|
330
|
+
console.log(
|
|
331
|
+
`[CLINIC_SERVICE] Applied single-tag filter: ${initialCount} → ${filteredClinics.length}`,
|
|
266
332
|
);
|
|
267
|
-
console.log(`[CLINIC_SERVICE] Applied single-tag filter: ${initialCount} → ${filteredClinics.length}`);
|
|
268
333
|
}
|
|
269
334
|
|
|
270
335
|
// 🆕 DODAJTE: Rating filter
|
|
271
336
|
if (filters.minRating !== undefined || filters.maxRating !== undefined) {
|
|
272
337
|
const initialCount = filteredClinics.length;
|
|
273
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
338
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
274
339
|
const rating = clinic.reviewInfo?.averageRating || 0;
|
|
275
340
|
if (filters.minRating !== undefined && rating < filters.minRating) return false;
|
|
276
341
|
if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
|
|
277
342
|
return true;
|
|
278
343
|
});
|
|
279
|
-
console.log(
|
|
344
|
+
console.log(
|
|
345
|
+
`[CLINIC_SERVICE] Applied rating filter (${filters.minRating}-${filters.maxRating}): ${initialCount} → ${filteredClinics.length}`,
|
|
346
|
+
);
|
|
280
347
|
}
|
|
281
348
|
|
|
282
349
|
// 🆕 DODAJTE: Name search filter
|
|
283
350
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
284
351
|
const initialCount = filteredClinics.length;
|
|
285
352
|
const searchTerm = filters.nameSearch.trim().toLowerCase();
|
|
286
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
353
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
287
354
|
const name = (clinic.name || '').toLowerCase();
|
|
288
355
|
const nameLower = clinic.nameLower || '';
|
|
289
356
|
return name.includes(searchTerm) || nameLower.includes(searchTerm);
|
|
290
357
|
});
|
|
291
|
-
console.log(
|
|
358
|
+
console.log(
|
|
359
|
+
`[CLINIC_SERVICE] Applied name search filter: ${initialCount} → ${filteredClinics.length}`,
|
|
360
|
+
);
|
|
292
361
|
}
|
|
293
362
|
|
|
294
363
|
// 🆕 DODAJTE: Procedure family filtering
|
|
295
364
|
if (filters.procedureFamily) {
|
|
296
365
|
const initialCount = filteredClinics.length;
|
|
297
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
366
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
298
367
|
const proceduresInfo = clinic.proceduresInfo || [];
|
|
299
|
-
return proceduresInfo.some(proc => proc.family === filters.procedureFamily);
|
|
368
|
+
return proceduresInfo.some((proc) => proc.family === filters.procedureFamily);
|
|
300
369
|
});
|
|
301
|
-
console.log(
|
|
370
|
+
console.log(
|
|
371
|
+
`[CLINIC_SERVICE] Applied procedure family filter: ${initialCount} → ${filteredClinics.length}`,
|
|
372
|
+
);
|
|
302
373
|
}
|
|
303
374
|
|
|
304
375
|
// 🆕 DODAJTE: Procedure category filtering
|
|
305
376
|
if (filters.procedureCategory) {
|
|
306
377
|
const initialCount = filteredClinics.length;
|
|
307
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
378
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
308
379
|
const proceduresInfo = clinic.proceduresInfo || [];
|
|
309
|
-
return proceduresInfo.some(proc => proc.categoryName === filters.procedureCategory);
|
|
380
|
+
return proceduresInfo.some((proc) => proc.categoryName === filters.procedureCategory);
|
|
310
381
|
});
|
|
311
|
-
console.log(
|
|
382
|
+
console.log(
|
|
383
|
+
`[CLINIC_SERVICE] Applied procedure category filter: ${initialCount} → ${filteredClinics.length}`,
|
|
384
|
+
);
|
|
312
385
|
}
|
|
313
386
|
|
|
314
387
|
// 🆕 DODAJTE: Procedure subcategory filtering
|
|
315
388
|
if (filters.procedureSubcategory) {
|
|
316
389
|
const initialCount = filteredClinics.length;
|
|
317
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
390
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
318
391
|
const proceduresInfo = clinic.proceduresInfo || [];
|
|
319
|
-
return proceduresInfo.some(proc => proc.subcategoryName === filters.procedureSubcategory);
|
|
392
|
+
return proceduresInfo.some((proc) => proc.subcategoryName === filters.procedureSubcategory);
|
|
320
393
|
});
|
|
321
|
-
console.log(
|
|
394
|
+
console.log(
|
|
395
|
+
`[CLINIC_SERVICE] Applied procedure subcategory filter: ${initialCount} → ${filteredClinics.length}`,
|
|
396
|
+
);
|
|
322
397
|
}
|
|
323
398
|
|
|
324
399
|
// 🆕 DODAJTE: Procedure technology filtering
|
|
325
400
|
if (filters.procedureTechnology) {
|
|
326
401
|
const initialCount = filteredClinics.length;
|
|
327
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
402
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
328
403
|
const proceduresInfo = clinic.proceduresInfo || [];
|
|
329
|
-
return proceduresInfo.some(proc => proc.technologyName === filters.procedureTechnology);
|
|
404
|
+
return proceduresInfo.some((proc) => proc.technologyName === filters.procedureTechnology);
|
|
330
405
|
});
|
|
331
|
-
console.log(
|
|
406
|
+
console.log(
|
|
407
|
+
`[CLINIC_SERVICE] Applied procedure technology filter: ${initialCount} → ${filteredClinics.length}`,
|
|
408
|
+
);
|
|
332
409
|
}
|
|
333
410
|
|
|
334
411
|
// ✅ Geo-radius filter (postojeći kod)
|
|
335
412
|
if (filters.center && filters.radiusInKm) {
|
|
336
413
|
const initialCount = filteredClinics.length;
|
|
337
|
-
filteredClinics = filteredClinics.filter(clinic => {
|
|
414
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
338
415
|
if (!clinic.location?.latitude || !clinic.location?.longitude) {
|
|
339
416
|
console.log(`[CLINIC_SERVICE] Clinic ${clinic.id} missing location data`);
|
|
340
417
|
return false;
|
|
341
418
|
}
|
|
342
|
-
|
|
343
|
-
const distance =
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
419
|
+
|
|
420
|
+
const distance =
|
|
421
|
+
distanceBetween(
|
|
422
|
+
[filters.center.latitude, filters.center.longitude],
|
|
423
|
+
[clinic.location.latitude, clinic.location.longitude],
|
|
424
|
+
) / 1000; // Convert to km
|
|
425
|
+
|
|
426
|
+
console.log(
|
|
427
|
+
`[CLINIC_SERVICE] Clinic ${clinic.name}: distance ${distance.toFixed(2)}km (limit: ${
|
|
428
|
+
filters.radiusInKm
|
|
429
|
+
}km)`,
|
|
430
|
+
);
|
|
431
|
+
|
|
350
432
|
// Attach distance for frontend sorting/display
|
|
351
433
|
(clinic as any).distance = distance;
|
|
352
434
|
return distance <= filters.radiusInKm;
|
|
353
435
|
});
|
|
354
|
-
console.log(
|
|
355
|
-
|
|
436
|
+
console.log(
|
|
437
|
+
`[CLINIC_SERVICE] Applied geo filter (${filters.radiusInKm}km): ${initialCount} → ${filteredClinics.length}`,
|
|
438
|
+
);
|
|
439
|
+
|
|
356
440
|
// Sort by distance when geo filtering is applied
|
|
357
441
|
filteredClinics.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
|
|
358
442
|
}
|
|
@@ -1215,6 +1215,37 @@ export class PractitionerService extends BaseService {
|
|
|
1215
1215
|
console.log("[PRACTITIONER_SERVICE] Strategy 3 failed:", error);
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
+
// Strategy 4: Client-side filtering fallback (kao u procedure/clinic services)
|
|
1219
|
+
try {
|
|
1220
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 4: Client-side filtering fallback");
|
|
1221
|
+
|
|
1222
|
+
const constraints: any[] = [
|
|
1223
|
+
where("isActive", "==", true),
|
|
1224
|
+
where("status", "==", PractitionerStatus.ACTIVE),
|
|
1225
|
+
orderBy("createdAt", "desc"),
|
|
1226
|
+
limit(filters.pagination || 10)
|
|
1227
|
+
];
|
|
1228
|
+
|
|
1229
|
+
const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
|
|
1230
|
+
const querySnapshot = await getDocs(q);
|
|
1231
|
+
let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
|
|
1232
|
+
|
|
1233
|
+
// Apply all client-side filters using centralized function
|
|
1234
|
+
practitioners = this.applyInMemoryFilters(practitioners, filters);
|
|
1235
|
+
|
|
1236
|
+
const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1237
|
+
console.log(`[PRACTITIONER_SERVICE] Strategy 4 success: ${practitioners.length} practitioners`);
|
|
1238
|
+
|
|
1239
|
+
// Fix Load More - ako je broj rezultata manji od pagination, nema više
|
|
1240
|
+
if (practitioners.length < (filters.pagination || 10)) {
|
|
1241
|
+
return { practitioners, lastDoc: null };
|
|
1242
|
+
}
|
|
1243
|
+
return { practitioners, lastDoc };
|
|
1244
|
+
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
console.log("[PRACTITIONER_SERVICE] Strategy 4 failed:", error);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1218
1249
|
// All strategies failed
|
|
1219
1250
|
console.log("[PRACTITIONER_SERVICE] All strategies failed, returning empty result");
|
|
1220
1251
|
return { practitioners: [], lastDoc: null };
|