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