@blackcode_sa/metaestetics-api 1.5.28 → 1.5.29
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 +1199 -1
- package/dist/admin/index.d.ts +1199 -1
- package/dist/admin/index.js +1337 -2
- package/dist/admin/index.mjs +1333 -2
- package/dist/backoffice/index.d.mts +99 -7
- package/dist/backoffice/index.d.ts +99 -7
- package/dist/index.d.mts +4035 -2364
- package/dist/index.d.ts +4035 -2364
- package/dist/index.js +2616 -1929
- package/dist/index.mjs +2646 -1952
- package/package.json +1 -1
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
- package/src/admin/index.ts +53 -4
- package/src/index.ts +28 -4
- package/src/services/clinic/clinic.service.ts +320 -107
- package/src/services/clinic/utils/clinic.utils.ts +66 -117
- 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/practitioner.service.ts +616 -5
- package/src/services/procedure/procedure.service.ts +599 -352
- 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,10 @@ import {
|
|
|
12
12
|
serverTimestamp,
|
|
13
13
|
DocumentData,
|
|
14
14
|
writeBatch,
|
|
15
|
+
arrayUnion,
|
|
16
|
+
arrayRemove,
|
|
17
|
+
FieldValue,
|
|
18
|
+
orderBy,
|
|
15
19
|
} from "firebase/firestore";
|
|
16
20
|
import { BaseService } from "../base.service";
|
|
17
21
|
import {
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
CreateProcedureData,
|
|
20
24
|
UpdateProcedureData,
|
|
21
25
|
PROCEDURES_COLLECTION,
|
|
26
|
+
ProcedureSummaryInfo,
|
|
22
27
|
} from "../../types/procedure";
|
|
23
28
|
import {
|
|
24
29
|
createProcedureSchema,
|
|
@@ -57,7 +62,8 @@ import {
|
|
|
57
62
|
CertificationSpecialty,
|
|
58
63
|
ProcedureFamily,
|
|
59
64
|
} from "../../backoffice/types";
|
|
60
|
-
import { CLINICS_COLLECTION } from "../../types/clinic";
|
|
65
|
+
import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
|
|
66
|
+
import { ProcedureReviewInfo } from "../../types/reviews";
|
|
61
67
|
|
|
62
68
|
export class ProcedureService extends BaseService {
|
|
63
69
|
private categoryService: CategoryService;
|
|
@@ -81,16 +87,150 @@ export class ProcedureService extends BaseService {
|
|
|
81
87
|
this.productService = productService;
|
|
82
88
|
}
|
|
83
89
|
|
|
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
|
+
|
|
84
225
|
/**
|
|
85
|
-
* Creates a new procedure
|
|
226
|
+
* Creates a new procedure and updates related practitioner/clinic aggregates
|
|
86
227
|
* @param data - The data for creating a new procedure
|
|
87
228
|
* @returns The created procedure
|
|
88
229
|
*/
|
|
89
230
|
async createProcedure(data: CreateProcedureData): Promise<Procedure> {
|
|
90
|
-
// Validate input data
|
|
91
231
|
const validatedData = createProcedureSchema.parse(data);
|
|
92
232
|
|
|
93
|
-
// Get references to related entities
|
|
233
|
+
// Get references to related entities (Category, Subcategory, Technology, Product)
|
|
94
234
|
const [category, subcategory, technology, product] = await Promise.all([
|
|
95
235
|
this.categoryService.getById(validatedData.categoryId),
|
|
96
236
|
this.subcategoryService.getById(
|
|
@@ -105,75 +245,66 @@ export class ProcedureService extends BaseService {
|
|
|
105
245
|
]);
|
|
106
246
|
|
|
107
247
|
if (!category || !subcategory || !technology || !product) {
|
|
108
|
-
throw new Error("One or more required entities not found");
|
|
248
|
+
throw new Error("One or more required base entities not found");
|
|
109
249
|
}
|
|
110
250
|
|
|
111
|
-
// Get clinic and practitioner information
|
|
251
|
+
// Get clinic and practitioner information for aggregation
|
|
112
252
|
const clinicRef = doc(
|
|
113
253
|
this.db,
|
|
114
254
|
CLINICS_COLLECTION,
|
|
115
255
|
validatedData.clinicBranchId
|
|
116
256
|
);
|
|
117
257
|
const clinicSnapshot = await getDoc(clinicRef);
|
|
118
|
-
|
|
119
258
|
if (!clinicSnapshot.exists()) {
|
|
120
259
|
throw new Error(
|
|
121
260
|
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
122
261
|
);
|
|
123
262
|
}
|
|
263
|
+
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
124
264
|
|
|
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
265
|
const practitionerRef = doc(
|
|
142
266
|
this.db,
|
|
143
267
|
PRACTITIONERS_COLLECTION,
|
|
144
268
|
validatedData.practitionerId
|
|
145
269
|
);
|
|
146
270
|
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
147
|
-
|
|
148
271
|
if (!practitionerSnapshot.exists()) {
|
|
149
272
|
throw new Error(
|
|
150
273
|
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
151
274
|
);
|
|
152
275
|
}
|
|
276
|
+
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
153
277
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
278
|
+
// Create aggregated clinic info for the procedure document
|
|
279
|
+
const clinicInfo = {
|
|
280
|
+
id: clinicSnapshot.id,
|
|
281
|
+
name: clinic.name,
|
|
282
|
+
description: clinic.description || "",
|
|
283
|
+
featuredPhoto:
|
|
284
|
+
clinic.featuredPhotos && clinic.featuredPhotos.length > 0
|
|
285
|
+
? clinic.featuredPhotos[0]
|
|
286
|
+
: clinic.coverPhoto || "",
|
|
287
|
+
location: clinic.location,
|
|
288
|
+
contactInfo: clinic.contactInfo,
|
|
289
|
+
};
|
|
160
290
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
291
|
+
// Create aggregated doctor info for the procedure document
|
|
292
|
+
// Re-use logic from previous implementation or simplify
|
|
293
|
+
const doctorInfo = {
|
|
294
|
+
id: practitionerSnapshot.id,
|
|
295
|
+
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
296
|
+
description: practitioner.basicInfo.bio || "",
|
|
297
|
+
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
298
|
+
rating: practitioner.reviewInfo?.averageRating || 0, // Example rating source
|
|
299
|
+
services: practitioner.procedures || [], // Link services to practitioner's procedures
|
|
300
|
+
};
|
|
172
301
|
|
|
173
302
|
// Create the procedure object
|
|
174
|
-
const
|
|
303
|
+
const procedureId = this.generateId();
|
|
304
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
305
|
+
id: procedureId,
|
|
175
306
|
...validatedData,
|
|
176
|
-
category,
|
|
307
|
+
category, // Embed full objects
|
|
177
308
|
subcategory,
|
|
178
309
|
technology,
|
|
179
310
|
product,
|
|
@@ -183,24 +314,67 @@ export class ProcedureService extends BaseService {
|
|
|
183
314
|
postRequirements: technology.requirements.post,
|
|
184
315
|
certificationRequirement: technology.certificationRequirement,
|
|
185
316
|
documentationTemplates: technology.documentationTemplates || [],
|
|
186
|
-
clinicInfo,
|
|
187
|
-
doctorInfo,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
317
|
+
clinicInfo, // Embed aggregated info
|
|
318
|
+
doctorInfo, // Embed aggregated info
|
|
319
|
+
reviewInfo: {
|
|
320
|
+
// Default empty reviews
|
|
321
|
+
totalReviews: 0,
|
|
322
|
+
averageRating: 0,
|
|
323
|
+
effectivenessOfTreatment: 0,
|
|
324
|
+
outcomeExplanation: 0,
|
|
325
|
+
painManagement: 0,
|
|
326
|
+
followUpCare: 0,
|
|
327
|
+
valueForMoney: 0,
|
|
328
|
+
recommendationPercentage: 0,
|
|
329
|
+
},
|
|
330
|
+
isActive: true, // Default to active
|
|
191
331
|
};
|
|
192
332
|
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
333
|
+
// --- Transaction/Batch Start ---
|
|
334
|
+
const batch = writeBatch(this.db);
|
|
335
|
+
|
|
336
|
+
// 1. Create the procedure document
|
|
337
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
|
|
338
|
+
batch.set(procedureRef, {
|
|
339
|
+
...newProcedure,
|
|
199
340
|
createdAt: serverTimestamp(),
|
|
200
341
|
updatedAt: serverTimestamp(),
|
|
201
342
|
});
|
|
202
343
|
|
|
203
|
-
|
|
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
|
+
// Return the created procedure (fetch again to get server timestamps)
|
|
376
|
+
const savedDoc = await getDoc(procedureRef);
|
|
377
|
+
return savedDoc.data() as Procedure;
|
|
204
378
|
}
|
|
205
379
|
|
|
206
380
|
/**
|
|
@@ -254,73 +428,394 @@ export class ProcedureService extends BaseService {
|
|
|
254
428
|
}
|
|
255
429
|
|
|
256
430
|
/**
|
|
257
|
-
* Updates a procedure
|
|
431
|
+
* Updates a procedure and its related aggregates in Practitioner and Clinic docs
|
|
258
432
|
* @param id - The ID of the procedure to update
|
|
259
|
-
* @param data - The data to update
|
|
433
|
+
* @param data - The data to update the procedure with
|
|
260
434
|
* @returns The updated procedure
|
|
261
435
|
*/
|
|
262
436
|
async updateProcedure(
|
|
263
437
|
id: string,
|
|
264
438
|
data: UpdateProcedureData
|
|
265
439
|
): Promise<Procedure> {
|
|
266
|
-
// Validate input data
|
|
267
440
|
const validatedData = updateProcedureSchema.parse(data);
|
|
441
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
442
|
+
const procedureSnapshot = await getDoc(procedureRef);
|
|
268
443
|
|
|
269
|
-
|
|
270
|
-
const existingProcedure = await this.getProcedure(id);
|
|
271
|
-
if (!existingProcedure) {
|
|
444
|
+
if (!procedureSnapshot.exists()) {
|
|
272
445
|
throw new Error(`Procedure with ID ${id} not found`);
|
|
273
446
|
}
|
|
274
447
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
448
|
+
const existingProcedure = procedureSnapshot.data() as Procedure;
|
|
449
|
+
let updatedProcedureData: Partial<Procedure> = { ...validatedData };
|
|
450
|
+
|
|
451
|
+
let practitionerChanged = false;
|
|
452
|
+
let clinicChanged = false;
|
|
453
|
+
const oldPractitionerId = existingProcedure.practitionerId;
|
|
454
|
+
const oldClinicId = existingProcedure.clinicBranchId;
|
|
455
|
+
let newPractitioner: Practitioner | null = null;
|
|
456
|
+
let newClinic: Clinic | null = null;
|
|
457
|
+
|
|
458
|
+
// --- Prepare updates and fetch new related data if IDs change ---
|
|
459
|
+
|
|
460
|
+
// Handle Practitioner Change
|
|
461
|
+
if (
|
|
462
|
+
validatedData.practitionerId &&
|
|
463
|
+
validatedData.practitionerId !== oldPractitionerId
|
|
464
|
+
) {
|
|
465
|
+
practitionerChanged = true;
|
|
466
|
+
const newPractitionerRef = doc(
|
|
467
|
+
this.db,
|
|
468
|
+
PRACTITIONERS_COLLECTION,
|
|
469
|
+
validatedData.practitionerId
|
|
470
|
+
);
|
|
471
|
+
const newPractitionerSnap = await getDoc(newPractitionerRef);
|
|
472
|
+
if (!newPractitionerSnap.exists())
|
|
473
|
+
throw new Error(
|
|
474
|
+
`New Practitioner ${validatedData.practitionerId} not found`
|
|
475
|
+
);
|
|
476
|
+
newPractitioner = newPractitionerSnap.data() as Practitioner;
|
|
477
|
+
// Update doctorInfo within the procedure document
|
|
478
|
+
updatedProcedureData.doctorInfo = {
|
|
479
|
+
id: newPractitioner.id,
|
|
480
|
+
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
481
|
+
description: newPractitioner.basicInfo.bio || "",
|
|
482
|
+
photo: newPractitioner.basicInfo.profileImageUrl || "",
|
|
483
|
+
rating: newPractitioner.reviewInfo?.averageRating || 0,
|
|
484
|
+
services: newPractitioner.procedures || [],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Handle Clinic Change
|
|
489
|
+
if (
|
|
490
|
+
validatedData.clinicBranchId &&
|
|
491
|
+
validatedData.clinicBranchId !== oldClinicId
|
|
492
|
+
) {
|
|
493
|
+
clinicChanged = true;
|
|
494
|
+
const newClinicRef = doc(
|
|
495
|
+
this.db,
|
|
496
|
+
CLINICS_COLLECTION,
|
|
497
|
+
validatedData.clinicBranchId
|
|
498
|
+
);
|
|
499
|
+
const newClinicSnap = await getDoc(newClinicRef);
|
|
500
|
+
if (!newClinicSnap.exists())
|
|
501
|
+
throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
|
|
502
|
+
newClinic = newClinicSnap.data() as Clinic;
|
|
503
|
+
// Update clinicInfo within the procedure document
|
|
504
|
+
updatedProcedureData.clinicInfo = {
|
|
505
|
+
id: newClinic.id,
|
|
506
|
+
name: newClinic.name,
|
|
507
|
+
description: newClinic.description || "",
|
|
508
|
+
featuredPhoto:
|
|
509
|
+
newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
|
|
510
|
+
? newClinic.featuredPhotos[0]
|
|
511
|
+
: newClinic.coverPhoto || "",
|
|
512
|
+
location: newClinic.location,
|
|
513
|
+
contactInfo: newClinic.contactInfo,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Handle Category/Subcategory/Technology/Product Changes
|
|
518
|
+
let finalCategoryId = existingProcedure.category.id;
|
|
519
|
+
if (validatedData.categoryId) {
|
|
520
|
+
const category = await this.categoryService.getById(
|
|
521
|
+
validatedData.categoryId
|
|
522
|
+
);
|
|
523
|
+
if (!category)
|
|
524
|
+
throw new Error(`Category ${validatedData.categoryId} not found`);
|
|
525
|
+
updatedProcedureData.category = category;
|
|
526
|
+
finalCategoryId = category.id; // Update finalCategoryId if category changed
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Only fetch subcategory if its ID is provided AND we have a valid finalCategoryId
|
|
530
|
+
if (validatedData.subcategoryId && finalCategoryId) {
|
|
531
|
+
const subcategory = await this.subcategoryService.getById(
|
|
532
|
+
finalCategoryId,
|
|
533
|
+
validatedData.subcategoryId
|
|
534
|
+
);
|
|
535
|
+
if (!subcategory)
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
|
|
538
|
+
);
|
|
539
|
+
updatedProcedureData.subcategory = subcategory;
|
|
540
|
+
} else if (validatedData.subcategoryId) {
|
|
541
|
+
console.warn(
|
|
542
|
+
"Attempted to update subcategory without a valid categoryId"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let finalTechnologyId = existingProcedure.technology.id;
|
|
547
|
+
if (validatedData.technologyId) {
|
|
548
|
+
const technology = await this.technologyService.getById(
|
|
549
|
+
validatedData.technologyId
|
|
550
|
+
);
|
|
551
|
+
if (!technology)
|
|
552
|
+
throw new Error(`Technology ${validatedData.technologyId} not found`);
|
|
553
|
+
updatedProcedureData.technology = technology;
|
|
554
|
+
finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
|
|
555
|
+
// Update related fields derived from technology
|
|
556
|
+
updatedProcedureData.blockingConditions = technology.blockingConditions;
|
|
557
|
+
updatedProcedureData.treatmentBenefits = technology.benefits;
|
|
558
|
+
updatedProcedureData.preRequirements = technology.requirements.pre;
|
|
559
|
+
updatedProcedureData.postRequirements = technology.requirements.post;
|
|
560
|
+
updatedProcedureData.certificationRequirement =
|
|
561
|
+
technology.certificationRequirement;
|
|
562
|
+
updatedProcedureData.documentationTemplates =
|
|
563
|
+
technology.documentationTemplates || [];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Only fetch product if its ID is provided AND we have a valid finalTechnologyId
|
|
567
|
+
if (validatedData.productId && finalTechnologyId) {
|
|
568
|
+
const product = await this.productService.getById(
|
|
569
|
+
finalTechnologyId,
|
|
570
|
+
validatedData.productId
|
|
571
|
+
);
|
|
572
|
+
if (!product)
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
|
|
575
|
+
);
|
|
576
|
+
updatedProcedureData.product = product;
|
|
577
|
+
} else if (validatedData.productId) {
|
|
578
|
+
console.warn("Attempted to update product without a valid technologyId");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// --- Perform updates using a batch ---
|
|
582
|
+
const batch = writeBatch(this.db);
|
|
583
|
+
|
|
584
|
+
// 1. Update the main procedure document
|
|
585
|
+
batch.update(procedureRef, {
|
|
586
|
+
...updatedProcedureData,
|
|
279
587
|
updatedAt: serverTimestamp(),
|
|
280
588
|
});
|
|
281
589
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
return {
|
|
590
|
+
// 2. Create the updated summary info object
|
|
591
|
+
// We need the final state of the procedure for the summary
|
|
592
|
+
const finalProcedureStateForSummary = {
|
|
286
593
|
...existingProcedure,
|
|
287
|
-
...
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
// Return the updated procedure
|
|
700
|
+
const updatedSnapshot = await getDoc(procedureRef);
|
|
701
|
+
return updatedSnapshot.data() as Procedure;
|
|
290
702
|
}
|
|
291
703
|
|
|
292
704
|
/**
|
|
293
|
-
* Deactivates a procedure
|
|
705
|
+
* Deactivates a procedure (soft delete) and updates aggregates
|
|
294
706
|
* @param id - The ID of the procedure to deactivate
|
|
295
707
|
*/
|
|
296
708
|
async deactivateProcedure(id: string): Promise<void> {
|
|
297
|
-
const
|
|
298
|
-
await
|
|
709
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
710
|
+
const procedureSnap = await getDoc(procedureRef);
|
|
711
|
+
if (!procedureSnap.exists()) {
|
|
712
|
+
console.warn(`Procedure ${id} not found for deactivation.`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
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
|
+
|
|
723
|
+
// 1. Mark procedure as inactive
|
|
724
|
+
batch.update(procedureRef, {
|
|
299
725
|
isActive: false,
|
|
300
726
|
updatedAt: serverTimestamp(),
|
|
301
727
|
});
|
|
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
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Deletes a procedure permanently and updates related aggregates
|
|
760
|
+
* @param id - The ID of the procedure to delete
|
|
761
|
+
* @returns A boolean indicating if the deletion was successful
|
|
762
|
+
*/
|
|
763
|
+
async deleteProcedure(id: string): Promise<boolean> {
|
|
764
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
765
|
+
const procedureSnapshot = await getDoc(procedureRef);
|
|
766
|
+
|
|
767
|
+
if (!procedureSnapshot.exists()) {
|
|
768
|
+
// Already deleted or never existed
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const procedure = procedureSnapshot.data() as Procedure;
|
|
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
|
+
|
|
812
|
+
return true;
|
|
302
813
|
}
|
|
303
814
|
|
|
304
815
|
/**
|
|
305
816
|
* Gets all procedures that a practitioner is certified to perform
|
|
306
817
|
* @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"]
|
|
818
|
+
* @returns Object containing allowed technologies, families, categories, subcategories
|
|
324
819
|
*/
|
|
325
820
|
async getAllowedTechnologies(practitioner: Practitioner): Promise<{
|
|
326
821
|
technologies: Technology[];
|
|
@@ -328,7 +823,7 @@ export class ProcedureService extends BaseService {
|
|
|
328
823
|
categories: string[];
|
|
329
824
|
subcategories: string[];
|
|
330
825
|
}> {
|
|
331
|
-
//
|
|
826
|
+
// This logic depends on TechnologyService and remains valid
|
|
332
827
|
const { technologies, families, categories, subcategories } =
|
|
333
828
|
await this.technologyService.getAllowedTechnologies(practitioner);
|
|
334
829
|
|
|
@@ -355,19 +850,26 @@ export class ProcedureService extends BaseService {
|
|
|
355
850
|
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
356
851
|
let proceduresQuery = query(proceduresCollection);
|
|
357
852
|
|
|
358
|
-
//
|
|
853
|
+
// Apply pagination if specified
|
|
359
854
|
if (pagination && pagination > 0) {
|
|
360
|
-
const { limit, startAfter } =
|
|
855
|
+
const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
|
|
361
856
|
|
|
362
857
|
if (lastDoc) {
|
|
363
858
|
proceduresQuery = query(
|
|
364
859
|
proceduresCollection,
|
|
860
|
+
orderBy("name"), // Use imported orderBy
|
|
365
861
|
startAfter(lastDoc),
|
|
366
862
|
limit(pagination)
|
|
367
863
|
);
|
|
368
864
|
} else {
|
|
369
|
-
proceduresQuery = query(
|
|
865
|
+
proceduresQuery = query(
|
|
866
|
+
proceduresCollection,
|
|
867
|
+
orderBy("name"),
|
|
868
|
+
limit(pagination)
|
|
869
|
+
); // Use imported orderBy
|
|
370
870
|
}
|
|
871
|
+
} else {
|
|
872
|
+
proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
|
|
371
873
|
}
|
|
372
874
|
|
|
373
875
|
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
@@ -376,17 +878,9 @@ export class ProcedureService extends BaseService {
|
|
|
376
878
|
|
|
377
879
|
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
378
880
|
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
881
|
return {
|
|
388
882
|
...data,
|
|
389
|
-
id: doc.id,
|
|
883
|
+
id: doc.id, // Ensure ID is present
|
|
390
884
|
};
|
|
391
885
|
});
|
|
392
886
|
|
|
@@ -399,251 +893,4 @@ export class ProcedureService extends BaseService {
|
|
|
399
893
|
throw error;
|
|
400
894
|
}
|
|
401
895
|
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Updates the clinicInfo and doctorInfo fields in procedures when the source data changes
|
|
405
|
-
*
|
|
406
|
-
* @param entityType - The type of entity that changed ("clinic" or "doctor")
|
|
407
|
-
* @param entityId - The ID of the entity that changed
|
|
408
|
-
* @param updatedData - The updated data for the entity
|
|
409
|
-
* @returns Number of procedures updated
|
|
410
|
-
*/
|
|
411
|
-
async updateProcedureAggregateData(
|
|
412
|
-
entityType: "clinic" | "doctor",
|
|
413
|
-
entityId: string,
|
|
414
|
-
updatedData: any
|
|
415
|
-
): Promise<number> {
|
|
416
|
-
let proceduresQuery;
|
|
417
|
-
const updatedField = entityType === "clinic" ? "clinicInfo" : "doctorInfo";
|
|
418
|
-
|
|
419
|
-
// Find all procedures related to this entity
|
|
420
|
-
if (entityType === "clinic") {
|
|
421
|
-
proceduresQuery = query(
|
|
422
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
423
|
-
where("clinicBranchId", "==", entityId)
|
|
424
|
-
);
|
|
425
|
-
} else {
|
|
426
|
-
proceduresQuery = query(
|
|
427
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
428
|
-
where("practitionerId", "==", entityId)
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const snapshot = await getDocs(proceduresQuery);
|
|
433
|
-
|
|
434
|
-
if (snapshot.empty) {
|
|
435
|
-
return 0;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Create the updated data object
|
|
439
|
-
let updatedFieldData;
|
|
440
|
-
|
|
441
|
-
if (entityType === "clinic") {
|
|
442
|
-
// Clinic info format
|
|
443
|
-
updatedFieldData = {
|
|
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
|
-
};
|
|
466
|
-
} else {
|
|
467
|
-
// If it's already a doctorInfo object
|
|
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
|
-
};
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Update all found procedures
|
|
480
|
-
const batch = writeBatch(this.db);
|
|
481
|
-
|
|
482
|
-
snapshot.docs.forEach((doc) => {
|
|
483
|
-
batch.update(doc.ref, {
|
|
484
|
-
[updatedField]: updatedFieldData,
|
|
485
|
-
updatedAt: serverTimestamp(),
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
await batch.commit();
|
|
490
|
-
|
|
491
|
-
return snapshot.size;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Updates the clinicInfo for all procedures associated with a specific clinic
|
|
496
|
-
*
|
|
497
|
-
* @param clinicId - The ID of the clinic that was updated
|
|
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
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Updates the doctorInfo for all procedures associated with a specific practitioner
|
|
510
|
-
*
|
|
511
|
-
* @param practitionerId - The ID of the practitioner that was updated
|
|
512
|
-
* @param practitionerData - The updated practitioner data
|
|
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);
|
|
536
|
-
|
|
537
|
-
if (proceduresSnapshot.empty) {
|
|
538
|
-
return 0;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
let updatedCount = 0;
|
|
542
|
-
const batch = writeBatch(this.db);
|
|
543
|
-
const batchLimit = 500; // Firestore batch limit
|
|
544
|
-
let batchCount = 0;
|
|
545
|
-
|
|
546
|
-
// Process each procedure
|
|
547
|
-
for (const procedureDoc of proceduresSnapshot.docs) {
|
|
548
|
-
const procedure = procedureDoc.data() as Procedure;
|
|
549
|
-
|
|
550
|
-
// Skip if already has both required fields
|
|
551
|
-
if (procedure.clinicInfo && procedure.doctorInfo) {
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
try {
|
|
556
|
-
// Get clinic data
|
|
557
|
-
const clinicRef = doc(
|
|
558
|
-
this.db,
|
|
559
|
-
CLINICS_COLLECTION,
|
|
560
|
-
procedure.clinicBranchId
|
|
561
|
-
);
|
|
562
|
-
const clinicSnapshot = await getDoc(clinicRef);
|
|
563
|
-
|
|
564
|
-
if (!clinicSnapshot.exists()) {
|
|
565
|
-
console.warn(
|
|
566
|
-
`Clinic ${procedure.clinicBranchId} not found for procedure ${procedure.id}`
|
|
567
|
-
);
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const clinic = clinicSnapshot.data();
|
|
572
|
-
|
|
573
|
-
// Create clinic info
|
|
574
|
-
const clinicInfo = {
|
|
575
|
-
id: clinicSnapshot.id,
|
|
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
|
-
};
|
|
585
|
-
|
|
586
|
-
// Get practitioner data
|
|
587
|
-
const practitionerRef = doc(
|
|
588
|
-
this.db,
|
|
589
|
-
PRACTITIONERS_COLLECTION,
|
|
590
|
-
procedure.practitionerId
|
|
591
|
-
);
|
|
592
|
-
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
593
|
-
|
|
594
|
-
if (!practitionerSnapshot.exists()) {
|
|
595
|
-
console.warn(
|
|
596
|
-
`Practitioner ${procedure.practitionerId} not found for procedure ${procedure.id}`
|
|
597
|
-
);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const practitioner = practitionerSnapshot.data();
|
|
602
|
-
|
|
603
|
-
// Find doctor info in clinic's doctorsInfo array
|
|
604
|
-
let doctorInfo = clinic.doctorsInfo?.find(
|
|
605
|
-
(doctor: any) => doctor.id === procedure.practitionerId
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
// If not found, create basic doctor info
|
|
609
|
-
if (!doctorInfo) {
|
|
610
|
-
doctorInfo = {
|
|
611
|
-
id: practitionerSnapshot.id,
|
|
612
|
-
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
613
|
-
description: practitioner.basicInfo.bio || "",
|
|
614
|
-
photo: practitioner.basicInfo.profileImageUrl || "",
|
|
615
|
-
rating: 0,
|
|
616
|
-
services: [],
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Add to batch
|
|
621
|
-
batch.update(procedureDoc.ref, {
|
|
622
|
-
clinicInfo,
|
|
623
|
-
doctorInfo,
|
|
624
|
-
updatedAt: serverTimestamp(),
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
batchCount++;
|
|
628
|
-
updatedCount++;
|
|
629
|
-
|
|
630
|
-
// Commit batch if we've reached the limit
|
|
631
|
-
if (batchCount >= batchLimit) {
|
|
632
|
-
await batch.commit();
|
|
633
|
-
console.log(`Committed batch of ${batchCount} procedure updates`);
|
|
634
|
-
batchCount = 0;
|
|
635
|
-
}
|
|
636
|
-
} catch (error) {
|
|
637
|
-
console.error(`Error updating procedure ${procedure.id}:`, error);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Commit any remaining updates
|
|
642
|
-
if (batchCount > 0) {
|
|
643
|
-
await batch.commit();
|
|
644
|
-
console.log(`Committed final batch of ${batchCount} procedure updates`);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
return updatedCount;
|
|
648
|
-
}
|
|
649
896
|
}
|