@blackcode_sa/metaestetics-api 1.8.15 → 1.8.17

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.
@@ -1116,8 +1116,9 @@ export class PractitionerService extends BaseService {
1116
1116
 
1117
1117
  // Add other filters that work well with Firestore
1118
1118
  if (filters.certifications && filters.certifications.length > 0) {
1119
+ const certificationsToMatch = filters.certifications as CertificationSpecialty[];
1119
1120
  constraints.push(
1120
- where("certification.specialties", "array-contains-any", filters.certifications)
1121
+ where("certification.specialties", "array-contains-any", certificationsToMatch)
1121
1122
  );
1122
1123
  }
1123
1124
 
@@ -1171,22 +1172,8 @@ export class PractitionerService extends BaseService {
1171
1172
  practitioners = practitioners.slice(0, filters.pagination || 10);
1172
1173
  }
1173
1174
 
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
- }
1175
+ // Apply all remaining client-side filters using centralized function
1176
+ practitioners = this.applyInMemoryFilters(practitioners, filters);
1190
1177
 
1191
1178
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1192
1179
  console.log(`[PRACTITIONER_SERVICE] Strategy 2 success: ${practitioners.length} practitioners`);
@@ -1213,22 +1200,8 @@ export class PractitionerService extends BaseService {
1213
1200
  const querySnapshot = await getDocs(q);
1214
1201
  let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
1215
1202
 
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);
1229
- });
1230
- console.log(`[PRACTITIONER_SERVICE] Applied name filter, results: ${practitioners.length}`);
1231
- }
1203
+ // Apply all client-side filters using centralized function
1204
+ practitioners = this.applyInMemoryFilters(practitioners, filters);
1232
1205
 
1233
1206
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1234
1207
  console.log(`[PRACTITIONER_SERVICE] Strategy 3 success: ${practitioners.length} practitioners`);
@@ -1252,6 +1225,118 @@ export class PractitionerService extends BaseService {
1252
1225
  }
1253
1226
  }
1254
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}`);
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;
1338
+ }
1339
+
1255
1340
  /**
1256
1341
  * Enables free consultation for a practitioner in a specific clinic
1257
1342
  * Creates a free consultation procedure with hardcoded parameters
@@ -960,7 +960,8 @@ export class ProcedureService extends BaseService {
960
960
  constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
961
961
  }
962
962
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
963
- constraints.push(where("treatmentBenefits", "array-contains-any", filters.treatmentBenefits));
963
+ const benefitsToMatch = filters.treatmentBenefits;
964
+ constraints.push(where("treatmentBenefits", "array-contains-any", benefitsToMatch));
964
965
  }
965
966
 
966
967
  return constraints;
@@ -1063,16 +1064,8 @@ export class ProcedureService extends BaseService {
1063
1064
  const querySnapshot = await getDocs(q);
1064
1065
  let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1065
1066
 
1066
- // Poboljšan client-side name filtering
1067
- if (filters.nameSearch && filters.nameSearch.trim()) {
1068
- const searchTerm = filters.nameSearch.trim().toLowerCase();
1069
- procedures = procedures.filter(procedure => {
1070
- const name = (procedure.name || '').toLowerCase();
1071
- const nameLower = procedure.nameLower || '';
1072
- return name.includes(searchTerm) || nameLower.includes(searchTerm);
1073
- });
1074
- console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${procedures.length}`);
1075
- }
1067
+ // Apply all client-side filters using centralized function
1068
+ procedures = this.applyInMemoryFilters(procedures, filters);
1076
1069
 
1077
1070
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1078
1071
  console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
@@ -1099,16 +1092,8 @@ export class ProcedureService extends BaseService {
1099
1092
  const querySnapshot = await getDocs(q);
1100
1093
  let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1101
1094
 
1102
- // Poboljšan client-side name filtering
1103
- if (filters.nameSearch && filters.nameSearch.trim()) {
1104
- const searchTerm = filters.nameSearch.trim().toLowerCase();
1105
- procedures = procedures.filter(procedure => {
1106
- const name = (procedure.name || '').toLowerCase();
1107
- const nameLower = procedure.nameLower || '';
1108
- return name.includes(searchTerm) || nameLower.includes(searchTerm);
1109
- });
1110
- console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${procedures.length}`);
1111
- }
1095
+ // Apply all client-side filters using centralized function
1096
+ procedures = this.applyInMemoryFilters(procedures, filters);
1112
1097
 
1113
1098
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1114
1099
  console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
@@ -1132,6 +1117,109 @@ export class ProcedureService extends BaseService {
1132
1117
  }
1133
1118
  }
1134
1119
 
1120
+ /**
1121
+ * Applies in-memory filters to procedures array
1122
+ * Used when Firestore queries fail or for complex filtering
1123
+ */
1124
+ private applyInMemoryFilters(procedures: Procedure[], filters: any): (Procedure & { distance?: number })[] {
1125
+ let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
1126
+
1127
+ // Name search filter
1128
+ if (filters.nameSearch && filters.nameSearch.trim()) {
1129
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
1130
+ filteredProcedures = filteredProcedures.filter(procedure => {
1131
+ const name = (procedure.name || '').toLowerCase();
1132
+ const nameLower = procedure.nameLower || '';
1133
+ return name.includes(searchTerm) || nameLower.includes(searchTerm);
1134
+ });
1135
+ console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`);
1136
+ }
1137
+
1138
+ // Price filtering
1139
+ if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
1140
+ filteredProcedures = filteredProcedures.filter(procedure => {
1141
+ const price = procedure.price || 0;
1142
+ if (filters.minPrice !== undefined && price < filters.minPrice) return false;
1143
+ if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
1144
+ return true;
1145
+ });
1146
+ console.log(`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`);
1147
+ }
1148
+
1149
+ // Rating filtering
1150
+ if (filters.minRating !== undefined || filters.maxRating !== undefined) {
1151
+ filteredProcedures = filteredProcedures.filter(procedure => {
1152
+ const rating = procedure.reviewInfo?.averageRating || 0;
1153
+ if (filters.minRating !== undefined && rating < filters.minRating) return false;
1154
+ if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
1155
+ return true;
1156
+ });
1157
+ console.log(`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`);
1158
+ }
1159
+
1160
+ // Treatment benefits filtering
1161
+ if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
1162
+ const benefitsToMatch = filters.treatmentBenefits;
1163
+ filteredProcedures = filteredProcedures.filter(procedure => {
1164
+ const procedureBenefits = procedure.treatmentBenefits || [];
1165
+ return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
1166
+ });
1167
+ console.log(`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`);
1168
+ }
1169
+
1170
+ // Procedure family filtering
1171
+ if (filters.procedureFamily) {
1172
+ filteredProcedures = filteredProcedures.filter(procedure => procedure.family === filters.procedureFamily);
1173
+ console.log(`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`);
1174
+ }
1175
+
1176
+ // Category filtering
1177
+ if (filters.procedureCategory) {
1178
+ filteredProcedures = filteredProcedures.filter(procedure => procedure.category?.id === filters.procedureCategory);
1179
+ console.log(`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`);
1180
+ }
1181
+
1182
+ // Subcategory filtering
1183
+ if (filters.procedureSubcategory) {
1184
+ filteredProcedures = filteredProcedures.filter(procedure => procedure.subcategory?.id === filters.procedureSubcategory);
1185
+ console.log(`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`);
1186
+ }
1187
+
1188
+ // Technology filtering
1189
+ if (filters.procedureTechnology) {
1190
+ filteredProcedures = filteredProcedures.filter(procedure => procedure.technology?.id === filters.procedureTechnology);
1191
+ console.log(`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`);
1192
+ }
1193
+
1194
+ // Geo-radius filter
1195
+ if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1196
+ const location = filters.location;
1197
+ const radiusInKm = filters.radiusInKm;
1198
+ filteredProcedures = filteredProcedures.filter(procedure => {
1199
+ const clinicLocation = procedure.clinicInfo?.location;
1200
+ if (!clinicLocation?.latitude || !clinicLocation?.longitude) {
1201
+ return false;
1202
+ }
1203
+
1204
+ const distance = distanceBetween(
1205
+ [location.latitude, location.longitude],
1206
+ [clinicLocation.latitude, clinicLocation.longitude]
1207
+ ) / 1000; // Convert to km
1208
+
1209
+ // Attach distance for frontend sorting/display
1210
+ (procedure as any).distance = distance;
1211
+
1212
+ return distance <= radiusInKm;
1213
+ });
1214
+ console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
1215
+
1216
+ // Sort by distance when geo filtering is applied
1217
+ filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
1218
+ }
1219
+
1220
+ return filteredProcedures as (Procedure & { distance?: number })[];
1221
+ }
1222
+
1135
1223
  private handleGeoQuery(filters: any): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
1136
1224
  console.log('[PROCEDURE_SERVICE] Executing geo query with enhanced debugging');
1137
1225
 
@@ -1160,29 +1248,8 @@ export class ProcedureService extends BaseService {
1160
1248
  return getDocs(q).then(querySnapshot => {
1161
1249
  let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1162
1250
 
1163
- // Apply geo filtering
1164
- procedures = procedures.filter(procedure => {
1165
- const clinicLocation = procedure.clinicInfo?.location;
1166
- if (!clinicLocation?.latitude || !clinicLocation?.longitude) {
1167
- return false;
1168
- }
1169
-
1170
- const distance = distanceBetween(
1171
- [location.latitude, location.longitude],
1172
- [clinicLocation.latitude, clinicLocation.longitude]
1173
- ) / 1000; // Convert to km
1174
-
1175
- // Add distance to procedure object
1176
- (procedure as any).distance = distance;
1177
-
1178
- return distance <= radiusInKm;
1179
- });
1180
-
1181
- // Sort by distance
1182
- procedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
1183
-
1184
- // Limit to pagination size
1185
- procedures = procedures.slice(0, filters.pagination || 10);
1251
+ // Apply all filters using centralized function (includes geo filtering)
1252
+ procedures = this.applyInMemoryFilters(procedures, filters);
1186
1253
 
1187
1254
  console.log(`[PROCEDURE_SERVICE] Geo query success: ${procedures.length} procedures within ${radiusInKm}km`);
1188
1255