@blackcode_sa/metaestetics-api 1.8.14 → 1.8.16

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.
@@ -1043,102 +1043,298 @@ 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
+ const certificationsToMatch = filters.certifications as CertificationSpecialty[];
1120
+ constraints.push(
1121
+ where("certification.specialties", "array-contains-any", certificationsToMatch)
1122
+ );
1123
+ }
1124
+
1125
+ if (filters.minRating !== undefined) {
1126
+ constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
1127
+ }
1128
+ if (filters.maxRating !== undefined) {
1129
+ constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
1130
+ }
1089
1131
 
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));
1132
+ constraints.push(orderBy("createdAt", "desc"));
1133
+
1134
+ // Pagination sa createdAt - poboljšano za geo queries
1135
+ if (filters.location && filters.radiusInKm) {
1136
+ // Ne koristiti lastDoc za geo queries, već preuzmi više rezultata
1137
+ constraints.push(limit((filters.pagination || 10) * 2)); // Dvostruko više za geo filter
1097
1138
  } else {
1098
- constraints.push(startAfter(filters.lastDoc));
1139
+ if (filters.lastDoc) {
1140
+ if (typeof filters.lastDoc.data === "function") {
1141
+ constraints.push(startAfter(filters.lastDoc));
1142
+ } else if (Array.isArray(filters.lastDoc)) {
1143
+ constraints.push(startAfter(...filters.lastDoc));
1144
+ } else {
1145
+ constraints.push(startAfter(filters.lastDoc));
1146
+ }
1147
+ }
1148
+ constraints.push(limit(filters.pagination || 10));
1099
1149
  }
1100
- }
1101
- constraints.push(limit(filters.pagination || 5));
1102
1150
 
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;
1151
+ const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
1152
+ const querySnapshot = await getDocs(q);
1153
+ let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
1154
+
1155
+ // Apply geo filter if needed (this is the only in-memory filter we keep)
1156
+ if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1157
+ const location = filters.location;
1158
+ const radiusInKm = filters.radiusInKm;
1159
+ practitioners = practitioners.filter((practitioner) => {
1160
+ const clinics = practitioner.clinicsInfo || [];
1161
+ return clinics.some((clinic) => {
1162
+ const distance = distanceBetween(
1163
+ [location.latitude, location.longitude],
1164
+ [clinic.location.latitude, clinic.location.longitude]
1165
+ );
1166
+ const distanceInKm = distance / 1000;
1167
+ return distanceInKm <= radiusInKm;
1168
+ });
1125
1169
  });
1126
- });
1170
+
1171
+ // Ograniči na pagination broj nakon geo filtera
1172
+ practitioners = practitioners.slice(0, filters.pagination || 10);
1173
+ }
1174
+
1175
+ // Apply all remaining client-side filters using centralized function
1176
+ practitioners = this.applyInMemoryFilters(practitioners, filters);
1177
+
1178
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1179
+ console.log(`[PRACTITIONER_SERVICE] Strategy 2 success: ${practitioners.length} practitioners`);
1180
+
1181
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
1182
+ if (practitioners.length < (filters.pagination || 10)) {
1183
+ return { practitioners, lastDoc: null };
1184
+ }
1185
+ return { practitioners, lastDoc };
1186
+ } catch (error) {
1187
+ console.log("[PRACTITIONER_SERVICE] Strategy 2 failed:", error);
1127
1188
  }
1128
1189
 
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
- };
1190
+ // Strategy 3: Minimal query fallback
1191
+ try {
1192
+ console.log("[PRACTITIONER_SERVICE] Strategy 3: Minimal query fallback");
1193
+ const constraints: any[] = [
1194
+ where("isActive", "==", true),
1195
+ orderBy("createdAt", "desc"),
1196
+ limit(filters.pagination || 10)
1197
+ ];
1198
+
1199
+ const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
1200
+ const querySnapshot = await getDocs(q);
1201
+ let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
1202
+
1203
+ // Apply all client-side filters using centralized function
1204
+ practitioners = this.applyInMemoryFilters(practitioners, filters);
1205
+
1206
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1207
+ console.log(`[PRACTITIONER_SERVICE] Strategy 3 success: ${practitioners.length} practitioners`);
1208
+
1209
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
1210
+ if (practitioners.length < (filters.pagination || 10)) {
1211
+ return { practitioners, lastDoc: null };
1212
+ }
1213
+ return { practitioners, lastDoc };
1214
+ } catch (error) {
1215
+ console.log("[PRACTITIONER_SERVICE] Strategy 3 failed:", error);
1216
+ }
1217
+
1218
+ // All strategies failed
1219
+ console.log("[PRACTITIONER_SERVICE] All strategies failed, returning empty result");
1220
+ return { practitioners: [], lastDoc: null };
1221
+
1135
1222
  } catch (error) {
1136
- console.error(
1137
- "[PRACTITIONER_SERVICE] Error filtering practitioners:",
1138
- error
1139
- );
1140
- throw error;
1223
+ console.error("[PRACTITIONER_SERVICE] Error filtering practitioners:", error);
1224
+ return { practitioners: [], lastDoc: null };
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Applies in-memory filters to practitioners array
1230
+ * Used when Firestore queries fail or for complex filtering
1231
+ */
1232
+ private applyInMemoryFilters(practitioners: Practitioner[], filters: any): Practitioner[] {
1233
+ let filteredPractitioners = [...practitioners]; // Create copy to avoid mutating original
1234
+
1235
+ // Name search filter
1236
+ if (filters.nameSearch && filters.nameSearch.trim()) {
1237
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
1238
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1239
+ const firstName = (practitioner.basicInfo?.firstName || '').toLowerCase();
1240
+ const lastName = (practitioner.basicInfo?.lastName || '').toLowerCase();
1241
+ const fullName = `${firstName} ${lastName}`.trim();
1242
+ const fullNameLower = practitioner.fullNameLower || '';
1243
+
1244
+ return firstName.includes(searchTerm) ||
1245
+ lastName.includes(searchTerm) ||
1246
+ fullName.includes(searchTerm) ||
1247
+ fullNameLower.includes(searchTerm);
1248
+ });
1249
+ console.log(`[PRACTITIONER_SERVICE] Applied name filter, results: ${filteredPractitioners.length}`);
1250
+ }
1251
+
1252
+ // Certifications filtering
1253
+ if (filters.certifications && filters.certifications.length > 0) {
1254
+ const certificationsToMatch = filters.certifications;
1255
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1256
+ const practitionerCerts = practitioner.certification?.specialties || [];
1257
+ return certificationsToMatch.some((cert: any) => practitionerCerts.includes(cert as CertificationSpecialty));
1258
+ });
1259
+ console.log(`[PRACTITIONER_SERVICE] Applied certifications filter, results: ${filteredPractitioners.length}`);
1141
1260
  }
1261
+
1262
+ // Specialties filtering
1263
+ if (filters.specialties && filters.specialties.length > 0) {
1264
+ const specialtiesToMatch = filters.specialties;
1265
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1266
+ const practitionerSpecs = practitioner.certification?.specialties || [];
1267
+ return specialtiesToMatch.some((spec: any) => practitionerSpecs.includes(spec));
1268
+ });
1269
+ console.log(`[PRACTITIONER_SERVICE] Applied specialties filter, results: ${filteredPractitioners.length}`);
1270
+ }
1271
+
1272
+ // Rating filtering
1273
+ if (filters.minRating !== undefined || filters.maxRating !== undefined) {
1274
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1275
+ const rating = practitioner.reviewInfo?.averageRating || 0;
1276
+ if (filters.minRating !== undefined && rating < filters.minRating) return false;
1277
+ if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
1278
+ return true;
1279
+ });
1280
+ console.log(`[PRACTITIONER_SERVICE] Applied rating filter, results: ${filteredPractitioners.length}`);
1281
+ }
1282
+
1283
+ // Procedure family filtering
1284
+ if (filters.procedureFamily) {
1285
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1286
+ const proceduresInfo = practitioner.proceduresInfo || [];
1287
+ return proceduresInfo.some(proc => proc.family === filters.procedureFamily);
1288
+ });
1289
+ console.log(`[PRACTITIONER_SERVICE] Applied procedure family filter, results: ${filteredPractitioners.length}`);
1290
+ }
1291
+
1292
+ // Procedure category filtering
1293
+ if (filters.procedureCategory) {
1294
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1295
+ const proceduresInfo = practitioner.proceduresInfo || [];
1296
+ return proceduresInfo.some(proc => proc.categoryName === filters.procedureCategory);
1297
+ });
1298
+ console.log(`[PRACTITIONER_SERVICE] Applied procedure category filter, results: ${filteredPractitioners.length}`);
1299
+ }
1300
+
1301
+ // Procedure subcategory filtering
1302
+ if (filters.procedureSubcategory) {
1303
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1304
+ const proceduresInfo = practitioner.proceduresInfo || [];
1305
+ return proceduresInfo.some(proc => proc.subcategoryName === filters.procedureSubcategory);
1306
+ });
1307
+ console.log(`[PRACTITIONER_SERVICE] Applied procedure subcategory filter, results: ${filteredPractitioners.length}`);
1308
+ }
1309
+
1310
+ // Procedure technology filtering
1311
+ if (filters.procedureTechnology) {
1312
+ filteredPractitioners = filteredPractitioners.filter(practitioner => {
1313
+ const proceduresInfo = practitioner.proceduresInfo || [];
1314
+ return proceduresInfo.some(proc => proc.technologyName === filters.procedureTechnology);
1315
+ });
1316
+ console.log(`[PRACTITIONER_SERVICE] Applied procedure technology filter, results: ${filteredPractitioners.length}`);
1317
+ }
1318
+
1319
+ // Geo-radius filter
1320
+ if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1321
+ const location = filters.location;
1322
+ const radiusInKm = filters.radiusInKm;
1323
+ filteredPractitioners = filteredPractitioners.filter((practitioner) => {
1324
+ const clinics = practitioner.clinicsInfo || [];
1325
+ return clinics.some((clinic) => {
1326
+ const distance = distanceBetween(
1327
+ [location.latitude, location.longitude],
1328
+ [clinic.location.latitude, clinic.location.longitude]
1329
+ );
1330
+ const distanceInKm = distance / 1000;
1331
+ return distanceInKm <= radiusInKm;
1332
+ });
1333
+ });
1334
+ console.log(`[PRACTITIONER_SERVICE] Applied geo filter, results: ${filteredPractitioners.length}`);
1335
+ }
1336
+
1337
+ return filteredPractitioners;
1142
1338
  }
1143
1339
 
1144
1340
  /**