@blackcode_sa/metaestetics-api 1.11.1 → 1.11.2

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.
Files changed (36) hide show
  1. package/dist/admin/index.d.mts +324 -330
  2. package/dist/admin/index.d.ts +324 -330
  3. package/dist/backoffice/index.d.mts +67 -283
  4. package/dist/backoffice/index.d.ts +67 -283
  5. package/dist/backoffice/index.js +6 -114
  6. package/dist/backoffice/index.mjs +6 -112
  7. package/dist/index.d.mts +3037 -3100
  8. package/dist/index.d.ts +3037 -3100
  9. package/dist/index.js +129 -379
  10. package/dist/index.mjs +130 -379
  11. package/package.json +1 -1
  12. package/src/backoffice/expo-safe/index.ts +0 -2
  13. package/src/backoffice/services/__tests__/brand.service.test.ts +196 -0
  14. package/src/backoffice/services/__tests__/category.service.test.ts +201 -0
  15. package/src/backoffice/services/__tests__/product.service.test.ts +358 -0
  16. package/src/backoffice/services/__tests__/requirement.service.test.ts +226 -0
  17. package/src/backoffice/services/__tests__/subcategory.service.test.ts +181 -0
  18. package/src/backoffice/services/__tests__/technology.service.test.ts +1097 -0
  19. package/src/backoffice/services/technology.service.ts +10 -122
  20. package/src/backoffice/types/index.ts +0 -1
  21. package/src/backoffice/types/product.types.ts +1 -3
  22. package/src/backoffice/types/technology.types.ts +4 -4
  23. package/src/backoffice/validations/schemas.ts +9 -35
  24. package/src/services/appointment/appointment.service.ts +5 -0
  25. package/src/services/appointment/utils/appointment.utils.ts +113 -124
  26. package/src/services/procedure/procedure.service.ts +234 -434
  27. package/src/types/appointment/index.ts +37 -43
  28. package/src/types/clinic/index.ts +6 -1
  29. package/src/types/patient/medical-info.types.ts +3 -3
  30. package/src/types/procedure/index.ts +17 -20
  31. package/src/validations/appointment.schema.ts +118 -170
  32. package/src/validations/clinic.schema.ts +6 -1
  33. package/src/validations/patient/medical-info.schema.ts +2 -7
  34. package/src/backoffice/services/README.md +0 -40
  35. package/src/backoffice/services/constants.service.ts +0 -268
  36. package/src/backoffice/types/admin-constants.types.ts +0 -69
@@ -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
- type TreatmentBenefitDynamic,
69
- } from "../../backoffice/types";
70
- import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
71
- import { ProcedureReviewInfo } from "../../types/reviews";
72
- import { distanceBetween, geohashQueryBounds } from "geofire-common";
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,22 +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
224
  // Ensure nameLower is always set even if omitted by client
267
- nameLower:
268
- (validatedData as any).nameLower || validatedData.name.toLowerCase(),
225
+ nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
269
226
  photos: processedPhotos,
270
227
  category, // Embed full objects
271
228
  subcategory,
@@ -273,9 +230,7 @@ export class ProcedureService extends BaseService {
273
230
  product,
274
231
  blockingConditions: technology.blockingConditions,
275
232
  contraindications: technology.contraindications || [],
276
- contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
277
233
  treatmentBenefits: technology.benefits,
278
- treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
279
234
  preRequirements: technology.requirements.pre,
280
235
  postRequirements: technology.requirements.post,
281
236
  certificationRequirement: technology.certificationRequirement,
@@ -318,12 +273,12 @@ export class ProcedureService extends BaseService {
318
273
  * @returns A promise that resolves to an array of the newly created procedures.
319
274
  */
320
275
  async bulkCreateProcedures(
321
- baseData: Omit<CreateProcedureData, "practitionerId">,
322
- practitionerIds: string[]
276
+ baseData: Omit<CreateProcedureData, 'practitionerId'>,
277
+ practitionerIds: string[],
323
278
  ): Promise<Procedure[]> {
324
279
  // 1. Validation
325
280
  if (!practitionerIds || practitionerIds.length === 0) {
326
- throw new Error("Practitioner IDs array cannot be empty.");
281
+ throw new Error('Practitioner IDs array cannot be empty.');
327
282
  }
328
283
 
329
284
  // Add a dummy practitionerId for the validation schema to pass
@@ -331,28 +286,19 @@ export class ProcedureService extends BaseService {
331
286
  const validatedData = createProcedureSchema.parse(validationData);
332
287
 
333
288
  // 2. Fetch common data once to avoid redundant reads
334
- const [category, subcategory, technology, product, clinicSnapshot] =
335
- await Promise.all([
336
- this.categoryService.getById(validatedData.categoryId),
337
- this.subcategoryService.getById(
338
- validatedData.categoryId,
339
- validatedData.subcategoryId
340
- ),
341
- this.technologyService.getById(validatedData.technologyId),
342
- this.productService.getById(
343
- validatedData.technologyId,
344
- validatedData.productId
345
- ),
346
- getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
347
- ]);
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
+ ]);
348
296
 
349
297
  if (!category || !subcategory || !technology || !product) {
350
- throw new Error("One or more required base entities not found");
298
+ throw new Error('One or more required base entities not found');
351
299
  }
352
300
  if (!clinicSnapshot.exists()) {
353
- throw new Error(
354
- `Clinic with ID ${validatedData.clinicBranchId} not found`
355
- );
301
+ throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
356
302
  }
357
303
  const clinic = clinicSnapshot.data() as Clinic;
358
304
 
@@ -363,7 +309,7 @@ export class ProcedureService extends BaseService {
363
309
  processedPhotos = await this.processMediaArray(
364
310
  validatedData.photos,
365
311
  batchId,
366
- "procedure-photos-batch"
312
+ 'procedure-photos-batch',
367
313
  );
368
314
  }
369
315
 
@@ -374,7 +320,7 @@ export class ProcedureService extends BaseService {
374
320
  const chunk = practitionerIds.slice(i, i + 30);
375
321
  const practitionersQuery = query(
376
322
  collection(this.db, PRACTITIONERS_COLLECTION),
377
- where(documentId(), "in", chunk)
323
+ where(documentId(), 'in', chunk),
378
324
  );
379
325
  const practitionersSnapshot = await getDocs(practitionersQuery);
380
326
  practitionersSnapshot.docs.forEach((doc) => {
@@ -385,12 +331,8 @@ export class ProcedureService extends BaseService {
385
331
  // Verify all practitioners were found
386
332
  if (practitionersMap.size !== practitionerIds.length) {
387
333
  const foundIds = Array.from(practitionersMap.keys());
388
- const notFoundIds = practitionerIds.filter(
389
- (id) => !foundIds.includes(id)
390
- );
391
- throw new Error(
392
- `The following practitioners were not found: ${notFoundIds.join(", ")}`
393
- );
334
+ const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
335
+ throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
394
336
  }
395
337
 
396
338
  // 5. Use a Firestore batch for atomic creation
@@ -399,15 +341,15 @@ export class ProcedureService extends BaseService {
399
341
  const clinicInfo = {
400
342
  id: clinicSnapshot.id,
401
343
  name: clinic.name,
402
- description: clinic.description || "",
344
+ description: clinic.description || '',
403
345
  featuredPhoto:
404
346
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
405
- ? typeof clinic.featuredPhotos[0] === "string"
347
+ ? typeof clinic.featuredPhotos[0] === 'string'
406
348
  ? clinic.featuredPhotos[0]
407
- : ""
408
- : typeof clinic.coverPhoto === "string"
349
+ : ''
350
+ : typeof clinic.coverPhoto === 'string'
409
351
  ? clinic.coverPhoto
410
- : "",
352
+ : '',
411
353
  location: clinic.location,
412
354
  contactInfo: clinic.contactInfo,
413
355
  };
@@ -418,11 +360,11 @@ export class ProcedureService extends BaseService {
418
360
  const doctorInfo = {
419
361
  id: practitioner.id,
420
362
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
421
- description: practitioner.basicInfo.bio || "",
363
+ description: practitioner.basicInfo.bio || '',
422
364
  photo:
423
- typeof practitioner.basicInfo.profileImageUrl === "string"
365
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
424
366
  ? practitioner.basicInfo.profileImageUrl
425
- : "",
367
+ : '',
426
368
  rating: practitioner.reviewInfo?.averageRating || 0,
427
369
  services: practitioner.procedures || [],
428
370
  };
@@ -432,11 +374,10 @@ export class ProcedureService extends BaseService {
432
374
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
433
375
 
434
376
  // Construct the new procedure, reusing common data
435
- const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
377
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
436
378
  id: procedureId,
437
379
  ...validatedData,
438
- nameLower:
439
- (validatedData as any).nameLower || validatedData.name.toLowerCase(),
380
+ nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
440
381
  practitionerId: practitionerId, // Override practitionerId with the correct one
441
382
  photos: processedPhotos,
442
383
  category,
@@ -445,10 +386,7 @@ export class ProcedureService extends BaseService {
445
386
  product,
446
387
  blockingConditions: technology.blockingConditions,
447
388
  contraindications: technology.contraindications || [],
448
- contraindicationIds:
449
- technology.contraindications?.map((c) => c.id) || [],
450
389
  treatmentBenefits: technology.benefits,
451
- treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
452
390
  preRequirements: technology.requirements.pre,
453
391
  postRequirements: technology.requirements.post,
454
392
  certificationRequirement: technology.certificationRequirement,
@@ -482,10 +420,7 @@ export class ProcedureService extends BaseService {
482
420
  const fetchedProcedures: Procedure[] = [];
483
421
  for (let i = 0; i < createdProcedureIds.length; i += 30) {
484
422
  const chunk = createdProcedureIds.slice(i, i + 30);
485
- const q = query(
486
- collection(this.db, PROCEDURES_COLLECTION),
487
- where(documentId(), "in", chunk)
488
- );
423
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), where(documentId(), 'in', chunk));
489
424
  const snapshot = await getDocs(q);
490
425
  snapshot.forEach((doc) => {
491
426
  fetchedProcedures.push(doc.data() as Procedure);
@@ -516,13 +451,11 @@ export class ProcedureService extends BaseService {
516
451
  * @param clinicBranchId - The ID of the clinic branch
517
452
  * @returns List of procedures
518
453
  */
519
- async getProceduresByClinicBranch(
520
- clinicBranchId: string
521
- ): Promise<Procedure[]> {
454
+ async getProceduresByClinicBranch(clinicBranchId: string): Promise<Procedure[]> {
522
455
  const q = query(
523
456
  collection(this.db, PROCEDURES_COLLECTION),
524
- where("clinicBranchId", "==", clinicBranchId),
525
- where("isActive", "==", true)
457
+ where('clinicBranchId', '==', clinicBranchId),
458
+ where('isActive', '==', true),
526
459
  );
527
460
  const snapshot = await getDocs(q);
528
461
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -533,13 +466,11 @@ export class ProcedureService extends BaseService {
533
466
  * @param practitionerId - The ID of the practitioner
534
467
  * @returns List of procedures
535
468
  */
536
- async getProceduresByPractitioner(
537
- practitionerId: string
538
- ): Promise<Procedure[]> {
469
+ async getProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
539
470
  const q = query(
540
471
  collection(this.db, PROCEDURES_COLLECTION),
541
- where("practitionerId", "==", practitionerId),
542
- where("isActive", "==", true)
472
+ where('practitionerId', '==', practitionerId),
473
+ where('isActive', '==', true),
543
474
  );
544
475
  const snapshot = await getDocs(q);
545
476
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -550,13 +481,11 @@ export class ProcedureService extends BaseService {
550
481
  * @param practitionerId - The ID of the practitioner
551
482
  * @returns List of inactive procedures
552
483
  */
553
- async getInactiveProceduresByPractitioner(
554
- practitionerId: string
555
- ): Promise<Procedure[]> {
484
+ async getInactiveProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
556
485
  const q = query(
557
486
  collection(this.db, PROCEDURES_COLLECTION),
558
- where("practitionerId", "==", practitionerId),
559
- where("isActive", "==", false)
487
+ where('practitionerId', '==', practitionerId),
488
+ where('isActive', '==', false),
560
489
  );
561
490
  const snapshot = await getDocs(q);
562
491
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -568,10 +497,7 @@ export class ProcedureService extends BaseService {
568
497
  * @param data - The data to update the procedure with
569
498
  * @returns The updated procedure
570
499
  */
571
- async updateProcedure(
572
- id: string,
573
- data: UpdateProcedureData
574
- ): Promise<Procedure> {
500
+ async updateProcedure(id: string, data: UpdateProcedureData): Promise<Procedure> {
575
501
  const validatedData = updateProcedureSchema.parse(data);
576
502
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
577
503
  const procedureSnapshot = await getDoc(procedureRef);
@@ -595,54 +521,42 @@ export class ProcedureService extends BaseService {
595
521
  updatedProcedureData.photos = await this.processMediaArray(
596
522
  validatedData.photos,
597
523
  id,
598
- "procedure-photos"
524
+ 'procedure-photos',
599
525
  );
600
526
  }
601
527
 
602
528
  // --- Prepare updates and fetch new related data if IDs change ---
603
529
 
604
530
  // Handle Practitioner Change
605
- if (
606
- validatedData.practitionerId &&
607
- validatedData.practitionerId !== oldPractitionerId
608
- ) {
531
+ if (validatedData.practitionerId && validatedData.practitionerId !== oldPractitionerId) {
609
532
  practitionerChanged = true;
610
533
  const newPractitionerRef = doc(
611
534
  this.db,
612
535
  PRACTITIONERS_COLLECTION,
613
- validatedData.practitionerId
536
+ validatedData.practitionerId,
614
537
  );
615
538
  const newPractitionerSnap = await getDoc(newPractitionerRef);
616
539
  if (!newPractitionerSnap.exists())
617
- throw new Error(
618
- `New Practitioner ${validatedData.practitionerId} not found`
619
- );
540
+ throw new Error(`New Practitioner ${validatedData.practitionerId} not found`);
620
541
  newPractitioner = newPractitionerSnap.data() as Practitioner;
621
542
  // Update doctorInfo within the procedure document
622
543
  updatedProcedureData.doctorInfo = {
623
544
  id: newPractitioner.id,
624
545
  name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
625
- description: newPractitioner.basicInfo.bio || "",
546
+ description: newPractitioner.basicInfo.bio || '',
626
547
  photo:
627
- typeof newPractitioner.basicInfo.profileImageUrl === "string"
548
+ typeof newPractitioner.basicInfo.profileImageUrl === 'string'
628
549
  ? newPractitioner.basicInfo.profileImageUrl
629
- : "", // Default to empty string if not a processed URL
550
+ : '', // Default to empty string if not a processed URL
630
551
  rating: newPractitioner.reviewInfo?.averageRating || 0,
631
552
  services: newPractitioner.procedures || [],
632
553
  };
633
554
  }
634
555
 
635
556
  // Handle Clinic Change
636
- if (
637
- validatedData.clinicBranchId &&
638
- validatedData.clinicBranchId !== oldClinicId
639
- ) {
557
+ if (validatedData.clinicBranchId && validatedData.clinicBranchId !== oldClinicId) {
640
558
  clinicChanged = true;
641
- const newClinicRef = doc(
642
- this.db,
643
- CLINICS_COLLECTION,
644
- validatedData.clinicBranchId
645
- );
559
+ const newClinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
646
560
  const newClinicSnap = await getDoc(newClinicRef);
647
561
  if (!newClinicSnap.exists())
648
562
  throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
@@ -651,15 +565,15 @@ export class ProcedureService extends BaseService {
651
565
  updatedProcedureData.clinicInfo = {
652
566
  id: newClinic.id,
653
567
  name: newClinic.name,
654
- description: newClinic.description || "",
568
+ description: newClinic.description || '',
655
569
  featuredPhoto:
656
570
  newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
657
- ? typeof newClinic.featuredPhotos[0] === "string"
571
+ ? typeof newClinic.featuredPhotos[0] === 'string'
658
572
  ? newClinic.featuredPhotos[0]
659
- : ""
660
- : typeof newClinic.coverPhoto === "string"
573
+ : ''
574
+ : typeof newClinic.coverPhoto === 'string'
661
575
  ? newClinic.coverPhoto
662
- : "",
576
+ : '',
663
577
  location: newClinic.location,
664
578
  contactInfo: newClinic.contactInfo,
665
579
  };
@@ -671,11 +585,8 @@ export class ProcedureService extends BaseService {
671
585
  updatedProcedureData.nameLower = validatedData.name.toLowerCase();
672
586
  }
673
587
  if (validatedData.categoryId) {
674
- const category = await this.categoryService.getById(
675
- validatedData.categoryId
676
- );
677
- if (!category)
678
- 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`);
679
590
  updatedProcedureData.category = category;
680
591
  finalCategoryId = category.id; // Update finalCategoryId if category changed
681
592
  }
@@ -684,58 +595,42 @@ export class ProcedureService extends BaseService {
684
595
  if (validatedData.subcategoryId && finalCategoryId) {
685
596
  const subcategory = await this.subcategoryService.getById(
686
597
  finalCategoryId,
687
- validatedData.subcategoryId
598
+ validatedData.subcategoryId,
688
599
  );
689
600
  if (!subcategory)
690
601
  throw new Error(
691
- `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
602
+ `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`,
692
603
  );
693
604
  updatedProcedureData.subcategory = subcategory;
694
605
  } else if (validatedData.subcategoryId) {
695
- console.warn(
696
- "Attempted to update subcategory without a valid categoryId"
697
- );
606
+ console.warn('Attempted to update subcategory without a valid categoryId');
698
607
  }
699
608
 
700
609
  let finalTechnologyId = existingProcedure.technology.id;
701
610
  if (validatedData.technologyId) {
702
- const technology = await this.technologyService.getById(
703
- validatedData.technologyId
704
- );
705
- if (!technology)
706
- 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`);
707
613
  updatedProcedureData.technology = technology;
708
614
  finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
709
615
  // Update related fields derived from technology
710
616
  updatedProcedureData.blockingConditions = technology.blockingConditions;
711
- updatedProcedureData.contraindications =
712
- technology.contraindications || [];
713
- updatedProcedureData.contraindicationIds =
714
- technology.contraindications?.map((c) => c.id) || [];
715
617
  updatedProcedureData.treatmentBenefits = technology.benefits;
716
- updatedProcedureData.treatmentBenefitIds =
717
- technology.benefits?.map((b) => b.id) || [];
718
618
  updatedProcedureData.preRequirements = technology.requirements.pre;
719
619
  updatedProcedureData.postRequirements = technology.requirements.post;
720
- updatedProcedureData.certificationRequirement =
721
- technology.certificationRequirement;
722
- updatedProcedureData.documentationTemplates =
723
- technology.documentationTemplates || [];
620
+ updatedProcedureData.certificationRequirement = technology.certificationRequirement;
621
+ updatedProcedureData.documentationTemplates = technology.documentationTemplates || [];
724
622
  }
725
623
 
726
624
  // Only fetch product if its ID is provided AND we have a valid finalTechnologyId
727
625
  if (validatedData.productId && finalTechnologyId) {
728
- const product = await this.productService.getById(
729
- finalTechnologyId,
730
- validatedData.productId
731
- );
626
+ const product = await this.productService.getById(finalTechnologyId, validatedData.productId);
732
627
  if (!product)
733
628
  throw new Error(
734
- `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
629
+ `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`,
735
630
  );
736
631
  updatedProcedureData.product = product;
737
632
  } else if (validatedData.productId) {
738
- console.warn("Attempted to update product without a valid technologyId");
633
+ console.warn('Attempted to update product without a valid technologyId');
739
634
  }
740
635
 
741
636
  // Update the procedure document
@@ -819,7 +714,7 @@ export class ProcedureService extends BaseService {
819
714
  */
820
715
  async getAllProcedures(
821
716
  pagination?: number,
822
- lastDoc?: any
717
+ lastDoc?: any,
823
718
  ): Promise<{ procedures: Procedure[]; lastDoc: any }> {
824
719
  try {
825
720
  const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
@@ -827,29 +722,24 @@ export class ProcedureService extends BaseService {
827
722
 
828
723
  // Apply pagination if specified
829
724
  if (pagination && pagination > 0) {
830
- 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
831
726
 
832
727
  if (lastDoc) {
833
728
  proceduresQuery = query(
834
729
  proceduresCollection,
835
- orderBy("name"), // Use imported orderBy
730
+ orderBy('name'), // Use imported orderBy
836
731
  startAfter(lastDoc),
837
- limit(pagination)
732
+ limit(pagination),
838
733
  );
839
734
  } else {
840
- proceduresQuery = query(
841
- proceduresCollection,
842
- orderBy("name"),
843
- limit(pagination)
844
- ); // Use imported orderBy
735
+ proceduresQuery = query(proceduresCollection, orderBy('name'), limit(pagination)); // Use imported orderBy
845
736
  }
846
737
  } else {
847
- proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
738
+ proceduresQuery = query(proceduresCollection, orderBy('name')); // Use imported orderBy
848
739
  }
849
740
 
850
741
  const proceduresSnapshot = await getDocs(proceduresQuery);
851
- const lastVisible =
852
- proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
742
+ const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
853
743
 
854
744
  const procedures = proceduresSnapshot.docs.map((doc) => {
855
745
  const data = doc.data() as Procedure;
@@ -864,7 +754,7 @@ export class ProcedureService extends BaseService {
864
754
  lastDoc: lastVisible,
865
755
  };
866
756
  } catch (error) {
867
- console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
757
+ console.error('[PROCEDURE_SERVICE] Error getting all procedures:', error);
868
758
  throw error;
869
759
  }
870
760
  }
@@ -876,7 +766,7 @@ export class ProcedureService extends BaseService {
876
766
  *
877
767
  * @param filters - Various filters to apply
878
768
  * @param filters.nameSearch - Optional search text for procedure name
879
- * @param filters.treatmentBenefitIds - Optional array of treatment benefits to filter by
769
+ * @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
880
770
  * @param filters.procedureFamily - Optional procedure family to filter by
881
771
  * @param filters.procedureCategory - Optional procedure category to filter by
882
772
  * @param filters.procedureSubcategory - Optional procedure subcategory to filter by
@@ -894,7 +784,7 @@ export class ProcedureService extends BaseService {
894
784
  */
895
785
  async getProceduresByFilters(filters: {
896
786
  nameSearch?: string;
897
- treatmentBenefits?: string[];
787
+ treatmentBenefits?: TreatmentBenefit[];
898
788
  procedureFamily?: ProcedureFamily;
899
789
  procedureCategory?: string;
900
790
  procedureSubcategory?: string;
@@ -913,32 +803,26 @@ export class ProcedureService extends BaseService {
913
803
  lastDoc: any;
914
804
  }> {
915
805
  try {
916
- console.log(
917
- "[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies"
918
- );
806
+ console.log('[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies');
919
807
 
920
808
  // Geo query debug i validacija
921
809
  if (filters.location && filters.radiusInKm) {
922
- console.log("[PROCEDURE_SERVICE] Executing geo query:", {
810
+ console.log('[PROCEDURE_SERVICE] Executing geo query:', {
923
811
  location: filters.location,
924
812
  radius: filters.radiusInKm,
925
- serviceName: "ProcedureService",
813
+ serviceName: 'ProcedureService',
926
814
  });
927
815
 
928
816
  // Validacija location podataka
929
817
  if (!filters.location.latitude || !filters.location.longitude) {
930
- console.warn(
931
- "[PROCEDURE_SERVICE] Invalid location data:",
932
- filters.location
933
- );
818
+ console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
934
819
  filters.location = undefined;
935
820
  filters.radiusInKm = undefined;
936
821
  }
937
822
  }
938
823
 
939
824
  // Handle geo queries separately (they work differently)
940
- const isGeoQuery =
941
- filters.location && filters.radiusInKm && filters.radiusInKm > 0;
825
+ const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
942
826
  if (isGeoQuery) {
943
827
  return this.handleGeoQuery(filters);
944
828
  }
@@ -949,55 +833,39 @@ export class ProcedureService extends BaseService {
949
833
 
950
834
  // Active status filter
951
835
  if (filters.isActive !== undefined) {
952
- constraints.push(where("isActive", "==", filters.isActive));
836
+ constraints.push(where('isActive', '==', filters.isActive));
953
837
  } else {
954
- constraints.push(where("isActive", "==", true));
838
+ constraints.push(where('isActive', '==', true));
955
839
  }
956
840
 
957
841
  // Filter constraints
958
842
  if (filters.procedureFamily) {
959
- constraints.push(where("family", "==", filters.procedureFamily));
843
+ constraints.push(where('family', '==', filters.procedureFamily));
960
844
  }
961
845
  if (filters.procedureCategory) {
962
- constraints.push(
963
- where("category.id", "==", filters.procedureCategory)
964
- );
846
+ constraints.push(where('category.id', '==', filters.procedureCategory));
965
847
  }
966
848
  if (filters.procedureSubcategory) {
967
- constraints.push(
968
- where("subcategory.id", "==", filters.procedureSubcategory)
969
- );
849
+ constraints.push(where('subcategory.id', '==', filters.procedureSubcategory));
970
850
  }
971
851
  if (filters.procedureTechnology) {
972
- constraints.push(
973
- where("technology.id", "==", filters.procedureTechnology)
974
- );
852
+ constraints.push(where('technology.id', '==', filters.procedureTechnology));
975
853
  }
976
854
  if (filters.minPrice !== undefined) {
977
- constraints.push(where("price", ">=", filters.minPrice));
855
+ constraints.push(where('price', '>=', filters.minPrice));
978
856
  }
979
857
  if (filters.maxPrice !== undefined) {
980
- constraints.push(where("price", "<=", filters.maxPrice));
858
+ constraints.push(where('price', '<=', filters.maxPrice));
981
859
  }
982
860
  if (filters.minRating !== undefined) {
983
- constraints.push(
984
- where("reviewInfo.averageRating", ">=", filters.minRating)
985
- );
861
+ constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
986
862
  }
987
863
  if (filters.maxRating !== undefined) {
988
- constraints.push(
989
- where("reviewInfo.averageRating", "<=", filters.maxRating)
990
- );
864
+ constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
991
865
  }
992
866
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
993
- const benefitIdsToMatch = filters.treatmentBenefits;
994
- constraints.push(
995
- where(
996
- "treatmentBenefitIds",
997
- "array-contains-any",
998
- benefitIdsToMatch
999
- )
1000
- );
867
+ const benefitsToMatch = filters.treatmentBenefits;
868
+ constraints.push(where('treatmentBenefits', 'array-contains-any', benefitsToMatch));
1001
869
  }
1002
870
 
1003
871
  return constraints;
@@ -1006,17 +874,15 @@ export class ProcedureService extends BaseService {
1006
874
  // Strategy 1: Try nameLower search if nameSearch exists
1007
875
  if (filters.nameSearch && filters.nameSearch.trim()) {
1008
876
  try {
1009
- console.log(
1010
- "[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search"
1011
- );
877
+ console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
1012
878
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1013
879
  const constraints = getBaseConstraints();
1014
- constraints.push(where("nameLower", ">=", searchTerm));
1015
- constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
1016
- constraints.push(orderBy("nameLower"));
880
+ constraints.push(where('nameLower', '>=', searchTerm));
881
+ constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
882
+ constraints.push(orderBy('nameLower'));
1017
883
 
1018
884
  if (filters.lastDoc) {
1019
- if (typeof filters.lastDoc.data === "function") {
885
+ if (typeof filters.lastDoc.data === 'function') {
1020
886
  constraints.push(startAfter(filters.lastDoc));
1021
887
  } else if (Array.isArray(filters.lastDoc)) {
1022
888
  constraints.push(startAfter(...filters.lastDoc));
@@ -1026,22 +892,17 @@ export class ProcedureService extends BaseService {
1026
892
  }
1027
893
  constraints.push(limit(filters.pagination || 10));
1028
894
 
1029
- const q = query(
1030
- collection(this.db, PROCEDURES_COLLECTION),
1031
- ...constraints
1032
- );
895
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1033
896
  const querySnapshot = await getDocs(q);
1034
897
  const procedures = querySnapshot.docs.map(
1035
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
898
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1036
899
  );
1037
900
  const lastDoc =
1038
901
  querySnapshot.docs.length > 0
1039
902
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
1040
903
  : null;
1041
904
 
1042
- console.log(
1043
- `[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`
1044
- );
905
+ console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
1045
906
 
1046
907
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1047
908
  if (procedures.length < (filters.pagination || 10)) {
@@ -1049,24 +910,22 @@ export class ProcedureService extends BaseService {
1049
910
  }
1050
911
  return { procedures, lastDoc };
1051
912
  } catch (error) {
1052
- console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
913
+ console.log('[PROCEDURE_SERVICE] Strategy 1 failed:', error);
1053
914
  }
1054
915
  }
1055
916
 
1056
917
  // Strategy 2: Try name field search as fallback
1057
918
  if (filters.nameSearch && filters.nameSearch.trim()) {
1058
919
  try {
1059
- console.log(
1060
- "[PROCEDURE_SERVICE] Strategy 2: Trying name field search"
1061
- );
920
+ console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
1062
921
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1063
922
  const constraints = getBaseConstraints();
1064
- constraints.push(where("name", ">=", searchTerm));
1065
- constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
1066
- constraints.push(orderBy("name"));
923
+ constraints.push(where('name', '>=', searchTerm));
924
+ constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
925
+ constraints.push(orderBy('name'));
1067
926
 
1068
927
  if (filters.lastDoc) {
1069
- if (typeof filters.lastDoc.data === "function") {
928
+ if (typeof filters.lastDoc.data === 'function') {
1070
929
  constraints.push(startAfter(filters.lastDoc));
1071
930
  } else if (Array.isArray(filters.lastDoc)) {
1072
931
  constraints.push(startAfter(...filters.lastDoc));
@@ -1076,22 +935,17 @@ export class ProcedureService extends BaseService {
1076
935
  }
1077
936
  constraints.push(limit(filters.pagination || 10));
1078
937
 
1079
- const q = query(
1080
- collection(this.db, PROCEDURES_COLLECTION),
1081
- ...constraints
1082
- );
938
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1083
939
  const querySnapshot = await getDocs(q);
1084
940
  const procedures = querySnapshot.docs.map(
1085
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
941
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1086
942
  );
1087
943
  const lastDoc =
1088
944
  querySnapshot.docs.length > 0
1089
945
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
1090
946
  : null;
1091
947
 
1092
- console.log(
1093
- `[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`
1094
- );
948
+ console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
1095
949
 
1096
950
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1097
951
  if (procedures.length < (filters.pagination || 10)) {
@@ -1099,20 +953,20 @@ export class ProcedureService extends BaseService {
1099
953
  }
1100
954
  return { procedures, lastDoc };
1101
955
  } catch (error) {
1102
- console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
956
+ console.log('[PROCEDURE_SERVICE] Strategy 2 failed:', error);
1103
957
  }
1104
958
  }
1105
959
 
1106
960
  // Strategy 3: orderBy createdAt with client-side filtering
1107
961
  try {
1108
962
  console.log(
1109
- "[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering"
963
+ '[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
1110
964
  );
1111
965
  const constraints = getBaseConstraints();
1112
- constraints.push(orderBy("createdAt", "desc"));
966
+ constraints.push(orderBy('createdAt', 'desc'));
1113
967
 
1114
968
  if (filters.lastDoc) {
1115
- if (typeof filters.lastDoc.data === "function") {
969
+ if (typeof filters.lastDoc.data === 'function') {
1116
970
  constraints.push(startAfter(filters.lastDoc));
1117
971
  } else if (Array.isArray(filters.lastDoc)) {
1118
972
  constraints.push(startAfter(...filters.lastDoc));
@@ -1122,25 +976,18 @@ export class ProcedureService extends BaseService {
1122
976
  }
1123
977
  constraints.push(limit(filters.pagination || 10));
1124
978
 
1125
- const q = query(
1126
- collection(this.db, PROCEDURES_COLLECTION),
1127
- ...constraints
1128
- );
979
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1129
980
  const querySnapshot = await getDocs(q);
1130
981
  let procedures = querySnapshot.docs.map(
1131
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
982
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1132
983
  );
1133
984
 
1134
985
  // Apply all client-side filters using centralized function
1135
986
  procedures = this.applyInMemoryFilters(procedures, filters);
1136
987
 
1137
988
  const lastDoc =
1138
- querySnapshot.docs.length > 0
1139
- ? querySnapshot.docs[querySnapshot.docs.length - 1]
1140
- : null;
1141
- console.log(
1142
- `[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`
1143
- );
989
+ querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
990
+ console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
1144
991
 
1145
992
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1146
993
  if (procedures.length < (filters.pagination || 10)) {
@@ -1148,37 +995,30 @@ export class ProcedureService extends BaseService {
1148
995
  }
1149
996
  return { procedures, lastDoc };
1150
997
  } catch (error) {
1151
- console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
998
+ console.log('[PROCEDURE_SERVICE] Strategy 3 failed:', error);
1152
999
  }
1153
1000
 
1154
1001
  // Strategy 4: Minimal query fallback
1155
1002
  try {
1156
- console.log("[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback");
1003
+ console.log('[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback');
1157
1004
  const constraints: QueryConstraint[] = [
1158
- where("isActive", "==", true),
1159
- orderBy("createdAt", "desc"),
1005
+ where('isActive', '==', true),
1006
+ orderBy('createdAt', 'desc'),
1160
1007
  limit(filters.pagination || 10),
1161
1008
  ];
1162
1009
 
1163
- const q = query(
1164
- collection(this.db, PROCEDURES_COLLECTION),
1165
- ...constraints
1166
- );
1010
+ const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1167
1011
  const querySnapshot = await getDocs(q);
1168
1012
  let procedures = querySnapshot.docs.map(
1169
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
1013
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1170
1014
  );
1171
1015
 
1172
1016
  // Apply all client-side filters using centralized function
1173
1017
  procedures = this.applyInMemoryFilters(procedures, filters);
1174
1018
 
1175
1019
  const lastDoc =
1176
- querySnapshot.docs.length > 0
1177
- ? querySnapshot.docs[querySnapshot.docs.length - 1]
1178
- : null;
1179
- console.log(
1180
- `[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`
1181
- );
1020
+ querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1021
+ console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
1182
1022
 
1183
1023
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1184
1024
  if (procedures.length < (filters.pagination || 10)) {
@@ -1186,16 +1026,14 @@ export class ProcedureService extends BaseService {
1186
1026
  }
1187
1027
  return { procedures, lastDoc };
1188
1028
  } catch (error) {
1189
- console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
1029
+ console.log('[PROCEDURE_SERVICE] Strategy 4 failed:', error);
1190
1030
  }
1191
1031
 
1192
1032
  // All strategies failed
1193
- console.log(
1194
- "[PROCEDURE_SERVICE] All strategies failed, returning empty result"
1195
- );
1033
+ console.log('[PROCEDURE_SERVICE] All strategies failed, returning empty result');
1196
1034
  return { procedures: [], lastDoc: null };
1197
1035
  } catch (error) {
1198
- console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
1036
+ console.error('[PROCEDURE_SERVICE] Error filtering procedures:', error);
1199
1037
  return { procedures: [], lastDoc: null };
1200
1038
  }
1201
1039
  }
@@ -1206,7 +1044,7 @@ export class ProcedureService extends BaseService {
1206
1044
  */
1207
1045
  private applyInMemoryFilters(
1208
1046
  procedures: Procedure[],
1209
- filters: any
1047
+ filters: any,
1210
1048
  ): (Procedure & { distance?: number })[] {
1211
1049
  let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
1212
1050
 
@@ -1214,27 +1052,23 @@ export class ProcedureService extends BaseService {
1214
1052
  if (filters.nameSearch && filters.nameSearch.trim()) {
1215
1053
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1216
1054
  filteredProcedures = filteredProcedures.filter((procedure) => {
1217
- const name = (procedure.name || "").toLowerCase();
1218
- const nameLower = procedure.nameLower || "";
1055
+ const name = (procedure.name || '').toLowerCase();
1056
+ const nameLower = procedure.nameLower || '';
1219
1057
  return name.includes(searchTerm) || nameLower.includes(searchTerm);
1220
1058
  });
1221
- console.log(
1222
- `[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`
1223
- );
1059
+ console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`);
1224
1060
  }
1225
1061
 
1226
1062
  // Price filtering
1227
1063
  if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
1228
1064
  filteredProcedures = filteredProcedures.filter((procedure) => {
1229
1065
  const price = procedure.price || 0;
1230
- if (filters.minPrice !== undefined && price < filters.minPrice)
1231
- return false;
1232
- if (filters.maxPrice !== undefined && price > filters.maxPrice)
1233
- return false;
1066
+ if (filters.minPrice !== undefined && price < filters.minPrice) return false;
1067
+ if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
1234
1068
  return true;
1235
1069
  });
1236
1070
  console.log(
1237
- `[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`
1071
+ `[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`,
1238
1072
  );
1239
1073
  }
1240
1074
 
@@ -1242,69 +1076,64 @@ export class ProcedureService extends BaseService {
1242
1076
  if (filters.minRating !== undefined || filters.maxRating !== undefined) {
1243
1077
  filteredProcedures = filteredProcedures.filter((procedure) => {
1244
1078
  const rating = procedure.reviewInfo?.averageRating || 0;
1245
- if (filters.minRating !== undefined && rating < filters.minRating)
1246
- return false;
1247
- if (filters.maxRating !== undefined && rating > filters.maxRating)
1248
- return false;
1079
+ if (filters.minRating !== undefined && rating < filters.minRating) return false;
1080
+ if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
1249
1081
  return true;
1250
1082
  });
1251
1083
  console.log(
1252
- `[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`
1084
+ `[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`,
1253
1085
  );
1254
1086
  }
1255
1087
 
1256
1088
  // Treatment benefits filtering
1257
1089
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
1258
- const benefitIdsToMatch = filters.treatmentBenefits;
1090
+ const benefitsToMatch = filters.treatmentBenefits;
1259
1091
  filteredProcedures = filteredProcedures.filter((procedure) => {
1260
- const procedureBenefitIds = procedure.treatmentBenefitIds || [];
1261
- return benefitIdsToMatch.some((benefitId: string) =>
1262
- procedureBenefitIds.includes(benefitId)
1263
- );
1092
+ const procedureBenefits = procedure.treatmentBenefits || [];
1093
+ return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
1264
1094
  });
1265
1095
  console.log(
1266
- `[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`
1096
+ `[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`,
1267
1097
  );
1268
1098
  }
1269
1099
 
1270
1100
  // Procedure family filtering
1271
1101
  if (filters.procedureFamily) {
1272
1102
  filteredProcedures = filteredProcedures.filter(
1273
- (procedure) => procedure.family === filters.procedureFamily
1103
+ (procedure) => procedure.family === filters.procedureFamily,
1274
1104
  );
1275
1105
  console.log(
1276
- `[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`
1106
+ `[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`,
1277
1107
  );
1278
1108
  }
1279
1109
 
1280
1110
  // Category filtering
1281
1111
  if (filters.procedureCategory) {
1282
1112
  filteredProcedures = filteredProcedures.filter(
1283
- (procedure) => procedure.category?.id === filters.procedureCategory
1113
+ (procedure) => procedure.category?.id === filters.procedureCategory,
1284
1114
  );
1285
1115
  console.log(
1286
- `[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`
1116
+ `[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`,
1287
1117
  );
1288
1118
  }
1289
1119
 
1290
1120
  // Subcategory filtering
1291
1121
  if (filters.procedureSubcategory) {
1292
1122
  filteredProcedures = filteredProcedures.filter(
1293
- (procedure) =>
1294
- procedure.subcategory?.id === filters.procedureSubcategory
1123
+ (procedure) => procedure.subcategory?.id === filters.procedureSubcategory,
1295
1124
  );
1296
1125
  console.log(
1297
- `[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`
1126
+ `[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`,
1298
1127
  );
1299
1128
  }
1300
1129
 
1301
1130
  // Technology filtering
1302
1131
  if (filters.procedureTechnology) {
1303
1132
  filteredProcedures = filteredProcedures.filter(
1304
- (procedure) => procedure.technology?.id === filters.procedureTechnology
1133
+ (procedure) => procedure.technology?.id === filters.procedureTechnology,
1305
1134
  );
1306
1135
  console.log(
1307
- `[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`
1136
+ `[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`,
1308
1137
  );
1309
1138
  }
1310
1139
 
@@ -1321,7 +1150,7 @@ export class ProcedureService extends BaseService {
1321
1150
  const distance =
1322
1151
  distanceBetween(
1323
1152
  [location.latitude, location.longitude],
1324
- [clinicLocation.latitude, clinicLocation.longitude]
1153
+ [clinicLocation.latitude, clinicLocation.longitude],
1325
1154
  ) / 1000; // Convert to km
1326
1155
 
1327
1156
  // Attach distance for frontend sorting/display
@@ -1329,24 +1158,19 @@ export class ProcedureService extends BaseService {
1329
1158
 
1330
1159
  return distance <= radiusInKm;
1331
1160
  });
1332
- console.log(
1333
- `[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`
1334
- );
1161
+ console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
1335
1162
 
1336
1163
  // Sort by distance when geo filtering is applied
1337
- filteredProcedures.sort(
1338
- (a, b) => ((a as any).distance || 0) - ((b as any).distance || 0)
1339
- );
1164
+ filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
1340
1165
  }
1341
1166
 
1342
1167
  return filteredProcedures as (Procedure & { distance?: number })[];
1343
1168
  }
1344
1169
 
1345
- private handleGeoQuery(filters: any): Promise<{
1346
- procedures: (Procedure & { distance?: number })[];
1347
- lastDoc: any;
1348
- }> {
1349
- console.log("[PROCEDURE_SERVICE] Executing geo query with geohash bounds");
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');
1350
1174
  try {
1351
1175
  const location = filters.location;
1352
1176
  const radiusInKm = filters.radiusInKm;
@@ -1355,33 +1179,22 @@ export class ProcedureService extends BaseService {
1355
1179
  return Promise.resolve({ procedures: [], lastDoc: null });
1356
1180
  }
1357
1181
 
1358
- const bounds = geohashQueryBounds(
1359
- [location.latitude, location.longitude],
1360
- radiusInKm * 1000
1361
- );
1182
+ const bounds = geohashQueryBounds([location.latitude, location.longitude], radiusInKm * 1000);
1362
1183
 
1363
1184
  const fetches = bounds.map((b) => {
1364
1185
  const constraints: QueryConstraint[] = [
1365
- where("clinicInfo.location.geohash", ">=", b[0]),
1366
- where("clinicInfo.location.geohash", "<=", b[1]),
1367
- where(
1368
- "isActive",
1369
- "==",
1370
- filters.isActive !== undefined ? filters.isActive : true
1371
- ),
1186
+ where('clinicInfo.location.geohash', '>=', b[0]),
1187
+ where('clinicInfo.location.geohash', '<=', b[1]),
1188
+ where('isActive', '==', filters.isActive !== undefined ? filters.isActive : true),
1372
1189
  ];
1373
- return getDocs(
1374
- query(collection(this.db, PROCEDURES_COLLECTION), ...constraints)
1375
- );
1190
+ return getDocs(query(collection(this.db, PROCEDURES_COLLECTION), ...constraints));
1376
1191
  });
1377
1192
 
1378
1193
  return Promise.all(fetches)
1379
1194
  .then((snaps) => {
1380
1195
  const collected: Procedure[] = [];
1381
1196
  snaps.forEach((snap) => {
1382
- snap.docs.forEach((d) =>
1383
- collected.push({ ...(d.data() as Procedure), id: d.id })
1384
- );
1197
+ snap.docs.forEach((d) => collected.push({ ...(d.data() as Procedure), id: d.id }));
1385
1198
  });
1386
1199
 
1387
1200
  // Deduplicate by id
@@ -1399,29 +1212,26 @@ export class ProcedureService extends BaseService {
1399
1212
  let startIndex = 0;
1400
1213
  if (
1401
1214
  filters.lastDoc &&
1402
- typeof filters.lastDoc === "object" &&
1215
+ typeof filters.lastDoc === 'object' &&
1403
1216
  (filters.lastDoc as any).id
1404
1217
  ) {
1405
- const idx = procedures.findIndex(
1406
- (p) => p.id === (filters.lastDoc as any).id
1407
- );
1218
+ const idx = procedures.findIndex((p) => p.id === (filters.lastDoc as any).id);
1408
1219
  if (idx >= 0) startIndex = idx + 1;
1409
1220
  }
1410
1221
  const page = procedures.slice(startIndex, startIndex + pageSize);
1411
- const newLastDoc =
1412
- page.length === pageSize ? page[page.length - 1] : null;
1222
+ const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
1413
1223
 
1414
1224
  console.log(
1415
- `[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`
1225
+ `[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`,
1416
1226
  );
1417
1227
  return { procedures: page, lastDoc: newLastDoc };
1418
1228
  })
1419
1229
  .catch((err) => {
1420
- console.error("[PROCEDURE_SERVICE] Geo bounds fetch failed:", err);
1230
+ console.error('[PROCEDURE_SERVICE] Geo bounds fetch failed:', err);
1421
1231
  return { procedures: [], lastDoc: null };
1422
1232
  });
1423
1233
  } catch (error) {
1424
- console.error("[PROCEDURE_SERVICE] Geo query failed:", error);
1234
+ console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
1425
1235
  return Promise.resolve({ procedures: [], lastDoc: null });
1426
1236
  }
1427
1237
  }
@@ -1433,7 +1243,7 @@ export class ProcedureService extends BaseService {
1433
1243
  * @returns The created procedure
1434
1244
  */
1435
1245
  async createConsultationProcedure(
1436
- data: Omit<CreateProcedureData, "productId">
1246
+ data: Omit<CreateProcedureData, 'productId'>,
1437
1247
  ): Promise<Procedure> {
1438
1248
  // Generate procedure ID first so we can use it for media uploads
1439
1249
  const procedureId = this.generateId();
@@ -1447,7 +1257,7 @@ export class ProcedureService extends BaseService {
1447
1257
  ]);
1448
1258
 
1449
1259
  if (!category || !subcategory || !technology) {
1450
- throw new Error("One or more required base entities not found");
1260
+ throw new Error('One or more required base entities not found');
1451
1261
  }
1452
1262
 
1453
1263
  // Get clinic and practitioner information for aggregation
@@ -1458,11 +1268,7 @@ export class ProcedureService extends BaseService {
1458
1268
  }
1459
1269
  const clinic = clinicSnapshot.data() as Clinic;
1460
1270
 
1461
- const practitionerRef = doc(
1462
- this.db,
1463
- PRACTITIONERS_COLLECTION,
1464
- data.practitionerId
1465
- );
1271
+ const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, data.practitionerId);
1466
1272
  const practitionerSnapshot = await getDoc(practitionerRef);
1467
1273
  if (!practitionerSnapshot.exists()) {
1468
1274
  throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
@@ -1472,26 +1278,22 @@ export class ProcedureService extends BaseService {
1472
1278
  // Process photos if provided
1473
1279
  let processedPhotos: string[] = [];
1474
1280
  if (data.photos && data.photos.length > 0) {
1475
- processedPhotos = await this.processMediaArray(
1476
- data.photos,
1477
- procedureId,
1478
- "procedure-photos"
1479
- );
1281
+ processedPhotos = await this.processMediaArray(data.photos, procedureId, 'procedure-photos');
1480
1282
  }
1481
1283
 
1482
1284
  // Create aggregated clinic info for the procedure document
1483
1285
  const clinicInfo = {
1484
1286
  id: clinicSnapshot.id,
1485
1287
  name: clinic.name,
1486
- description: clinic.description || "",
1288
+ description: clinic.description || '',
1487
1289
  featuredPhoto:
1488
1290
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
1489
- ? typeof clinic.featuredPhotos[0] === "string"
1291
+ ? typeof clinic.featuredPhotos[0] === 'string'
1490
1292
  ? clinic.featuredPhotos[0]
1491
- : ""
1492
- : typeof clinic.coverPhoto === "string"
1293
+ : ''
1294
+ : typeof clinic.coverPhoto === 'string'
1493
1295
  ? clinic.coverPhoto
1494
- : "",
1296
+ : '',
1495
1297
  location: clinic.location,
1496
1298
  contactInfo: clinic.contactInfo,
1497
1299
  };
@@ -1500,22 +1302,22 @@ export class ProcedureService extends BaseService {
1500
1302
  const doctorInfo = {
1501
1303
  id: practitionerSnapshot.id,
1502
1304
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
1503
- description: practitioner.basicInfo.bio || "",
1305
+ description: practitioner.basicInfo.bio || '',
1504
1306
  photo:
1505
- typeof practitioner.basicInfo.profileImageUrl === "string"
1307
+ typeof practitioner.basicInfo.profileImageUrl === 'string'
1506
1308
  ? practitioner.basicInfo.profileImageUrl
1507
- : "",
1309
+ : '',
1508
1310
  rating: practitioner.reviewInfo?.averageRating || 0,
1509
1311
  services: practitioner.procedures || [],
1510
1312
  };
1511
1313
 
1512
1314
  // Create a placeholder product for consultation procedures
1513
1315
  const consultationProduct: Product = {
1514
- id: "consultation-no-product",
1515
- name: "No Product Required",
1516
- description: "Consultation procedures do not require specific products",
1517
- brandId: "consultation-brand",
1518
- 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',
1519
1321
  technologyId: data.technologyId,
1520
1322
  technologyName: technology.name,
1521
1323
  isActive: true,
@@ -1524,7 +1326,7 @@ export class ProcedureService extends BaseService {
1524
1326
  };
1525
1327
 
1526
1328
  // Create the procedure object
1527
- const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
1329
+ const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
1528
1330
  id: procedureId,
1529
1331
  ...data,
1530
1332
  nameLower: (data as any).nameLower || data.name.toLowerCase(),
@@ -1535,9 +1337,7 @@ export class ProcedureService extends BaseService {
1535
1337
  product: consultationProduct, // Use placeholder product
1536
1338
  blockingConditions: technology.blockingConditions,
1537
1339
  contraindications: technology.contraindications || [],
1538
- contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
1539
1340
  treatmentBenefits: technology.benefits,
1540
- treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
1541
1341
  preRequirements: technology.requirements.pre,
1542
1342
  postRequirements: technology.requirements.post,
1543
1343
  certificationRequirement: technology.certificationRequirement,
@@ -1595,7 +1395,7 @@ export class ProcedureService extends BaseService {
1595
1395
  name: data.name,
1596
1396
  clinicId: data.clinicInfo?.id,
1597
1397
  clinicName: data.clinicInfo?.name,
1598
- address: data.clinicInfo?.location?.address || "",
1398
+ address: data.clinicInfo?.location?.address || '',
1599
1399
  latitude: data.clinicInfo?.location?.latitude,
1600
1400
  longitude: data.clinicInfo?.location?.longitude,
1601
1401
  };