@blackcode_sa/metaestetics-api 1.8.18 → 1.10.0

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.
@@ -20,57 +20,39 @@ import {
20
20
  startAfter,
21
21
  QueryConstraint,
22
22
  documentId,
23
- } from "firebase/firestore";
24
- import { BaseService } from "../base.service";
23
+ } from 'firebase/firestore';
24
+ import { BaseService } from '../base.service';
25
25
  import {
26
26
  Procedure,
27
27
  CreateProcedureData,
28
28
  UpdateProcedureData,
29
29
  PROCEDURES_COLLECTION,
30
30
  ProcedureSummaryInfo,
31
- } from "../../types/procedure";
32
- import {
33
- createProcedureSchema,
34
- updateProcedureSchema,
35
- } from "../../validations/procedure.schema";
36
- import { z } from "zod";
37
- import { Auth } from "firebase/auth";
38
- import { Firestore } from "firebase/firestore";
39
- import { FirebaseApp } from "firebase/app";
40
- import {
41
- Category,
42
- CATEGORIES_COLLECTION,
43
- } from "../../backoffice/types/category.types";
44
- import {
45
- Subcategory,
46
- SUBCATEGORIES_COLLECTION,
47
- } from "../../backoffice/types/subcategory.types";
48
- import {
49
- Technology,
50
- TECHNOLOGIES_COLLECTION,
51
- } from "../../backoffice/types/technology.types";
52
- import {
53
- Product,
54
- PRODUCTS_COLLECTION,
55
- } from "../../backoffice/types/product.types";
56
- import { CategoryService } from "../../backoffice/services/category.service";
57
- import { SubcategoryService } from "../../backoffice/services/subcategory.service";
58
- import { TechnologyService } from "../../backoffice/services/technology.service";
59
- import { ProductService } from "../../backoffice/services/product.service";
60
- import {
61
- Practitioner,
62
- PRACTITIONERS_COLLECTION,
63
- } from "../../types/practitioner";
31
+ } from '../../types/procedure';
32
+ import { createProcedureSchema, updateProcedureSchema } from '../../validations/procedure.schema';
33
+ import { z } from 'zod';
34
+ import { Auth } from 'firebase/auth';
35
+ import { Firestore } from 'firebase/firestore';
36
+ import { FirebaseApp } from 'firebase/app';
37
+ import { Category, CATEGORIES_COLLECTION } from '../../backoffice/types/category.types';
38
+ import { Subcategory, SUBCATEGORIES_COLLECTION } from '../../backoffice/types/subcategory.types';
39
+ import { Technology, TECHNOLOGIES_COLLECTION } from '../../backoffice/types/technology.types';
40
+ import { Product, PRODUCTS_COLLECTION } from '../../backoffice/types/product.types';
41
+ import { CategoryService } from '../../backoffice/services/category.service';
42
+ import { SubcategoryService } from '../../backoffice/services/subcategory.service';
43
+ import { TechnologyService } from '../../backoffice/services/technology.service';
44
+ import { ProductService } from '../../backoffice/services/product.service';
45
+ import { Practitioner, PRACTITIONERS_COLLECTION } from '../../types/practitioner';
64
46
  import {
65
47
  CertificationLevel,
66
48
  CertificationSpecialty,
67
49
  ProcedureFamily,
68
- } from "../../backoffice/types";
69
- import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
70
- import { ProcedureReviewInfo } from "../../types/reviews";
71
- import { distanceBetween, geohashQueryBounds } from "geofire-common";
72
- import { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
73
- import { MediaService, MediaAccessLevel } from "../media/media.service";
50
+ } from '../../backoffice/types';
51
+ import { Clinic, CLINICS_COLLECTION } from '../../types/clinic';
52
+ import { ProcedureReviewInfo } from '../../types/reviews';
53
+ import { distanceBetween, geohashQueryBounds } from 'geofire-common';
54
+ import { TreatmentBenefit } from '../../backoffice/types/static/treatment-benefit.types';
55
+ import { MediaService, MediaAccessLevel } from '../media/media.service';
74
56
 
75
57
  export class ProcedureService extends BaseService {
76
58
  private categoryService: CategoryService;
@@ -87,7 +69,7 @@ export class ProcedureService extends BaseService {
87
69
  subcategoryService: SubcategoryService,
88
70
  technologyService: TechnologyService,
89
71
  productService: ProductService,
90
- mediaService: MediaService
72
+ mediaService: MediaService,
91
73
  ) {
92
74
  super(db, auth, app);
93
75
  this.categoryService = categoryService;
@@ -107,25 +89,23 @@ export class ProcedureService extends BaseService {
107
89
  private async processMedia(
108
90
  media: string | File | Blob | null | undefined,
109
91
  ownerId: string,
110
- collectionName: string
92
+ collectionName: string,
111
93
  ): Promise<string | null> {
112
94
  if (!media) return null;
113
95
 
114
96
  // If already a string URL, return it directly
115
- if (typeof media === "string") {
97
+ if (typeof media === 'string') {
116
98
  return media;
117
99
  }
118
100
 
119
101
  // If it's a File, upload it using MediaService
120
102
  if (media instanceof File || media instanceof Blob) {
121
- console.log(
122
- `[ProcedureService] Uploading ${collectionName} media for ${ownerId}`
123
- );
103
+ console.log(`[ProcedureService] Uploading ${collectionName} media for ${ownerId}`);
124
104
  const metadata = await this.mediaService.uploadMedia(
125
105
  media,
126
106
  ownerId,
127
107
  MediaAccessLevel.PUBLIC,
128
- collectionName
108
+ collectionName,
129
109
  );
130
110
  return metadata.url;
131
111
  }
@@ -143,18 +123,14 @@ export class ProcedureService extends BaseService {
143
123
  private async processMediaArray(
144
124
  mediaArray: (string | File | Blob)[] | undefined,
145
125
  ownerId: string,
146
- collectionName: string
126
+ collectionName: string,
147
127
  ): Promise<string[]> {
148
128
  if (!mediaArray || mediaArray.length === 0) return [];
149
129
 
150
130
  const result: string[] = [];
151
131
 
152
132
  for (const media of mediaArray) {
153
- const processedUrl = await this.processMedia(
154
- media,
155
- ownerId,
156
- collectionName
157
- );
133
+ const processedUrl = await this.processMedia(media, ownerId, collectionName);
158
134
  if (processedUrl) {
159
135
  result.push(processedUrl);
160
136
  }
@@ -177,45 +153,27 @@ export class ProcedureService extends BaseService {
177
153
  // Get references to related entities (Category, Subcategory, Technology, Product)
178
154
  const [category, subcategory, technology, product] = await Promise.all([
179
155
  this.categoryService.getById(validatedData.categoryId),
180
- this.subcategoryService.getById(
181
- validatedData.categoryId,
182
- validatedData.subcategoryId
183
- ),
156
+ this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
184
157
  this.technologyService.getById(validatedData.technologyId),
185
- this.productService.getById(
186
- validatedData.technologyId,
187
- validatedData.productId
188
- ),
158
+ this.productService.getById(validatedData.technologyId, validatedData.productId),
189
159
  ]);
190
160
 
191
161
  if (!category || !subcategory || !technology || !product) {
192
- throw new Error("One or more required base entities not found");
162
+ throw new Error('One or more required base entities not found');
193
163
  }
194
164
 
195
165
  // Get clinic and practitioner information for aggregation
196
- const clinicRef = doc(
197
- this.db,
198
- CLINICS_COLLECTION,
199
- validatedData.clinicBranchId
200
- );
166
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
201
167
  const clinicSnapshot = await getDoc(clinicRef);
202
168
  if (!clinicSnapshot.exists()) {
203
- throw new Error(
204
- `Clinic with ID ${validatedData.clinicBranchId} not found`
205
- );
169
+ throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
206
170
  }
207
171
  const clinic = clinicSnapshot.data() as Clinic; // Assert type
208
172
 
209
- const practitionerRef = doc(
210
- this.db,
211
- PRACTITIONERS_COLLECTION,
212
- validatedData.practitionerId
213
- );
173
+ const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, validatedData.practitionerId);
214
174
  const practitionerSnapshot = await getDoc(practitionerRef);
215
175
  if (!practitionerSnapshot.exists()) {
216
- throw new Error(
217
- `Practitioner with ID ${validatedData.practitionerId} not found`
218
- );
176
+ throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
219
177
  }
220
178
  const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
221
179
 
@@ -225,7 +183,7 @@ export class ProcedureService extends BaseService {
225
183
  processedPhotos = await this.processMediaArray(
226
184
  validatedData.photos,
227
185
  procedureId,
228
- "procedure-photos"
186
+ 'procedure-photos',
229
187
  );
230
188
  }
231
189
 
@@ -233,15 +191,15 @@ export class ProcedureService extends BaseService {
233
191
  const clinicInfo = {
234
192
  id: clinicSnapshot.id,
235
193
  name: clinic.name,
236
- description: clinic.description || "",
194
+ description: clinic.description || '',
237
195
  featuredPhoto:
238
196
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
239
- ? typeof clinic.featuredPhotos[0] === "string"
197
+ ? typeof clinic.featuredPhotos[0] === 'string'
240
198
  ? clinic.featuredPhotos[0]
241
- : ""
242
- : typeof clinic.coverPhoto === "string"
199
+ : ''
200
+ : typeof clinic.coverPhoto === 'string'
243
201
  ? clinic.coverPhoto
244
- : "",
202
+ : '',
245
203
  location: clinic.location,
246
204
  contactInfo: clinic.contactInfo,
247
205
  };
@@ -250,17 +208,17 @@ export class ProcedureService extends BaseService {
250
208
  const doctorInfo = {
251
209
  id: practitionerSnapshot.id,
252
210
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
253
- description: practitioner.basicInfo.bio || "",
211
+ description: practitioner.basicInfo.bio || '',
254
212
  photo:
255
- typeof practitioner.basicInfo.profileImageUrl === "string"
213
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
256
214
  ? practitioner.basicInfo.profileImageUrl
257
- : "", // Default to empty string if not a processed URL
215
+ : '', // Default to empty string if not a processed URL
258
216
  rating: practitioner.reviewInfo?.averageRating || 0,
259
217
  services: practitioner.procedures || [],
260
218
  };
261
219
 
262
220
  // Create the procedure object
263
- const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
221
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
264
222
  id: procedureId,
265
223
  ...validatedData,
266
224
  nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
@@ -314,12 +272,12 @@ export class ProcedureService extends BaseService {
314
272
  * @returns A promise that resolves to an array of the newly created procedures.
315
273
  */
316
274
  async bulkCreateProcedures(
317
- baseData: Omit<CreateProcedureData, "practitionerId">,
318
- practitionerIds: string[]
275
+ baseData: Omit<CreateProcedureData, 'practitionerId'>,
276
+ practitionerIds: string[],
319
277
  ): Promise<Procedure[]> {
320
278
  // 1. Validation
321
279
  if (!practitionerIds || practitionerIds.length === 0) {
322
- throw new Error("Practitioner IDs array cannot be empty.");
280
+ throw new Error('Practitioner IDs array cannot be empty.');
323
281
  }
324
282
 
325
283
  // Add a dummy practitionerId for the validation schema to pass
@@ -327,28 +285,19 @@ export class ProcedureService extends BaseService {
327
285
  const validatedData = createProcedureSchema.parse(validationData);
328
286
 
329
287
  // 2. Fetch common data once to avoid redundant reads
330
- const [category, subcategory, technology, product, clinicSnapshot] =
331
- await Promise.all([
332
- this.categoryService.getById(validatedData.categoryId),
333
- this.subcategoryService.getById(
334
- validatedData.categoryId,
335
- validatedData.subcategoryId
336
- ),
337
- this.technologyService.getById(validatedData.technologyId),
338
- this.productService.getById(
339
- validatedData.technologyId,
340
- validatedData.productId
341
- ),
342
- getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
343
- ]);
288
+ const [category, subcategory, technology, product, clinicSnapshot] = await Promise.all([
289
+ this.categoryService.getById(validatedData.categoryId),
290
+ this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
291
+ this.technologyService.getById(validatedData.technologyId),
292
+ this.productService.getById(validatedData.technologyId, validatedData.productId),
293
+ getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
294
+ ]);
344
295
 
345
296
  if (!category || !subcategory || !technology || !product) {
346
- throw new Error("One or more required base entities not found");
297
+ throw new Error('One or more required base entities not found');
347
298
  }
348
299
  if (!clinicSnapshot.exists()) {
349
- throw new Error(
350
- `Clinic with ID ${validatedData.clinicBranchId} not found`
351
- );
300
+ throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
352
301
  }
353
302
  const clinic = clinicSnapshot.data() as Clinic;
354
303
 
@@ -359,7 +308,7 @@ export class ProcedureService extends BaseService {
359
308
  processedPhotos = await this.processMediaArray(
360
309
  validatedData.photos,
361
310
  batchId,
362
- "procedure-photos-batch"
311
+ 'procedure-photos-batch',
363
312
  );
364
313
  }
365
314
 
@@ -370,7 +319,7 @@ export class ProcedureService extends BaseService {
370
319
  const chunk = practitionerIds.slice(i, i + 30);
371
320
  const practitionersQuery = query(
372
321
  collection(this.db, PRACTITIONERS_COLLECTION),
373
- where(documentId(), "in", chunk)
322
+ where(documentId(), 'in', chunk),
374
323
  );
375
324
  const practitionersSnapshot = await getDocs(practitionersQuery);
376
325
  practitionersSnapshot.docs.forEach((doc) => {
@@ -381,12 +330,8 @@ export class ProcedureService extends BaseService {
381
330
  // Verify all practitioners were found
382
331
  if (practitionersMap.size !== practitionerIds.length) {
383
332
  const foundIds = Array.from(practitionersMap.keys());
384
- const notFoundIds = practitionerIds.filter(
385
- (id) => !foundIds.includes(id)
386
- );
387
- throw new Error(
388
- `The following practitioners were not found: ${notFoundIds.join(", ")}`
389
- );
333
+ const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
334
+ throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
390
335
  }
391
336
 
392
337
  // 5. Use a Firestore batch for atomic creation
@@ -395,15 +340,15 @@ export class ProcedureService extends BaseService {
395
340
  const clinicInfo = {
396
341
  id: clinicSnapshot.id,
397
342
  name: clinic.name,
398
- description: clinic.description || "",
343
+ description: clinic.description || '',
399
344
  featuredPhoto:
400
345
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
401
- ? typeof clinic.featuredPhotos[0] === "string"
346
+ ? typeof clinic.featuredPhotos[0] === 'string'
402
347
  ? clinic.featuredPhotos[0]
403
- : ""
404
- : typeof clinic.coverPhoto === "string"
348
+ : ''
349
+ : typeof clinic.coverPhoto === 'string'
405
350
  ? clinic.coverPhoto
406
- : "",
351
+ : '',
407
352
  location: clinic.location,
408
353
  contactInfo: clinic.contactInfo,
409
354
  };
@@ -414,11 +359,11 @@ export class ProcedureService extends BaseService {
414
359
  const doctorInfo = {
415
360
  id: practitioner.id,
416
361
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
417
- description: practitioner.basicInfo.bio || "",
362
+ description: practitioner.basicInfo.bio || '',
418
363
  photo:
419
- typeof practitioner.basicInfo.profileImageUrl === "string"
364
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
420
365
  ? practitioner.basicInfo.profileImageUrl
421
- : "",
366
+ : '',
422
367
  rating: practitioner.reviewInfo?.averageRating || 0,
423
368
  services: practitioner.procedures || [],
424
369
  };
@@ -428,7 +373,7 @@ export class ProcedureService extends BaseService {
428
373
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
429
374
 
430
375
  // Construct the new procedure, reusing common data
431
- const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
376
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
432
377
  id: procedureId,
433
378
  ...validatedData,
434
379
  nameLower: validatedData.nameLower || validatedData.name.toLowerCase(),
@@ -474,10 +419,7 @@ export class ProcedureService extends BaseService {
474
419
  const fetchedProcedures: Procedure[] = [];
475
420
  for (let i = 0; i < createdProcedureIds.length; i += 30) {
476
421
  const chunk = createdProcedureIds.slice(i, i + 30);
477
- const q = query(
478
- collection(this.db, PROCEDURES_COLLECTION),
479
- where(documentId(), "in", chunk)
480
- );
422
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), where(documentId(), 'in', chunk));
481
423
  const snapshot = await getDocs(q);
482
424
  snapshot.forEach((doc) => {
483
425
  fetchedProcedures.push(doc.data() as Procedure);
@@ -508,13 +450,11 @@ export class ProcedureService extends BaseService {
508
450
  * @param clinicBranchId - The ID of the clinic branch
509
451
  * @returns List of procedures
510
452
  */
511
- async getProceduresByClinicBranch(
512
- clinicBranchId: string
513
- ): Promise<Procedure[]> {
453
+ async getProceduresByClinicBranch(clinicBranchId: string): Promise<Procedure[]> {
514
454
  const q = query(
515
455
  collection(this.db, PROCEDURES_COLLECTION),
516
- where("clinicBranchId", "==", clinicBranchId),
517
- where("isActive", "==", true)
456
+ where('clinicBranchId', '==', clinicBranchId),
457
+ where('isActive', '==', true),
518
458
  );
519
459
  const snapshot = await getDocs(q);
520
460
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -525,13 +465,11 @@ export class ProcedureService extends BaseService {
525
465
  * @param practitionerId - The ID of the practitioner
526
466
  * @returns List of procedures
527
467
  */
528
- async getProceduresByPractitioner(
529
- practitionerId: string
530
- ): Promise<Procedure[]> {
468
+ async getProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
531
469
  const q = query(
532
470
  collection(this.db, PROCEDURES_COLLECTION),
533
- where("practitionerId", "==", practitionerId),
534
- where("isActive", "==", true)
471
+ where('practitionerId', '==', practitionerId),
472
+ where('isActive', '==', true),
535
473
  );
536
474
  const snapshot = await getDocs(q);
537
475
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -542,13 +480,11 @@ export class ProcedureService extends BaseService {
542
480
  * @param practitionerId - The ID of the practitioner
543
481
  * @returns List of inactive procedures
544
482
  */
545
- async getInactiveProceduresByPractitioner(
546
- practitionerId: string
547
- ): Promise<Procedure[]> {
483
+ async getInactiveProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
548
484
  const q = query(
549
485
  collection(this.db, PROCEDURES_COLLECTION),
550
- where("practitionerId", "==", practitionerId),
551
- where("isActive", "==", false)
486
+ where('practitionerId', '==', practitionerId),
487
+ where('isActive', '==', false),
552
488
  );
553
489
  const snapshot = await getDocs(q);
554
490
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -560,10 +496,7 @@ export class ProcedureService extends BaseService {
560
496
  * @param data - The data to update the procedure with
561
497
  * @returns The updated procedure
562
498
  */
563
- async updateProcedure(
564
- id: string,
565
- data: UpdateProcedureData
566
- ): Promise<Procedure> {
499
+ async updateProcedure(id: string, data: UpdateProcedureData): Promise<Procedure> {
567
500
  const validatedData = updateProcedureSchema.parse(data);
568
501
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
569
502
  const procedureSnapshot = await getDoc(procedureRef);
@@ -587,54 +520,42 @@ export class ProcedureService extends BaseService {
587
520
  updatedProcedureData.photos = await this.processMediaArray(
588
521
  validatedData.photos,
589
522
  id,
590
- "procedure-photos"
523
+ 'procedure-photos',
591
524
  );
592
525
  }
593
526
 
594
527
  // --- Prepare updates and fetch new related data if IDs change ---
595
528
 
596
529
  // Handle Practitioner Change
597
- if (
598
- validatedData.practitionerId &&
599
- validatedData.practitionerId !== oldPractitionerId
600
- ) {
530
+ if (validatedData.practitionerId && validatedData.practitionerId !== oldPractitionerId) {
601
531
  practitionerChanged = true;
602
532
  const newPractitionerRef = doc(
603
533
  this.db,
604
534
  PRACTITIONERS_COLLECTION,
605
- validatedData.practitionerId
535
+ validatedData.practitionerId,
606
536
  );
607
537
  const newPractitionerSnap = await getDoc(newPractitionerRef);
608
538
  if (!newPractitionerSnap.exists())
609
- throw new Error(
610
- `New Practitioner ${validatedData.practitionerId} not found`
611
- );
539
+ throw new Error(`New Practitioner ${validatedData.practitionerId} not found`);
612
540
  newPractitioner = newPractitionerSnap.data() as Practitioner;
613
541
  // Update doctorInfo within the procedure document
614
542
  updatedProcedureData.doctorInfo = {
615
543
  id: newPractitioner.id,
616
544
  name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
617
- description: newPractitioner.basicInfo.bio || "",
545
+ description: newPractitioner.basicInfo.bio || '',
618
546
  photo:
619
- typeof newPractitioner.basicInfo.profileImageUrl === "string"
547
+ typeof newPractitioner.basicInfo.profileImageUrl === 'string'
620
548
  ? newPractitioner.basicInfo.profileImageUrl
621
- : "", // Default to empty string if not a processed URL
549
+ : '', // Default to empty string if not a processed URL
622
550
  rating: newPractitioner.reviewInfo?.averageRating || 0,
623
551
  services: newPractitioner.procedures || [],
624
552
  };
625
553
  }
626
554
 
627
555
  // Handle Clinic Change
628
- if (
629
- validatedData.clinicBranchId &&
630
- validatedData.clinicBranchId !== oldClinicId
631
- ) {
556
+ if (validatedData.clinicBranchId && validatedData.clinicBranchId !== oldClinicId) {
632
557
  clinicChanged = true;
633
- const newClinicRef = doc(
634
- this.db,
635
- CLINICS_COLLECTION,
636
- validatedData.clinicBranchId
637
- );
558
+ const newClinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
638
559
  const newClinicSnap = await getDoc(newClinicRef);
639
560
  if (!newClinicSnap.exists())
640
561
  throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
@@ -643,15 +564,15 @@ export class ProcedureService extends BaseService {
643
564
  updatedProcedureData.clinicInfo = {
644
565
  id: newClinic.id,
645
566
  name: newClinic.name,
646
- description: newClinic.description || "",
567
+ description: newClinic.description || '',
647
568
  featuredPhoto:
648
569
  newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
649
- ? typeof newClinic.featuredPhotos[0] === "string"
570
+ ? typeof newClinic.featuredPhotos[0] === 'string'
650
571
  ? newClinic.featuredPhotos[0]
651
- : ""
652
- : typeof newClinic.coverPhoto === "string"
572
+ : ''
573
+ : typeof newClinic.coverPhoto === 'string'
653
574
  ? newClinic.coverPhoto
654
- : "",
575
+ : '',
655
576
  location: newClinic.location,
656
577
  contactInfo: newClinic.contactInfo,
657
578
  };
@@ -663,11 +584,8 @@ export class ProcedureService extends BaseService {
663
584
  updatedProcedureData.nameLower = validatedData.name.toLowerCase();
664
585
  }
665
586
  if (validatedData.categoryId) {
666
- const category = await this.categoryService.getById(
667
- validatedData.categoryId
668
- );
669
- if (!category)
670
- throw new Error(`Category ${validatedData.categoryId} not found`);
587
+ const category = await this.categoryService.getById(validatedData.categoryId);
588
+ if (!category) throw new Error(`Category ${validatedData.categoryId} not found`);
671
589
  updatedProcedureData.category = category;
672
590
  finalCategoryId = category.id; // Update finalCategoryId if category changed
673
591
  }
@@ -676,26 +594,21 @@ export class ProcedureService extends BaseService {
676
594
  if (validatedData.subcategoryId && finalCategoryId) {
677
595
  const subcategory = await this.subcategoryService.getById(
678
596
  finalCategoryId,
679
- validatedData.subcategoryId
597
+ validatedData.subcategoryId,
680
598
  );
681
599
  if (!subcategory)
682
600
  throw new Error(
683
- `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
601
+ `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`,
684
602
  );
685
603
  updatedProcedureData.subcategory = subcategory;
686
604
  } else if (validatedData.subcategoryId) {
687
- console.warn(
688
- "Attempted to update subcategory without a valid categoryId"
689
- );
605
+ console.warn('Attempted to update subcategory without a valid categoryId');
690
606
  }
691
607
 
692
608
  let finalTechnologyId = existingProcedure.technology.id;
693
609
  if (validatedData.technologyId) {
694
- const technology = await this.technologyService.getById(
695
- validatedData.technologyId
696
- );
697
- if (!technology)
698
- throw new Error(`Technology ${validatedData.technologyId} not found`);
610
+ const technology = await this.technologyService.getById(validatedData.technologyId);
611
+ if (!technology) throw new Error(`Technology ${validatedData.technologyId} not found`);
699
612
  updatedProcedureData.technology = technology;
700
613
  finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
701
614
  // Update related fields derived from technology
@@ -703,25 +616,20 @@ export class ProcedureService extends BaseService {
703
616
  updatedProcedureData.treatmentBenefits = technology.benefits;
704
617
  updatedProcedureData.preRequirements = technology.requirements.pre;
705
618
  updatedProcedureData.postRequirements = technology.requirements.post;
706
- updatedProcedureData.certificationRequirement =
707
- technology.certificationRequirement;
708
- updatedProcedureData.documentationTemplates =
709
- technology.documentationTemplates || [];
619
+ updatedProcedureData.certificationRequirement = technology.certificationRequirement;
620
+ updatedProcedureData.documentationTemplates = technology.documentationTemplates || [];
710
621
  }
711
622
 
712
623
  // Only fetch product if its ID is provided AND we have a valid finalTechnologyId
713
624
  if (validatedData.productId && finalTechnologyId) {
714
- const product = await this.productService.getById(
715
- finalTechnologyId,
716
- validatedData.productId
717
- );
625
+ const product = await this.productService.getById(finalTechnologyId, validatedData.productId);
718
626
  if (!product)
719
627
  throw new Error(
720
- `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
628
+ `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`,
721
629
  );
722
630
  updatedProcedureData.product = product;
723
631
  } else if (validatedData.productId) {
724
- console.warn("Attempted to update product without a valid technologyId");
632
+ console.warn('Attempted to update product without a valid technologyId');
725
633
  }
726
634
 
727
635
  // Update the procedure document
@@ -805,7 +713,7 @@ export class ProcedureService extends BaseService {
805
713
  */
806
714
  async getAllProcedures(
807
715
  pagination?: number,
808
- lastDoc?: any
716
+ lastDoc?: any,
809
717
  ): Promise<{ procedures: Procedure[]; lastDoc: any }> {
810
718
  try {
811
719
  const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
@@ -813,29 +721,24 @@ export class ProcedureService extends BaseService {
813
721
 
814
722
  // Apply pagination if specified
815
723
  if (pagination && pagination > 0) {
816
- const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
724
+ const { limit, startAfter } = await import('firebase/firestore'); // Use dynamic import if needed top-level
817
725
 
818
726
  if (lastDoc) {
819
727
  proceduresQuery = query(
820
728
  proceduresCollection,
821
- orderBy("name"), // Use imported orderBy
729
+ orderBy('name'), // Use imported orderBy
822
730
  startAfter(lastDoc),
823
- limit(pagination)
731
+ limit(pagination),
824
732
  );
825
733
  } else {
826
- proceduresQuery = query(
827
- proceduresCollection,
828
- orderBy("name"),
829
- limit(pagination)
830
- ); // Use imported orderBy
734
+ proceduresQuery = query(proceduresCollection, orderBy('name'), limit(pagination)); // Use imported orderBy
831
735
  }
832
736
  } else {
833
- proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
737
+ proceduresQuery = query(proceduresCollection, orderBy('name')); // Use imported orderBy
834
738
  }
835
739
 
836
740
  const proceduresSnapshot = await getDocs(proceduresQuery);
837
- const lastVisible =
838
- proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
741
+ const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
839
742
 
840
743
  const procedures = proceduresSnapshot.docs.map((doc) => {
841
744
  const data = doc.data() as Procedure;
@@ -850,7 +753,7 @@ export class ProcedureService extends BaseService {
850
753
  lastDoc: lastVisible,
851
754
  };
852
755
  } catch (error) {
853
- console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
756
+ console.error('[PROCEDURE_SERVICE] Error getting all procedures:', error);
854
757
  throw error;
855
758
  }
856
759
  }
@@ -899,16 +802,16 @@ export class ProcedureService extends BaseService {
899
802
  lastDoc: any;
900
803
  }> {
901
804
  try {
902
- console.log("[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies");
805
+ console.log('[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies');
903
806
 
904
807
  // Geo query debug i validacija
905
808
  if (filters.location && filters.radiusInKm) {
906
809
  console.log('[PROCEDURE_SERVICE] Executing geo query:', {
907
810
  location: filters.location,
908
811
  radius: filters.radiusInKm,
909
- serviceName: 'ProcedureService'
812
+ serviceName: 'ProcedureService',
910
813
  });
911
-
814
+
912
815
  // Validacija location podataka
913
816
  if (!filters.location.latitude || !filters.location.longitude) {
914
817
  console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
@@ -926,42 +829,42 @@ export class ProcedureService extends BaseService {
926
829
  // Base constraints (used in all strategies)
927
830
  const getBaseConstraints = () => {
928
831
  const constraints: QueryConstraint[] = [];
929
-
832
+
930
833
  // Active status filter
931
834
  if (filters.isActive !== undefined) {
932
- constraints.push(where("isActive", "==", filters.isActive));
835
+ constraints.push(where('isActive', '==', filters.isActive));
933
836
  } else {
934
- constraints.push(where("isActive", "==", true));
837
+ constraints.push(where('isActive', '==', true));
935
838
  }
936
839
 
937
840
  // Filter constraints
938
841
  if (filters.procedureFamily) {
939
- constraints.push(where("family", "==", filters.procedureFamily));
842
+ constraints.push(where('family', '==', filters.procedureFamily));
940
843
  }
941
844
  if (filters.procedureCategory) {
942
- constraints.push(where("category.id", "==", filters.procedureCategory));
845
+ constraints.push(where('category.id', '==', filters.procedureCategory));
943
846
  }
944
847
  if (filters.procedureSubcategory) {
945
- constraints.push(where("subcategory.id", "==", filters.procedureSubcategory));
848
+ constraints.push(where('subcategory.id', '==', filters.procedureSubcategory));
946
849
  }
947
850
  if (filters.procedureTechnology) {
948
- constraints.push(where("technology.id", "==", filters.procedureTechnology));
851
+ constraints.push(where('technology.id', '==', filters.procedureTechnology));
949
852
  }
950
853
  if (filters.minPrice !== undefined) {
951
- constraints.push(where("price", ">=", filters.minPrice));
854
+ constraints.push(where('price', '>=', filters.minPrice));
952
855
  }
953
856
  if (filters.maxPrice !== undefined) {
954
- constraints.push(where("price", "<=", filters.maxPrice));
857
+ constraints.push(where('price', '<=', filters.maxPrice));
955
858
  }
956
859
  if (filters.minRating !== undefined) {
957
- constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
860
+ constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
958
861
  }
959
862
  if (filters.maxRating !== undefined) {
960
- constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
863
+ constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
961
864
  }
962
865
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
963
866
  const benefitsToMatch = filters.treatmentBenefits;
964
- constraints.push(where("treatmentBenefits", "array-contains-any", benefitsToMatch));
867
+ constraints.push(where('treatmentBenefits', 'array-contains-any', benefitsToMatch));
965
868
  }
966
869
 
967
870
  return constraints;
@@ -970,15 +873,15 @@ export class ProcedureService extends BaseService {
970
873
  // Strategy 1: Try nameLower search if nameSearch exists
971
874
  if (filters.nameSearch && filters.nameSearch.trim()) {
972
875
  try {
973
- console.log("[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search");
876
+ console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
974
877
  const searchTerm = filters.nameSearch.trim().toLowerCase();
975
878
  const constraints = getBaseConstraints();
976
- constraints.push(where("nameLower", ">=", searchTerm));
977
- constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
978
- constraints.push(orderBy("nameLower"));
879
+ constraints.push(where('nameLower', '>=', searchTerm));
880
+ constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
881
+ constraints.push(orderBy('nameLower'));
979
882
 
980
883
  if (filters.lastDoc) {
981
- if (typeof filters.lastDoc.data === "function") {
884
+ if (typeof filters.lastDoc.data === 'function') {
982
885
  constraints.push(startAfter(filters.lastDoc));
983
886
  } else if (Array.isArray(filters.lastDoc)) {
984
887
  constraints.push(startAfter(...filters.lastDoc));
@@ -990,33 +893,38 @@ export class ProcedureService extends BaseService {
990
893
 
991
894
  const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
992
895
  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
-
896
+ const procedures = querySnapshot.docs.map(
897
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
898
+ );
899
+ const lastDoc =
900
+ querySnapshot.docs.length > 0
901
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
902
+ : null;
903
+
996
904
  console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
997
-
905
+
998
906
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
999
907
  if (procedures.length < (filters.pagination || 10)) {
1000
908
  return { procedures, lastDoc: null };
1001
909
  }
1002
910
  return { procedures, lastDoc };
1003
911
  } catch (error) {
1004
- console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
912
+ console.log('[PROCEDURE_SERVICE] Strategy 1 failed:', error);
1005
913
  }
1006
914
  }
1007
915
 
1008
916
  // Strategy 2: Try name field search as fallback
1009
917
  if (filters.nameSearch && filters.nameSearch.trim()) {
1010
918
  try {
1011
- console.log("[PROCEDURE_SERVICE] Strategy 2: Trying name field search");
919
+ console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
1012
920
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1013
921
  const constraints = getBaseConstraints();
1014
- constraints.push(where("name", ">=", searchTerm));
1015
- constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
1016
- constraints.push(orderBy("name"));
922
+ constraints.push(where('name', '>=', searchTerm));
923
+ constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
924
+ constraints.push(orderBy('name'));
1017
925
 
1018
926
  if (filters.lastDoc) {
1019
- if (typeof filters.lastDoc.data === "function") {
927
+ if (typeof filters.lastDoc.data === 'function') {
1020
928
  constraints.push(startAfter(filters.lastDoc));
1021
929
  } else if (Array.isArray(filters.lastDoc)) {
1022
930
  constraints.push(startAfter(...filters.lastDoc));
@@ -1028,29 +936,36 @@ export class ProcedureService extends BaseService {
1028
936
 
1029
937
  const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1030
938
  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
-
939
+ const procedures = querySnapshot.docs.map(
940
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
941
+ );
942
+ const lastDoc =
943
+ querySnapshot.docs.length > 0
944
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
945
+ : null;
946
+
1034
947
  console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
1035
-
948
+
1036
949
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1037
950
  if (procedures.length < (filters.pagination || 10)) {
1038
951
  return { procedures, lastDoc: null };
1039
952
  }
1040
953
  return { procedures, lastDoc };
1041
954
  } catch (error) {
1042
- console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
955
+ console.log('[PROCEDURE_SERVICE] Strategy 2 failed:', error);
1043
956
  }
1044
957
  }
1045
958
 
1046
959
  // Strategy 3: orderBy createdAt with client-side filtering
1047
960
  try {
1048
- console.log("[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering");
961
+ console.log(
962
+ '[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
963
+ );
1049
964
  const constraints = getBaseConstraints();
1050
- constraints.push(orderBy("createdAt", "desc"));
965
+ constraints.push(orderBy('createdAt', 'desc'));
1051
966
 
1052
967
  if (filters.lastDoc) {
1053
- if (typeof filters.lastDoc.data === "function") {
968
+ if (typeof filters.lastDoc.data === 'function') {
1054
969
  constraints.push(startAfter(filters.lastDoc));
1055
970
  } else if (Array.isArray(filters.lastDoc)) {
1056
971
  constraints.push(startAfter(...filters.lastDoc));
@@ -1062,57 +977,62 @@ export class ProcedureService extends BaseService {
1062
977
 
1063
978
  const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1064
979
  const querySnapshot = await getDocs(q);
1065
- let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
980
+ let procedures = querySnapshot.docs.map(
981
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
982
+ );
1066
983
 
1067
984
  // Apply all client-side filters using centralized function
1068
985
  procedures = this.applyInMemoryFilters(procedures, filters);
1069
986
 
1070
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
987
+ const lastDoc =
988
+ querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1071
989
  console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
1072
-
990
+
1073
991
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1074
992
  if (procedures.length < (filters.pagination || 10)) {
1075
993
  return { procedures, lastDoc: null };
1076
994
  }
1077
995
  return { procedures, lastDoc };
1078
996
  } catch (error) {
1079
- console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
997
+ console.log('[PROCEDURE_SERVICE] Strategy 3 failed:', error);
1080
998
  }
1081
999
 
1082
1000
  // Strategy 4: Minimal query fallback
1083
1001
  try {
1084
- console.log("[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback");
1002
+ console.log('[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback');
1085
1003
  const constraints: QueryConstraint[] = [
1086
- where("isActive", "==", true),
1087
- orderBy("createdAt", "desc"),
1088
- limit(filters.pagination || 10)
1004
+ where('isActive', '==', true),
1005
+ orderBy('createdAt', 'desc'),
1006
+ limit(filters.pagination || 10),
1089
1007
  ];
1090
1008
 
1091
1009
  const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1092
1010
  const querySnapshot = await getDocs(q);
1093
- let procedures = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Procedure));
1011
+ let procedures = querySnapshot.docs.map(
1012
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1013
+ );
1094
1014
 
1095
1015
  // Apply all client-side filters using centralized function
1096
1016
  procedures = this.applyInMemoryFilters(procedures, filters);
1097
1017
 
1098
- const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1018
+ const lastDoc =
1019
+ querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1099
1020
  console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
1100
-
1021
+
1101
1022
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1102
1023
  if (procedures.length < (filters.pagination || 10)) {
1103
1024
  return { procedures, lastDoc: null };
1104
1025
  }
1105
1026
  return { procedures, lastDoc };
1106
1027
  } catch (error) {
1107
- console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
1028
+ console.log('[PROCEDURE_SERVICE] Strategy 4 failed:', error);
1108
1029
  }
1109
1030
 
1110
1031
  // All strategies failed
1111
- console.log("[PROCEDURE_SERVICE] All strategies failed, returning empty result");
1032
+ console.log('[PROCEDURE_SERVICE] All strategies failed, returning empty result');
1112
1033
  return { procedures: [], lastDoc: null };
1113
-
1114
1034
  } catch (error) {
1115
- console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
1035
+ console.error('[PROCEDURE_SERVICE] Error filtering procedures:', error);
1116
1036
  return { procedures: [], lastDoc: null };
1117
1037
  }
1118
1038
  }
@@ -1121,13 +1041,16 @@ export class ProcedureService extends BaseService {
1121
1041
  * Applies in-memory filters to procedures array
1122
1042
  * Used when Firestore queries fail or for complex filtering
1123
1043
  */
1124
- private applyInMemoryFilters(procedures: Procedure[], filters: any): (Procedure & { distance?: number })[] {
1044
+ private applyInMemoryFilters(
1045
+ procedures: Procedure[],
1046
+ filters: any,
1047
+ ): (Procedure & { distance?: number })[] {
1125
1048
  let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
1126
1049
 
1127
1050
  // Name search filter
1128
1051
  if (filters.nameSearch && filters.nameSearch.trim()) {
1129
1052
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1130
- filteredProcedures = filteredProcedures.filter(procedure => {
1053
+ filteredProcedures = filteredProcedures.filter((procedure) => {
1131
1054
  const name = (procedure.name || '').toLowerCase();
1132
1055
  const nameLower = procedure.nameLower || '';
1133
1056
  return name.includes(searchTerm) || nameLower.includes(searchTerm);
@@ -1137,82 +1060,105 @@ export class ProcedureService extends BaseService {
1137
1060
 
1138
1061
  // Price filtering
1139
1062
  if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
1140
- filteredProcedures = filteredProcedures.filter(procedure => {
1063
+ filteredProcedures = filteredProcedures.filter((procedure) => {
1141
1064
  const price = procedure.price || 0;
1142
1065
  if (filters.minPrice !== undefined && price < filters.minPrice) return false;
1143
1066
  if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
1144
1067
  return true;
1145
1068
  });
1146
- console.log(`[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`);
1069
+ console.log(
1070
+ `[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`,
1071
+ );
1147
1072
  }
1148
1073
 
1149
1074
  // Rating filtering
1150
1075
  if (filters.minRating !== undefined || filters.maxRating !== undefined) {
1151
- filteredProcedures = filteredProcedures.filter(procedure => {
1076
+ filteredProcedures = filteredProcedures.filter((procedure) => {
1152
1077
  const rating = procedure.reviewInfo?.averageRating || 0;
1153
1078
  if (filters.minRating !== undefined && rating < filters.minRating) return false;
1154
1079
  if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
1155
1080
  return true;
1156
1081
  });
1157
- console.log(`[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`);
1082
+ console.log(
1083
+ `[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`,
1084
+ );
1158
1085
  }
1159
1086
 
1160
1087
  // Treatment benefits filtering
1161
1088
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
1162
1089
  const benefitsToMatch = filters.treatmentBenefits;
1163
- filteredProcedures = filteredProcedures.filter(procedure => {
1090
+ filteredProcedures = filteredProcedures.filter((procedure) => {
1164
1091
  const procedureBenefits = procedure.treatmentBenefits || [];
1165
1092
  return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
1166
1093
  });
1167
- console.log(`[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`);
1094
+ console.log(
1095
+ `[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`,
1096
+ );
1168
1097
  }
1169
1098
 
1170
1099
  // Procedure family filtering
1171
1100
  if (filters.procedureFamily) {
1172
- filteredProcedures = filteredProcedures.filter(procedure => procedure.family === filters.procedureFamily);
1173
- console.log(`[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`);
1101
+ filteredProcedures = filteredProcedures.filter(
1102
+ (procedure) => procedure.family === filters.procedureFamily,
1103
+ );
1104
+ console.log(
1105
+ `[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`,
1106
+ );
1174
1107
  }
1175
1108
 
1176
1109
  // Category filtering
1177
1110
  if (filters.procedureCategory) {
1178
- filteredProcedures = filteredProcedures.filter(procedure => procedure.category?.id === filters.procedureCategory);
1179
- console.log(`[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`);
1111
+ filteredProcedures = filteredProcedures.filter(
1112
+ (procedure) => procedure.category?.id === filters.procedureCategory,
1113
+ );
1114
+ console.log(
1115
+ `[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`,
1116
+ );
1180
1117
  }
1181
1118
 
1182
1119
  // Subcategory filtering
1183
1120
  if (filters.procedureSubcategory) {
1184
- filteredProcedures = filteredProcedures.filter(procedure => procedure.subcategory?.id === filters.procedureSubcategory);
1185
- console.log(`[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`);
1121
+ filteredProcedures = filteredProcedures.filter(
1122
+ (procedure) => procedure.subcategory?.id === filters.procedureSubcategory,
1123
+ );
1124
+ console.log(
1125
+ `[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`,
1126
+ );
1186
1127
  }
1187
1128
 
1188
1129
  // Technology filtering
1189
1130
  if (filters.procedureTechnology) {
1190
- filteredProcedures = filteredProcedures.filter(procedure => procedure.technology?.id === filters.procedureTechnology);
1191
- console.log(`[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`);
1131
+ filteredProcedures = filteredProcedures.filter(
1132
+ (procedure) => procedure.technology?.id === filters.procedureTechnology,
1133
+ );
1134
+ console.log(
1135
+ `[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`,
1136
+ );
1192
1137
  }
1193
1138
 
1194
1139
  // Geo-radius filter
1195
1140
  if (filters.location && filters.radiusInKm && filters.radiusInKm > 0) {
1196
1141
  const location = filters.location;
1197
1142
  const radiusInKm = filters.radiusInKm;
1198
- filteredProcedures = filteredProcedures.filter(procedure => {
1143
+ filteredProcedures = filteredProcedures.filter((procedure) => {
1199
1144
  const clinicLocation = procedure.clinicInfo?.location;
1200
1145
  if (!clinicLocation?.latitude || !clinicLocation?.longitude) {
1201
1146
  return false;
1202
1147
  }
1203
-
1204
- const distance = distanceBetween(
1205
- [location.latitude, location.longitude],
1206
- [clinicLocation.latitude, clinicLocation.longitude]
1207
- ) / 1000; // Convert to km
1208
-
1148
+
1149
+ const distance =
1150
+ distanceBetween(
1151
+ [location.latitude, location.longitude],
1152
+ [clinicLocation.latitude, clinicLocation.longitude],
1153
+ ) / 1000; // Convert to km
1154
+
1209
1155
  // Attach distance for frontend sorting/display
1210
1156
  (procedure as any).distance = distance;
1211
-
1157
+
1212
1158
  return distance <= radiusInKm;
1213
1159
  });
1214
1160
  console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
1215
-
1161
+
1216
1162
  // Sort by distance when geo filtering is applied
1217
1163
  filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
1218
1164
  }
@@ -1220,45 +1166,69 @@ export class ProcedureService extends BaseService {
1220
1166
  return filteredProcedures as (Procedure & { distance?: number })[];
1221
1167
  }
1222
1168
 
1223
- private handleGeoQuery(filters: any): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
1224
- console.log('[PROCEDURE_SERVICE] Executing geo query with enhanced debugging');
1225
-
1169
+ private handleGeoQuery(
1170
+ filters: any,
1171
+ ): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
1172
+ console.log('[PROCEDURE_SERVICE] Executing geo query with geohash bounds');
1226
1173
  try {
1227
- // Enhanced geo query implementation with proper debugging
1228
1174
  const location = filters.location;
1229
1175
  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
1176
 
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 };
1177
+ if (!location || !radiusInKm) {
1178
+ return Promise.resolve({ procedures: [], lastDoc: null });
1179
+ }
1180
+
1181
+ const bounds = geohashQueryBounds([location.latitude, location.longitude], radiusInKm * 1000);
1182
+
1183
+ const fetches = bounds.map((b) => {
1184
+ const constraints: QueryConstraint[] = [
1185
+ where('clinicInfo.location.geohash', '>=', b[0]),
1186
+ where('clinicInfo.location.geohash', '<=', b[1]),
1187
+ where('isActive', '==', filters.isActive !== undefined ? filters.isActive : true),
1188
+ ];
1189
+ return getDocs(query(collection(this.db, PROCEDURES_COLLECTION), ...constraints));
1260
1190
  });
1261
-
1191
+
1192
+ return Promise.all(fetches)
1193
+ .then((snaps) => {
1194
+ const collected: Procedure[] = [];
1195
+ snaps.forEach((snap) => {
1196
+ snap.docs.forEach((d) => collected.push({ ...(d.data() as Procedure), id: d.id }));
1197
+ });
1198
+
1199
+ // Deduplicate by id
1200
+ const uniqueMap = new Map<string, Procedure>();
1201
+ for (const p of collected) {
1202
+ uniqueMap.set(p.id, p);
1203
+ }
1204
+ let procedures = Array.from(uniqueMap.values());
1205
+
1206
+ // Apply remaining filters including precise distance and sorting
1207
+ procedures = this.applyInMemoryFilters(procedures, filters);
1208
+
1209
+ // Manual pagination
1210
+ const pageSize = filters.pagination || 10;
1211
+ let startIndex = 0;
1212
+ if (
1213
+ filters.lastDoc &&
1214
+ typeof filters.lastDoc === 'object' &&
1215
+ (filters.lastDoc as any).id
1216
+ ) {
1217
+ const idx = procedures.findIndex((p) => p.id === (filters.lastDoc as any).id);
1218
+ if (idx >= 0) startIndex = idx + 1;
1219
+ }
1220
+ const page = procedures.slice(startIndex, startIndex + pageSize);
1221
+ const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
1222
+
1223
+ console.log(
1224
+ `[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`,
1225
+ );
1226
+ return { procedures: page, lastDoc: newLastDoc };
1227
+ })
1228
+ .catch((err) => {
1229
+ console.error('[PROCEDURE_SERVICE] Geo bounds fetch failed:', err);
1230
+ return { procedures: [], lastDoc: null };
1231
+ });
1262
1232
  } catch (error) {
1263
1233
  console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
1264
1234
  return Promise.resolve({ procedures: [], lastDoc: null });
@@ -1272,7 +1242,7 @@ export class ProcedureService extends BaseService {
1272
1242
  * @returns The created procedure
1273
1243
  */
1274
1244
  async createConsultationProcedure(
1275
- data: Omit<CreateProcedureData, "productId">
1245
+ data: Omit<CreateProcedureData, 'productId'>,
1276
1246
  ): Promise<Procedure> {
1277
1247
  // Generate procedure ID first so we can use it for media uploads
1278
1248
  const procedureId = this.generateId();
@@ -1286,7 +1256,7 @@ export class ProcedureService extends BaseService {
1286
1256
  ]);
1287
1257
 
1288
1258
  if (!category || !subcategory || !technology) {
1289
- throw new Error("One or more required base entities not found");
1259
+ throw new Error('One or more required base entities not found');
1290
1260
  }
1291
1261
 
1292
1262
  // Get clinic and practitioner information for aggregation
@@ -1297,11 +1267,7 @@ export class ProcedureService extends BaseService {
1297
1267
  }
1298
1268
  const clinic = clinicSnapshot.data() as Clinic;
1299
1269
 
1300
- const practitionerRef = doc(
1301
- this.db,
1302
- PRACTITIONERS_COLLECTION,
1303
- data.practitionerId
1304
- );
1270
+ const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, data.practitionerId);
1305
1271
  const practitionerSnapshot = await getDoc(practitionerRef);
1306
1272
  if (!practitionerSnapshot.exists()) {
1307
1273
  throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
@@ -1311,26 +1277,22 @@ export class ProcedureService extends BaseService {
1311
1277
  // Process photos if provided
1312
1278
  let processedPhotos: string[] = [];
1313
1279
  if (data.photos && data.photos.length > 0) {
1314
- processedPhotos = await this.processMediaArray(
1315
- data.photos,
1316
- procedureId,
1317
- "procedure-photos"
1318
- );
1280
+ processedPhotos = await this.processMediaArray(data.photos, procedureId, 'procedure-photos');
1319
1281
  }
1320
1282
 
1321
1283
  // Create aggregated clinic info for the procedure document
1322
1284
  const clinicInfo = {
1323
1285
  id: clinicSnapshot.id,
1324
1286
  name: clinic.name,
1325
- description: clinic.description || "",
1287
+ description: clinic.description || '',
1326
1288
  featuredPhoto:
1327
1289
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
1328
- ? typeof clinic.featuredPhotos[0] === "string"
1290
+ ? typeof clinic.featuredPhotos[0] === 'string'
1329
1291
  ? clinic.featuredPhotos[0]
1330
- : ""
1331
- : typeof clinic.coverPhoto === "string"
1292
+ : ''
1293
+ : typeof clinic.coverPhoto === 'string'
1332
1294
  ? clinic.coverPhoto
1333
- : "",
1295
+ : '',
1334
1296
  location: clinic.location,
1335
1297
  contactInfo: clinic.contactInfo,
1336
1298
  };
@@ -1339,22 +1301,22 @@ export class ProcedureService extends BaseService {
1339
1301
  const doctorInfo = {
1340
1302
  id: practitionerSnapshot.id,
1341
1303
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
1342
- description: practitioner.basicInfo.bio || "",
1304
+ description: practitioner.basicInfo.bio || '',
1343
1305
  photo:
1344
- typeof practitioner.basicInfo.profileImageUrl === "string"
1306
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
1345
1307
  ? practitioner.basicInfo.profileImageUrl
1346
- : "",
1308
+ : '',
1347
1309
  rating: practitioner.reviewInfo?.averageRating || 0,
1348
1310
  services: practitioner.procedures || [],
1349
1311
  };
1350
1312
 
1351
1313
  // Create a placeholder product for consultation procedures
1352
1314
  const consultationProduct: Product = {
1353
- id: "consultation-no-product",
1354
- name: "No Product Required",
1355
- description: "Consultation procedures do not require specific products",
1356
- brandId: "consultation-brand",
1357
- brandName: "Consultation",
1315
+ id: 'consultation-no-product',
1316
+ name: 'No Product Required',
1317
+ description: 'Consultation procedures do not require specific products',
1318
+ brandId: 'consultation-brand',
1319
+ brandName: 'Consultation',
1358
1320
  technologyId: data.technologyId,
1359
1321
  technologyName: technology.name,
1360
1322
  isActive: true,
@@ -1363,7 +1325,7 @@ export class ProcedureService extends BaseService {
1363
1325
  };
1364
1326
 
1365
1327
  // Create the procedure object
1366
- const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
1328
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
1367
1329
  id: procedureId,
1368
1330
  ...data,
1369
1331
  nameLower: (data as any).nameLower || data.name.toLowerCase(),
@@ -1412,18 +1374,20 @@ export class ProcedureService extends BaseService {
1412
1374
  * This is optimized for mobile map usage to reduce payload size.
1413
1375
  * @returns Array of minimal procedure info for map
1414
1376
  */
1415
- async getProceduresForMap(): Promise<{
1416
- id: string;
1417
- name: string;
1418
- clinicId: string | undefined;
1419
- clinicName: string | undefined;
1420
- address: string;
1421
- latitude: number | undefined;
1422
- longitude: number | undefined;
1423
- }[]> {
1377
+ async getProceduresForMap(): Promise<
1378
+ {
1379
+ id: string;
1380
+ name: string;
1381
+ clinicId: string | undefined;
1382
+ clinicName: string | undefined;
1383
+ address: string;
1384
+ latitude: number | undefined;
1385
+ longitude: number | undefined;
1386
+ }[]
1387
+ > {
1424
1388
  const proceduresRef = collection(this.db, PROCEDURES_COLLECTION);
1425
1389
  const snapshot = await getDocs(proceduresRef);
1426
- const proceduresForMap = snapshot.docs.map(doc => {
1390
+ const proceduresForMap = snapshot.docs.map((doc) => {
1427
1391
  const data = doc.data();
1428
1392
  return {
1429
1393
  id: doc.id,