@blackcode_sa/metaestetics-api 1.5.27 → 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 +4184 -2426
- package/dist/index.d.ts +4184 -2426
- package/dist/index.js +2692 -1546
- package/dist/index.mjs +2663 -1502
- 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/calendar/calendar-refactored.service.ts +1 -1
- package/src/services/clinic/clinic.service.ts +344 -77
- package/src/services/clinic/utils/clinic.utils.ts +187 -8
- 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 +678 -52
- 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 +39 -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 +14 -3
- package/src/validations/reviews.schema.ts +189 -0
- package/src/services/clinic/utils/review.utils.ts +0 -93
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
Timestamp,
|
|
12
12
|
serverTimestamp,
|
|
13
13
|
DocumentData,
|
|
14
|
+
writeBatch,
|
|
15
|
+
arrayUnion,
|
|
16
|
+
arrayRemove,
|
|
17
|
+
FieldValue,
|
|
18
|
+
orderBy,
|
|
14
19
|
} from "firebase/firestore";
|
|
15
20
|
import { BaseService } from "../base.service";
|
|
16
21
|
import {
|
|
@@ -18,6 +23,7 @@ import {
|
|
|
18
23
|
CreateProcedureData,
|
|
19
24
|
UpdateProcedureData,
|
|
20
25
|
PROCEDURES_COLLECTION,
|
|
26
|
+
ProcedureSummaryInfo,
|
|
21
27
|
} from "../../types/procedure";
|
|
22
28
|
import {
|
|
23
29
|
createProcedureSchema,
|
|
@@ -47,12 +53,17 @@ import { CategoryService } from "../../backoffice/services/category.service";
|
|
|
47
53
|
import { SubcategoryService } from "../../backoffice/services/subcategory.service";
|
|
48
54
|
import { TechnologyService } from "../../backoffice/services/technology.service";
|
|
49
55
|
import { ProductService } from "../../backoffice/services/product.service";
|
|
50
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
Practitioner,
|
|
58
|
+
PRACTITIONERS_COLLECTION,
|
|
59
|
+
} from "../../types/practitioner";
|
|
51
60
|
import {
|
|
52
61
|
CertificationLevel,
|
|
53
62
|
CertificationSpecialty,
|
|
54
63
|
ProcedureFamily,
|
|
55
64
|
} from "../../backoffice/types";
|
|
65
|
+
import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
|
|
66
|
+
import { ProcedureReviewInfo } from "../../types/reviews";
|
|
56
67
|
|
|
57
68
|
export class ProcedureService extends BaseService {
|
|
58
69
|
private categoryService: CategoryService;
|
|
@@ -76,16 +87,150 @@ export class ProcedureService extends BaseService {
|
|
|
76
87
|
this.productService = productService;
|
|
77
88
|
}
|
|
78
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
|
+
|
|
79
225
|
/**
|
|
80
|
-
* Creates a new procedure
|
|
226
|
+
* Creates a new procedure and updates related practitioner/clinic aggregates
|
|
81
227
|
* @param data - The data for creating a new procedure
|
|
82
228
|
* @returns The created procedure
|
|
83
229
|
*/
|
|
84
230
|
async createProcedure(data: CreateProcedureData): Promise<Procedure> {
|
|
85
|
-
// Validate input data
|
|
86
231
|
const validatedData = createProcedureSchema.parse(data);
|
|
87
232
|
|
|
88
|
-
// Get references to related entities
|
|
233
|
+
// Get references to related entities (Category, Subcategory, Technology, Product)
|
|
89
234
|
const [category, subcategory, technology, product] = await Promise.all([
|
|
90
235
|
this.categoryService.getById(validatedData.categoryId),
|
|
91
236
|
this.subcategoryService.getById(
|
|
@@ -100,13 +245,66 @@ export class ProcedureService extends BaseService {
|
|
|
100
245
|
]);
|
|
101
246
|
|
|
102
247
|
if (!category || !subcategory || !technology || !product) {
|
|
103
|
-
throw new Error("One or more required entities not found");
|
|
248
|
+
throw new Error("One or more required base entities not found");
|
|
104
249
|
}
|
|
105
250
|
|
|
251
|
+
// Get clinic and practitioner information for aggregation
|
|
252
|
+
const clinicRef = doc(
|
|
253
|
+
this.db,
|
|
254
|
+
CLINICS_COLLECTION,
|
|
255
|
+
validatedData.clinicBranchId
|
|
256
|
+
);
|
|
257
|
+
const clinicSnapshot = await getDoc(clinicRef);
|
|
258
|
+
if (!clinicSnapshot.exists()) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Clinic with ID ${validatedData.clinicBranchId} not found`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const clinic = clinicSnapshot.data() as Clinic; // Assert type
|
|
264
|
+
|
|
265
|
+
const practitionerRef = doc(
|
|
266
|
+
this.db,
|
|
267
|
+
PRACTITIONERS_COLLECTION,
|
|
268
|
+
validatedData.practitionerId
|
|
269
|
+
);
|
|
270
|
+
const practitionerSnapshot = await getDoc(practitionerRef);
|
|
271
|
+
if (!practitionerSnapshot.exists()) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Practitioner with ID ${validatedData.practitionerId} not found`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
|
|
277
|
+
|
|
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
|
+
};
|
|
290
|
+
|
|
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
|
+
};
|
|
301
|
+
|
|
106
302
|
// Create the procedure object
|
|
107
|
-
const
|
|
303
|
+
const procedureId = this.generateId();
|
|
304
|
+
const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
|
|
305
|
+
id: procedureId,
|
|
108
306
|
...validatedData,
|
|
109
|
-
category,
|
|
307
|
+
category, // Embed full objects
|
|
110
308
|
subcategory,
|
|
111
309
|
technology,
|
|
112
310
|
product,
|
|
@@ -116,22 +314,67 @@ export class ProcedureService extends BaseService {
|
|
|
116
314
|
postRequirements: technology.requirements.post,
|
|
117
315
|
certificationRequirement: technology.certificationRequirement,
|
|
118
316
|
documentationTemplates: technology.documentationTemplates || [],
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
122
331
|
};
|
|
123
332
|
|
|
124
|
-
//
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
130
340
|
createdAt: serverTimestamp(),
|
|
131
341
|
updatedAt: serverTimestamp(),
|
|
132
342
|
});
|
|
133
343
|
|
|
134
|
-
|
|
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;
|
|
135
378
|
}
|
|
136
379
|
|
|
137
380
|
/**
|
|
@@ -185,70 +428,394 @@ export class ProcedureService extends BaseService {
|
|
|
185
428
|
}
|
|
186
429
|
|
|
187
430
|
/**
|
|
188
|
-
* Updates a procedure
|
|
431
|
+
* Updates a procedure and its related aggregates in Practitioner and Clinic docs
|
|
189
432
|
* @param id - The ID of the procedure to update
|
|
190
|
-
* @param data - The data to update
|
|
433
|
+
* @param data - The data to update the procedure with
|
|
191
434
|
* @returns The updated procedure
|
|
192
435
|
*/
|
|
193
436
|
async updateProcedure(
|
|
194
437
|
id: string,
|
|
195
438
|
data: UpdateProcedureData
|
|
196
439
|
): Promise<Procedure> {
|
|
197
|
-
// Validate input data
|
|
198
440
|
const validatedData = updateProcedureSchema.parse(data);
|
|
441
|
+
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
|
|
442
|
+
const procedureSnapshot = await getDoc(procedureRef);
|
|
199
443
|
|
|
200
|
-
|
|
201
|
-
const existingProcedure = await this.getProcedure(id);
|
|
202
|
-
if (!existingProcedure) {
|
|
444
|
+
if (!procedureSnapshot.exists()) {
|
|
203
445
|
throw new Error(`Procedure with ID ${id} not found`);
|
|
204
446
|
}
|
|
205
447
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
210
587
|
updatedAt: serverTimestamp(),
|
|
211
588
|
});
|
|
212
589
|
|
|
213
|
-
|
|
590
|
+
// 2. Create the updated summary info object
|
|
591
|
+
// We need the final state of the procedure for the summary
|
|
592
|
+
const finalProcedureStateForSummary = {
|
|
214
593
|
...existingProcedure,
|
|
215
|
-
...
|
|
216
|
-
|
|
217
|
-
|
|
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;
|
|
218
702
|
}
|
|
219
703
|
|
|
220
704
|
/**
|
|
221
|
-
* Deactivates a procedure
|
|
705
|
+
* Deactivates a procedure (soft delete) and updates aggregates
|
|
222
706
|
* @param id - The ID of the procedure to deactivate
|
|
223
707
|
*/
|
|
224
708
|
async deactivateProcedure(id: string): Promise<void> {
|
|
225
|
-
const
|
|
226
|
-
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, {
|
|
227
725
|
isActive: false,
|
|
228
726
|
updatedAt: serverTimestamp(),
|
|
229
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;
|
|
230
813
|
}
|
|
231
814
|
|
|
232
815
|
/**
|
|
233
816
|
* Gets all procedures that a practitioner is certified to perform
|
|
234
817
|
* @param practitioner - The practitioner's profile
|
|
235
|
-
* @returns Object containing
|
|
236
|
-
* - procedures: List of procedures the practitioner can perform
|
|
237
|
-
* - families: List of procedure families the practitioner can perform
|
|
238
|
-
* - categories: List of category IDs the practitioner can perform
|
|
239
|
-
* - subcategories: List of subcategory IDs the practitioner can perform
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* const practitioner = {
|
|
243
|
-
* certification: {
|
|
244
|
-
* level: CertificationLevel.DOCTOR,
|
|
245
|
-
* specialties: [CertificationSpecialty.INJECTABLES]
|
|
246
|
-
* }
|
|
247
|
-
* };
|
|
248
|
-
* const allowedProcedures = await procedureService.getAllowedProcedures(practitioner);
|
|
249
|
-
* console.log(allowedProcedures.families); // [ProcedureFamily.AESTHETICS]
|
|
250
|
-
* console.log(allowedProcedures.categories); // ["category1", "category2"]
|
|
251
|
-
* console.log(allowedProcedures.subcategories); // ["subcategory1", "subcategory2"]
|
|
818
|
+
* @returns Object containing allowed technologies, families, categories, subcategories
|
|
252
819
|
*/
|
|
253
820
|
async getAllowedTechnologies(practitioner: Practitioner): Promise<{
|
|
254
821
|
technologies: Technology[];
|
|
@@ -256,7 +823,7 @@ export class ProcedureService extends BaseService {
|
|
|
256
823
|
categories: string[];
|
|
257
824
|
subcategories: string[];
|
|
258
825
|
}> {
|
|
259
|
-
//
|
|
826
|
+
// This logic depends on TechnologyService and remains valid
|
|
260
827
|
const { technologies, families, categories, subcategories } =
|
|
261
828
|
await this.technologyService.getAllowedTechnologies(practitioner);
|
|
262
829
|
|
|
@@ -267,4 +834,63 @@ export class ProcedureService extends BaseService {
|
|
|
267
834
|
subcategories,
|
|
268
835
|
};
|
|
269
836
|
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Gets all procedures with optional pagination
|
|
840
|
+
*
|
|
841
|
+
* @param pagination - Optional number of procedures per page (0 or undefined returns all)
|
|
842
|
+
* @param lastDoc - Optional last document for pagination (if continuing from a previous page)
|
|
843
|
+
* @returns Object containing procedures array and the last document for pagination
|
|
844
|
+
*/
|
|
845
|
+
async getAllProcedures(
|
|
846
|
+
pagination?: number,
|
|
847
|
+
lastDoc?: any
|
|
848
|
+
): Promise<{ procedures: Procedure[]; lastDoc: any }> {
|
|
849
|
+
try {
|
|
850
|
+
const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
|
|
851
|
+
let proceduresQuery = query(proceduresCollection);
|
|
852
|
+
|
|
853
|
+
// Apply pagination if specified
|
|
854
|
+
if (pagination && pagination > 0) {
|
|
855
|
+
const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
|
|
856
|
+
|
|
857
|
+
if (lastDoc) {
|
|
858
|
+
proceduresQuery = query(
|
|
859
|
+
proceduresCollection,
|
|
860
|
+
orderBy("name"), // Use imported orderBy
|
|
861
|
+
startAfter(lastDoc),
|
|
862
|
+
limit(pagination)
|
|
863
|
+
);
|
|
864
|
+
} else {
|
|
865
|
+
proceduresQuery = query(
|
|
866
|
+
proceduresCollection,
|
|
867
|
+
orderBy("name"),
|
|
868
|
+
limit(pagination)
|
|
869
|
+
); // Use imported orderBy
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const proceduresSnapshot = await getDocs(proceduresQuery);
|
|
876
|
+
const lastVisible =
|
|
877
|
+
proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
|
|
878
|
+
|
|
879
|
+
const procedures = proceduresSnapshot.docs.map((doc) => {
|
|
880
|
+
const data = doc.data() as Procedure;
|
|
881
|
+
return {
|
|
882
|
+
...data,
|
|
883
|
+
id: doc.id, // Ensure ID is present
|
|
884
|
+
};
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
procedures,
|
|
889
|
+
lastDoc: lastVisible,
|
|
890
|
+
};
|
|
891
|
+
} catch (error) {
|
|
892
|
+
console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
270
896
|
}
|