@blackcode_sa/metaestetics-api 1.11.0 → 1.11.1

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