@blackcode_sa/metaestetics-api 1.5.28 → 1.5.30
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.
- package/dist/admin/index.d.mts +1324 -1
- package/dist/admin/index.d.ts +1324 -1
- package/dist/admin/index.js +1674 -2
- package/dist/admin/index.mjs +1668 -2
- package/dist/backoffice/index.d.mts +99 -7
- package/dist/backoffice/index.d.ts +99 -7
- package/dist/index.d.mts +4036 -2372
- package/dist/index.d.ts +4036 -2372
- package/dist/index.js +2331 -2009
- package/dist/index.mjs +2279 -1954
- package/package.json +2 -1
- package/src/admin/aggregation/README.md +79 -0
- package/src/admin/aggregation/clinic/README.md +52 -0
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
- package/src/admin/aggregation/patient/README.md +27 -0
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
- package/src/admin/index.ts +60 -4
- package/src/admin/mailing/README.md +95 -0
- package/src/admin/mailing/base.mailing.service.ts +131 -0
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/mailing/practitionerInvite/index.ts +1 -0
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
- package/src/index.ts +28 -4
- package/src/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +197 -107
- package/src/services/clinic/utils/clinic.utils.ts +68 -119
- package/src/services/clinic/utils/filter.utils.d.ts +23 -0
- package/src/services/clinic/utils/filter.utils.ts +264 -0
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +439 -104
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +521 -311
- package/src/services/reviews/reviews.service.ts +842 -0
- package/src/types/clinic/index.ts +24 -56
- package/src/types/practitioner/index.ts +34 -33
- package/src/types/procedure/index.ts +32 -0
- package/src/types/profile/index.ts +1 -1
- package/src/types/reviews/index.ts +126 -0
- package/src/validations/clinic.schema.ts +37 -64
- package/src/validations/practitioner.schema.ts +42 -32
- package/src/validations/procedure.schema.ts +11 -3
- package/src/validations/reviews.schema.ts +189 -0
- package/src/services/clinic/utils/review.utils.ts +0 -93
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
serverTimestamp,
|
|
13
13
|
DocumentData,
|
|
14
14
|
writeBatch,
|
|
15
|
+
arrayUnion,
|
|
16
|
+
arrayRemove,
|
|
17
|
+
FieldValue,
|
|
18
|
+
orderBy,
|
|
19
|
+
limit,
|
|
20
|
+
startAfter,
|
|
21
|
+
QueryConstraint,
|
|
22
|
+
documentId,
|
|
15
23
|
} from "firebase/firestore";
|
|
16
24
|
import { BaseService } from "../base.service";
|
|
17
25
|
import {
|
|
@@ -19,6 +27,7 @@ import {
|
|
|
19
27
|
CreateProcedureData,
|
|
20
28
|
UpdateProcedureData,
|
|
21
29
|
PROCEDURES_COLLECTION,
|
|
30
|
+
ProcedureSummaryInfo,
|
|
22
31
|
} from "../../types/procedure";
|
|
23
32
|
import {
|
|
24
33
|
createProcedureSchema,
|
|
@@ -57,7 +66,10 @@ import {
|
|
|
57
66
|
CertificationSpecialty,
|
|
58
67
|
ProcedureFamily,
|
|
59
68
|
} from "../../backoffice/types";
|
|
60
|
-
import { CLINICS_COLLECTION } from "../../types/clinic";
|
|
69
|
+
import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
|
|
70
|
+
import { ProcedureReviewInfo } from "../../types/reviews";
|
|
71
|
+
import { distanceBetween, geohashQueryBounds } from "geofire-common";
|
|
72
|
+
import { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
|
|
61
73
|
|
|
62
74
|
export class ProcedureService extends BaseService {
|
|
63
75
|
private categoryService: CategoryService;
|
|
@@ -87,10 +99,9 @@ export class ProcedureService extends BaseService {
|
|
|
87
99
|
* @returns The created procedure
|
|
88
100
|
*/
|
|
89
101
|
async createProcedure(data: CreateProcedureData): Promise<Procedure> {
|
|
90
|
-
// Validate input data
|
|
91
102
|
const validatedData = createProcedureSchema.parse(data);
|
|
92
103
|
|
|
93
|
-
// Get references to related entities
|
|
104
|
+
// Get references to related entities (Category, Subcategory, Technology, Product)
|
|
94
105
|
const [category, subcategory, technology, product] = await Promise.all([
|
|
95
106
|
this.categoryService.getById(validatedData.categoryId),
|
|
96
107
|
this.subcategoryService.getById(
|
|
@@ -105,75 +116,65 @@ export class ProcedureService extends BaseService {
|
|
|
105
116
|
]);
|
|
106
117
|
|
|
107
118
|
if (!category || !subcategory || !technology || !product) {
|
|
108
|
-
throw new Error("One or more required entities not found");
|
|
119
|
+
throw new Error("One or more required base entities not found");
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
// Get clinic and practitioner information
|
|
122
|
+
// Get clinic and practitioner information for aggregation
|
|
112
123
|
const clinicRef = doc(
|
|
113
124
|
this.db,
|
|
114
125
|
CLINICS_COLLECTION,
|
|
115
126
|
validatedData.clinicBranchId
|
|
116
127
|
);
|
|
117
128
|
const clinicSnapshot = await getDoc(clinicRef);
|
|
118
|
-
|
|
119
129
|
if (!clinicSnapshot.exists()) {
|
|
120
130
|
throw new Error(
|
|
121
131
|
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
122
132
|
);
|
|
123
133
|
}
|
|
134
|
+
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
124
135
|
|
|
125
|
-
const clinic = clinicSnapshot.data();
|
|
126
|
-
|
|
127
|
-
// Create clinic info
|
|
128
|
-
const clinicInfo = {
|
|
129
|
-
id: clinicSnapshot.id,
|
|
130
|
-
name: clinic.name,
|
|
131
|
-
description: clinic.description || "",
|
|
132
|
-
featuredPhoto:
|
|
133
|
-
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
134
|
-
? clinic.featuredPhotos[0]
|
|
135
|
-
: clinic.coverPhoto || "",
|
|
136
|
-
location: clinic.location,
|
|
137
|
-
contactInfo: clinic.contactInfo,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// Get practitioner information
|
|
141
136
|
const practitionerRef = doc(
|
|
142
137
|
this.db,
|
|
143
138
|
PRACTITIONERS_COLLECTION,
|
|
144
139
|
validatedData.practitionerId
|
|
145
140
|
);
|
|
146
141
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
147
|
-
|
|
148
142
|
if (!practitionerSnapshot.exists()) {
|
|
149
143
|
throw new Error(
|
|
150
144
|
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
151
145
|
);
|
|
152
146
|
}
|
|
147
|
+
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
// Create aggregated clinic info for the procedure document
|
|
150
|
+
const clinicInfo = {
|
|
151
|
+
id: clinicSnapshot.id,
|
|
152
|
+
name: clinic.name,
|
|
153
|
+
description: clinic.description || "",
|
|
154
|
+
featuredPhoto:
|
|
155
|
+
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
156
|
+
? clinic.featuredPhotos[0]
|
|
157
|
+
: clinic.coverPhoto || "",
|
|
158
|
+
location: clinic.location,
|
|
159
|
+
contactInfo: clinic.contactInfo,
|
|
160
|
+
};
|
|
160
161
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
171
|
-
}
|
|
162
|
+
// Create aggregated doctor info for the procedure document
|
|
163
|
+
const doctorInfo = {
|
|
164
|
+
id: practitionerSnapshot.id,
|
|
165
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
166
|
+
description: practitioner.basicInfo.bio || "",
|
|
167
|
+
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
168
|
+
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
169
|
+
services: practitioner.procedures || [],
|
|
170
|
+
};
|
|
172
171
|
|
|
173
172
|
// Create the procedure object
|
|
174
|
-
const
|
|
173
|
+
const procedureId = this.generateId();
|
|
174
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
175
|
+
id: procedureId,
|
|
175
176
|
...validatedData,
|
|
176
|
-
category,
|
|
177
|
+
category, // Embed full objects
|
|
177
178
|
subcategory,
|
|
178
179
|
technology,
|
|
179
180
|
product,
|
|
@@ -183,24 +184,33 @@ export class ProcedureService extends BaseService {
|
|
|
183
184
|
postRequirements: technology.requirements.post,
|
|
184
185
|
certificationRequirement: technology.certificationRequirement,
|
|
185
186
|
documentationTemplates: technology.documentationTemplates || [],
|
|
186
|
-
clinicInfo,
|
|
187
|
-
doctorInfo,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
clinicInfo, // Embed aggregated info
|
|
188
|
+
doctorInfo, // Embed aggregated info
|
|
189
|
+
reviewInfo: {
|
|
190
|
+
// Default empty reviews
|
|
191
|
+
totalReviews: 0,
|
|
192
|
+
averageRating: 0,
|
|
193
|
+
effectivenessOfTreatment: 0,
|
|
194
|
+
outcomeExplanation: 0,
|
|
195
|
+
painManagement: 0,
|
|
196
|
+
followUpCare: 0,
|
|
197
|
+
valueForMoney: 0,
|
|
198
|
+
recommendationPercentage: 0,
|
|
199
|
+
},
|
|
200
|
+
isActive: true, // Default to active
|
|
191
201
|
};
|
|
192
202
|
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
...procedure,
|
|
198
|
-
id,
|
|
203
|
+
// Create the procedure document
|
|
204
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
205
|
+
await setDoc(procedureRef, {
|
|
206
|
+
...newProcedure,
|
|
199
207
|
createdAt: serverTimestamp(),
|
|
200
208
|
updatedAt: serverTimestamp(),
|
|
201
209
|
});
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
// Return the created procedure (fetch again to get server timestamps)
|
|
212
|
+
const savedDoc = await getDoc(procedureRef);
|
|
213
|
+
return savedDoc.data() as Procedure;
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
/**
|
|
@@ -256,71 +266,207 @@ export class ProcedureService extends BaseService {
|
|
|
256
266
|
/**
|
|
257
267
|
* Updates a procedure
|
|
258
268
|
* @param id - The ID of the procedure to update
|
|
259
|
-
* @param data - The data to update
|
|
269
|
+
* @param data - The data to update the procedure with
|
|
260
270
|
* @returns The updated procedure
|
|
261
271
|
*/
|
|
262
272
|
async updateProcedure(
|
|
263
273
|
id: string,
|
|
264
274
|
data: UpdateProcedureData
|
|
265
275
|
): Promise<Procedure> {
|
|
266
|
-
// Validate input data
|
|
267
276
|
const validatedData = updateProcedureSchema.parse(data);
|
|
277
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
278
|
+
const procedureSnapshot = await getDoc(procedureRef);
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
const existingProcedure = await this.getProcedure(id);
|
|
271
|
-
if (!existingProcedure) {
|
|
280
|
+
if (!procedureSnapshot.exists()) {
|
|
272
281
|
throw new Error(`Procedure with ID ${id} not found`);
|
|
273
282
|
}
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
const existingProcedure = procedureSnapshot.data() as Procedure;
|
|
285
|
+
let updatedProcedureData: Partial<Procedure> = { ...validatedData };
|
|
286
|
+
|
|
287
|
+
let practitionerChanged = false;
|
|
288
|
+
let clinicChanged = false;
|
|
289
|
+
const oldPractitionerId = existingProcedure.practitionerId;
|
|
290
|
+
const oldClinicId = existingProcedure.clinicBranchId;
|
|
291
|
+
let newPractitioner: Practitioner | null = null;
|
|
292
|
+
let newClinic: Clinic | null = null;
|
|
293
|
+
|
|
294
|
+
// --- Prepare updates and fetch new related data if IDs change ---
|
|
295
|
+
|
|
296
|
+
// Handle Practitioner Change
|
|
297
|
+
if (
|
|
298
|
+
validatedData.practitionerId &&
|
|
299
|
+
validatedData.practitionerId !== oldPractitionerId
|
|
300
|
+
) {
|
|
301
|
+
practitionerChanged = true;
|
|
302
|
+
const newPractitionerRef = doc(
|
|
303
|
+
this.db,
|
|
304
|
+
PRACTITIONERS_COLLECTION,
|
|
305
|
+
validatedData.practitionerId
|
|
306
|
+
);
|
|
307
|
+
const newPractitionerSnap = await getDoc(newPractitionerRef);
|
|
308
|
+
if (!newPractitionerSnap.exists())
|
|
309
|
+
throw new Error(
|
|
310
|
+
`New Practitioner ${validatedData.practitionerId} not found`
|
|
311
|
+
);
|
|
312
|
+
newPractitioner = newPractitionerSnap.data() as Practitioner;
|
|
313
|
+
// Update doctorInfo within the procedure document
|
|
314
|
+
updatedProcedureData.doctorInfo = {
|
|
315
|
+
id: newPractitioner.id,
|
|
316
|
+
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
317
|
+
description: newPractitioner.basicInfo.bio || "",
|
|
318
|
+
photo: newPractitioner.basicInfo.profileImageUrl || "",
|
|
319
|
+
rating: newPractitioner.reviewInfo?.averageRating || 0,
|
|
320
|
+
services: newPractitioner.procedures || [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Handle Clinic Change
|
|
325
|
+
if (
|
|
326
|
+
validatedData.clinicBranchId &&
|
|
327
|
+
validatedData.clinicBranchId !== oldClinicId
|
|
328
|
+
) {
|
|
329
|
+
clinicChanged = true;
|
|
330
|
+
const newClinicRef = doc(
|
|
331
|
+
this.db,
|
|
332
|
+
CLINICS_COLLECTION,
|
|
333
|
+
validatedData.clinicBranchId
|
|
334
|
+
);
|
|
335
|
+
const newClinicSnap = await getDoc(newClinicRef);
|
|
336
|
+
if (!newClinicSnap.exists())
|
|
337
|
+
throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
|
|
338
|
+
newClinic = newClinicSnap.data() as Clinic;
|
|
339
|
+
// Update clinicInfo within the procedure document
|
|
340
|
+
updatedProcedureData.clinicInfo = {
|
|
341
|
+
id: newClinic.id,
|
|
342
|
+
name: newClinic.name,
|
|
343
|
+
description: newClinic.description || "",
|
|
344
|
+
featuredPhoto:
|
|
345
|
+
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
346
|
+
? newClinic.featuredPhotos[0]
|
|
347
|
+
: newClinic.coverPhoto || "",
|
|
348
|
+
location: newClinic.location,
|
|
349
|
+
contactInfo: newClinic.contactInfo,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle Category/Subcategory/Technology/Product Changes
|
|
354
|
+
let finalCategoryId = existingProcedure.category.id;
|
|
355
|
+
if (validatedData.categoryId) {
|
|
356
|
+
const category = await this.categoryService.getById(
|
|
357
|
+
validatedData.categoryId
|
|
358
|
+
);
|
|
359
|
+
if (!category)
|
|
360
|
+
throw new Error(`Category ${validatedData.categoryId} not found`);
|
|
361
|
+
updatedProcedureData.category = category;
|
|
362
|
+
finalCategoryId = category.id; // Update finalCategoryId if category changed
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Only fetch subcategory if its ID is provided AND we have a valid finalCategoryId
|
|
366
|
+
if (validatedData.subcategoryId && finalCategoryId) {
|
|
367
|
+
const subcategory = await this.subcategoryService.getById(
|
|
368
|
+
finalCategoryId,
|
|
369
|
+
validatedData.subcategoryId
|
|
370
|
+
);
|
|
371
|
+
if (!subcategory)
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
|
|
374
|
+
);
|
|
375
|
+
updatedProcedureData.subcategory = subcategory;
|
|
376
|
+
} else if (validatedData.subcategoryId) {
|
|
377
|
+
console.warn(
|
|
378
|
+
"Attempted to update subcategory without a valid categoryId"
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let finalTechnologyId = existingProcedure.technology.id;
|
|
383
|
+
if (validatedData.technologyId) {
|
|
384
|
+
const technology = await this.technologyService.getById(
|
|
385
|
+
validatedData.technologyId
|
|
386
|
+
);
|
|
387
|
+
if (!technology)
|
|
388
|
+
throw new Error(`Technology ${validatedData.technologyId} not found`);
|
|
389
|
+
updatedProcedureData.technology = technology;
|
|
390
|
+
finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
|
|
391
|
+
// Update related fields derived from technology
|
|
392
|
+
updatedProcedureData.blockingConditions = technology.blockingConditions;
|
|
393
|
+
updatedProcedureData.treatmentBenefits = technology.benefits;
|
|
394
|
+
updatedProcedureData.preRequirements = technology.requirements.pre;
|
|
395
|
+
updatedProcedureData.postRequirements = technology.requirements.post;
|
|
396
|
+
updatedProcedureData.certificationRequirement =
|
|
397
|
+
technology.certificationRequirement;
|
|
398
|
+
updatedProcedureData.documentationTemplates =
|
|
399
|
+
technology.documentationTemplates || [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Only fetch product if its ID is provided AND we have a valid finalTechnologyId
|
|
403
|
+
if (validatedData.productId && finalTechnologyId) {
|
|
404
|
+
const product = await this.productService.getById(
|
|
405
|
+
finalTechnologyId,
|
|
406
|
+
validatedData.productId
|
|
407
|
+
);
|
|
408
|
+
if (!product)
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
|
|
411
|
+
);
|
|
412
|
+
updatedProcedureData.product = product;
|
|
413
|
+
} else if (validatedData.productId) {
|
|
414
|
+
console.warn("Attempted to update product without a valid technologyId");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Update the procedure document
|
|
418
|
+
await updateDoc(procedureRef, {
|
|
419
|
+
...updatedProcedureData,
|
|
279
420
|
updatedAt: serverTimestamp(),
|
|
280
421
|
});
|
|
281
422
|
|
|
282
|
-
// Return the updated procedure
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
...existingProcedure,
|
|
287
|
-
...validatedData,
|
|
288
|
-
updatedAt: new Date(),
|
|
289
|
-
};
|
|
423
|
+
// Return the updated procedure
|
|
424
|
+
const updatedSnapshot = await getDoc(procedureRef);
|
|
425
|
+
return updatedSnapshot.data() as Procedure;
|
|
290
426
|
}
|
|
291
427
|
|
|
292
428
|
/**
|
|
293
|
-
* Deactivates a procedure
|
|
429
|
+
* Deactivates a procedure (soft delete)
|
|
294
430
|
* @param id - The ID of the procedure to deactivate
|
|
295
431
|
*/
|
|
296
432
|
async deactivateProcedure(id: string): Promise<void> {
|
|
297
|
-
const
|
|
298
|
-
await
|
|
433
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
434
|
+
const procedureSnap = await getDoc(procedureRef);
|
|
435
|
+
if (!procedureSnap.exists()) {
|
|
436
|
+
console.warn(`Procedure ${id} not found for deactivation.`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Mark procedure as inactive
|
|
441
|
+
await updateDoc(procedureRef, {
|
|
299
442
|
isActive: false,
|
|
300
443
|
updatedAt: serverTimestamp(),
|
|
301
444
|
});
|
|
302
445
|
}
|
|
303
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Deletes a procedure permanently
|
|
449
|
+
* @param id - The ID of the procedure to delete
|
|
450
|
+
* @returns A boolean indicating if the deletion was successful
|
|
451
|
+
*/
|
|
452
|
+
async deleteProcedure(id: string): Promise<boolean> {
|
|
453
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
454
|
+
const procedureSnapshot = await getDoc(procedureRef);
|
|
455
|
+
|
|
456
|
+
if (!procedureSnapshot.exists()) {
|
|
457
|
+
// Already deleted or never existed
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Delete the procedure document
|
|
462
|
+
await deleteDoc(procedureRef);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
304
466
|
/**
|
|
305
467
|
* Gets all procedures that a practitioner is certified to perform
|
|
306
468
|
* @param practitioner - The practitioner's profile
|
|
307
|
-
* @returns Object containing
|
|
308
|
-
* - procedures: List of procedures the practitioner can perform
|
|
309
|
-
* - families: List of procedure families the practitioner can perform
|
|
310
|
-
* - categories: List of category IDs the practitioner can perform
|
|
311
|
-
* - subcategories: List of subcategory IDs the practitioner can perform
|
|
312
|
-
*
|
|
313
|
-
* @example
|
|
314
|
-
* const practitioner = {
|
|
315
|
-
* certification: {
|
|
316
|
-
* level: CertificationLevel.DOCTOR,
|
|
317
|
-
* specialties: [CertificationSpecialty.INJECTABLES]
|
|
318
|
-
* }
|
|
319
|
-
* };
|
|
320
|
-
* const allowedProcedures = await procedureService.getAllowedProcedures(practitioner);
|
|
321
|
-
* console.log(allowedProcedures.families); // [ProcedureFamily.AESTHETICS]
|
|
322
|
-
* console.log(allowedProcedures.categories); // ["category1", "category2"]
|
|
323
|
-
* console.log(allowedProcedures.subcategories); // ["subcategory1", "subcategory2"]
|
|
469
|
+
* @returns Object containing allowed technologies, families, categories, subcategories
|
|
324
470
|
*/
|
|
325
471
|
async getAllowedTechnologies(practitioner: Practitioner): Promise<{
|
|
326
472
|
technologies: Technology[];
|
|
@@ -328,7 +474,7 @@ export class ProcedureService extends BaseService {
|
|
|
328
474
|
categories: string[];
|
|
329
475
|
subcategories: string[];
|
|
330
476
|
}> {
|
|
331
|
-
//
|
|
477
|
+
// This logic depends on TechnologyService and remains valid
|
|
332
478
|
const { technologies, families, categories, subcategories } =
|
|
333
479
|
await this.technologyService.getAllowedTechnologies(practitioner);
|
|
334
480
|
|
|
@@ -355,19 +501,26 @@ export class ProcedureService extends BaseService {
|
|
|
355
501
|
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
356
502
|
let proceduresQuery = query(proceduresCollection);
|
|
357
503
|
|
|
358
|
-
//
|
|
504
|
+
// Apply pagination if specified
|
|
359
505
|
if (pagination && pagination > 0) {
|
|
360
|
-
const { limit, startAfter } =
|
|
506
|
+
const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
|
|
361
507
|
|
|
362
508
|
if (lastDoc) {
|
|
363
509
|
proceduresQuery = query(
|
|
364
510
|
proceduresCollection,
|
|
511
|
+
orderBy("name"), // Use imported orderBy
|
|
365
512
|
startAfter(lastDoc),
|
|
366
513
|
limit(pagination)
|
|
367
514
|
);
|
|
368
515
|
} else {
|
|
369
|
-
proceduresQuery = query(
|
|
516
|
+
proceduresQuery = query(
|
|
517
|
+
proceduresCollection,
|
|
518
|
+
orderBy("name"),
|
|
519
|
+
limit(pagination)
|
|
520
|
+
); // Use imported orderBy
|
|
370
521
|
}
|
|
522
|
+
} else {
|
|
523
|
+
proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
|
|
371
524
|
}
|
|
372
525
|
|
|
373
526
|
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
@@ -376,17 +529,9 @@ export class ProcedureService extends BaseService {
|
|
|
376
529
|
|
|
377
530
|
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
378
531
|
const data = doc.data() as Procedure;
|
|
379
|
-
|
|
380
|
-
// Ensure clinicInfo and doctorInfo are present
|
|
381
|
-
if (!data.clinicInfo || !data.doctorInfo) {
|
|
382
|
-
console.warn(
|
|
383
|
-
`Procedure ${data.id} is missing clinicInfo or doctorInfo fields. These should be updated.`
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
532
|
return {
|
|
388
533
|
...data,
|
|
389
|
-
id: doc.id,
|
|
534
|
+
id: doc.id, // Ensure ID is present
|
|
390
535
|
};
|
|
391
536
|
});
|
|
392
537
|
|
|
@@ -401,249 +546,314 @@ export class ProcedureService extends BaseService {
|
|
|
401
546
|
}
|
|
402
547
|
|
|
403
548
|
/**
|
|
404
|
-
*
|
|
549
|
+
* Searches and filters procedures based on multiple criteria
|
|
405
550
|
*
|
|
406
|
-
* @param
|
|
407
|
-
* @param
|
|
408
|
-
* @param
|
|
409
|
-
* @
|
|
551
|
+
* @param filters - Various filters to apply
|
|
552
|
+
* @param filters.nameSearch - Optional search text for procedure name
|
|
553
|
+
* @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
|
|
554
|
+
* @param filters.procedureFamily - Optional procedure family to filter by
|
|
555
|
+
* @param filters.procedureCategory - Optional procedure category to filter by
|
|
556
|
+
* @param filters.procedureSubcategory - Optional procedure subcategory to filter by
|
|
557
|
+
* @param filters.procedureTechnology - Optional procedure technology to filter by
|
|
558
|
+
* @param filters.location - Optional location for distance-based search
|
|
559
|
+
* @param filters.radiusInKm - Optional radius in kilometers (required if location is provided)
|
|
560
|
+
* @param filters.minPrice - Optional minimum price
|
|
561
|
+
* @param filters.maxPrice - Optional maximum price
|
|
562
|
+
* @param filters.minRating - Optional minimum rating (0-5)
|
|
563
|
+
* @param filters.maxRating - Optional maximum rating (0-5)
|
|
564
|
+
* @param filters.pagination - Optional number of results per page
|
|
565
|
+
* @param filters.lastDoc - Optional last document for pagination
|
|
566
|
+
* @param filters.isActive - Optional filter for active procedures only
|
|
567
|
+
* @returns Filtered procedures and the last document for pagination
|
|
410
568
|
*/
|
|
411
|
-
async
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
569
|
+
async getProceduresByFilters(filters: {
|
|
570
|
+
nameSearch?: string;
|
|
571
|
+
treatmentBenefits?: TreatmentBenefit[];
|
|
572
|
+
procedureFamily?: ProcedureFamily;
|
|
573
|
+
procedureCategory?: string;
|
|
574
|
+
procedureSubcategory?: string;
|
|
575
|
+
procedureTechnology?: string;
|
|
576
|
+
location?: { latitude: number; longitude: number };
|
|
577
|
+
radiusInKm?: number;
|
|
578
|
+
minPrice?: number;
|
|
579
|
+
maxPrice?: number;
|
|
580
|
+
minRating?: number;
|
|
581
|
+
maxRating?: number;
|
|
582
|
+
pagination?: number;
|
|
583
|
+
lastDoc?: any;
|
|
584
|
+
isActive?: boolean;
|
|
585
|
+
}): Promise<{
|
|
586
|
+
procedures: (Procedure & { distance?: number })[];
|
|
587
|
+
lastDoc: any;
|
|
588
|
+
}> {
|
|
589
|
+
try {
|
|
590
|
+
console.log(
|
|
591
|
+
"[PROCEDURE_SERVICE] Starting procedure filtering with criteria:",
|
|
592
|
+
filters
|
|
429
593
|
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const snapshot = await getDocs(proceduresQuery);
|
|
433
594
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
595
|
+
// Determine if we're doing a geo query or a regular query
|
|
596
|
+
const isGeoQuery =
|
|
597
|
+
filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
437
598
|
|
|
438
|
-
|
|
439
|
-
|
|
599
|
+
// Initialize base constraints
|
|
600
|
+
const constraints: QueryConstraint[] = [];
|
|
440
601
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
id: entityId,
|
|
445
|
-
name: updatedData.name,
|
|
446
|
-
description: updatedData.description || "",
|
|
447
|
-
featuredPhoto:
|
|
448
|
-
updatedData.featuredPhotos && updatedData.featuredPhotos.length > 0
|
|
449
|
-
? updatedData.featuredPhotos[0]
|
|
450
|
-
: updatedData.coverPhoto || "",
|
|
451
|
-
location: updatedData.location,
|
|
452
|
-
contactInfo: updatedData.contactInfo,
|
|
453
|
-
};
|
|
454
|
-
} else {
|
|
455
|
-
// Doctor info format
|
|
456
|
-
if (updatedData.basicInfo) {
|
|
457
|
-
// If it's a practitioner object
|
|
458
|
-
updatedFieldData = {
|
|
459
|
-
id: entityId,
|
|
460
|
-
name: `${updatedData.basicInfo.firstName} ${updatedData.basicInfo.lastName}`,
|
|
461
|
-
description: updatedData.basicInfo.bio || "",
|
|
462
|
-
photo: updatedData.basicInfo.profileImageUrl || "",
|
|
463
|
-
rating: 0, // This would need to be calculated or passed in
|
|
464
|
-
services: [], // This would need to be determined or passed in
|
|
465
|
-
};
|
|
602
|
+
// Add active status filter (default to active if not specified)
|
|
603
|
+
if (filters.isActive !== undefined) {
|
|
604
|
+
constraints.push(where("isActive", "==", filters.isActive));
|
|
466
605
|
} else {
|
|
467
|
-
|
|
468
|
-
updatedFieldData = {
|
|
469
|
-
id: entityId,
|
|
470
|
-
name: updatedData.name,
|
|
471
|
-
description: updatedData.description || "",
|
|
472
|
-
photo: updatedData.photo || "",
|
|
473
|
-
rating: updatedData.rating || 0,
|
|
474
|
-
services: updatedData.services || [],
|
|
475
|
-
};
|
|
606
|
+
constraints.push(where("isActive", "==", true));
|
|
476
607
|
}
|
|
477
|
-
}
|
|
478
608
|
|
|
479
|
-
|
|
480
|
-
|
|
609
|
+
// Filter by procedure family if specified
|
|
610
|
+
if (filters.procedureFamily) {
|
|
611
|
+
constraints.push(where("family", "==", filters.procedureFamily));
|
|
612
|
+
}
|
|
481
613
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
[updatedField]: updatedFieldData,
|
|
485
|
-
updatedAt: serverTimestamp(),
|
|
486
|
-
});
|
|
487
|
-
});
|
|
614
|
+
// Add ordering to make pagination consistent
|
|
615
|
+
constraints.push(orderBy(documentId()));
|
|
488
616
|
|
|
489
|
-
|
|
617
|
+
// Add pagination if specified
|
|
618
|
+
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
619
|
+
constraints.push(startAfter(filters.lastDoc));
|
|
620
|
+
constraints.push(limit(filters.pagination));
|
|
621
|
+
} else if (filters.pagination && filters.pagination > 0) {
|
|
622
|
+
constraints.push(limit(filters.pagination));
|
|
623
|
+
}
|
|
490
624
|
|
|
491
|
-
|
|
492
|
-
|
|
625
|
+
let proceduresResult: (Procedure & { distance?: number })[] = [];
|
|
626
|
+
let lastVisibleDoc = null;
|
|
493
627
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
* @param clinicData - The updated clinic data
|
|
499
|
-
* @returns Number of procedures updated
|
|
500
|
-
*/
|
|
501
|
-
async updateClinicInfoInProcedures(
|
|
502
|
-
clinicId: string,
|
|
503
|
-
clinicData: any
|
|
504
|
-
): Promise<number> {
|
|
505
|
-
return this.updateProcedureAggregateData("clinic", clinicId, clinicData);
|
|
506
|
-
}
|
|
628
|
+
// For geo queries, we need a different approach
|
|
629
|
+
if (isGeoQuery) {
|
|
630
|
+
const center = filters.location!;
|
|
631
|
+
const radiusInKm = filters.radiusInKm!;
|
|
507
632
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
* @returns Number of procedures updated
|
|
514
|
-
*/
|
|
515
|
-
async updateDoctorInfoInProcedures(
|
|
516
|
-
practitionerId: string,
|
|
517
|
-
practitionerData: any
|
|
518
|
-
): Promise<number> {
|
|
519
|
-
return this.updateProcedureAggregateData(
|
|
520
|
-
"doctor",
|
|
521
|
-
practitionerId,
|
|
522
|
-
practitionerData
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Updates all existing procedures to include clinicInfo and doctorInfo
|
|
528
|
-
* This is a migration helper method that can be used to update existing procedures
|
|
529
|
-
*
|
|
530
|
-
* @returns Number of procedures updated
|
|
531
|
-
*/
|
|
532
|
-
async migrateAllProceduresWithAggregateData(): Promise<number> {
|
|
533
|
-
// Get all procedures
|
|
534
|
-
const proceduresQuery = query(collection(this.db, PROCEDURES_COLLECTION));
|
|
535
|
-
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
633
|
+
// Get the geohash query bounds
|
|
634
|
+
const bounds = geohashQueryBounds(
|
|
635
|
+
[center.latitude, center.longitude],
|
|
636
|
+
radiusInKm * 1000 // Convert to meters
|
|
637
|
+
);
|
|
536
638
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
639
|
+
// Collect matching procedures from all bounds
|
|
640
|
+
const matchingProcedures: (Procedure & { distance: number })[] = [];
|
|
641
|
+
|
|
642
|
+
// Execute queries for each bound
|
|
643
|
+
for (const bound of bounds) {
|
|
644
|
+
// Create a geo query for this bound
|
|
645
|
+
const geoConstraints = [
|
|
646
|
+
...constraints,
|
|
647
|
+
where("clinicInfo.location.geohash", ">=", bound[0]),
|
|
648
|
+
where("clinicInfo.location.geohash", "<=", bound[1]),
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
const q = query(
|
|
652
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
653
|
+
...geoConstraints
|
|
654
|
+
);
|
|
655
|
+
const querySnapshot = await getDocs(q);
|
|
540
656
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
let batchCount = 0;
|
|
657
|
+
console.log(
|
|
658
|
+
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures in geo bound`
|
|
659
|
+
);
|
|
545
660
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
661
|
+
// Process results and filter by actual distance
|
|
662
|
+
for (const doc of querySnapshot.docs) {
|
|
663
|
+
const procedure = { ...doc.data(), id: doc.id } as Procedure;
|
|
664
|
+
|
|
665
|
+
// Calculate actual distance
|
|
666
|
+
const distance = distanceBetween(
|
|
667
|
+
[center.latitude, center.longitude],
|
|
668
|
+
[
|
|
669
|
+
procedure.clinicInfo.location.latitude,
|
|
670
|
+
procedure.clinicInfo.location.longitude,
|
|
671
|
+
]
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// Convert to kilometers
|
|
675
|
+
const distanceInKm = distance / 1000;
|
|
676
|
+
|
|
677
|
+
// Check if within radius
|
|
678
|
+
if (distanceInKm <= radiusInKm) {
|
|
679
|
+
// Add distance to procedure object
|
|
680
|
+
matchingProcedures.push({
|
|
681
|
+
...procedure,
|
|
682
|
+
distance: distanceInKm,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
549
687
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
688
|
+
// Apply additional filters that couldn't be applied in the query
|
|
689
|
+
let filteredProcedures = matchingProcedures;
|
|
554
690
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
CLINICS_COLLECTION,
|
|
560
|
-
procedure.clinicBranchId
|
|
691
|
+
// Apply remaining filters in memory
|
|
692
|
+
filteredProcedures = this.applyInMemoryFilters(
|
|
693
|
+
filteredProcedures,
|
|
694
|
+
filters
|
|
561
695
|
);
|
|
562
|
-
const clinicSnapshot = await getDoc(clinicRef);
|
|
563
696
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
697
|
+
// Sort by distance
|
|
698
|
+
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
699
|
+
|
|
700
|
+
// Apply pagination after all filters have been applied
|
|
701
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
702
|
+
// If we have a lastDoc, find its index
|
|
703
|
+
let startIndex = 0;
|
|
704
|
+
if (filters.lastDoc) {
|
|
705
|
+
const lastDocIndex = filteredProcedures.findIndex(
|
|
706
|
+
(procedure) => procedure.id === filters.lastDoc.id
|
|
707
|
+
);
|
|
708
|
+
if (lastDocIndex !== -1) {
|
|
709
|
+
startIndex = lastDocIndex + 1;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Get paginated subset
|
|
714
|
+
const paginatedProcedures = filteredProcedures.slice(
|
|
715
|
+
startIndex,
|
|
716
|
+
startIndex + filters.pagination
|
|
567
717
|
);
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
718
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
name: clinic.name,
|
|
577
|
-
description: clinic.description || "",
|
|
578
|
-
featuredPhoto:
|
|
579
|
-
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
580
|
-
? clinic.featuredPhotos[0]
|
|
581
|
-
: clinic.coverPhoto || "",
|
|
582
|
-
location: clinic.location,
|
|
583
|
-
contactInfo: clinic.contactInfo,
|
|
584
|
-
};
|
|
719
|
+
// Set last document for next pagination
|
|
720
|
+
lastVisibleDoc =
|
|
721
|
+
paginatedProcedures.length > 0
|
|
722
|
+
? paginatedProcedures[paginatedProcedures.length - 1]
|
|
723
|
+
: null;
|
|
585
724
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
725
|
+
proceduresResult = paginatedProcedures;
|
|
726
|
+
} else {
|
|
727
|
+
proceduresResult = filteredProcedures;
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
// For non-geo queries, execute a single query with all constraints
|
|
731
|
+
const q = query(
|
|
732
|
+
collection(this.db, PROCEDURES_COLLECTION),
|
|
733
|
+
...constraints
|
|
591
734
|
);
|
|
592
|
-
const
|
|
735
|
+
const querySnapshot = await getDocs(q);
|
|
593
736
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
737
|
+
console.log(
|
|
738
|
+
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures with regular query`
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Convert docs to procedures
|
|
742
|
+
const procedures = querySnapshot.docs.map((doc) => {
|
|
743
|
+
return { ...doc.data(), id: doc.id } as Procedure;
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Apply filters that couldn't be applied in the query
|
|
747
|
+
let filteredProcedures = this.applyInMemoryFilters(procedures, filters);
|
|
748
|
+
|
|
749
|
+
// Set last document for pagination
|
|
750
|
+
lastVisibleDoc =
|
|
751
|
+
querySnapshot.docs.length > 0
|
|
752
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
753
|
+
: null;
|
|
600
754
|
|
|
601
|
-
|
|
755
|
+
proceduresResult = filteredProcedures;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
procedures: proceduresResult,
|
|
760
|
+
lastDoc: lastVisibleDoc,
|
|
761
|
+
};
|
|
762
|
+
} catch (error) {
|
|
763
|
+
console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Helper method to apply in-memory filters to procedures
|
|
770
|
+
* Used by getProceduresByFilters to apply filters that can't be done in Firestore queries
|
|
771
|
+
*
|
|
772
|
+
* @param procedures - The procedures to filter
|
|
773
|
+
* @param filters - The filters to apply
|
|
774
|
+
* @returns Filtered procedures
|
|
775
|
+
*/
|
|
776
|
+
private applyInMemoryFilters<T extends Procedure & { distance?: number }>(
|
|
777
|
+
procedures: T[],
|
|
778
|
+
filters: {
|
|
779
|
+
nameSearch?: string;
|
|
780
|
+
treatmentBenefits?: TreatmentBenefit[];
|
|
781
|
+
procedureCategory?: string;
|
|
782
|
+
procedureSubcategory?: string;
|
|
783
|
+
procedureTechnology?: string;
|
|
784
|
+
minPrice?: number;
|
|
785
|
+
maxPrice?: number;
|
|
786
|
+
minRating?: number;
|
|
787
|
+
maxRating?: number;
|
|
788
|
+
}
|
|
789
|
+
): T[] {
|
|
790
|
+
let filteredProcedures = procedures;
|
|
791
|
+
|
|
792
|
+
// Filter by name search if specified
|
|
793
|
+
if (filters.nameSearch && filters.nameSearch.trim() !== "") {
|
|
794
|
+
const searchTerm = filters.nameSearch.toLowerCase().trim();
|
|
795
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
796
|
+
return procedure.name.toLowerCase().includes(searchTerm);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
602
799
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
800
|
+
// Filter by treatment benefits if specified
|
|
801
|
+
if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
|
|
802
|
+
filteredProcedures = filteredProcedures.filter((procedure) => {
|
|
803
|
+
// Check if procedure has all specified treatment benefits
|
|
804
|
+
return filters.treatmentBenefits!.every((benefit) =>
|
|
805
|
+
procedure.treatmentBenefits.includes(benefit)
|
|
606
806
|
);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
607
809
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
615
|
-
rating: 0,
|
|
616
|
-
services: [],
|
|
617
|
-
};
|
|
618
|
-
}
|
|
810
|
+
// Filter by procedure category if specified
|
|
811
|
+
if (filters.procedureCategory) {
|
|
812
|
+
filteredProcedures = filteredProcedures.filter(
|
|
813
|
+
(procedure) => procedure.category.id === filters.procedureCategory
|
|
814
|
+
);
|
|
815
|
+
}
|
|
619
816
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
817
|
+
// Filter by procedure subcategory if specified
|
|
818
|
+
if (filters.procedureSubcategory) {
|
|
819
|
+
filteredProcedures = filteredProcedures.filter(
|
|
820
|
+
(procedure) => procedure.subcategory.id === filters.procedureSubcategory
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Filter by procedure technology if specified
|
|
825
|
+
if (filters.procedureTechnology) {
|
|
826
|
+
filteredProcedures = filteredProcedures.filter(
|
|
827
|
+
(procedure) => procedure.technology.id === filters.procedureTechnology
|
|
828
|
+
);
|
|
829
|
+
}
|
|
626
830
|
|
|
627
|
-
|
|
628
|
-
|
|
831
|
+
// Filter by price range if specified
|
|
832
|
+
if (filters.minPrice !== undefined) {
|
|
833
|
+
filteredProcedures = filteredProcedures.filter(
|
|
834
|
+
(procedure) => procedure.price >= filters.minPrice!
|
|
835
|
+
);
|
|
836
|
+
}
|
|
629
837
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
batchCount = 0;
|
|
635
|
-
}
|
|
636
|
-
} catch (error) {
|
|
637
|
-
console.error(`Error updating procedure ${procedure.id}:`, error);
|
|
638
|
-
}
|
|
838
|
+
if (filters.maxPrice !== undefined) {
|
|
839
|
+
filteredProcedures = filteredProcedures.filter(
|
|
840
|
+
(procedure) => procedure.price <= filters.maxPrice!
|
|
841
|
+
);
|
|
639
842
|
}
|
|
640
843
|
|
|
641
|
-
//
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
844
|
+
// Filter by rating if specified
|
|
845
|
+
if (filters.minRating !== undefined) {
|
|
846
|
+
filteredProcedures = filteredProcedures.filter(
|
|
847
|
+
(procedure) => procedure.reviewInfo.averageRating >= filters.minRating!
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (filters.maxRating !== undefined) {
|
|
852
|
+
filteredProcedures = filteredProcedures.filter(
|
|
853
|
+
(procedure) => procedure.reviewInfo.averageRating <= filters.maxRating!
|
|
854
|
+
);
|
|
645
855
|
}
|
|
646
856
|
|
|
647
|
-
return
|
|
857
|
+
return filteredProcedures;
|
|
648
858
|
}
|
|
649
859
|
}
|