@blackcode_sa/metaestetics-api 1.8.12 → 1.8.13

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.
@@ -32,6 +32,7 @@ export async function getClinicsByFilters(
32
32
  procedureTechnology?: string;
33
33
  minRating?: number;
34
34
  maxRating?: number;
35
+ nameSearch?: string;
35
36
  pagination?: number;
36
37
  lastDoc?: any;
37
38
  isActive?: boolean;
@@ -40,251 +41,79 @@ export async function getClinicsByFilters(
40
41
  clinics: (Clinic & { distance?: number })[];
41
42
  lastDoc: any;
42
43
  }> {
43
- console.log(
44
- "[FILTER_UTILS] Starting clinic filtering with criteria:",
45
- filters
46
- );
47
-
48
- // Determine if we're doing a geo query or a regular query
49
- const isGeoQuery =
50
- filters.center && filters.radiusInKm && filters.radiusInKm > 0;
51
-
52
- // Initialize base constraints
44
+ // 1. Prepare Firestore constraints
53
45
  const constraints: QueryConstraint[] = [];
46
+ constraints.push(where("isActive", "==", filters.isActive ?? true));
54
47
 
55
- // Add active status filter (default to active if not specified)
56
- if (filters.isActive !== undefined) {
57
- constraints.push(where("isActive", "==", filters.isActive));
58
- } else {
59
- constraints.push(where("isActive", "==", true));
60
- }
61
-
62
- // Add tag filtering if specified
48
+ // Tag (only first, Firestore limitation)
63
49
  if (filters.tags && filters.tags.length > 0) {
64
- // Note: We can only use array-contains-any once per query, so we're selecting one tag
65
- // for the query and we'll filter the remaining tags in memory
66
50
  constraints.push(where("tags", "array-contains", filters.tags[0]));
67
51
  }
68
52
 
69
- // Hierarchical procedure filter - only apply the most specific one provided
70
- // Order of specificity: technology > subcategory > category > family
53
+ // Procedure filters (most specific)
71
54
  if (filters.procedureTechnology) {
72
- constraints.push(
73
- where("servicesInfo.technology", "==", filters.procedureTechnology)
74
- );
55
+ constraints.push(where("servicesInfo.technology", "==", filters.procedureTechnology));
75
56
  } else if (filters.procedureSubcategory) {
76
- constraints.push(
77
- where("servicesInfo.subCategory", "==", filters.procedureSubcategory)
78
- );
57
+ constraints.push(where("servicesInfo.subCategory", "==", filters.procedureSubcategory));
79
58
  } else if (filters.procedureCategory) {
80
- constraints.push(
81
- where("servicesInfo.category", "==", filters.procedureCategory)
82
- );
59
+ constraints.push(where("servicesInfo.category", "==", filters.procedureCategory));
83
60
  } else if (filters.procedureFamily) {
84
- constraints.push(
85
- where("servicesInfo.procedureFamily", "==", filters.procedureFamily)
86
- );
61
+ constraints.push(where("servicesInfo.procedureFamily", "==", filters.procedureFamily));
87
62
  }
88
63
 
89
- // Add pagination if specified
90
- if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
91
- constraints.push(startAfter(filters.lastDoc));
92
- constraints.push(limit(filters.pagination));
93
- } else if (filters.pagination && filters.pagination > 0) {
94
- constraints.push(limit(filters.pagination));
64
+ // Text search by nameLower
65
+ let useNameLower = false;
66
+ let searchTerm = "";
67
+ if (filters.nameSearch && filters.nameSearch.trim()) {
68
+ searchTerm = filters.nameSearch.trim().toLowerCase();
69
+ constraints.push(where("nameLower", ">=", searchTerm));
70
+ constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
71
+ useNameLower = true;
95
72
  }
96
73
 
97
- // Add ordering to make pagination consistent
98
- constraints.push(orderBy("location.geohash"));
99
-
100
- let clinicsResult: (Clinic & { distance?: number })[] = [];
101
- let lastVisibleDoc = null;
102
-
103
- // For geo queries, we need a different approach
104
- if (isGeoQuery) {
105
- const center = filters.center!;
106
- const radiusInKm = filters.radiusInKm!;
107
-
108
- // Get the geohash query bounds
109
- const bounds = geohashQueryBounds(
110
- [center.latitude, center.longitude],
111
- radiusInKm * 1000 // Convert to meters
112
- );
113
-
114
- // Collect matching clinics from all bounds
115
- const matchingClinics: (Clinic & { distance: number })[] = [];
116
-
117
- // Execute queries for each bound
118
- for (const bound of bounds) {
119
- // Create a geo query for this bound
120
- const geoConstraints = [
121
- ...constraints,
122
- where("location.geohash", ">=", bound[0]),
123
- where("location.geohash", "<=", bound[1]),
124
- ];
125
-
126
- const q = query(collection(db, CLINICS_COLLECTION), ...geoConstraints);
127
- const querySnapshot = await getDocs(q);
128
-
129
- console.log(
130
- `[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
131
- );
132
-
133
- // Process results and filter by actual distance
134
- for (const doc of querySnapshot.docs) {
135
- const clinic = { ...doc.data(), id: doc.id } as Clinic;
136
-
137
- // Calculate actual distance
138
- const distance = distanceBetween(
139
- [center.latitude, center.longitude],
140
- [clinic.location.latitude, clinic.location.longitude]
141
- );
142
-
143
- // Convert to kilometers
144
- const distanceInKm = distance / 1000;
145
-
146
- // Check if within radius
147
- if (distanceInKm <= radiusInKm) {
148
- // Add distance to clinic object
149
- matchingClinics.push({
150
- ...clinic,
151
- distance: distanceInKm,
152
- });
153
- }
154
- }
155
- }
156
-
157
- // Apply additional filters that couldn't be applied in the query
158
- let filteredClinics = matchingClinics;
159
-
160
- // Filter by multiple tags if more than one tag was specified
161
- if (filters.tags && filters.tags.length > 1) {
162
- filteredClinics = filteredClinics.filter((clinic) => {
163
- // Check if clinic has all specified tags
164
- return filters.tags!.every((tag) => clinic.tags.includes(tag));
165
- });
166
- }
167
-
168
- // Filter by rating
169
- if (filters.minRating !== undefined) {
170
- filteredClinics = filteredClinics.filter(
171
- (clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
172
- );
173
- }
174
-
175
- if (filters.maxRating !== undefined) {
176
- filteredClinics = filteredClinics.filter(
177
- (clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
178
- );
179
- }
180
-
181
- // Sort by distance
182
- filteredClinics.sort((a, b) => a.distance - b.distance);
183
-
184
- // Apply pagination after all filters have been applied
185
- if (filters.pagination && filters.pagination > 0) {
186
- // If we have a lastDoc, find its index
187
- let startIndex = 0;
188
- if (filters.lastDoc) {
189
- const lastDocIndex = filteredClinics.findIndex(
190
- (clinic) => clinic.id === filters.lastDoc.id
191
- );
192
- if (lastDocIndex !== -1) {
193
- startIndex = lastDocIndex + 1;
194
- }
195
- }
196
-
197
- // Get paginated subset
198
- const paginatedClinics = filteredClinics.slice(
199
- startIndex,
200
- startIndex + filters.pagination
201
- );
202
-
203
- // Set last document for next pagination
204
- lastVisibleDoc =
205
- paginatedClinics.length > 0
206
- ? paginatedClinics[paginatedClinics.length - 1]
207
- : null;
74
+ // Rating filters
75
+ if (filters.minRating !== undefined) {
76
+ constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
77
+ }
78
+ if (filters.maxRating !== undefined) {
79
+ constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
80
+ }
208
81
 
209
- clinicsResult = paginatedClinics;
82
+ // Pagination and ordering
83
+ constraints.push(orderBy("nameLower"));
84
+ if (filters.lastDoc) {
85
+ if (typeof filters.lastDoc.data === "function") {
86
+ constraints.push(startAfter(filters.lastDoc));
87
+ } else if (Array.isArray(filters.lastDoc)) {
88
+ constraints.push(startAfter(...filters.lastDoc));
210
89
  } else {
211
- clinicsResult = filteredClinics;
90
+ constraints.push(startAfter(filters.lastDoc));
212
91
  }
213
- } else {
214
- // For non-geo queries, execute a single query with all constraints
215
- const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
216
- const querySnapshot = await getDocs(q);
92
+ }
93
+ constraints.push(limit(filters.pagination || 5));
217
94
 
218
- console.log(
219
- `[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
220
- );
95
+ // 2. Firestore query
96
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
97
+ const querySnapshot = await getDocs(q);
98
+ let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
221
99
 
222
- // Convert docs to clinics
223
- const clinics = querySnapshot.docs.map((doc) => {
224
- return { ...doc.data(), id: doc.id } as Clinic;
100
+ // 3. In-memory filters for multi-tag and geo-radius
101
+ if (filters.tags && filters.tags.length > 1) {
102
+ clinics = clinics.filter(clinic => filters.tags!.every(tag => clinic.tags.includes(tag)));
103
+ }
104
+ if (filters.center && filters.radiusInKm) {
105
+ clinics = clinics.filter(clinic => {
106
+ const distance = distanceBetween(
107
+ [filters.center!.latitude, filters.center!.longitude],
108
+ [clinic.location.latitude, clinic.location.longitude]
109
+ ) / 1000;
110
+ // Optionally attach distance for frontend
111
+ (clinic as any).distance = distance;
112
+ return distance <= filters.radiusInKm!;
225
113
  });
226
-
227
- // Apply filters that couldn't be applied in the query
228
- let filteredClinics = clinics;
229
-
230
- // Calculate distance for each clinic if center coordinates are provided
231
- if (filters.center) {
232
- const center = filters.center;
233
- const clinicsWithDistance: (Clinic & { distance: number })[] = [];
234
-
235
- filteredClinics.forEach((clinic) => {
236
- const distance = distanceBetween(
237
- [center.latitude, center.longitude],
238
- [clinic.location.latitude, clinic.location.longitude]
239
- );
240
-
241
- clinicsWithDistance.push({
242
- ...clinic,
243
- distance: distance / 1000, // Convert to kilometers
244
- });
245
- });
246
-
247
- // Replace filtered clinics with the version that includes distances
248
- filteredClinics = clinicsWithDistance;
249
-
250
- // Sort by distance - use type assertion to fix type error
251
- (filteredClinics as (Clinic & { distance: number })[]).sort(
252
- (a, b) => a.distance - b.distance
253
- );
254
- }
255
-
256
- // Filter by multiple tags if more than one tag was specified
257
- if (filters.tags && filters.tags.length > 1) {
258
- filteredClinics = filteredClinics.filter((clinic) => {
259
- // Check if clinic has all specified tags
260
- return filters.tags!.every((tag) => clinic.tags.includes(tag));
261
- });
262
- }
263
-
264
- // Filter by rating
265
- if (filters.minRating !== undefined) {
266
- filteredClinics = filteredClinics.filter(
267
- (clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
268
- );
269
- }
270
-
271
- if (filters.maxRating !== undefined) {
272
- filteredClinics = filteredClinics.filter(
273
- (clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
274
- );
275
- }
276
-
277
- // Set last document for pagination
278
- lastVisibleDoc =
279
- querySnapshot.docs.length > 0
280
- ? querySnapshot.docs[querySnapshot.docs.length - 1]
281
- : null;
282
-
283
- clinicsResult = filteredClinics;
284
114
  }
285
115
 
286
- return {
287
- clinics: clinicsResult,
288
- lastDoc: lastVisibleDoc,
289
- };
116
+ // 4. Return results and lastDoc
117
+ const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
118
+ return { clinics, lastDoc: lastVisibleDoc };
290
119
  }
@@ -193,6 +193,7 @@ export class PractitionerService extends BaseService {
193
193
  };
194
194
 
195
195
  // Create practitioner object
196
+ const fullNameLower = `${validData.basicInfo.firstName} ${validData.basicInfo.lastName}`.toLowerCase();
196
197
  const practitioner: Omit<Practitioner, "createdAt" | "updatedAt"> & {
197
198
  createdAt: FieldValue;
198
199
  updatedAt: FieldValue;
@@ -203,6 +204,7 @@ export class PractitionerService extends BaseService {
203
204
  validData.basicInfo,
204
205
  practitionerId
205
206
  ),
207
+ fullNameLower: fullNameLower, // Ensure this is present
206
208
  certification: validData.certification,
207
209
  clinics: validData.clinics || [],
208
210
  clinicWorkingHours: validData.clinicWorkingHours || [],
@@ -346,6 +348,8 @@ export class PractitionerService extends BaseService {
346
348
 
347
349
  const proceduresInfo: ProcedureSummaryInfo[] = [];
348
350
 
351
+ // Add fullNameLower for draft
352
+ const fullNameLowerDraft = `${validatedData.basicInfo.firstName} ${validatedData.basicInfo.lastName}`.toLowerCase();
349
353
  const practitionerData: Omit<Practitioner, "createdAt" | "updatedAt"> & {
350
354
  createdAt: ReturnType<typeof serverTimestamp>;
351
355
  updatedAt: ReturnType<typeof serverTimestamp>;
@@ -356,6 +360,7 @@ export class PractitionerService extends BaseService {
356
360
  validatedData.basicInfo,
357
361
  practitionerId
358
362
  ),
363
+ fullNameLower: fullNameLowerDraft, // Ensure this is present
359
364
  certification: validatedData.certification,
360
365
  clinics: clinics,
361
366
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
@@ -1036,22 +1041,14 @@ export class PractitionerService extends BaseService {
1036
1041
  includeDraftPractitioners?: boolean;
1037
1042
  }): Promise<{ practitioners: Practitioner[]; lastDoc: any }> {
1038
1043
  try {
1039
- console.log(
1040
- "[PRACTITIONER_SERVICE] Starting practitioner filtering with criteria:",
1041
- filters
1042
- );
1043
-
1044
- const constraints = [];
1045
-
1046
- // Filter by status if not including drafts
1044
+ // 1. Prepare Firestore constraints
1045
+ const constraints: any[] = [];
1047
1046
  if (!filters.includeDraftPractitioners) {
1048
1047
  constraints.push(where("status", "==", PractitionerStatus.ACTIVE));
1049
1048
  }
1050
-
1051
- // Filter by active status
1052
1049
  constraints.push(where("isActive", "==", true));
1053
1050
 
1054
- // Add certifications filter if specified
1051
+ // Certifications
1055
1052
  if (filters.certifications && filters.certifications.length > 0) {
1056
1053
  constraints.push(
1057
1054
  where(
@@ -1062,138 +1059,73 @@ export class PractitionerService extends BaseService {
1062
1059
  );
1063
1060
  }
1064
1061
 
1065
- // Add ordering for consistent pagination
1066
- constraints.push(orderBy("basicInfo.lastName", "asc"));
1067
- constraints.push(orderBy("basicInfo.firstName", "asc"));
1068
-
1069
- // Add pagination if specified
1070
- if (filters.pagination && filters.pagination > 0) {
1071
- if (filters.lastDoc) {
1072
- constraints.push(startAfter(filters.lastDoc));
1073
- }
1074
- constraints.push(limit(filters.pagination));
1062
+ // Text search by fullNameLower
1063
+ if (filters.nameSearch && filters.nameSearch.trim()) {
1064
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
1065
+ constraints.push(where("fullNameLower", ">=", searchTerm));
1066
+ constraints.push(where("fullNameLower", "<=", searchTerm + "\uf8ff"));
1075
1067
  }
1076
1068
 
1077
- // Execute the query
1078
- const q = query(
1079
- collection(this.db, PRACTITIONERS_COLLECTION),
1080
- ...constraints
1081
- );
1082
- const querySnapshot = await getDocs(q);
1083
-
1084
- console.log(
1085
- `[PRACTITIONER_SERVICE] Found ${querySnapshot.docs.length} practitioners with base query`
1086
- );
1087
-
1088
- // Convert docs to practitioners
1089
- let practitioners = querySnapshot.docs.map((doc) => {
1090
- return { ...doc.data(), id: doc.id } as Practitioner;
1091
- });
1092
-
1093
- // Get last document for pagination
1094
- const lastDoc =
1095
- querySnapshot.docs.length > 0
1096
- ? querySnapshot.docs[querySnapshot.docs.length - 1]
1097
- : null;
1098
-
1099
- // Further filter results in memory
1100
-
1101
- // Filter by name search if specified
1102
- if (filters.nameSearch && filters.nameSearch.trim() !== "") {
1103
- const searchTerm = filters.nameSearch.toLowerCase().trim();
1104
- practitioners = practitioners.filter((practitioner) => {
1105
- const fullName =
1106
- `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`.toLowerCase();
1107
- return fullName.includes(searchTerm);
1108
- });
1069
+ // Procedure filters (if mapped to fields)
1070
+ if (filters.procedureTechnology) {
1071
+ constraints.push(where("proceduresInfo.technologyName", "==", filters.procedureTechnology));
1072
+ } else if (filters.procedureSubcategory) {
1073
+ constraints.push(where("proceduresInfo.subcategoryName", "==", filters.procedureSubcategory));
1074
+ } else if (filters.procedureCategory) {
1075
+ constraints.push(where("proceduresInfo.categoryName", "==", filters.procedureCategory));
1076
+ } else if (filters.procedureFamily) {
1077
+ constraints.push(where("proceduresInfo.family", "==", filters.procedureFamily));
1109
1078
  }
1110
1079
 
1111
- // Filter by specialties
1112
- if (filters.specialties && filters.specialties.length > 0) {
1113
- practitioners = practitioners.filter((practitioner) => {
1114
- return filters.specialties!.every((specialty) =>
1115
- practitioner.certification.specialties.includes(specialty)
1116
- );
1117
- });
1080
+ // Rating filters
1081
+ if (filters.minRating !== undefined) {
1082
+ constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
1083
+ }
1084
+ if (filters.maxRating !== undefined) {
1085
+ constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
1118
1086
  }
1119
1087
 
1120
- // Filter by procedure attributes using the aggregated proceduresInfo
1121
- if (
1122
- filters.procedureTechnology ||
1123
- filters.procedureSubcategory ||
1124
- filters.procedureCategory ||
1125
- filters.procedureFamily
1126
- ) {
1127
- practitioners = practitioners.filter((practitioner) => {
1128
- const procedures = practitioner.proceduresInfo || [];
1129
- return procedures.some((procedure: ProcedureSummaryInfo) => {
1130
- // Apply hierarchical filter - most specific first
1131
- if (filters.procedureTechnology) {
1132
- return procedure.technologyName === filters.procedureTechnology;
1133
- }
1134
- if (filters.procedureSubcategory) {
1135
- return procedure.subcategoryName === filters.procedureSubcategory;
1136
- }
1137
- if (filters.procedureCategory) {
1138
- return procedure.categoryName === filters.procedureCategory;
1139
- }
1140
- if (filters.procedureFamily) {
1141
- return procedure.family === filters.procedureFamily;
1142
- }
1143
- return false;
1144
- });
1145
- });
1088
+ // Pagination and ordering
1089
+ constraints.push(orderBy("fullNameLower"));
1090
+ if (filters.lastDoc) {
1091
+ if (typeof filters.lastDoc.data === "function") {
1092
+ constraints.push(startAfter(filters.lastDoc));
1093
+ } else if (Array.isArray(filters.lastDoc)) {
1094
+ constraints.push(startAfter(...filters.lastDoc));
1095
+ } else {
1096
+ constraints.push(startAfter(filters.lastDoc));
1097
+ }
1146
1098
  }
1099
+ constraints.push(limit(filters.pagination || 5));
1100
+
1101
+ // 2. Firestore query
1102
+ const q = query(collection(this.db, PRACTITIONERS_COLLECTION), ...constraints);
1103
+ const querySnapshot = await getDocs(q);
1104
+ let practitioners = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Practitioner));
1147
1105
 
1148
- // Filter by location/distance if specified
1106
+ // 3. In-memory filter ONLY for geo-radius (if needed)
1149
1107
  if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1150
1108
  const location = filters.location;
1151
1109
  const radiusInKm = filters.radiusInKm;
1152
-
1153
1110
  practitioners = practitioners.filter((practitioner) => {
1154
1111
  // Use the aggregated clinicsInfo to check if any clinic is within range
1155
1112
  const clinics = practitioner.clinicsInfo || [];
1156
-
1157
- // Check if any clinic is within the specified radius
1158
1113
  return clinics.some((clinic) => {
1159
1114
  // Calculate distance
1160
1115
  const distance = distanceBetween(
1161
1116
  [location.latitude, location.longitude],
1162
1117
  [clinic.location.latitude, clinic.location.longitude]
1163
1118
  );
1164
-
1165
1119
  // Convert to kilometers
1166
1120
  const distanceInKm = distance / 1000;
1167
-
1168
1121
  // Check if within radius
1169
1122
  return distanceInKm <= radiusInKm;
1170
1123
  });
1171
1124
  });
1172
1125
  }
1173
1126
 
1174
- // Filter by rating
1175
- if (filters.minRating !== undefined) {
1176
- practitioners = practitioners.filter(
1177
- (p) => p.reviewInfo.averageRating >= filters.minRating!
1178
- );
1179
- }
1180
-
1181
- if (filters.maxRating !== undefined) {
1182
- practitioners = practitioners.filter(
1183
- (p) => p.reviewInfo.averageRating <= filters.maxRating!
1184
- );
1185
- }
1186
-
1187
- console.log(
1188
- `[PRACTITIONER_SERVICE] Filtered to ${practitioners.length} practitioners`
1189
- );
1190
-
1191
- // Apply pagination after all filters have been applied
1192
- // This is a secondary pagination for in-memory filtered results
1193
- if (filters.pagination && filters.pagination > 0) {
1194
- practitioners = practitioners.slice(0, filters.pagination);
1195
- }
1196
-
1127
+ // 4. Return results and lastDoc
1128
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1197
1129
  return {
1198
1130
  practitioners,
1199
1131
  lastDoc,
@@ -660,7 +660,7 @@ export class ProcedureService extends BaseService {
660
660
  // Handle Category/Subcategory/Technology/Product Changes
661
661
  let finalCategoryId = existingProcedure.category.id;
662
662
  if (validatedData.name) {
663
- updatedProcedureData.nameLower = validatedData.nameLower || validatedData.name.toLowerCase();
663
+ updatedProcedureData.nameLower = validatedData.name.toLowerCase();
664
664
  }
665
665
  if (validatedData.categoryId) {
666
666
  const category = await this.categoryService.getById(
@@ -955,8 +955,17 @@ export class ProcedureService extends BaseService {
955
955
  constraints.push(orderBy("nameLower"));
956
956
  }
957
957
  if (filters.lastDoc) {
958
- // lastDoc treba da bude ceo snapshot, ne samo id!
959
- constraints.push(startAfter(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));
965
+ } else {
966
+ // Single value
967
+ constraints.push(startAfter(filters.lastDoc));
968
+ }
960
969
  }
961
970
  if (filters.pagination && filters.pagination > 0) {
962
971
  constraints.push(limit(filters.pagination));
@@ -1174,4 +1183,35 @@ export class ProcedureService extends BaseService {
1174
1183
  const savedDoc = await getDoc(procedureRef);
1175
1184
  return savedDoc.data() as Procedure;
1176
1185
  }
1186
+
1187
+ /**
1188
+ * Gets all procedures with minimal info for map display (id, name, clinicId, clinicName, address, latitude, longitude)
1189
+ * This is optimized for mobile map usage to reduce payload size.
1190
+ * @returns Array of minimal procedure info for map
1191
+ */
1192
+ async getProceduresForMap(): Promise<{
1193
+ id: string;
1194
+ name: string;
1195
+ clinicId: string | undefined;
1196
+ clinicName: string | undefined;
1197
+ address: string;
1198
+ latitude: number | undefined;
1199
+ longitude: number | undefined;
1200
+ }[]> {
1201
+ const proceduresRef = collection(this.db, PROCEDURES_COLLECTION);
1202
+ const snapshot = await getDocs(proceduresRef);
1203
+ const proceduresForMap = snapshot.docs.map(doc => {
1204
+ const data = doc.data();
1205
+ return {
1206
+ id: doc.id,
1207
+ name: data.name,
1208
+ clinicId: data.clinicInfo?.id,
1209
+ clinicName: data.clinicInfo?.name,
1210
+ address: data.clinicInfo?.location?.address || '',
1211
+ latitude: data.clinicInfo?.location?.latitude,
1212
+ longitude: data.clinicInfo?.location?.longitude,
1213
+ };
1214
+ });
1215
+ return proceduresForMap;
1216
+ }
1177
1217
  }
@@ -269,6 +269,7 @@ export interface Clinic {
269
269
  id: string;
270
270
  clinicGroupId: string;
271
271
  name: string;
272
+ nameLower: string; // Add this line
272
273
  description?: string;
273
274
  location: ClinicLocation;
274
275
  contactInfo: ClinicContactInfo;
@@ -89,6 +89,7 @@ export interface Practitioner {
89
89
  id: string;
90
90
  userRef: string;
91
91
  basicInfo: PractitionerBasicInfo;
92
+ fullNameLower: string; // Add this line
92
93
  certification: PractitionerCertification;
93
94
  clinics: string[]; // Reference na klinike gde radi
94
95
  clinicWorkingHours: PractitionerClinicWorkingHours[]; // Radno vreme za svaku kliniku