@blackcode_sa/metaestetics-api 1.7.10 → 1.7.12
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 +5 -4
- package/dist/admin/index.d.ts +5 -4
- package/dist/index.d.mts +246 -144
- package/dist/index.d.ts +246 -144
- package/dist/index.js +555 -875
- package/dist/index.mjs +597 -922
- package/package.json +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -0
- package/src/index.ts +8 -0
- 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/media/media.service.ts +6 -1
- package/src/services/reviews/reviews.service.ts +6 -572
- package/src/types/clinic/index.ts +17 -5
- package/src/validations/clinic.schema.ts +25 -9
- package/src/validations/media.schema.ts +10 -0
|
@@ -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
|
/**
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ClinicTag,
|
|
20
20
|
ClinicPhotoTag,
|
|
21
21
|
} from "./preferences.types";
|
|
22
|
+
import type { MediaResource } from "../../services/media/media.service";
|
|
22
23
|
|
|
23
24
|
export * from "./preferences.types";
|
|
24
25
|
|
|
@@ -203,6 +204,10 @@ export interface ClinicGroup {
|
|
|
203
204
|
calendarSyncEnabled?: boolean;
|
|
204
205
|
autoConfirmAppointments?: boolean;
|
|
205
206
|
businessIdentificationNumber?: string | null;
|
|
207
|
+
onboarding?: {
|
|
208
|
+
completed?: boolean;
|
|
209
|
+
step?: number;
|
|
210
|
+
};
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
/**
|
|
@@ -223,6 +228,10 @@ export interface CreateClinicGroupData {
|
|
|
223
228
|
calendarSyncEnabled?: boolean;
|
|
224
229
|
autoConfirmAppointments?: boolean;
|
|
225
230
|
businessIdentificationNumber?: string | null;
|
|
231
|
+
onboarding?: {
|
|
232
|
+
completed?: boolean;
|
|
233
|
+
step?: number;
|
|
234
|
+
};
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
/**
|
|
@@ -253,11 +262,6 @@ export interface DoctorInfo {
|
|
|
253
262
|
// TODO: Add aggregated fields, like rating, reviews, services this doctor provides in this clinic and similar
|
|
254
263
|
}
|
|
255
264
|
|
|
256
|
-
/**
|
|
257
|
-
* Type that allows a field to be either a URL string or a File object
|
|
258
|
-
*/
|
|
259
|
-
export type MediaResource = string | File | Blob;
|
|
260
|
-
|
|
261
265
|
/**
|
|
262
266
|
* Interface for clinic
|
|
263
267
|
*/
|
|
@@ -330,6 +334,10 @@ export interface CreateDefaultClinicGroupData {
|
|
|
330
334
|
practiceType?: PracticeType;
|
|
331
335
|
languages?: Language[];
|
|
332
336
|
subscriptionModel?: SubscriptionModel;
|
|
337
|
+
onboarding?: {
|
|
338
|
+
completed?: boolean;
|
|
339
|
+
step?: number;
|
|
340
|
+
};
|
|
333
341
|
}
|
|
334
342
|
|
|
335
343
|
/**
|
|
@@ -364,6 +372,10 @@ export interface ClinicGroupSetupData {
|
|
|
364
372
|
calendarSyncEnabled: boolean;
|
|
365
373
|
autoConfirmAppointments: boolean;
|
|
366
374
|
businessIdentificationNumber: string | null;
|
|
375
|
+
onboarding?: {
|
|
376
|
+
completed?: boolean;
|
|
377
|
+
step?: number;
|
|
378
|
+
};
|
|
367
379
|
}
|
|
368
380
|
|
|
369
381
|
/**
|