@blackcode_sa/metaestetics-api 1.5.31 → 1.5.33

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.
@@ -0,0 +1,590 @@
1
+ import {
2
+ Firestore,
3
+ collection,
4
+ doc,
5
+ getDoc,
6
+ getDocs,
7
+ query,
8
+ where,
9
+ setDoc,
10
+ updateDoc,
11
+ serverTimestamp,
12
+ Timestamp,
13
+ orderBy,
14
+ limit,
15
+ startAfter,
16
+ QueryConstraint,
17
+ DocumentSnapshot,
18
+ } from "firebase/firestore";
19
+ import {
20
+ Appointment,
21
+ AppointmentStatus,
22
+ CreateAppointmentData,
23
+ UpdateAppointmentData,
24
+ APPOINTMENTS_COLLECTION,
25
+ SearchAppointmentsParams,
26
+ PaymentStatus,
27
+ } from "../../../types/appointment";
28
+ import { CalendarEvent, CALENDAR_COLLECTION } from "../../../types/calendar";
29
+ import { ProcedureSummaryInfo } from "../../../types/procedure";
30
+ import {
31
+ ClinicInfo,
32
+ PatientProfileInfo,
33
+ PractitionerProfileInfo,
34
+ } from "../../../types/profile";
35
+ import { BlockingCondition } from "../../../backoffice/types/static/blocking-condition.types";
36
+ import { Contraindication } from "../../../backoffice/types/static/contraindication.types";
37
+ import { Requirement } from "../../../backoffice/types/requirement.types";
38
+ import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
39
+ import { CLINICS_COLLECTION } from "../../../types/clinic";
40
+ import { PATIENTS_COLLECTION } from "../../../types/patient";
41
+ import { PROCEDURES_COLLECTION } from "../../../types/procedure";
42
+ import {
43
+ Technology,
44
+ TECHNOLOGIES_COLLECTION,
45
+ } from "../../../backoffice/types/technology.types";
46
+
47
+ /**
48
+ * Fetches all the necessary information for an appointment by IDs.
49
+ *
50
+ * @param db Firestore instance
51
+ * @param clinicId Clinic ID
52
+ * @param practitionerId Practitioner ID
53
+ * @param patientId Patient ID
54
+ * @param procedureId Procedure ID
55
+ * @returns Object containing the aggregated information
56
+ */
57
+ export async function fetchAggregatedInfoUtil(
58
+ db: Firestore,
59
+ clinicId: string,
60
+ practitionerId: string,
61
+ patientId: string,
62
+ procedureId: string
63
+ ): Promise<{
64
+ clinicInfo: ClinicInfo;
65
+ practitionerInfo: PractitionerProfileInfo;
66
+ patientInfo: PatientProfileInfo;
67
+ procedureInfo: ProcedureSummaryInfo;
68
+ blockingConditions: BlockingCondition[];
69
+ contraindications: Contraindication[];
70
+ preProcedureRequirements: Requirement[];
71
+ postProcedureRequirements: Requirement[];
72
+ }> {
73
+ try {
74
+ // Fetch all data in parallel for efficiency
75
+ const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] =
76
+ await Promise.all([
77
+ getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
78
+ getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
79
+ getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
80
+ getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
81
+ ]);
82
+
83
+ // Check if all required entities exist
84
+ if (!clinicDoc.exists()) {
85
+ throw new Error(`Clinic with ID ${clinicId} not found`);
86
+ }
87
+ if (!practitionerDoc.exists()) {
88
+ throw new Error(`Practitioner with ID ${practitionerId} not found`);
89
+ }
90
+ if (!patientDoc.exists()) {
91
+ throw new Error(`Patient with ID ${patientId} not found`);
92
+ }
93
+ if (!procedureDoc.exists()) {
94
+ throw new Error(`Procedure with ID ${procedureId} not found`);
95
+ }
96
+
97
+ const clinicData = clinicDoc.data();
98
+ const practitionerData = practitionerDoc.data();
99
+ const patientData = patientDoc.data();
100
+ const procedureData = procedureDoc.data();
101
+
102
+ // Extract relevant info for ClinicInfo
103
+ const clinicInfo: ClinicInfo = {
104
+ id: clinicId,
105
+ featuredPhoto: clinicData.featuredPhotos?.[0] || "",
106
+ name: clinicData.name,
107
+ description: clinicData.description || null,
108
+ location: clinicData.location,
109
+ contactInfo: clinicData.contactInfo,
110
+ };
111
+
112
+ // Extract relevant info for PractitionerProfileInfo
113
+ const practitionerInfo: PractitionerProfileInfo = {
114
+ id: practitionerId,
115
+ practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
116
+ name: `${practitionerData.basicInfo?.firstName || ""} ${
117
+ practitionerData.basicInfo?.lastName || ""
118
+ }`.trim(),
119
+ email: practitionerData.basicInfo?.email || "",
120
+ phone: practitionerData.basicInfo?.phoneNumber || null,
121
+ certification: practitionerData.certification,
122
+ };
123
+
124
+ // Extract relevant info for PatientProfileInfo
125
+ // Note: This may need adjustment depending on how patient data is structured
126
+ const patientInfo: PatientProfileInfo = {
127
+ id: patientId,
128
+ fullName: patientData.displayName || "",
129
+ email: patientData.email || "",
130
+ phone: patientData.phoneNumber || null,
131
+ dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
132
+ gender: patientData.gender || "other",
133
+ };
134
+
135
+ // Extract procedureInfo from the procedure document
136
+ // Assuming procedureData already has a procedureInfo property or similar structure
137
+ const procedureInfo: ProcedureSummaryInfo = {
138
+ id: procedureId,
139
+ name: procedureData.name,
140
+ description: procedureData.description,
141
+ photo: procedureData.photo || "",
142
+ family: procedureData.family,
143
+ categoryName: procedureData.category?.name || "",
144
+ subcategoryName: procedureData.subcategory?.name || "",
145
+ technologyName: procedureData.technology?.name || "",
146
+ brandName: procedureData.product?.brand || "",
147
+ productName: procedureData.product?.name || "",
148
+ price: procedureData.price || 0,
149
+ pricingMeasure: procedureData.pricingMeasure,
150
+ currency: procedureData.currency,
151
+ duration: procedureData.duration || 0,
152
+ clinicId: clinicId,
153
+ clinicName: clinicInfo.name,
154
+ practitionerId: practitionerId,
155
+ practitionerName: practitionerInfo.name,
156
+ };
157
+
158
+ // Fetch the technology document to get procedure requirements
159
+ let technologyId = "";
160
+ if (procedureData.technology?.id) {
161
+ technologyId = procedureData.technology.id;
162
+ }
163
+
164
+ let blockingConditions: BlockingCondition[] = [];
165
+ let contraindications: Contraindication[] = [];
166
+ let preProcedureRequirements: Requirement[] = [];
167
+ let postProcedureRequirements: Requirement[] = [];
168
+
169
+ // If we have a technology ID, fetch its details
170
+ if (technologyId) {
171
+ const technologyDoc = await getDoc(
172
+ doc(db, TECHNOLOGIES_COLLECTION, technologyId)
173
+ );
174
+ if (technologyDoc.exists()) {
175
+ const technologyData = technologyDoc.data() as Technology;
176
+
177
+ // Extract technology-related info
178
+ blockingConditions = technologyData.blockingConditions || [];
179
+ contraindications = technologyData.contraindications || [];
180
+ preProcedureRequirements = technologyData.requirements?.pre || [];
181
+ postProcedureRequirements = technologyData.requirements?.post || [];
182
+ }
183
+ } else {
184
+ // Fallback to procedure-level data if technology not available
185
+ blockingConditions = procedureData.blockingConditions || [];
186
+ contraindications = procedureData.contraindications || [];
187
+ preProcedureRequirements = procedureData.preRequirements || [];
188
+ postProcedureRequirements = procedureData.postRequirements || [];
189
+ }
190
+
191
+ return {
192
+ clinicInfo,
193
+ practitionerInfo,
194
+ patientInfo,
195
+ procedureInfo,
196
+ blockingConditions,
197
+ contraindications,
198
+ preProcedureRequirements,
199
+ postProcedureRequirements,
200
+ };
201
+ } catch (error) {
202
+ console.error("Error fetching aggregated info:", error);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Creates a new appointment in Firestore.
209
+ *
210
+ * @param db Firestore instance
211
+ * @param data Data needed to create the appointment
212
+ * @param aggregatedInfo Already fetched and aggregated info
213
+ * @param generateId Function to generate a unique ID
214
+ * @returns The created Appointment
215
+ */
216
+ export async function createAppointmentUtil(
217
+ db: Firestore,
218
+ data: CreateAppointmentData,
219
+ aggregatedInfo: {
220
+ clinicInfo: ClinicInfo;
221
+ practitionerInfo: PractitionerProfileInfo;
222
+ patientInfo: PatientProfileInfo;
223
+ procedureInfo: ProcedureSummaryInfo;
224
+ blockingConditions: BlockingCondition[];
225
+ contraindications: Contraindication[];
226
+ preProcedureRequirements: Requirement[];
227
+ postProcedureRequirements: Requirement[];
228
+ },
229
+ generateId: () => string
230
+ ): Promise<Appointment> {
231
+ try {
232
+ const appointmentId = generateId();
233
+
234
+ // Create appointment object
235
+ const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
236
+ createdAt: any;
237
+ updatedAt: any;
238
+ } = {
239
+ id: appointmentId,
240
+ calendarEventId: data.calendarEventId,
241
+ clinicBranchId: data.clinicBranchId,
242
+ clinicInfo: aggregatedInfo.clinicInfo,
243
+ practitionerId: data.practitionerId,
244
+ practitionerInfo: aggregatedInfo.practitionerInfo,
245
+ patientId: data.patientId,
246
+ patientInfo: aggregatedInfo.patientInfo,
247
+ procedureId: data.procedureId,
248
+ procedureInfo: aggregatedInfo.procedureInfo,
249
+ status: data.initialStatus,
250
+ bookingTime: Timestamp.now(),
251
+ appointmentStartTime: data.appointmentStartTime,
252
+ appointmentEndTime: data.appointmentEndTime,
253
+ patientNotes: data.patientNotes || null,
254
+ cost: data.cost,
255
+ currency: data.currency,
256
+ paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
257
+ blockingConditions: aggregatedInfo.blockingConditions,
258
+ contraindications: aggregatedInfo.contraindications,
259
+ preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
260
+ postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
261
+ completedPreRequirements: [],
262
+ completedPostRequirements: [],
263
+ createdAt: serverTimestamp(),
264
+ updatedAt: serverTimestamp(),
265
+ };
266
+
267
+ // Add additional fields for confirmation if appointment is already confirmed
268
+ if (data.initialStatus === AppointmentStatus.CONFIRMED) {
269
+ appointment.confirmationTime = Timestamp.now();
270
+ }
271
+
272
+ // Save to Firestore
273
+ await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
274
+
275
+ // Update the calendar event with the appointment ID
276
+ const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
277
+ await updateDoc(calendarEventRef, {
278
+ appointmentId: appointmentId,
279
+ updatedAt: serverTimestamp(),
280
+ });
281
+
282
+ // Return the created appointment
283
+ // Convert serverTimestamp to regular Timestamp for immediate use
284
+ const now = Timestamp.now();
285
+ return {
286
+ ...appointment,
287
+ createdAt: now,
288
+ updatedAt: now,
289
+ } as Appointment;
290
+ } catch (error) {
291
+ console.error("Error creating appointment:", error);
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Updates an existing appointment in Firestore.
298
+ *
299
+ * @param db Firestore instance
300
+ * @param appointmentId ID of the appointment to update
301
+ * @param data Update data for the appointment
302
+ * @returns The updated Appointment
303
+ */
304
+ export async function updateAppointmentUtil(
305
+ db: Firestore,
306
+ appointmentId: string,
307
+ data: UpdateAppointmentData
308
+ ): Promise<Appointment> {
309
+ try {
310
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
311
+ const appointmentDoc = await getDoc(appointmentRef);
312
+
313
+ if (!appointmentDoc.exists()) {
314
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
315
+ }
316
+
317
+ const currentAppointment = appointmentDoc.data() as Appointment;
318
+
319
+ // Handle requirement completion tracking
320
+ let completedPreRequirements =
321
+ currentAppointment.completedPreRequirements || [];
322
+ let completedPostRequirements =
323
+ currentAppointment.completedPostRequirements || [];
324
+
325
+ if (data.completedPreRequirements) {
326
+ // Validate that all IDs exist in the pre-requirements
327
+ const validPreReqIds = currentAppointment.preProcedureRequirements.map(
328
+ (req) => req.id
329
+ );
330
+ const invalidPreReqIds = data.completedPreRequirements.filter(
331
+ (id) => !validPreReqIds.includes(id)
332
+ );
333
+
334
+ if (invalidPreReqIds.length > 0) {
335
+ throw new Error(
336
+ `Invalid pre-requirement IDs: ${invalidPreReqIds.join(", ")}`
337
+ );
338
+ }
339
+
340
+ // Update the completed pre-requirements
341
+ completedPreRequirements = [
342
+ ...new Set([
343
+ ...completedPreRequirements,
344
+ ...data.completedPreRequirements,
345
+ ]),
346
+ ];
347
+ }
348
+
349
+ if (data.completedPostRequirements) {
350
+ // Validate that all IDs exist in the post-requirements
351
+ const validPostReqIds = currentAppointment.postProcedureRequirements.map(
352
+ (req) => req.id
353
+ );
354
+ const invalidPostReqIds = data.completedPostRequirements.filter(
355
+ (id) => !validPostReqIds.includes(id)
356
+ );
357
+
358
+ if (invalidPostReqIds.length > 0) {
359
+ throw new Error(
360
+ `Invalid post-requirement IDs: ${invalidPostReqIds.join(", ")}`
361
+ );
362
+ }
363
+
364
+ // Update the completed post-requirements
365
+ completedPostRequirements = [
366
+ ...new Set([
367
+ ...completedPostRequirements,
368
+ ...data.completedPostRequirements,
369
+ ]),
370
+ ];
371
+ }
372
+
373
+ // Prepare update data
374
+ const updateData: any = {
375
+ ...data,
376
+ completedPreRequirements,
377
+ completedPostRequirements,
378
+ updatedAt: serverTimestamp(),
379
+ };
380
+
381
+ // Remove undefined fields
382
+ Object.keys(updateData).forEach((key) => {
383
+ if (updateData[key] === undefined) {
384
+ delete updateData[key];
385
+ }
386
+ });
387
+
388
+ // Handle status changes
389
+ if (data.status && data.status !== currentAppointment.status) {
390
+ // Handle confirmation
391
+ if (
392
+ data.status === AppointmentStatus.CONFIRMED &&
393
+ !updateData.confirmationTime
394
+ ) {
395
+ updateData.confirmationTime = Timestamp.now();
396
+ }
397
+
398
+ // Update the related calendar event status if needed
399
+ if (currentAppointment.calendarEventId) {
400
+ await updateCalendarEventStatus(
401
+ db,
402
+ currentAppointment.calendarEventId,
403
+ data.status
404
+ );
405
+ }
406
+ }
407
+
408
+ // Update the appointment
409
+ await updateDoc(appointmentRef, updateData);
410
+
411
+ // Fetch the updated appointment
412
+ const updatedAppointmentDoc = await getDoc(appointmentRef);
413
+ if (!updatedAppointmentDoc.exists()) {
414
+ throw new Error(
415
+ `Failed to retrieve updated appointment ${appointmentId}`
416
+ );
417
+ }
418
+
419
+ return updatedAppointmentDoc.data() as Appointment;
420
+ } catch (error) {
421
+ console.error(`Error updating appointment ${appointmentId}:`, error);
422
+ throw error;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Updates the status of a calendar event based on appointment status changes.
428
+ *
429
+ * @param db Firestore instance
430
+ * @param calendarEventId ID of the calendar event
431
+ * @param appointmentStatus New appointment status
432
+ */
433
+ async function updateCalendarEventStatus(
434
+ db: Firestore,
435
+ calendarEventId: string,
436
+ appointmentStatus: AppointmentStatus
437
+ ): Promise<void> {
438
+ try {
439
+ const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
440
+ const calendarEventDoc = await getDoc(calendarEventRef);
441
+
442
+ if (!calendarEventDoc.exists()) {
443
+ console.warn(`Calendar event with ID ${calendarEventId} not found`);
444
+ return;
445
+ }
446
+
447
+ // Map appointment status to calendar event status
448
+ let calendarStatus;
449
+ switch (appointmentStatus) {
450
+ case AppointmentStatus.CONFIRMED:
451
+ calendarStatus = "confirmed";
452
+ break;
453
+ case AppointmentStatus.CANCELED_PATIENT:
454
+ case AppointmentStatus.CANCELED_CLINIC:
455
+ calendarStatus = "canceled";
456
+ break;
457
+ case AppointmentStatus.RESCHEDULED:
458
+ calendarStatus = "rescheduled";
459
+ break;
460
+ case AppointmentStatus.COMPLETED:
461
+ calendarStatus = "completed";
462
+ break;
463
+ default:
464
+ // For other states, don't update the calendar status
465
+ return;
466
+ }
467
+
468
+ await updateDoc(calendarEventRef, {
469
+ status: calendarStatus,
470
+ updatedAt: serverTimestamp(),
471
+ });
472
+ } catch (error) {
473
+ console.error(`Error updating calendar event ${calendarEventId}:`, error);
474
+ // Don't throw error to avoid failing the appointment update
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Gets an appointment by its ID.
480
+ *
481
+ * @param db Firestore instance
482
+ * @param appointmentId Appointment ID
483
+ * @returns The appointment or null if not found
484
+ */
485
+ export async function getAppointmentByIdUtil(
486
+ db: Firestore,
487
+ appointmentId: string
488
+ ): Promise<Appointment | null> {
489
+ try {
490
+ const appointmentDoc = await getDoc(
491
+ doc(db, APPOINTMENTS_COLLECTION, appointmentId)
492
+ );
493
+
494
+ if (!appointmentDoc.exists()) {
495
+ return null;
496
+ }
497
+
498
+ return appointmentDoc.data() as Appointment;
499
+ } catch (error) {
500
+ console.error(`Error getting appointment ${appointmentId}:`, error);
501
+ throw error;
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Searches for appointments based on various criteria.
507
+ *
508
+ * @param db Firestore instance
509
+ * @param params Search parameters
510
+ * @returns Found appointments and the last document for pagination
511
+ */
512
+ export async function searchAppointmentsUtil(
513
+ db: Firestore,
514
+ params: SearchAppointmentsParams
515
+ ): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
516
+ try {
517
+ const constraints: QueryConstraint[] = [];
518
+
519
+ // Add filters based on provided params
520
+ if (params.patientId) {
521
+ constraints.push(where("patientId", "==", params.patientId));
522
+ }
523
+
524
+ if (params.practitionerId) {
525
+ constraints.push(where("practitionerId", "==", params.practitionerId));
526
+ }
527
+
528
+ if (params.clinicBranchId) {
529
+ constraints.push(where("clinicBranchId", "==", params.clinicBranchId));
530
+ }
531
+
532
+ if (params.startDate) {
533
+ constraints.push(
534
+ where(
535
+ "appointmentStartTime",
536
+ ">=",
537
+ Timestamp.fromDate(params.startDate)
538
+ )
539
+ );
540
+ }
541
+
542
+ if (params.endDate) {
543
+ constraints.push(
544
+ where("appointmentStartTime", "<=", Timestamp.fromDate(params.endDate))
545
+ );
546
+ }
547
+
548
+ if (params.status) {
549
+ if (Array.isArray(params.status)) {
550
+ // If multiple statuses, use in operator
551
+ constraints.push(where("status", "in", params.status));
552
+ } else {
553
+ // Single status
554
+ constraints.push(where("status", "==", params.status));
555
+ }
556
+ }
557
+
558
+ // Add ordering
559
+ constraints.push(orderBy("appointmentStartTime", "asc"));
560
+
561
+ // Add pagination if specified
562
+ if (params.limit) {
563
+ constraints.push(limit(params.limit));
564
+ }
565
+
566
+ if (params.startAfter) {
567
+ constraints.push(startAfter(params.startAfter));
568
+ }
569
+
570
+ // Execute query
571
+ const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
572
+ const querySnapshot = await getDocs(q);
573
+
574
+ // Extract results
575
+ const appointments = querySnapshot.docs.map(
576
+ (doc) => doc.data() as Appointment
577
+ );
578
+
579
+ // Get last document for pagination
580
+ const lastDoc =
581
+ querySnapshot.docs.length > 0
582
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
583
+ : null;
584
+
585
+ return { appointments, lastDoc };
586
+ } catch (error) {
587
+ console.error("Error searching appointments:", error);
588
+ throw error;
589
+ }
590
+ }
@@ -426,6 +426,12 @@ export class ClinicService extends BaseService {
426
426
  );
427
427
  }
428
428
 
429
+ /**
430
+ * Get clinics based on multiple filtering criteria
431
+ *
432
+ * @param filters - Various filters to apply
433
+ * @returns Filtered clinics and the last document for pagination
434
+ */
429
435
  async getClinicsByFilters(filters: {
430
436
  center?: { latitude: number; longitude: number };
431
437
  radiusInKm?: number;
@@ -95,7 +95,7 @@ export async function getClinicsByFilters(
95
95
  }
96
96
 
97
97
  // Add ordering to make pagination consistent
98
- constraints.push(orderBy(documentId()));
98
+ constraints.push(orderBy("location.geohash"));
99
99
 
100
100
  let clinicsResult: (Clinic & { distance?: number })[] = [];
101
101
  let lastVisibleDoc = null;
@@ -227,6 +227,32 @@ export async function getClinicsByFilters(
227
227
  // Apply filters that couldn't be applied in the query
228
228
  let filteredClinics = clinics;
229
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
+
230
256
  // Filter by multiple tags if more than one tag was specified
231
257
  if (filters.tags && filters.tags.length > 1) {
232
258
  filteredClinics = filteredClinics.filter((clinic) => {
@@ -612,7 +612,7 @@ export class ProcedureService extends BaseService {
612
612
  }
613
613
 
614
614
  // Add ordering to make pagination consistent
615
- constraints.push(orderBy(documentId()));
615
+ constraints.push(orderBy("clinicInfo.location.geohash"));
616
616
 
617
617
  // Add pagination if specified
618
618
  if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
@@ -743,16 +743,54 @@ export class ProcedureService extends BaseService {
743
743
  return { ...doc.data(), id: doc.id } as Procedure;
744
744
  });
745
745
 
746
- // Apply filters that couldn't be applied in the query
747
- let filteredProcedures = this.applyInMemoryFilters(procedures, filters);
746
+ // Calculate distance for each procedure if location is provided
747
+ if (filters.location) {
748
+ const center = filters.location;
749
+ const proceduresWithDistance: (Procedure & { distance: number })[] =
750
+ [];
751
+
752
+ procedures.forEach((procedure) => {
753
+ const distance = distanceBetween(
754
+ [center.latitude, center.longitude],
755
+ [
756
+ procedure.clinicInfo.location.latitude,
757
+ procedure.clinicInfo.location.longitude,
758
+ ]
759
+ );
760
+
761
+ proceduresWithDistance.push({
762
+ ...procedure,
763
+ distance: distance / 1000, // Convert to kilometers
764
+ });
765
+ });
766
+
767
+ // Replace procedures with version that includes distances
768
+ let filteredProcedures = proceduresWithDistance;
769
+
770
+ // Apply in-memory filters
771
+ filteredProcedures = this.applyInMemoryFilters(
772
+ filteredProcedures,
773
+ filters
774
+ );
775
+
776
+ // Sort by distance
777
+ filteredProcedures.sort((a, b) => a.distance - b.distance);
778
+
779
+ proceduresResult = filteredProcedures;
780
+ } else {
781
+ // Apply filters that couldn't be applied in the query
782
+ let filteredProcedures = this.applyInMemoryFilters(
783
+ procedures,
784
+ filters
785
+ );
786
+ proceduresResult = filteredProcedures;
787
+ }
748
788
 
749
789
  // Set last document for pagination
750
790
  lastVisibleDoc =
751
791
  querySnapshot.docs.length > 0
752
792
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
753
793
  : null;
754
-
755
- proceduresResult = filteredProcedures;
756
794
  }
757
795
 
758
796
  return {