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