@blackcode_sa/metaestetics-api 1.5.29 → 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 +126 -1
- package/dist/admin/index.d.ts +126 -1
- package/dist/admin/index.js +347 -10
- package/dist/admin/index.mjs +345 -10
- package/dist/index.d.mts +64 -71
- package/dist/index.d.ts +64 -71
- package/dist/index.js +323 -688
- package/dist/index.mjs +359 -728
- 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/patient/README.md +27 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/index.ts +9 -2
- 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/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +3 -126
- package/src/services/clinic/utils/clinic.utils.ts +2 -2
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +119 -395
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +332 -369
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
arrayRemove,
|
|
17
17
|
FieldValue,
|
|
18
18
|
orderBy,
|
|
19
|
+
limit,
|
|
20
|
+
startAfter,
|
|
21
|
+
QueryConstraint,
|
|
22
|
+
documentId,
|
|
19
23
|
} from "firebase/firestore";
|
|
20
24
|
import { BaseService } from "../base.service";
|
|
21
25
|
import {
|
|
@@ -64,6 +68,8 @@ import {
|
|
|
64
68
|
} from "../../backoffice/types";
|
|
65
69
|
import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
|
|
66
70
|
import { ProcedureReviewInfo } from "../../types/reviews";
|
|
71
|
+
import { distanceBetween, geohashQueryBounds } from "geofire-common";
|
|
72
|
+
import { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
|
|
67
73
|
|
|
68
74
|
export class ProcedureService extends BaseService {
|
|
69
75
|
private categoryService: CategoryService;
|
|
@@ -87,143 +93,8 @@ export class ProcedureService extends BaseService {
|
|
|
87
93
|
this.productService = productService;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
// Helper function to create ProcedureSummaryInfo
|
|
91
|
-
private _createProcedureSummaryInfo(
|
|
92
|
-
procedure: Procedure
|
|
93
|
-
): ProcedureSummaryInfo {
|
|
94
|
-
// Ensure nested objects and names exist before accessing them
|
|
95
|
-
const categoryName = procedure.category?.name || "N/A";
|
|
96
|
-
const subcategoryName = procedure.subcategory?.name || "N/A";
|
|
97
|
-
const technologyName = procedure.technology?.name || "N/A";
|
|
98
|
-
const clinicName = procedure.clinicInfo?.name || "N/A";
|
|
99
|
-
const practitionerName = procedure.doctorInfo?.name || "N/A";
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
id: procedure.id,
|
|
103
|
-
name: procedure.name,
|
|
104
|
-
description: procedure.description || "",
|
|
105
|
-
photo: "", // No photo source identified yet
|
|
106
|
-
family: procedure.family,
|
|
107
|
-
categoryName: categoryName,
|
|
108
|
-
subcategoryName: subcategoryName,
|
|
109
|
-
technologyName: technologyName,
|
|
110
|
-
price: procedure.price,
|
|
111
|
-
pricingMeasure: procedure.pricingMeasure,
|
|
112
|
-
currency: procedure.currency,
|
|
113
|
-
duration: procedure.duration,
|
|
114
|
-
clinicId: procedure.clinicBranchId,
|
|
115
|
-
clinicName: clinicName,
|
|
116
|
-
practitionerId: procedure.practitionerId,
|
|
117
|
-
practitionerName: practitionerName,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Helper to update practitioner's procedures
|
|
122
|
-
private async _updatePractitionerProcedures(
|
|
123
|
-
practitionerId: string,
|
|
124
|
-
procedureSummary: ProcedureSummaryInfo | null, // Null means remove
|
|
125
|
-
procedureIdToRemove?: string
|
|
126
|
-
) {
|
|
127
|
-
const practitionerRef = doc(
|
|
128
|
-
this.db,
|
|
129
|
-
PRACTITIONERS_COLLECTION,
|
|
130
|
-
practitionerId
|
|
131
|
-
);
|
|
132
|
-
const updateData: { [key: string]: FieldValue } = {};
|
|
133
|
-
|
|
134
|
-
if (procedureSummary) {
|
|
135
|
-
// Add/Update
|
|
136
|
-
updateData["procedures"] = arrayUnion(procedureSummary.id);
|
|
137
|
-
// Remove old summary first (by ID) then add new one to ensure update
|
|
138
|
-
updateData["proceduresInfo"] = arrayRemove(
|
|
139
|
-
...[
|
|
140
|
-
// Need to spread the arrayRemove arguments
|
|
141
|
-
// Create a 'matcher' object with only the ID for removal
|
|
142
|
-
{ id: procedureSummary.id },
|
|
143
|
-
]
|
|
144
|
-
);
|
|
145
|
-
// We'll add the new summary in a separate update to avoid conflicts if needed, or rely on a subsequent transaction/batch commit
|
|
146
|
-
} else if (procedureIdToRemove) {
|
|
147
|
-
// Remove
|
|
148
|
-
updateData["procedures"] = arrayRemove(procedureIdToRemove);
|
|
149
|
-
updateData["proceduresInfo"] = arrayRemove(
|
|
150
|
-
...[{ id: procedureIdToRemove }] // Use matcher for removal
|
|
151
|
-
);
|
|
152
|
-
} else {
|
|
153
|
-
return; // No operation needed
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
updateData["updatedAt"] = serverTimestamp();
|
|
157
|
-
|
|
158
|
-
// Perform the update
|
|
159
|
-
const batch = writeBatch(this.db);
|
|
160
|
-
batch.update(practitionerRef, updateData);
|
|
161
|
-
|
|
162
|
-
// If adding/updating, add the new summary info after removing the old one
|
|
163
|
-
if (procedureSummary) {
|
|
164
|
-
batch.update(practitionerRef, {
|
|
165
|
-
proceduresInfo: arrayUnion(procedureSummary),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
await batch.commit();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Helper to update clinic's procedures
|
|
173
|
-
private async _updateClinicProcedures(
|
|
174
|
-
clinicId: string,
|
|
175
|
-
procedureSummary: ProcedureSummaryInfo | null, // Null means remove
|
|
176
|
-
procedureIdToRemove?: string
|
|
177
|
-
) {
|
|
178
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
179
|
-
|
|
180
|
-
// Check if clinic exists before attempting update
|
|
181
|
-
const clinicSnap = await getDoc(clinicRef);
|
|
182
|
-
if (!clinicSnap.exists()) {
|
|
183
|
-
console.warn(
|
|
184
|
-
`Clinic ${clinicId} not found, skipping procedure aggregation update.`
|
|
185
|
-
);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const updateData: { [key: string]: FieldValue } = {};
|
|
190
|
-
|
|
191
|
-
if (procedureSummary) {
|
|
192
|
-
// Add/Update - Using arrayUnion for simplicity, assuming procedure IDs are unique in services array too
|
|
193
|
-
// Note: 'services' array might be deprecated or used differently now
|
|
194
|
-
updateData["services"] = arrayUnion(procedureSummary.id);
|
|
195
|
-
// Remove old summary first (by ID) then add new one to ensure update
|
|
196
|
-
updateData["proceduresInfo"] = arrayRemove(
|
|
197
|
-
...[{ id: procedureSummary.id }] // Use matcher for removal
|
|
198
|
-
);
|
|
199
|
-
} else if (procedureIdToRemove) {
|
|
200
|
-
// Remove
|
|
201
|
-
updateData["services"] = arrayRemove(procedureIdToRemove);
|
|
202
|
-
updateData["proceduresInfo"] = arrayRemove(
|
|
203
|
-
...[{ id: procedureIdToRemove }] // Use matcher for removal
|
|
204
|
-
);
|
|
205
|
-
} else {
|
|
206
|
-
return; // No operation needed
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
updateData["updatedAt"] = serverTimestamp();
|
|
210
|
-
|
|
211
|
-
// Perform the update
|
|
212
|
-
const batch = writeBatch(this.db);
|
|
213
|
-
batch.update(clinicRef, updateData);
|
|
214
|
-
|
|
215
|
-
// If adding/updating, add the new summary info after removing the old one
|
|
216
|
-
if (procedureSummary) {
|
|
217
|
-
batch.update(clinicRef, {
|
|
218
|
-
proceduresInfo: arrayUnion(procedureSummary),
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
await batch.commit();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
96
|
/**
|
|
226
|
-
* Creates a new procedure
|
|
97
|
+
* Creates a new procedure
|
|
227
98
|
* @param data - The data for creating a new procedure
|
|
228
99
|
* @returns The created procedure
|
|
229
100
|
*/
|
|
@@ -289,14 +160,13 @@ export class ProcedureService extends BaseService {
|
|
|
289
160
|
};
|
|
290
161
|
|
|
291
162
|
// Create aggregated doctor info for the procedure document
|
|
292
|
-
// Re-use logic from previous implementation or simplify
|
|
293
163
|
const doctorInfo = {
|
|
294
164
|
id: practitionerSnapshot.id,
|
|
295
165
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
296
166
|
description: practitioner.basicInfo.bio || "",
|
|
297
167
|
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
298
|
-
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
299
|
-
services: practitioner.procedures || [],
|
|
168
|
+
rating: practitioner.reviewInfo?.averageRating || 0,
|
|
169
|
+
services: practitioner.procedures || [],
|
|
300
170
|
};
|
|
301
171
|
|
|
302
172
|
// Create the procedure object
|
|
@@ -330,48 +200,14 @@ export class ProcedureService extends BaseService {
|
|
|
330
200
|
isActive: true, // Default to active
|
|
331
201
|
};
|
|
332
202
|
|
|
333
|
-
//
|
|
334
|
-
const batch = writeBatch(this.db);
|
|
335
|
-
|
|
336
|
-
// 1. Create the procedure document
|
|
203
|
+
// Create the procedure document
|
|
337
204
|
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
338
|
-
|
|
205
|
+
await setDoc(procedureRef, {
|
|
339
206
|
...newProcedure,
|
|
340
207
|
createdAt: serverTimestamp(),
|
|
341
208
|
updatedAt: serverTimestamp(),
|
|
342
209
|
});
|
|
343
210
|
|
|
344
|
-
// 2. Create the summary object AFTER the main object is defined
|
|
345
|
-
const procedureSummary = this._createProcedureSummaryInfo({
|
|
346
|
-
...newProcedure,
|
|
347
|
-
createdAt: new Date(), // Use placeholder date for summary creation
|
|
348
|
-
updatedAt: new Date(),
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// 3. Update Practitioner
|
|
352
|
-
const practitionerUpdateData: { [key: string]: FieldValue } = {
|
|
353
|
-
procedures: arrayUnion(procedureId),
|
|
354
|
-
proceduresInfo: arrayUnion(procedureSummary),
|
|
355
|
-
updatedAt: serverTimestamp(),
|
|
356
|
-
};
|
|
357
|
-
batch.update(practitionerRef, practitionerUpdateData);
|
|
358
|
-
|
|
359
|
-
// 4. Update Clinic
|
|
360
|
-
const clinicUpdateData: { [key: string]: FieldValue } = {
|
|
361
|
-
// services: arrayUnion(procedureId), // Decide if 'services' array is still needed
|
|
362
|
-
proceduresInfo: arrayUnion(procedureSummary),
|
|
363
|
-
// Potentially update clinic.doctors array if not already present
|
|
364
|
-
doctors: arrayUnion(validatedData.practitionerId),
|
|
365
|
-
// Potentially update clinic.doctorsInfo array
|
|
366
|
-
// This requires fetching existing doctorsInfo and adding/updating
|
|
367
|
-
// For simplicity now, we'll just add the procedure summary
|
|
368
|
-
updatedAt: serverTimestamp(),
|
|
369
|
-
};
|
|
370
|
-
batch.update(clinicRef, clinicUpdateData);
|
|
371
|
-
|
|
372
|
-
// --- Transaction/Batch Commit ---
|
|
373
|
-
await batch.commit();
|
|
374
|
-
|
|
375
211
|
// Return the created procedure (fetch again to get server timestamps)
|
|
376
212
|
const savedDoc = await getDoc(procedureRef);
|
|
377
213
|
return savedDoc.data() as Procedure;
|
|
@@ -428,7 +264,7 @@ export class ProcedureService extends BaseService {
|
|
|
428
264
|
}
|
|
429
265
|
|
|
430
266
|
/**
|
|
431
|
-
* Updates a procedure
|
|
267
|
+
* Updates a procedure
|
|
432
268
|
* @param id - The ID of the procedure to update
|
|
433
269
|
* @param data - The data to update the procedure with
|
|
434
270
|
* @returns The updated procedure
|
|
@@ -578,131 +414,19 @@ export class ProcedureService extends BaseService {
|
|
|
578
414
|
console.warn("Attempted to update product without a valid technologyId");
|
|
579
415
|
}
|
|
580
416
|
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// 1. Update the main procedure document
|
|
585
|
-
batch.update(procedureRef, {
|
|
417
|
+
// Update the procedure document
|
|
418
|
+
await updateDoc(procedureRef, {
|
|
586
419
|
...updatedProcedureData,
|
|
587
420
|
updatedAt: serverTimestamp(),
|
|
588
421
|
});
|
|
589
422
|
|
|
590
|
-
// 2. Create the updated summary info object
|
|
591
|
-
// We need the final state of the procedure for the summary
|
|
592
|
-
const finalProcedureStateForSummary = {
|
|
593
|
-
...existingProcedure,
|
|
594
|
-
...updatedProcedureData, // Apply updates
|
|
595
|
-
// Ensure nested objects needed for summary are present
|
|
596
|
-
category: updatedProcedureData.category || existingProcedure.category,
|
|
597
|
-
subcategory:
|
|
598
|
-
updatedProcedureData.subcategory || existingProcedure.subcategory,
|
|
599
|
-
technology:
|
|
600
|
-
updatedProcedureData.technology || existingProcedure.technology,
|
|
601
|
-
product: updatedProcedureData.product || existingProcedure.product,
|
|
602
|
-
clinicInfo:
|
|
603
|
-
updatedProcedureData.clinicInfo || existingProcedure.clinicInfo,
|
|
604
|
-
doctorInfo:
|
|
605
|
-
updatedProcedureData.doctorInfo || existingProcedure.doctorInfo,
|
|
606
|
-
practitionerId:
|
|
607
|
-
validatedData.practitionerId || existingProcedure.practitionerId, // Use potentially updated IDs
|
|
608
|
-
clinicBranchId:
|
|
609
|
-
validatedData.clinicBranchId || existingProcedure.clinicBranchId,
|
|
610
|
-
} as Procedure; // Cast because we're merging potentially partial data
|
|
611
|
-
|
|
612
|
-
const updatedProcedureSummary = this._createProcedureSummaryInfo(
|
|
613
|
-
finalProcedureStateForSummary
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
// 3. Update Practitioner(s)
|
|
617
|
-
if (practitionerChanged) {
|
|
618
|
-
// Remove from old practitioner
|
|
619
|
-
const oldPractitionerRef = doc(
|
|
620
|
-
this.db,
|
|
621
|
-
PRACTITIONERS_COLLECTION,
|
|
622
|
-
oldPractitionerId
|
|
623
|
-
);
|
|
624
|
-
batch.update(oldPractitionerRef, {
|
|
625
|
-
procedures: arrayRemove(id),
|
|
626
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
627
|
-
updatedAt: serverTimestamp(),
|
|
628
|
-
});
|
|
629
|
-
// Add to new practitioner
|
|
630
|
-
const newPractitionerRef = doc(
|
|
631
|
-
this.db,
|
|
632
|
-
PRACTITIONERS_COLLECTION,
|
|
633
|
-
updatedProcedureSummary.practitionerId
|
|
634
|
-
);
|
|
635
|
-
batch.update(newPractitionerRef, {
|
|
636
|
-
procedures: arrayUnion(id),
|
|
637
|
-
proceduresInfo: arrayUnion(updatedProcedureSummary),
|
|
638
|
-
updatedAt: serverTimestamp(),
|
|
639
|
-
});
|
|
640
|
-
} else {
|
|
641
|
-
// Update in place for the current practitioner
|
|
642
|
-
const currentPractitionerRef = doc(
|
|
643
|
-
this.db,
|
|
644
|
-
PRACTITIONERS_COLLECTION,
|
|
645
|
-
oldPractitionerId
|
|
646
|
-
);
|
|
647
|
-
// Remove old first
|
|
648
|
-
batch.update(currentPractitionerRef, {
|
|
649
|
-
proceduresInfo: arrayRemove(...[{ id: id }]),
|
|
650
|
-
updatedAt: serverTimestamp(),
|
|
651
|
-
});
|
|
652
|
-
// Add updated
|
|
653
|
-
batch.update(currentPractitionerRef, {
|
|
654
|
-
proceduresInfo: arrayUnion(updatedProcedureSummary),
|
|
655
|
-
updatedAt: serverTimestamp(),
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// 4. Update Clinic(s)
|
|
660
|
-
if (clinicChanged) {
|
|
661
|
-
// Remove from old clinic
|
|
662
|
-
const oldClinicRef = doc(this.db, CLINICS_COLLECTION, oldClinicId);
|
|
663
|
-
batch.update(oldClinicRef, {
|
|
664
|
-
// services: arrayRemove(id),
|
|
665
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
666
|
-
updatedAt: serverTimestamp(),
|
|
667
|
-
// Potentially remove from clinic.doctors and clinic.doctorsInfo if practitioner also changed or was last one for this clinic
|
|
668
|
-
});
|
|
669
|
-
// Add to new clinic
|
|
670
|
-
const newClinicRef = doc(
|
|
671
|
-
this.db,
|
|
672
|
-
CLINICS_COLLECTION,
|
|
673
|
-
updatedProcedureSummary.clinicId
|
|
674
|
-
);
|
|
675
|
-
batch.update(newClinicRef, {
|
|
676
|
-
// services: arrayUnion(id),
|
|
677
|
-
proceduresInfo: arrayUnion(updatedProcedureSummary),
|
|
678
|
-
doctors: arrayUnion(updatedProcedureSummary.practitionerId), // Ensure practitioner is listed
|
|
679
|
-
updatedAt: serverTimestamp(),
|
|
680
|
-
});
|
|
681
|
-
} else {
|
|
682
|
-
// Update in place for the current clinic
|
|
683
|
-
const currentClinicRef = doc(this.db, CLINICS_COLLECTION, oldClinicId);
|
|
684
|
-
// Remove old first
|
|
685
|
-
batch.update(currentClinicRef, {
|
|
686
|
-
proceduresInfo: arrayRemove(...[{ id: id }]),
|
|
687
|
-
updatedAt: serverTimestamp(),
|
|
688
|
-
});
|
|
689
|
-
// Add updated
|
|
690
|
-
batch.update(currentClinicRef, {
|
|
691
|
-
proceduresInfo: arrayUnion(updatedProcedureSummary),
|
|
692
|
-
updatedAt: serverTimestamp(),
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// --- Commit Batch ---
|
|
697
|
-
await batch.commit();
|
|
698
|
-
|
|
699
423
|
// Return the updated procedure
|
|
700
424
|
const updatedSnapshot = await getDoc(procedureRef);
|
|
701
425
|
return updatedSnapshot.data() as Procedure;
|
|
702
426
|
}
|
|
703
427
|
|
|
704
428
|
/**
|
|
705
|
-
* Deactivates a procedure (soft delete)
|
|
429
|
+
* Deactivates a procedure (soft delete)
|
|
706
430
|
* @param id - The ID of the procedure to deactivate
|
|
707
431
|
*/
|
|
708
432
|
async deactivateProcedure(id: string): Promise<void> {
|
|
@@ -712,51 +436,16 @@ export class ProcedureService extends BaseService {
|
|
|
712
436
|
console.warn(`Procedure ${id} not found for deactivation.`);
|
|
713
437
|
return;
|
|
714
438
|
}
|
|
715
|
-
const procedure = procedureSnap.data() as Procedure;
|
|
716
|
-
|
|
717
|
-
// We treat deactivation like a deletion for the aggregated lists
|
|
718
|
-
// Alternatively, keep inactive procedures but filter them client-side
|
|
719
|
-
|
|
720
|
-
// --- Perform updates using a batch ---
|
|
721
|
-
const batch = writeBatch(this.db);
|
|
722
439
|
|
|
723
|
-
//
|
|
724
|
-
|
|
440
|
+
// Mark procedure as inactive
|
|
441
|
+
await updateDoc(procedureRef, {
|
|
725
442
|
isActive: false,
|
|
726
443
|
updatedAt: serverTimestamp(),
|
|
727
444
|
});
|
|
728
|
-
|
|
729
|
-
// 2. Remove from Practitioner aggregates
|
|
730
|
-
const practitionerRef = doc(
|
|
731
|
-
this.db,
|
|
732
|
-
PRACTITIONERS_COLLECTION,
|
|
733
|
-
procedure.practitionerId
|
|
734
|
-
);
|
|
735
|
-
batch.update(practitionerRef, {
|
|
736
|
-
procedures: arrayRemove(id),
|
|
737
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
738
|
-
updatedAt: serverTimestamp(),
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// 3. Remove from Clinic aggregates
|
|
742
|
-
const clinicRef = doc(
|
|
743
|
-
this.db,
|
|
744
|
-
CLINICS_COLLECTION,
|
|
745
|
-
procedure.clinicBranchId
|
|
746
|
-
);
|
|
747
|
-
batch.update(clinicRef, {
|
|
748
|
-
// services: arrayRemove(id),
|
|
749
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
750
|
-
updatedAt: serverTimestamp(),
|
|
751
|
-
// Potentially update clinic.doctors/doctorsInfo if this was the last active procedure for this doctor at this clinic
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// --- Commit Batch ---
|
|
755
|
-
await batch.commit();
|
|
756
445
|
}
|
|
757
446
|
|
|
758
447
|
/**
|
|
759
|
-
* Deletes a procedure permanently
|
|
448
|
+
* Deletes a procedure permanently
|
|
760
449
|
* @param id - The ID of the procedure to delete
|
|
761
450
|
* @returns A boolean indicating if the deletion was successful
|
|
762
451
|
*/
|
|
@@ -769,46 +458,8 @@ export class ProcedureService extends BaseService {
|
|
|
769
458
|
return false;
|
|
770
459
|
}
|
|
771
460
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
// --- Perform updates using a batch ---
|
|
775
|
-
const batch = writeBatch(this.db);
|
|
776
|
-
|
|
777
|
-
// 1. Remove from Practitioner aggregates
|
|
778
|
-
if (procedure.practitionerId) {
|
|
779
|
-
const practitionerRef = doc(
|
|
780
|
-
this.db,
|
|
781
|
-
PRACTITIONERS_COLLECTION,
|
|
782
|
-
procedure.practitionerId
|
|
783
|
-
);
|
|
784
|
-
batch.update(practitionerRef, {
|
|
785
|
-
procedures: arrayRemove(id),
|
|
786
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
787
|
-
updatedAt: serverTimestamp(),
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// 2. Remove from Clinic aggregates
|
|
792
|
-
if (procedure.clinicBranchId) {
|
|
793
|
-
const clinicRef = doc(
|
|
794
|
-
this.db,
|
|
795
|
-
CLINICS_COLLECTION,
|
|
796
|
-
procedure.clinicBranchId
|
|
797
|
-
);
|
|
798
|
-
batch.update(clinicRef, {
|
|
799
|
-
// services: arrayRemove(id),
|
|
800
|
-
proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
|
|
801
|
-
updatedAt: serverTimestamp(),
|
|
802
|
-
// Potentially update clinic.doctors/doctorsInfo
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// 3. Delete the procedure document itself
|
|
807
|
-
batch.delete(procedureRef);
|
|
808
|
-
|
|
809
|
-
// --- Commit Batch ---
|
|
810
|
-
await batch.commit();
|
|
811
|
-
|
|
461
|
+
// Delete the procedure document
|
|
462
|
+
await deleteDoc(procedureRef);
|
|
812
463
|
return true;
|
|
813
464
|
}
|
|
814
465
|
|
|
@@ -893,4 +544,316 @@ export class ProcedureService extends BaseService {
|
|
|
893
544
|
throw error;
|
|
894
545
|
}
|
|
895
546
|
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Searches and filters procedures based on multiple criteria
|
|
550
|
+
*
|
|
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
|
|
568
|
+
*/
|
|
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
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Determine if we're doing a geo query or a regular query
|
|
596
|
+
const isGeoQuery =
|
|
597
|
+
filters.location && filters.radiusInKm && filters.radiusInKm > 0;
|
|
598
|
+
|
|
599
|
+
// Initialize base constraints
|
|
600
|
+
const constraints: QueryConstraint[] = [];
|
|
601
|
+
|
|
602
|
+
// Add active status filter (default to active if not specified)
|
|
603
|
+
if (filters.isActive !== undefined) {
|
|
604
|
+
constraints.push(where("isActive", "==", filters.isActive));
|
|
605
|
+
} else {
|
|
606
|
+
constraints.push(where("isActive", "==", true));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Filter by procedure family if specified
|
|
610
|
+
if (filters.procedureFamily) {
|
|
611
|
+
constraints.push(where("family", "==", filters.procedureFamily));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Add ordering to make pagination consistent
|
|
615
|
+
constraints.push(orderBy(documentId()));
|
|
616
|
+
|
|
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
|
+
}
|
|
624
|
+
|
|
625
|
+
let proceduresResult: (Procedure & { distance?: number })[] = [];
|
|
626
|
+
let lastVisibleDoc = null;
|
|
627
|
+
|
|
628
|
+
// For geo queries, we need a different approach
|
|
629
|
+
if (isGeoQuery) {
|
|
630
|
+
const center = filters.location!;
|
|
631
|
+
const radiusInKm = filters.radiusInKm!;
|
|
632
|
+
|
|
633
|
+
// Get the geohash query bounds
|
|
634
|
+
const bounds = geohashQueryBounds(
|
|
635
|
+
[center.latitude, center.longitude],
|
|
636
|
+
radiusInKm * 1000 // Convert to meters
|
|
637
|
+
);
|
|
638
|
+
|
|
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);
|
|
656
|
+
|
|
657
|
+
console.log(
|
|
658
|
+
`[PROCEDURE_SERVICE] Found ${querySnapshot.docs.length} procedures in geo bound`
|
|
659
|
+
);
|
|
660
|
+
|
|
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
|
+
}
|
|
687
|
+
|
|
688
|
+
// Apply additional filters that couldn't be applied in the query
|
|
689
|
+
let filteredProcedures = matchingProcedures;
|
|
690
|
+
|
|
691
|
+
// Apply remaining filters in memory
|
|
692
|
+
filteredProcedures = this.applyInMemoryFilters(
|
|
693
|
+
filteredProcedures,
|
|
694
|
+
filters
|
|
695
|
+
);
|
|
696
|
+
|
|
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
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
// Set last document for next pagination
|
|
720
|
+
lastVisibleDoc =
|
|
721
|
+
paginatedProcedures.length > 0
|
|
722
|
+
? paginatedProcedures[paginatedProcedures.length - 1]
|
|
723
|
+
: null;
|
|
724
|
+
|
|
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
|
|
734
|
+
);
|
|
735
|
+
const querySnapshot = await getDocs(q);
|
|
736
|
+
|
|
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;
|
|
754
|
+
|
|
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
|
+
}
|
|
799
|
+
|
|
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)
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Filter by procedure category if specified
|
|
811
|
+
if (filters.procedureCategory) {
|
|
812
|
+
filteredProcedures = filteredProcedures.filter(
|
|
813
|
+
(procedure) => procedure.category.id === filters.procedureCategory
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
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
|
+
}
|
|
830
|
+
|
|
831
|
+
// Filter by price range if specified
|
|
832
|
+
if (filters.minPrice !== undefined) {
|
|
833
|
+
filteredProcedures = filteredProcedures.filter(
|
|
834
|
+
(procedure) => procedure.price >= filters.minPrice!
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (filters.maxPrice !== undefined) {
|
|
839
|
+
filteredProcedures = filteredProcedures.filter(
|
|
840
|
+
(procedure) => procedure.price <= filters.maxPrice!
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
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
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return filteredProcedures;
|
|
858
|
+
}
|
|
896
859
|
}
|