@blackcode_sa/metaestetics-api 1.7.11 → 1.7.13
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 +200 -1
- package/dist/admin/index.d.ts +200 -1
- package/dist/admin/index.js +691 -35
- package/dist/admin/index.mjs +689 -35
- package/dist/index.d.mts +138 -37
- package/dist/index.d.ts +138 -37
- package/dist/index.js +543 -875
- package/dist/index.mjs +588 -922
- package/package.json +1 -1
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +316 -0
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -0
- package/src/admin/index.ts +8 -1
- package/src/services/auth.service.ts +4 -0
- package/src/services/clinic/clinic-group.service.ts +49 -0
- package/src/services/clinic/clinic.service.ts +12 -0
- package/src/services/reviews/reviews.service.ts +6 -572
- package/src/types/clinic/index.ts +16 -0
- package/src/validations/clinic.schema.ts +25 -9
- package/src/validations/media.schema.ts +10 -0
|
@@ -261,4 +261,53 @@ export class ClinicGroupService extends BaseService {
|
|
|
261
261
|
|
|
262
262
|
// TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
|
|
263
263
|
// TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Updates the onboarding status for a clinic group
|
|
267
|
+
*
|
|
268
|
+
* @param groupId - The ID of the clinic group to update
|
|
269
|
+
* @param onboardingData - The onboarding data to update
|
|
270
|
+
* @returns The updated clinic group
|
|
271
|
+
*/
|
|
272
|
+
async setOnboarding(
|
|
273
|
+
groupId: string,
|
|
274
|
+
onboardingData: {
|
|
275
|
+
completed?: boolean;
|
|
276
|
+
step?: number;
|
|
277
|
+
}
|
|
278
|
+
): Promise<ClinicGroup> {
|
|
279
|
+
console.log("[CLINIC_GROUP] Updating onboarding status", {
|
|
280
|
+
groupId,
|
|
281
|
+
onboardingData,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return this.updateClinicGroup(groupId, {
|
|
285
|
+
onboarding: onboardingData,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Sets the current onboarding step for a clinic group
|
|
291
|
+
*
|
|
292
|
+
* @param groupId - The ID of the clinic group to update
|
|
293
|
+
* @param step - The current onboarding step number
|
|
294
|
+
* @returns The updated clinic group
|
|
295
|
+
*/
|
|
296
|
+
async setOnboardingStep(groupId: string, step: number): Promise<ClinicGroup> {
|
|
297
|
+
console.log("[CLINIC_GROUP] Setting onboarding step", { groupId, step });
|
|
298
|
+
|
|
299
|
+
return this.setOnboarding(groupId, { step, completed: false });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Marks the onboarding process as completed for a clinic group
|
|
304
|
+
*
|
|
305
|
+
* @param groupId - The ID of the clinic group to update
|
|
306
|
+
* @returns The updated clinic group
|
|
307
|
+
*/
|
|
308
|
+
async completeOnboarding(groupId: string): Promise<ClinicGroup> {
|
|
309
|
+
console.log("[CLINIC_GROUP] Completing onboarding", { groupId });
|
|
310
|
+
|
|
311
|
+
return this.setOnboarding(groupId, { completed: true });
|
|
312
|
+
}
|
|
264
313
|
}
|
|
@@ -425,6 +425,18 @@ export class ClinicService extends BaseService {
|
|
|
425
425
|
});
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Activates a clinic.
|
|
430
|
+
*/
|
|
431
|
+
async activateClinic(clinicId: string, adminId: string): Promise<void> {
|
|
432
|
+
// Permission check omitted
|
|
433
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
434
|
+
await updateDoc(clinicRef, {
|
|
435
|
+
isActive: true,
|
|
436
|
+
updatedAt: serverTimestamp(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
/**
|
|
429
441
|
* Dohvata kliniku po ID-u
|
|
430
442
|
*/
|
|
@@ -5,13 +5,9 @@ import {
|
|
|
5
5
|
getDocs,
|
|
6
6
|
query,
|
|
7
7
|
where,
|
|
8
|
-
updateDoc,
|
|
9
8
|
setDoc,
|
|
10
9
|
deleteDoc,
|
|
11
|
-
Timestamp,
|
|
12
10
|
serverTimestamp,
|
|
13
|
-
DocumentData,
|
|
14
|
-
writeBatch,
|
|
15
11
|
} from "firebase/firestore";
|
|
16
12
|
import { BaseService } from "../base.service";
|
|
17
13
|
import {
|
|
@@ -20,9 +16,6 @@ import {
|
|
|
20
16
|
PractitionerReview,
|
|
21
17
|
ProcedureReview,
|
|
22
18
|
REVIEWS_COLLECTION,
|
|
23
|
-
ClinicReviewInfo,
|
|
24
|
-
PractitionerReviewInfo,
|
|
25
|
-
ProcedureReviewInfo,
|
|
26
19
|
} from "../../types/reviews";
|
|
27
20
|
import {
|
|
28
21
|
createReviewSchema,
|
|
@@ -32,9 +25,6 @@ import { z } from "zod";
|
|
|
32
25
|
import { Auth } from "firebase/auth";
|
|
33
26
|
import { Firestore } from "firebase/firestore";
|
|
34
27
|
import { FirebaseApp } from "firebase/app";
|
|
35
|
-
import { CLINICS_COLLECTION } from "../../types/clinic";
|
|
36
|
-
import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
|
|
37
|
-
import { PROCEDURES_COLLECTION } from "../../types/procedure";
|
|
38
28
|
|
|
39
29
|
export class ReviewService extends BaseService {
|
|
40
30
|
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
@@ -42,7 +32,7 @@ export class ReviewService extends BaseService {
|
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
/**
|
|
45
|
-
* Creates a new review
|
|
35
|
+
* Creates a new review
|
|
46
36
|
* @param data - The review data to create
|
|
47
37
|
* @param appointmentId - ID of the completed appointment
|
|
48
38
|
* @returns The created review
|
|
@@ -147,34 +137,8 @@ export class ReviewService extends BaseService {
|
|
|
147
137
|
updatedAt: serverTimestamp(),
|
|
148
138
|
});
|
|
149
139
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Update clinic if clinic review exists
|
|
154
|
-
if (data.clinicReview) {
|
|
155
|
-
updatePromises.push(
|
|
156
|
-
this.updateClinicReviewInfo(data.clinicReview.clinicId)
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Update practitioner if practitioner review exists
|
|
161
|
-
if (data.practitionerReview) {
|
|
162
|
-
updatePromises.push(
|
|
163
|
-
this.updatePractitionerReviewInfo(
|
|
164
|
-
data.practitionerReview.practitionerId
|
|
165
|
-
)
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Update procedure if procedure review exists
|
|
170
|
-
if (data.procedureReview) {
|
|
171
|
-
updatePromises.push(
|
|
172
|
-
this.updateProcedureReviewInfo(data.procedureReview.procedureId)
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Wait for all updates to complete
|
|
177
|
-
await Promise.all(updatePromises);
|
|
140
|
+
// Note: Related entity updates (clinic, practitioner, procedure) are now handled
|
|
141
|
+
// by cloud functions through the ReviewsAggregationService
|
|
178
142
|
|
|
179
143
|
return review;
|
|
180
144
|
} catch (error) {
|
|
@@ -277,7 +241,7 @@ export class ReviewService extends BaseService {
|
|
|
277
241
|
}
|
|
278
242
|
|
|
279
243
|
/**
|
|
280
|
-
* Deletes a review
|
|
244
|
+
* Deletes a review
|
|
281
245
|
* @param reviewId The ID of the review to delete
|
|
282
246
|
*/
|
|
283
247
|
async deleteReview(reviewId: string): Promise<void> {
|
|
@@ -289,538 +253,8 @@ export class ReviewService extends BaseService {
|
|
|
289
253
|
// Delete the review
|
|
290
254
|
await deleteDoc(doc(this.db, REVIEWS_COLLECTION, reviewId));
|
|
291
255
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (review.clinicReview) {
|
|
296
|
-
updatePromises.push(
|
|
297
|
-
this.updateClinicReviewInfo(
|
|
298
|
-
review.clinicReview.clinicId,
|
|
299
|
-
review.clinicReview,
|
|
300
|
-
true
|
|
301
|
-
)
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (review.practitionerReview) {
|
|
306
|
-
updatePromises.push(
|
|
307
|
-
this.updatePractitionerReviewInfo(
|
|
308
|
-
review.practitionerReview.practitionerId,
|
|
309
|
-
review.practitionerReview,
|
|
310
|
-
true
|
|
311
|
-
)
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (review.procedureReview) {
|
|
316
|
-
updatePromises.push(
|
|
317
|
-
this.updateProcedureReviewInfo(
|
|
318
|
-
review.procedureReview.procedureId,
|
|
319
|
-
review.procedureReview,
|
|
320
|
-
true
|
|
321
|
-
)
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Wait for all updates to complete
|
|
326
|
-
await Promise.all(updatePromises);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Updates the review info for a clinic
|
|
331
|
-
* @param clinicId The ID of the clinic to update
|
|
332
|
-
* @param newReview Optional new review being added or removed
|
|
333
|
-
* @param isRemoval Whether this update is for a review removal
|
|
334
|
-
* @returns The updated clinic review info
|
|
335
|
-
*/
|
|
336
|
-
async updateClinicReviewInfo(
|
|
337
|
-
clinicId: string,
|
|
338
|
-
newReview?: ClinicReview,
|
|
339
|
-
isRemoval: boolean = false
|
|
340
|
-
): Promise<ClinicReviewInfo> {
|
|
341
|
-
// Get the current clinic document
|
|
342
|
-
const clinicDoc = await getDoc(doc(this.db, CLINICS_COLLECTION, clinicId));
|
|
343
|
-
|
|
344
|
-
if (!clinicDoc.exists()) {
|
|
345
|
-
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const clinicData = clinicDoc.data();
|
|
349
|
-
const currentReviewInfo = (clinicData.reviewInfo as ClinicReviewInfo) || {
|
|
350
|
-
totalReviews: 0,
|
|
351
|
-
averageRating: 0,
|
|
352
|
-
cleanliness: 0,
|
|
353
|
-
facilities: 0,
|
|
354
|
-
staffFriendliness: 0,
|
|
355
|
-
waitingTime: 0,
|
|
356
|
-
accessibility: 0,
|
|
357
|
-
recommendationPercentage: 0,
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// If we have no reviews and aren't adding a new one, return default values
|
|
361
|
-
if (currentReviewInfo.totalReviews === 0 && !newReview) {
|
|
362
|
-
await updateDoc(doc(this.db, CLINICS_COLLECTION, clinicId), {
|
|
363
|
-
reviewInfo: currentReviewInfo,
|
|
364
|
-
updatedAt: serverTimestamp(),
|
|
365
|
-
});
|
|
366
|
-
return currentReviewInfo;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
let updatedReviewInfo: ClinicReviewInfo;
|
|
370
|
-
|
|
371
|
-
if (newReview) {
|
|
372
|
-
// Calculate new values based on existing averages and the new review
|
|
373
|
-
const oldTotal = currentReviewInfo.totalReviews;
|
|
374
|
-
const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
|
|
375
|
-
|
|
376
|
-
// If removing the last review, set all values to 0
|
|
377
|
-
if (newTotal === 0) {
|
|
378
|
-
updatedReviewInfo = {
|
|
379
|
-
totalReviews: 0,
|
|
380
|
-
averageRating: 0,
|
|
381
|
-
cleanliness: 0,
|
|
382
|
-
facilities: 0,
|
|
383
|
-
staffFriendliness: 0,
|
|
384
|
-
waitingTime: 0,
|
|
385
|
-
accessibility: 0,
|
|
386
|
-
recommendationPercentage: 0,
|
|
387
|
-
};
|
|
388
|
-
} else {
|
|
389
|
-
// Calculate new averages
|
|
390
|
-
const updateAverage = (
|
|
391
|
-
currentAvg: number,
|
|
392
|
-
newValue: number
|
|
393
|
-
): number => {
|
|
394
|
-
const currentSum = currentAvg * oldTotal;
|
|
395
|
-
const newSum = isRemoval
|
|
396
|
-
? currentSum - newValue
|
|
397
|
-
: currentSum + newValue;
|
|
398
|
-
const newAvg = newSum / newTotal;
|
|
399
|
-
return Math.round(newAvg * 10) / 10; // Round to 1 decimal
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
// Update recommendation percentage
|
|
403
|
-
const currentRecommendations =
|
|
404
|
-
(currentReviewInfo.recommendationPercentage / 100) * oldTotal;
|
|
405
|
-
const newRecommendations = isRemoval
|
|
406
|
-
? newReview.wouldRecommend
|
|
407
|
-
? currentRecommendations - 1
|
|
408
|
-
: currentRecommendations
|
|
409
|
-
: newReview.wouldRecommend
|
|
410
|
-
? currentRecommendations + 1
|
|
411
|
-
: currentRecommendations;
|
|
412
|
-
const newRecommendationPercentage =
|
|
413
|
-
(newRecommendations / newTotal) * 100;
|
|
414
|
-
|
|
415
|
-
updatedReviewInfo = {
|
|
416
|
-
totalReviews: newTotal,
|
|
417
|
-
averageRating: updateAverage(
|
|
418
|
-
currentReviewInfo.averageRating,
|
|
419
|
-
newReview.overallRating
|
|
420
|
-
),
|
|
421
|
-
cleanliness: updateAverage(
|
|
422
|
-
currentReviewInfo.cleanliness,
|
|
423
|
-
newReview.cleanliness
|
|
424
|
-
),
|
|
425
|
-
facilities: updateAverage(
|
|
426
|
-
currentReviewInfo.facilities,
|
|
427
|
-
newReview.facilities
|
|
428
|
-
),
|
|
429
|
-
staffFriendliness: updateAverage(
|
|
430
|
-
currentReviewInfo.staffFriendliness,
|
|
431
|
-
newReview.staffFriendliness
|
|
432
|
-
),
|
|
433
|
-
waitingTime: updateAverage(
|
|
434
|
-
currentReviewInfo.waitingTime,
|
|
435
|
-
newReview.waitingTime
|
|
436
|
-
),
|
|
437
|
-
accessibility: updateAverage(
|
|
438
|
-
currentReviewInfo.accessibility,
|
|
439
|
-
newReview.accessibility
|
|
440
|
-
),
|
|
441
|
-
recommendationPercentage:
|
|
442
|
-
Math.round(newRecommendationPercentage * 10) / 10,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
} else {
|
|
446
|
-
// If no new review provided, keep the current info
|
|
447
|
-
updatedReviewInfo = { ...currentReviewInfo };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Update the clinic with the new review info
|
|
451
|
-
await updateDoc(doc(this.db, CLINICS_COLLECTION, clinicId), {
|
|
452
|
-
reviewInfo: updatedReviewInfo,
|
|
453
|
-
updatedAt: serverTimestamp(),
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
return updatedReviewInfo;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Updates the review info for a practitioner
|
|
461
|
-
* @param practitionerId The ID of the practitioner to update
|
|
462
|
-
* @param newReview Optional new review being added or removed
|
|
463
|
-
* @param isRemoval Whether this update is for a review removal
|
|
464
|
-
* @returns The updated practitioner review info
|
|
465
|
-
*/
|
|
466
|
-
async updatePractitionerReviewInfo(
|
|
467
|
-
practitionerId: string,
|
|
468
|
-
newReview?: PractitionerReview,
|
|
469
|
-
isRemoval: boolean = false
|
|
470
|
-
): Promise<PractitionerReviewInfo> {
|
|
471
|
-
// Get the current practitioner document
|
|
472
|
-
const practitionerDoc = await getDoc(
|
|
473
|
-
doc(this.db, PRACTITIONERS_COLLECTION, practitionerId)
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
if (!practitionerDoc.exists()) {
|
|
477
|
-
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const practitionerData = practitionerDoc.data();
|
|
481
|
-
const currentReviewInfo =
|
|
482
|
-
(practitionerData.reviewInfo as PractitionerReviewInfo) || {
|
|
483
|
-
totalReviews: 0,
|
|
484
|
-
averageRating: 0,
|
|
485
|
-
knowledgeAndExpertise: 0,
|
|
486
|
-
communicationSkills: 0,
|
|
487
|
-
bedSideManner: 0,
|
|
488
|
-
thoroughness: 0,
|
|
489
|
-
trustworthiness: 0,
|
|
490
|
-
recommendationPercentage: 0,
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
// If we have no reviews and aren't adding a new one, return default values
|
|
494
|
-
if (currentReviewInfo.totalReviews === 0 && !newReview) {
|
|
495
|
-
await updateDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId), {
|
|
496
|
-
reviewInfo: currentReviewInfo,
|
|
497
|
-
updatedAt: serverTimestamp(),
|
|
498
|
-
});
|
|
499
|
-
return currentReviewInfo;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
let updatedReviewInfo: PractitionerReviewInfo;
|
|
503
|
-
|
|
504
|
-
if (newReview) {
|
|
505
|
-
// Calculate new values based on existing averages and the new review
|
|
506
|
-
const oldTotal = currentReviewInfo.totalReviews;
|
|
507
|
-
const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
|
|
508
|
-
|
|
509
|
-
// If removing the last review, set all values to 0
|
|
510
|
-
if (newTotal === 0) {
|
|
511
|
-
updatedReviewInfo = {
|
|
512
|
-
totalReviews: 0,
|
|
513
|
-
averageRating: 0,
|
|
514
|
-
knowledgeAndExpertise: 0,
|
|
515
|
-
communicationSkills: 0,
|
|
516
|
-
bedSideManner: 0,
|
|
517
|
-
thoroughness: 0,
|
|
518
|
-
trustworthiness: 0,
|
|
519
|
-
recommendationPercentage: 0,
|
|
520
|
-
};
|
|
521
|
-
} else {
|
|
522
|
-
// Calculate new averages
|
|
523
|
-
const updateAverage = (
|
|
524
|
-
currentAvg: number,
|
|
525
|
-
newValue: number
|
|
526
|
-
): number => {
|
|
527
|
-
const currentSum = currentAvg * oldTotal;
|
|
528
|
-
const newSum = isRemoval
|
|
529
|
-
? currentSum - newValue
|
|
530
|
-
: currentSum + newValue;
|
|
531
|
-
const newAvg = newSum / newTotal;
|
|
532
|
-
return Math.round(newAvg * 10) / 10; // Round to 1 decimal
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
// Update recommendation percentage
|
|
536
|
-
const currentRecommendations =
|
|
537
|
-
(currentReviewInfo.recommendationPercentage / 100) * oldTotal;
|
|
538
|
-
const newRecommendations = isRemoval
|
|
539
|
-
? newReview.wouldRecommend
|
|
540
|
-
? currentRecommendations - 1
|
|
541
|
-
: currentRecommendations
|
|
542
|
-
: newReview.wouldRecommend
|
|
543
|
-
? currentRecommendations + 1
|
|
544
|
-
: currentRecommendations;
|
|
545
|
-
const newRecommendationPercentage =
|
|
546
|
-
(newRecommendations / newTotal) * 100;
|
|
547
|
-
|
|
548
|
-
updatedReviewInfo = {
|
|
549
|
-
totalReviews: newTotal,
|
|
550
|
-
averageRating: updateAverage(
|
|
551
|
-
currentReviewInfo.averageRating,
|
|
552
|
-
newReview.overallRating
|
|
553
|
-
),
|
|
554
|
-
knowledgeAndExpertise: updateAverage(
|
|
555
|
-
currentReviewInfo.knowledgeAndExpertise,
|
|
556
|
-
newReview.knowledgeAndExpertise
|
|
557
|
-
),
|
|
558
|
-
communicationSkills: updateAverage(
|
|
559
|
-
currentReviewInfo.communicationSkills,
|
|
560
|
-
newReview.communicationSkills
|
|
561
|
-
),
|
|
562
|
-
bedSideManner: updateAverage(
|
|
563
|
-
currentReviewInfo.bedSideManner,
|
|
564
|
-
newReview.bedSideManner
|
|
565
|
-
),
|
|
566
|
-
thoroughness: updateAverage(
|
|
567
|
-
currentReviewInfo.thoroughness,
|
|
568
|
-
newReview.thoroughness
|
|
569
|
-
),
|
|
570
|
-
trustworthiness: updateAverage(
|
|
571
|
-
currentReviewInfo.trustworthiness,
|
|
572
|
-
newReview.trustworthiness
|
|
573
|
-
),
|
|
574
|
-
recommendationPercentage:
|
|
575
|
-
Math.round(newRecommendationPercentage * 10) / 10,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
} else {
|
|
579
|
-
// If no new review provided, keep the current info
|
|
580
|
-
updatedReviewInfo = { ...currentReviewInfo };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Update the practitioner with the new review info
|
|
584
|
-
await updateDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId), {
|
|
585
|
-
reviewInfo: updatedReviewInfo,
|
|
586
|
-
updatedAt: serverTimestamp(),
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Also update doctor info in procedures with the new rating
|
|
590
|
-
await this.updateDoctorInfoInProcedures(
|
|
591
|
-
practitionerId,
|
|
592
|
-
updatedReviewInfo.averageRating
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
return updatedReviewInfo;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Updates the review info for a procedure
|
|
600
|
-
* @param procedureId The ID of the procedure to update
|
|
601
|
-
* @param newReview Optional new review being added or removed
|
|
602
|
-
* @param isRemoval Whether this update is for a review removal
|
|
603
|
-
* @returns The updated procedure review info
|
|
604
|
-
*/
|
|
605
|
-
async updateProcedureReviewInfo(
|
|
606
|
-
procedureId: string,
|
|
607
|
-
newReview?: ProcedureReview,
|
|
608
|
-
isRemoval: boolean = false
|
|
609
|
-
): Promise<ProcedureReviewInfo> {
|
|
610
|
-
// Get the current procedure document
|
|
611
|
-
const procedureDoc = await getDoc(
|
|
612
|
-
doc(this.db, PROCEDURES_COLLECTION, procedureId)
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
if (!procedureDoc.exists()) {
|
|
616
|
-
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const procedureData = procedureDoc.data();
|
|
620
|
-
const currentReviewInfo =
|
|
621
|
-
(procedureData.reviewInfo as ProcedureReviewInfo) || {
|
|
622
|
-
totalReviews: 0,
|
|
623
|
-
averageRating: 0,
|
|
624
|
-
effectivenessOfTreatment: 0,
|
|
625
|
-
outcomeExplanation: 0,
|
|
626
|
-
painManagement: 0,
|
|
627
|
-
followUpCare: 0,
|
|
628
|
-
valueForMoney: 0,
|
|
629
|
-
recommendationPercentage: 0,
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
// If we have no reviews and aren't adding a new one, return default values
|
|
633
|
-
if (currentReviewInfo.totalReviews === 0 && !newReview) {
|
|
634
|
-
await updateDoc(doc(this.db, PROCEDURES_COLLECTION, procedureId), {
|
|
635
|
-
reviewInfo: currentReviewInfo,
|
|
636
|
-
updatedAt: serverTimestamp(),
|
|
637
|
-
});
|
|
638
|
-
return currentReviewInfo;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
let updatedReviewInfo: ProcedureReviewInfo;
|
|
642
|
-
|
|
643
|
-
if (newReview) {
|
|
644
|
-
// Calculate new values based on existing averages and the new review
|
|
645
|
-
const oldTotal = currentReviewInfo.totalReviews;
|
|
646
|
-
const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
|
|
647
|
-
|
|
648
|
-
// If removing the last review, set all values to 0
|
|
649
|
-
if (newTotal === 0) {
|
|
650
|
-
updatedReviewInfo = {
|
|
651
|
-
totalReviews: 0,
|
|
652
|
-
averageRating: 0,
|
|
653
|
-
effectivenessOfTreatment: 0,
|
|
654
|
-
outcomeExplanation: 0,
|
|
655
|
-
painManagement: 0,
|
|
656
|
-
followUpCare: 0,
|
|
657
|
-
valueForMoney: 0,
|
|
658
|
-
recommendationPercentage: 0,
|
|
659
|
-
};
|
|
660
|
-
} else {
|
|
661
|
-
// Calculate new averages
|
|
662
|
-
const updateAverage = (
|
|
663
|
-
currentAvg: number,
|
|
664
|
-
newValue: number
|
|
665
|
-
): number => {
|
|
666
|
-
const currentSum = currentAvg * oldTotal;
|
|
667
|
-
const newSum = isRemoval
|
|
668
|
-
? currentSum - newValue
|
|
669
|
-
: currentSum + newValue;
|
|
670
|
-
const newAvg = newSum / newTotal;
|
|
671
|
-
return Math.round(newAvg * 10) / 10; // Round to 1 decimal
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
// Update recommendation percentage
|
|
675
|
-
const currentRecommendations =
|
|
676
|
-
(currentReviewInfo.recommendationPercentage / 100) * oldTotal;
|
|
677
|
-
const newRecommendations = isRemoval
|
|
678
|
-
? newReview.wouldRecommend
|
|
679
|
-
? currentRecommendations - 1
|
|
680
|
-
: currentRecommendations
|
|
681
|
-
: newReview.wouldRecommend
|
|
682
|
-
? currentRecommendations + 1
|
|
683
|
-
: currentRecommendations;
|
|
684
|
-
const newRecommendationPercentage =
|
|
685
|
-
(newRecommendations / newTotal) * 100;
|
|
686
|
-
|
|
687
|
-
updatedReviewInfo = {
|
|
688
|
-
totalReviews: newTotal,
|
|
689
|
-
averageRating: updateAverage(
|
|
690
|
-
currentReviewInfo.averageRating,
|
|
691
|
-
newReview.overallRating
|
|
692
|
-
),
|
|
693
|
-
effectivenessOfTreatment: updateAverage(
|
|
694
|
-
currentReviewInfo.effectivenessOfTreatment,
|
|
695
|
-
newReview.effectivenessOfTreatment
|
|
696
|
-
),
|
|
697
|
-
outcomeExplanation: updateAverage(
|
|
698
|
-
currentReviewInfo.outcomeExplanation,
|
|
699
|
-
newReview.outcomeExplanation
|
|
700
|
-
),
|
|
701
|
-
painManagement: updateAverage(
|
|
702
|
-
currentReviewInfo.painManagement,
|
|
703
|
-
newReview.painManagement
|
|
704
|
-
),
|
|
705
|
-
followUpCare: updateAverage(
|
|
706
|
-
currentReviewInfo.followUpCare,
|
|
707
|
-
newReview.followUpCare
|
|
708
|
-
),
|
|
709
|
-
valueForMoney: updateAverage(
|
|
710
|
-
currentReviewInfo.valueForMoney,
|
|
711
|
-
newReview.valueForMoney
|
|
712
|
-
),
|
|
713
|
-
recommendationPercentage:
|
|
714
|
-
Math.round(newRecommendationPercentage * 10) / 10,
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
} else {
|
|
718
|
-
// If no new review provided, keep the current info
|
|
719
|
-
updatedReviewInfo = { ...currentReviewInfo };
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Update the procedure with the new review info
|
|
723
|
-
await updateDoc(doc(this.db, PROCEDURES_COLLECTION, procedureId), {
|
|
724
|
-
reviewInfo: updatedReviewInfo,
|
|
725
|
-
updatedAt: serverTimestamp(),
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
return updatedReviewInfo;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Updates doctorInfo rating in all procedures for a practitioner
|
|
733
|
-
* @param practitionerId The ID of the practitioner
|
|
734
|
-
* @param rating The new rating to set
|
|
735
|
-
*/
|
|
736
|
-
private async updateDoctorInfoInProcedures(
|
|
737
|
-
practitionerId: string,
|
|
738
|
-
rating: number
|
|
739
|
-
): Promise<void> {
|
|
740
|
-
// Find all procedures for this practitioner
|
|
741
|
-
const q = query(
|
|
742
|
-
collection(this.db, PROCEDURES_COLLECTION),
|
|
743
|
-
where("practitionerId", "==", practitionerId)
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
const snapshot = await getDocs(q);
|
|
747
|
-
|
|
748
|
-
if (snapshot.empty) {
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Batch update all procedures
|
|
753
|
-
const batch = writeBatch(this.db);
|
|
754
|
-
|
|
755
|
-
snapshot.docs.forEach((docSnapshot) => {
|
|
756
|
-
const procedureRef = doc(this.db, PROCEDURES_COLLECTION, docSnapshot.id);
|
|
757
|
-
batch.update(procedureRef, {
|
|
758
|
-
"doctorInfo.rating": rating,
|
|
759
|
-
updatedAt: serverTimestamp(),
|
|
760
|
-
});
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
await batch.commit();
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Verifies a review as checked by admin/staff
|
|
768
|
-
* @param reviewId The ID of the review to verify
|
|
769
|
-
*/
|
|
770
|
-
async verifyReview(reviewId: string): Promise<void> {
|
|
771
|
-
const review = await this.getReview(reviewId);
|
|
772
|
-
if (!review) {
|
|
773
|
-
throw new Error(`Review with ID ${reviewId} not found`);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const batch = writeBatch(this.db);
|
|
777
|
-
|
|
778
|
-
// Update main review document
|
|
779
|
-
batch.update(doc(this.db, REVIEWS_COLLECTION, reviewId), {
|
|
780
|
-
updatedAt: serverTimestamp(),
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
// Update clinic review if it exists
|
|
784
|
-
if (review.clinicReview) {
|
|
785
|
-
review.clinicReview.isVerified = true;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Update practitioner review if it exists
|
|
789
|
-
if (review.practitionerReview) {
|
|
790
|
-
review.practitionerReview.isVerified = true;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Update procedure review if it exists
|
|
794
|
-
if (review.procedureReview) {
|
|
795
|
-
review.procedureReview.isVerified = true;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
await batch.commit();
|
|
799
|
-
|
|
800
|
-
// Update all related entities
|
|
801
|
-
const updatePromises: Promise<any>[] = [];
|
|
802
|
-
|
|
803
|
-
if (review.clinicReview) {
|
|
804
|
-
updatePromises.push(
|
|
805
|
-
this.updateClinicReviewInfo(review.clinicReview.clinicId)
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (review.practitionerReview) {
|
|
810
|
-
updatePromises.push(
|
|
811
|
-
this.updatePractitionerReviewInfo(
|
|
812
|
-
review.practitionerReview.practitionerId
|
|
813
|
-
)
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (review.procedureReview) {
|
|
818
|
-
updatePromises.push(
|
|
819
|
-
this.updateProcedureReviewInfo(review.procedureReview.procedureId)
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
await Promise.all(updatePromises);
|
|
256
|
+
// Note: Updates to related entities after deletion are now handled
|
|
257
|
+
// by cloud functions through the ReviewsAggregationService
|
|
824
258
|
}
|
|
825
259
|
|
|
826
260
|
/**
|