@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.
@@ -899,146 +899,369 @@ export class ProcedureService extends BaseService {
899
899
  lastDoc: any;
900
900
  }> {
901
901
  try {
902
- // Determine if we're doing a geo query or a regular query
903
- const isGeoQuery =
904
- filters.location && filters.radiusInKm && filters.radiusInKm > 0;
905
-
906
- // Initialize base constraints
907
- const constraints: QueryConstraint[] = [];
908
-
909
- // Add active status filter (default to active if not specified)
910
- if (filters.isActive !== undefined) {
911
- constraints.push(where("isActive", "==", filters.isActive));
912
- } else {
913
- constraints.push(where("isActive", "==", true));
902
+ console.log("[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies");
903
+
904
+ // Geo query debug i validacija
905
+ if (filters.location && filters.radiusInKm) {
906
+ console.log('[PROCEDURE_SERVICE] Executing geo query:', {
907
+ location: filters.location,
908
+ radius: filters.radiusInKm,
909
+ serviceName: 'ProcedureService'
910
+ });
911
+
912
+ // Validacija location podataka
913
+ if (!filters.location.latitude || !filters.location.longitude) {
914
+ console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
915
+ filters.location = undefined;
916
+ filters.radiusInKm = undefined;
917
+ }
914
918
  }
915
919
 
916
- // Filter by procedure family if specified
917
- if (filters.procedureFamily) {
918
- constraints.push(where("family", "==", filters.procedureFamily));
919
- }
920
- if (filters.procedureCategory) {
921
- constraints.push(where("category.id", "==", filters.procedureCategory));
922
- }
923
- if (filters.procedureSubcategory) {
924
- constraints.push(where("subcategory.id", "==", filters.procedureSubcategory));
925
- }
926
- if (filters.procedureTechnology) {
927
- constraints.push(where("technology.id", "==", filters.procedureTechnology));
928
- }
929
- if (filters.minPrice !== undefined) {
930
- constraints.push(where("price", ">=", filters.minPrice));
931
- }
932
- if (filters.maxPrice !== undefined) {
933
- constraints.push(where("price", "<=", filters.maxPrice));
934
- }
935
- if (filters.minRating !== undefined) {
936
- constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
937
- }
938
- if (filters.maxRating !== undefined) {
939
- constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
940
- }
941
- if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
942
- // Firestore ne podržava array-contains-all, koristi array-contains-any
943
- constraints.push(where("treatmentBenefits", "array-contains-any", filters.treatmentBenefits));
944
- }
945
- // Text search by name (case-sensitive, Firestore limitation)
946
- let useNameLower = false;
947
- let searchTerm = "";
948
- if (filters.nameSearch && filters.nameSearch.trim() !== "") {
949
- searchTerm = filters.nameSearch.trim().toLowerCase();
950
- useNameLower = true;
951
- constraints.push(where("nameLower", ">=", searchTerm));
952
- constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
953
- constraints.push(orderBy("nameLower"));
954
- } else {
955
- constraints.push(orderBy("nameLower"));
920
+ // Handle geo queries separately (they work differently)
921
+ const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
922
+ if (isGeoQuery) {
923
+ return this.handleGeoQuery(filters);
956
924
  }
957
- if (filters.lastDoc) {
958
- // Firestore pagination: support both QueryDocumentSnapshot and value arrays
959
- if (typeof filters.lastDoc.data === "function") {
960
- // QueryDocumentSnapshot
961
- constraints.push(startAfter(filters.lastDoc));
962
- } else if (Array.isArray(filters.lastDoc)) {
963
- // Array of values for multiple orderBy fields
964
- constraints.push(startAfter(...filters.lastDoc));
925
+
926
+ // Base constraints (used in all strategies)
927
+ const getBaseConstraints = () => {
928
+ const constraints: QueryConstraint[] = [];
929
+
930
+ // Active status filter
931
+ if (filters.isActive !== undefined) {
932
+ constraints.push(where("isActive", "==", filters.isActive));
965
933
  } else {
966
- // Single value
967
- constraints.push(startAfter(filters.lastDoc));
934
+ constraints.push(where("isActive", "==", true));
968
935
  }
969
- }
970
- if (filters.pagination && filters.pagination > 0) {
971
- constraints.push(limit(filters.pagination));
972
- }
973
936
 
974
- // Geo-query: special handling
975
- if (isGeoQuery) {
976
- // For geo queries, use geohash bounds and add all other constraints
977
- const center = filters.location!;
978
- const radiusInKm = filters.radiusInKm!;
979
- const bounds = geohashQueryBounds(
980
- [center.latitude, center.longitude],
981
- radiusInKm * 1000 // Convert to meters
982
- );
983
- let allDocs: (Procedure & { distance: number })[] = [];
984
- for (const bound of bounds) {
985
- const geoConstraints = [
986
- ...constraints.filter(c => !(c as any).fieldPath || (c as any).fieldPath !== "name"), // Remove name orderBy for geo
987
- where("clinicInfo.location.geohash", ">=", bound[0]),
988
- where("clinicInfo.location.geohash", "<=", bound[1]),
989
- orderBy("clinicInfo.location.geohash"),
990
- ];
991
- const q = query(collection(this.db, PROCEDURES_COLLECTION), ...geoConstraints);
992
- const querySnapshot = await getDocs(q);
993
- for (const doc of querySnapshot.docs) {
994
- const procedure = { ...doc.data(), id: doc.id } as Procedure;
995
- const distance = distanceBetween(
996
- [center.latitude, center.longitude],
997
- [procedure.clinicInfo.location.latitude, procedure.clinicInfo.location.longitude]
998
- );
999
- const distanceInKm = distance / 1000;
1000
- if (distanceInKm <= radiusInKm) {
1001
- allDocs.push({ ...procedure, distance: distanceInKm });
1002
- }
1003
- }
937
+ // Filter constraints
938
+ if (filters.procedureFamily) {
939
+ constraints.push(where("family", "==", filters.procedureFamily));
940
+ }
941
+ if (filters.procedureCategory) {
942
+ constraints.push(where("category.id", "==", filters.procedureCategory));
943
+ }
944
+ if (filters.procedureSubcategory) {
945
+ constraints.push(where("subcategory.id", "==", filters.procedureSubcategory));
1004
946
  }
1005
- // Sort by distance
1006
- allDocs.sort((a, b) => a.distance - b.distance);
1007
- // Paginate
1008
- let paginated = allDocs;
1009
- if (filters.pagination && filters.pagination > 0) {
1010
- let startIndex = 0;
947
+ if (filters.procedureTechnology) {
948
+ constraints.push(where("technology.id", "==", filters.procedureTechnology));
949
+ }
950
+ if (filters.minPrice !== undefined) {
951
+ constraints.push(where("price", ">=", filters.minPrice));
952
+ }
953
+ if (filters.maxPrice !== undefined) {
954
+ constraints.push(where("price", "<=", filters.maxPrice));
955
+ }
956
+ if (filters.minRating !== undefined) {
957
+ constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
958
+ }
959
+ if (filters.maxRating !== undefined) {
960
+ constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
961
+ }
962
+ if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
963
+ const benefitsToMatch = filters.treatmentBenefits;
964
+ constraints.push(where("treatmentBenefits", "array-contains-any", benefitsToMatch));
965
+ }
966
+
967
+ return constraints;
968
+ };
969
+
970
+ // Strategy 1: Try nameLower search if nameSearch exists
971
+ if (filters.nameSearch && filters.nameSearch.trim()) {
972
+ try {
973
+ console.log("[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search");
974
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
975
+ const constraints = getBaseConstraints();
976
+ constraints.push(where("nameLower", ">=", searchTerm));
977
+ constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
978
+ constraints.push(orderBy("nameLower"));
979
+
1011
980
  if (filters.lastDoc) {
1012
- const lastDocIndex = allDocs.findIndex(p => p.id === filters.lastDoc.id);
1013
- if (lastDocIndex !== -1) startIndex = lastDocIndex + 1;
981
+ if (typeof filters.lastDoc.data === "function") {
982
+ constraints.push(startAfter(filters.lastDoc));
983
+ } else if (Array.isArray(filters.lastDoc)) {
984
+ constraints.push(startAfter(...filters.lastDoc));
985
+ } else {
986
+ constraints.push(startAfter(filters.lastDoc));
987
+ }
1014
988
  }
1015
- paginated = allDocs.slice(startIndex, startIndex + filters.pagination);
989
+ constraints.push(limit(filters.pagination || 10));
990
+
991
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
992
+ const querySnapshot = await getDocs(q);
993
+ const procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
994
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
995
+
996
+ console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
997
+
998
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
999
+ if (procedures.length < (filters.pagination || 10)) {
1000
+ return { procedures, lastDoc: null };
1001
+ }
1002
+ return { procedures, lastDoc };
1003
+ } catch (error) {
1004
+ console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
1016
1005
  }
1017
- const lastVisibleDoc = paginated.length > 0 ? paginated[paginated.length - 1] : null;
1018
- return { procedures: paginated, lastDoc: lastVisibleDoc };
1019
- } else {
1020
- // Regular query
1021
- let q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1022
- let querySnapshot = await getDocs(q);
1023
- // Fallback na name ako nema rezultata i koristi se nameLower
1024
- if (useNameLower && querySnapshot.empty && searchTerm) {
1025
- // Ukloni poslednja 3 constraints (nameLower >=, nameLower <=, orderBy nameLower)
1026
- constraints.pop();
1027
- constraints.pop();
1028
- constraints.pop();
1006
+ }
1007
+
1008
+ // Strategy 2: Try name field search as fallback
1009
+ if (filters.nameSearch && filters.nameSearch.trim()) {
1010
+ try {
1011
+ console.log("[PROCEDURE_SERVICE] Strategy 2: Trying name field search");
1012
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
1013
+ const constraints = getBaseConstraints();
1029
1014
  constraints.push(where("name", ">=", searchTerm));
1030
1015
  constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
1031
1016
  constraints.push(orderBy("name"));
1032
- q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1033
- querySnapshot = await getDocs(q);
1017
+
1018
+ if (filters.lastDoc) {
1019
+ if (typeof filters.lastDoc.data === "function") {
1020
+ constraints.push(startAfter(filters.lastDoc));
1021
+ } else if (Array.isArray(filters.lastDoc)) {
1022
+ constraints.push(startAfter(...filters.lastDoc));
1023
+ } else {
1024
+ constraints.push(startAfter(filters.lastDoc));
1025
+ }
1026
+ }
1027
+ constraints.push(limit(filters.pagination || 10));
1028
+
1029
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1030
+ const querySnapshot = await getDocs(q);
1031
+ const procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1032
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1033
+
1034
+ console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
1035
+
1036
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
1037
+ if (procedures.length < (filters.pagination || 10)) {
1038
+ return { procedures, lastDoc: null };
1039
+ }
1040
+ return { procedures, lastDoc };
1041
+ } catch (error) {
1042
+ console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
1034
1043
  }
1035
- const procedures = querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id } as Procedure));
1036
- const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1037
- return { procedures, lastDoc: lastVisibleDoc };
1038
1044
  }
1045
+
1046
+ // Strategy 3: orderBy createdAt with client-side filtering
1047
+ try {
1048
+ console.log("[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering");
1049
+ const constraints = getBaseConstraints();
1050
+ constraints.push(orderBy("createdAt", "desc"));
1051
+
1052
+ if (filters.lastDoc) {
1053
+ if (typeof filters.lastDoc.data === "function") {
1054
+ constraints.push(startAfter(filters.lastDoc));
1055
+ } else if (Array.isArray(filters.lastDoc)) {
1056
+ constraints.push(startAfter(...filters.lastDoc));
1057
+ } else {
1058
+ constraints.push(startAfter(filters.lastDoc));
1059
+ }
1060
+ }
1061
+ constraints.push(limit(filters.pagination || 10));
1062
+
1063
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1064
+ const querySnapshot = await getDocs(q);
1065
+ let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1066
+
1067
+ // Apply all client-side filters using centralized function
1068
+ procedures = this.applyInMemoryFilters(procedures, filters);
1069
+
1070
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1071
+ console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
1072
+
1073
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
1074
+ if (procedures.length < (filters.pagination || 10)) {
1075
+ return { procedures, lastDoc: null };
1076
+ }
1077
+ return { procedures, lastDoc };
1078
+ } catch (error) {
1079
+ console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
1080
+ }
1081
+
1082
+ // Strategy 4: Minimal query fallback
1083
+ try {
1084
+ console.log("[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback");
1085
+ const constraints: QueryConstraint[] = [
1086
+ where("isActive", "==", true),
1087
+ orderBy("createdAt", "desc"),
1088
+ limit(filters.pagination || 10)
1089
+ ];
1090
+
1091
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1092
+ const querySnapshot = await getDocs(q);
1093
+ let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1094
+
1095
+ // Apply all client-side filters using centralized function
1096
+ procedures = this.applyInMemoryFilters(procedures, filters);
1097
+
1098
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1099
+ console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
1100
+
1101
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
1102
+ if (procedures.length < (filters.pagination || 10)) {
1103
+ return { procedures, lastDoc: null };
1104
+ }
1105
+ return { procedures, lastDoc };
1106
+ } catch (error) {
1107
+ console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
1108
+ }
1109
+
1110
+ // All strategies failed
1111
+ console.log("[PROCEDURE_SERVICE] All strategies failed, returning empty result");
1112
+ return { procedures: [], lastDoc: null };
1113
+
1039
1114
  } catch (error) {
1040
1115
  console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
1041
- throw error;
1116
+ return { procedures: [], lastDoc: null };
1117
+ }
1118
+ }
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
+
1223
+ private handleGeoQuery(filters: any): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
1224
+ console.log('[PROCEDURE_SERVICE] Executing geo query with enhanced debugging');
1225
+
1226
+ try {
1227
+ // Enhanced geo query implementation with proper debugging
1228
+ const location = filters.location;
1229
+ const radiusInKm = filters.radiusInKm;
1230
+
1231
+ console.log('[PROCEDURE_SERVICE] Geo query parameters:', {
1232
+ latitude: location.latitude,
1233
+ longitude: location.longitude,
1234
+ radiusInKm: radiusInKm,
1235
+ pagination: filters.pagination || 10
1236
+ });
1237
+
1238
+ // For now, return a basic query and apply geo filtering
1239
+ // This can be enhanced with proper geohash bounds later
1240
+ const constraints: QueryConstraint[] = [
1241
+ where("isActive", "==", true),
1242
+ orderBy("createdAt", "desc"),
1243
+ limit((filters.pagination || 10) * 3) // Get more results for geo filtering
1244
+ ];
1245
+
1246
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1247
+
1248
+ return getDocs(q).then(querySnapshot => {
1249
+ let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1250
+
1251
+ // Apply all filters using centralized function (includes geo filtering)
1252
+ procedures = this.applyInMemoryFilters(procedures, filters);
1253
+
1254
+ console.log(`[PROCEDURE_SERVICE] Geo query success: ${procedures.length} procedures within ${radiusInKm}km`);
1255
+
1256
+ // Fix Load More for geo queries
1257
+ const lastDoc = procedures.length < (filters.pagination || 10) ? null : querySnapshot.docs[querySnapshot.docs.length - 1];
1258
+
1259
+ return { procedures, lastDoc };
1260
+ });
1261
+
1262
+ } catch (error) {
1263
+ console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
1264
+ return Promise.resolve({ procedures: [], lastDoc: null });
1042
1265
  }
1043
1266
  }
1044
1267