@blackcode_sa/metaestetics-api 1.8.13 → 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 +536 -213
- package/dist/index.mjs +537 -214
- package/package.json +1 -1
- package/src/services/clinic/utils/filter.utils.ts +242 -57
- package/src/services/practitioner/practitioner.service.ts +195 -82
- package/src/services/procedure/procedure.service.ts +279 -123
- package/src/admin/scripts/migrateProcedures.js +0 -23
- package/src/admin/scripts/serviceAccountKey.json +0 -13
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
|
}
|