@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.
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +666 -214
- package/dist/index.mjs +667 -215
- package/package.json +1 -1
- package/src/services/clinic/utils/filter.utils.ts +242 -57
- package/src/services/practitioner/practitioner.service.ts +277 -81
- package/src/services/procedure/procedure.service.ts +346 -123
|
@@ -1043,102 +1043,298 @@ export class PractitionerService extends BaseService {
|
|
|
1043
1043
|
includeDraftPractitioners?: boolean;
|
|
1044
1044
|
}): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
|
|
1045
1045
|
try {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
//
|
|
1064
|
+
// Strategy 1: Try fullNameLower search if nameSearch exists
|
|
1065
1065
|
if (filters.nameSearch && filters.nameSearch.trim()) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
//
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
constraints.push(where("
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
//
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
/**
|