@blackcode_sa/metaestetics-api 1.13.0 → 1.13.1

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.
@@ -616,8 +616,8 @@ var NotificationsAdmin = class {
616
616
  * Dohvata notifikaciju po ID-u
617
617
  */
618
618
  async getNotification(id) {
619
- const doc2 = await this.db.collection("notifications").doc(id).get();
620
- return doc2.exists ? { id: doc2.id, ...doc2.data() } : null;
619
+ const doc3 = await this.db.collection("notifications").doc(id).get();
620
+ return doc3.exists ? { id: doc3.id, ...doc3.data() } : null;
621
621
  }
622
622
  /**
623
623
  * Kreira novu notifikaciju
@@ -804,10 +804,10 @@ var NotificationsAdmin = class {
804
804
  return;
805
805
  }
806
806
  const results = await Promise.allSettled(
807
- pendingNotifications.docs.map(async (doc2) => {
807
+ pendingNotifications.docs.map(async (doc3) => {
808
808
  const notification = {
809
- id: doc2.id,
810
- ...doc2.data()
809
+ id: doc3.id,
810
+ ...doc3.data()
811
811
  };
812
812
  Logger.info(
813
813
  `[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
@@ -848,8 +848,8 @@ var NotificationsAdmin = class {
848
848
  break;
849
849
  }
850
850
  const batch = this.db.batch();
851
- oldNotifications.docs.forEach((doc2) => {
852
- batch.delete(doc2.ref);
851
+ oldNotifications.docs.forEach((doc3) => {
852
+ batch.delete(doc3.ref);
853
853
  });
854
854
  await batch.commit();
855
855
  totalDeleted += oldNotifications.size;
@@ -3537,10 +3537,10 @@ var AppointmentAggregationService = class {
3537
3537
  }
3538
3538
  const batch = this.db.batch();
3539
3539
  let instancesUpdatedCount = 0;
3540
- instancesSnapshot.docs.forEach((doc2) => {
3541
- const instance = doc2.data();
3540
+ instancesSnapshot.docs.forEach((doc3) => {
3541
+ const instance = doc3.data();
3542
3542
  if (instance.overallStatus !== newOverallStatus && instance.overallStatus !== "failedToProcess" /* FAILED_TO_PROCESS */) {
3543
- batch.update(doc2.ref, {
3543
+ batch.update(doc3.ref, {
3544
3544
  overallStatus: newOverallStatus,
3545
3545
  updatedAt: admin6.firestore.FieldValue.serverTimestamp()
3546
3546
  // Cast for now
@@ -3549,7 +3549,7 @@ var AppointmentAggregationService = class {
3549
3549
  });
3550
3550
  instancesUpdatedCount++;
3551
3551
  Logger.debug(
3552
- `[AggService] Added update for PatientRequirementInstance ${doc2.id} to batch. New status: ${newOverallStatus}`
3552
+ `[AggService] Added update for PatientRequirementInstance ${doc3.id} to batch. New status: ${newOverallStatus}`
3553
3553
  );
3554
3554
  }
3555
3555
  });
@@ -3724,8 +3724,8 @@ var AppointmentAggregationService = class {
3724
3724
  // --- Data Fetching Helpers (Consider moving to a data access layer or using existing services if available) ---
3725
3725
  async fetchPatientProfile(patientId) {
3726
3726
  try {
3727
- const doc2 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).get();
3728
- return doc2.exists ? doc2.data() : null;
3727
+ const doc3 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).get();
3728
+ return doc3.exists ? doc3.data() : null;
3729
3729
  } catch (error) {
3730
3730
  Logger.error(`[AggService] Error fetching patient profile ${patientId}:`, error);
3731
3731
  return null;
@@ -3738,12 +3738,12 @@ var AppointmentAggregationService = class {
3738
3738
  */
3739
3739
  async fetchPatientSensitiveInfo(patientId) {
3740
3740
  try {
3741
- const doc2 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(patientId).get();
3742
- if (!doc2.exists) {
3741
+ const doc3 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(patientId).get();
3742
+ if (!doc3.exists) {
3743
3743
  Logger.warn(`[AggService] No sensitive info found for patient ${patientId}`);
3744
3744
  return null;
3745
3745
  }
3746
- return doc2.data();
3746
+ return doc3.data();
3747
3747
  } catch (error) {
3748
3748
  Logger.error(`[AggService] Error fetching patient sensitive info ${patientId}:`, error);
3749
3749
  return null;
@@ -3760,12 +3760,12 @@ var AppointmentAggregationService = class {
3760
3760
  return null;
3761
3761
  }
3762
3762
  try {
3763
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
3764
- if (!doc2.exists) {
3763
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
3764
+ if (!doc3.exists) {
3765
3765
  Logger.warn(`[AggService] No practitioner profile found for ID ${practitionerId}`);
3766
3766
  return null;
3767
3767
  }
3768
- return doc2.data();
3768
+ return doc3.data();
3769
3769
  } catch (error) {
3770
3770
  Logger.error(`[AggService] Error fetching practitioner profile ${practitionerId}:`, error);
3771
3771
  return null;
@@ -3782,12 +3782,12 @@ var AppointmentAggregationService = class {
3782
3782
  return null;
3783
3783
  }
3784
3784
  try {
3785
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
3786
- if (!doc2.exists) {
3785
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
3786
+ if (!doc3.exists) {
3787
3787
  Logger.warn(`[AggService] No clinic info found for ID ${clinicId}`);
3788
3788
  return null;
3789
3789
  }
3790
- return doc2.data();
3790
+ return doc3.data();
3791
3791
  } catch (error) {
3792
3792
  Logger.error(`[AggService] Error fetching clinic info ${clinicId}:`, error);
3793
3793
  return null;
@@ -4254,11 +4254,11 @@ var ClinicAggregationService = class {
4254
4254
  return;
4255
4255
  }
4256
4256
  const batch = this.db.batch();
4257
- snapshot.docs.forEach((doc2) => {
4257
+ snapshot.docs.forEach((doc3) => {
4258
4258
  console.log(
4259
- `[ClinicAggregationService] Updating location for calendar event ${doc2.ref.path}`
4259
+ `[ClinicAggregationService] Updating location for calendar event ${doc3.ref.path}`
4260
4260
  );
4261
- batch.update(doc2.ref, {
4261
+ batch.update(doc3.ref, {
4262
4262
  eventLocation: newLocation,
4263
4263
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
4264
4264
  });
@@ -4301,11 +4301,11 @@ var ClinicAggregationService = class {
4301
4301
  return;
4302
4302
  }
4303
4303
  const batch = this.db.batch();
4304
- snapshot.docs.forEach((doc2) => {
4304
+ snapshot.docs.forEach((doc3) => {
4305
4305
  console.log(
4306
- `[ClinicAggregationService] Updating clinic info for calendar event ${doc2.ref.path}`
4306
+ `[ClinicAggregationService] Updating clinic info for calendar event ${doc3.ref.path}`
4307
4307
  );
4308
- batch.update(doc2.ref, {
4308
+ batch.update(doc3.ref, {
4309
4309
  clinicInfo,
4310
4310
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
4311
4311
  });
@@ -4516,11 +4516,11 @@ var ClinicAggregationService = class {
4516
4516
  return;
4517
4517
  }
4518
4518
  const batch = this.db.batch();
4519
- snapshot.docs.forEach((doc2) => {
4519
+ snapshot.docs.forEach((doc3) => {
4520
4520
  console.log(
4521
- `[ClinicAggregationService] Canceling calendar event ${doc2.ref.path}`
4521
+ `[ClinicAggregationService] Canceling calendar event ${doc3.ref.path}`
4522
4522
  );
4523
- batch.update(doc2.ref, {
4523
+ batch.update(doc3.ref, {
4524
4524
  status: "CANCELED",
4525
4525
  cancelReason: "Clinic deleted",
4526
4526
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
@@ -4788,11 +4788,11 @@ var PatientAggregationService = class {
4788
4788
  return;
4789
4789
  }
4790
4790
  const batch = this.db.batch();
4791
- snapshot.docs.forEach((doc2) => {
4791
+ snapshot.docs.forEach((doc3) => {
4792
4792
  console.log(
4793
- `[PatientAggregationService] Updating patient info for calendar event ${doc2.ref.path}`
4793
+ `[PatientAggregationService] Updating patient info for calendar event ${doc3.ref.path}`
4794
4794
  );
4795
- batch.update(doc2.ref, {
4795
+ batch.update(doc3.ref, {
4796
4796
  patientInfo,
4797
4797
  updatedAt: admin9.firestore.FieldValue.serverTimestamp()
4798
4798
  });
@@ -4836,11 +4836,11 @@ var PatientAggregationService = class {
4836
4836
  return;
4837
4837
  }
4838
4838
  const batch = this.db.batch();
4839
- snapshot.docs.forEach((doc2) => {
4839
+ snapshot.docs.forEach((doc3) => {
4840
4840
  console.log(
4841
- `[PatientAggregationService] Canceling calendar event ${doc2.ref.path}`
4841
+ `[PatientAggregationService] Canceling calendar event ${doc3.ref.path}`
4842
4842
  );
4843
- batch.update(doc2.ref, {
4843
+ batch.update(doc3.ref, {
4844
4844
  status: "CANCELED",
4845
4845
  cancelReason: "Patient deleted",
4846
4846
  updatedAt: admin9.firestore.FieldValue.serverTimestamp()
@@ -5011,11 +5011,11 @@ var PractitionerAggregationService = class {
5011
5011
  return;
5012
5012
  }
5013
5013
  const batch = this.db.batch();
5014
- snapshot.docs.forEach((doc2) => {
5014
+ snapshot.docs.forEach((doc3) => {
5015
5015
  console.log(
5016
- `[PractitionerAggregationService] Updating practitioner info for calendar event ${doc2.ref.path}`
5016
+ `[PractitionerAggregationService] Updating practitioner info for calendar event ${doc3.ref.path}`
5017
5017
  );
5018
- batch.update(doc2.ref, {
5018
+ batch.update(doc3.ref, {
5019
5019
  practitionerInfo,
5020
5020
  updatedAt: admin10.firestore.FieldValue.serverTimestamp()
5021
5021
  });
@@ -5099,11 +5099,11 @@ var PractitionerAggregationService = class {
5099
5099
  return;
5100
5100
  }
5101
5101
  const batch = this.db.batch();
5102
- snapshot.docs.forEach((doc2) => {
5102
+ snapshot.docs.forEach((doc3) => {
5103
5103
  console.log(
5104
- `[PractitionerAggregationService] Canceling calendar event ${doc2.ref.path}`
5104
+ `[PractitionerAggregationService] Canceling calendar event ${doc3.ref.path}`
5105
5105
  );
5106
- batch.update(doc2.ref, {
5106
+ batch.update(doc3.ref, {
5107
5107
  status: "CANCELED",
5108
5108
  cancelReason: "Practitioner deleted",
5109
5109
  updatedAt: admin10.firestore.FieldValue.serverTimestamp()
@@ -5655,8 +5655,8 @@ var PractitionerInviteAggregationService = class {
5655
5655
  */
5656
5656
  async fetchClinicAdminById(adminId) {
5657
5657
  try {
5658
- const doc2 = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
5659
- return doc2.exists ? doc2.data() : null;
5658
+ const doc3 = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
5659
+ return doc3.exists ? doc3.data() : null;
5660
5660
  } catch (error) {
5661
5661
  Logger.error(
5662
5662
  `[PractitionerInviteAggService] Error fetching clinic admin ${adminId}:`,
@@ -5672,8 +5672,8 @@ var PractitionerInviteAggregationService = class {
5672
5672
  */
5673
5673
  async fetchPractitionerById(practitionerId) {
5674
5674
  try {
5675
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
5676
- return doc2.exists ? doc2.data() : null;
5675
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
5676
+ return doc3.exists ? doc3.data() : null;
5677
5677
  } catch (error) {
5678
5678
  Logger.error(
5679
5679
  `[PractitionerInviteAggService] Error fetching practitioner ${practitionerId}:`,
@@ -5689,8 +5689,8 @@ var PractitionerInviteAggregationService = class {
5689
5689
  */
5690
5690
  async fetchClinicById(clinicId) {
5691
5691
  try {
5692
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
5693
- return doc2.exists ? doc2.data() : null;
5692
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
5693
+ return doc3.exists ? doc3.data() : null;
5694
5694
  } catch (error) {
5695
5695
  Logger.error(
5696
5696
  `[PractitionerInviteAggService] Error fetching clinic ${clinicId}:`,
@@ -6125,11 +6125,11 @@ var ProcedureAggregationService = class {
6125
6125
  return;
6126
6126
  }
6127
6127
  const batch = this.db.batch();
6128
- snapshot.docs.forEach((doc2) => {
6128
+ snapshot.docs.forEach((doc3) => {
6129
6129
  console.log(
6130
- `[ProcedureAggregationService] Updating procedure info for calendar event ${doc2.ref.path}`
6130
+ `[ProcedureAggregationService] Updating procedure info for calendar event ${doc3.ref.path}`
6131
6131
  );
6132
- batch.update(doc2.ref, {
6132
+ batch.update(doc3.ref, {
6133
6133
  procedureInfo,
6134
6134
  updatedAt: admin12.firestore.FieldValue.serverTimestamp()
6135
6135
  });
@@ -6172,11 +6172,11 @@ var ProcedureAggregationService = class {
6172
6172
  return;
6173
6173
  }
6174
6174
  const batch = this.db.batch();
6175
- snapshot.docs.forEach((doc2) => {
6175
+ snapshot.docs.forEach((doc3) => {
6176
6176
  console.log(
6177
- `[ProcedureAggregationService] Canceling calendar event ${doc2.ref.path}`
6177
+ `[ProcedureAggregationService] Canceling calendar event ${doc3.ref.path}`
6178
6178
  );
6179
- batch.update(doc2.ref, {
6179
+ batch.update(doc3.ref, {
6180
6180
  status: "CANCELED",
6181
6181
  cancelReason: "Procedure deleted or inactivated",
6182
6182
  updatedAt: admin12.firestore.FieldValue.serverTimestamp()
@@ -6557,7 +6557,7 @@ var ReviewsAggregationService = class {
6557
6557
  );
6558
6558
  return updatedReviewInfo2;
6559
6559
  }
6560
- const reviews = reviewsQuery.docs.map((doc2) => doc2.data());
6560
+ const reviews = reviewsQuery.docs.map((doc3) => doc3.data());
6561
6561
  const clinicReviews = reviews.map((review) => review.clinicReview).filter((review) => review !== void 0);
6562
6562
  let totalRating = 0;
6563
6563
  let totalCleanliness = 0;
@@ -6647,7 +6647,7 @@ var ReviewsAggregationService = class {
6647
6647
  );
6648
6648
  return updatedReviewInfo2;
6649
6649
  }
6650
- const reviews = reviewsQuery.docs.map((doc2) => doc2.data());
6650
+ const reviews = reviewsQuery.docs.map((doc3) => doc3.data());
6651
6651
  const practitionerReviews = reviews.map((review) => review.practitionerReview).filter((review) => review !== void 0);
6652
6652
  let totalRating = 0;
6653
6653
  let totalKnowledgeAndExpertise = 0;
@@ -6720,7 +6720,7 @@ var ReviewsAggregationService = class {
6720
6720
  recommendationPercentage: 0
6721
6721
  };
6722
6722
  const allReviewsQuery = await this.db.collection(REVIEWS_COLLECTION).get();
6723
- const reviews = allReviewsQuery.docs.map((doc2) => doc2.data());
6723
+ const reviews = allReviewsQuery.docs.map((doc3) => doc3.data());
6724
6724
  const procedureReviews = [];
6725
6725
  reviews.forEach((review) => {
6726
6726
  if (review.procedureReview && review.procedureReview.procedureId === procedureId) {
@@ -6890,7 +6890,7 @@ var ReviewsAggregationService = class {
6890
6890
  import * as admin14 from "firebase-admin";
6891
6891
 
6892
6892
  // src/services/analytics/analytics.service.ts
6893
- import { where, Timestamp as Timestamp2 } from "firebase/firestore";
6893
+ import { where as where2, Timestamp as Timestamp3 } from "firebase/firestore";
6894
6894
 
6895
6895
  // src/services/base.service.ts
6896
6896
  import { getStorage } from "firebase/storage";
@@ -6990,7 +6990,9 @@ function extractProductUsage(appointment) {
6990
6990
  if (item.type === "item" && item.productId) {
6991
6991
  const price = item.priceOverrideAmount || item.price || 0;
6992
6992
  const quantity = item.quantity || 1;
6993
- const subtotal = item.subtotal || price * quantity;
6993
+ const calculatedSubtotal = price * quantity;
6994
+ const storedSubtotal = item.subtotal || 0;
6995
+ const subtotal = Math.abs(storedSubtotal - calculatedSubtotal) < 0.01 ? storedSubtotal : calculatedSubtotal;
6994
6996
  products.push({
6995
6997
  productId: item.productId,
6996
6998
  productName: item.productName || "Unknown Product",
@@ -7119,6 +7121,17 @@ function calculateCancellationLeadTime(appointment) {
7119
7121
  }
7120
7122
 
7121
7123
  // src/services/analytics/utils/appointment-filtering.utils.ts
7124
+ function filterByDateRange(appointments, dateRange) {
7125
+ if (!dateRange) {
7126
+ return appointments;
7127
+ }
7128
+ const startTime = dateRange.start.getTime();
7129
+ const endTime = dateRange.end.getTime();
7130
+ return appointments.filter((appointment) => {
7131
+ const appointmentTime = appointment.appointmentStartTime.toMillis();
7132
+ return appointmentTime >= startTime && appointmentTime <= endTime;
7133
+ });
7134
+ }
7122
7135
  function filterAppointments(appointments, filters) {
7123
7136
  if (!filters) {
7124
7137
  return appointments;
@@ -7394,6 +7407,7 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7394
7407
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7395
7408
  const completed = getCompletedAppointments(appointments);
7396
7409
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7410
+ var _a;
7397
7411
  const entityAppointments = data.appointments;
7398
7412
  const entityCompleted = entityAppointments.filter(
7399
7413
  (a) => completed.some((c) => c.id === a.id)
@@ -7417,6 +7431,13 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7417
7431
  refundedRevenue += costData.cost;
7418
7432
  }
7419
7433
  });
7434
+ let practitionerId;
7435
+ let practitionerName;
7436
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7437
+ const firstAppointment = entityAppointments[0];
7438
+ practitionerId = firstAppointment.practitionerId;
7439
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7440
+ }
7420
7441
  return {
7421
7442
  entityId,
7422
7443
  entityName: data.name,
@@ -7429,7 +7450,9 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7429
7450
  unpaidRevenue,
7430
7451
  refundedRevenue,
7431
7452
  totalTax,
7432
- totalSubtotal
7453
+ totalSubtotal,
7454
+ ...practitionerId && { practitionerId },
7455
+ ...practitionerName && { practitionerName }
7433
7456
  };
7434
7457
  });
7435
7458
  }
@@ -7437,6 +7460,7 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7437
7460
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7438
7461
  const completed = getCompletedAppointments(appointments);
7439
7462
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7463
+ var _a;
7440
7464
  const entityAppointments = data.appointments;
7441
7465
  const entityCompleted = entityAppointments.filter(
7442
7466
  (a) => completed.some((c) => c.id === a.id)
@@ -7471,6 +7495,13 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7471
7495
  })).sort((a, b) => b.totalRevenue - a.totalRevenue).slice(0, 10);
7472
7496
  const totalProductRevenue = topProducts.reduce((sum, p) => sum + p.totalRevenue, 0);
7473
7497
  const totalProductQuantity = topProducts.reduce((sum, p) => sum + p.totalQuantity, 0);
7498
+ let practitionerId;
7499
+ let practitionerName;
7500
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7501
+ const firstAppointment = entityAppointments[0];
7502
+ practitionerId = firstAppointment.practitionerId;
7503
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7504
+ }
7474
7505
  return {
7475
7506
  entityId,
7476
7507
  entityName: data.name,
@@ -7480,7 +7511,9 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7480
7511
  totalProductRevenue,
7481
7512
  totalProductQuantity,
7482
7513
  averageProductsPerAppointment: entityCompleted.length > 0 ? productMap.size / entityCompleted.length : 0,
7483
- topProducts
7514
+ topProducts,
7515
+ ...practitionerId && { practitionerId },
7516
+ ...practitionerName && { practitionerName }
7484
7517
  };
7485
7518
  });
7486
7519
  }
@@ -7488,11 +7521,19 @@ function calculateGroupedTimeEfficiencyMetrics(appointments, entityType) {
7488
7521
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7489
7522
  const completed = getCompletedAppointments(appointments);
7490
7523
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7524
+ var _a;
7491
7525
  const entityAppointments = data.appointments;
7492
7526
  const entityCompleted = entityAppointments.filter(
7493
7527
  (a) => completed.some((c) => c.id === a.id)
7494
7528
  );
7495
7529
  const timeMetrics = calculateAverageTimeMetrics(entityCompleted);
7530
+ let practitionerId;
7531
+ let practitionerName;
7532
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7533
+ const firstAppointment = entityAppointments[0];
7534
+ practitionerId = firstAppointment.practitionerId;
7535
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7536
+ }
7496
7537
  return {
7497
7538
  entityId,
7498
7539
  entityName: data.name,
@@ -7505,7 +7546,9 @@ function calculateGroupedTimeEfficiencyMetrics(appointments, entityType) {
7505
7546
  totalOverrun: timeMetrics.totalOverrun,
7506
7547
  totalUnderutilization: timeMetrics.totalUnderutilization,
7507
7548
  averageOverrun: timeMetrics.averageOverrun,
7508
- averageUnderutilization: timeMetrics.averageUnderutilization
7549
+ averageUnderutilization: timeMetrics.averageUnderutilization,
7550
+ ...practitionerId && { practitionerId },
7551
+ ...practitionerName && { practitionerName }
7509
7552
  };
7510
7553
  });
7511
7554
  }
@@ -7587,6 +7630,763 @@ function calculateGroupedPatientBehaviorMetrics(appointments, entityType) {
7587
7630
  });
7588
7631
  }
7589
7632
 
7633
+ // src/services/analytics/utils/trend-calculation.utils.ts
7634
+ function getPeriodDates(date, period) {
7635
+ const year = date.getFullYear();
7636
+ const month = date.getMonth();
7637
+ const day = date.getDate();
7638
+ let startDate;
7639
+ let endDate;
7640
+ let periodString;
7641
+ switch (period) {
7642
+ case "week": {
7643
+ const dayOfWeek = date.getDay();
7644
+ const diff = date.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
7645
+ startDate = new Date(year, month, diff);
7646
+ startDate.setHours(0, 0, 0, 0);
7647
+ endDate = new Date(startDate);
7648
+ endDate.setDate(endDate.getDate() + 6);
7649
+ endDate.setHours(23, 59, 59, 999);
7650
+ const weekNumber = getWeekNumber(date);
7651
+ periodString = `${year}-W${weekNumber.toString().padStart(2, "0")}`;
7652
+ break;
7653
+ }
7654
+ case "month": {
7655
+ startDate = new Date(year, month, 1);
7656
+ startDate.setHours(0, 0, 0, 0);
7657
+ endDate = new Date(year, month + 1, 0);
7658
+ endDate.setHours(23, 59, 59, 999);
7659
+ periodString = `${year}-${(month + 1).toString().padStart(2, "0")}`;
7660
+ break;
7661
+ }
7662
+ case "quarter": {
7663
+ const quarter = Math.floor(month / 3);
7664
+ const quarterStartMonth = quarter * 3;
7665
+ startDate = new Date(year, quarterStartMonth, 1);
7666
+ startDate.setHours(0, 0, 0, 0);
7667
+ endDate = new Date(year, quarterStartMonth + 3, 0);
7668
+ endDate.setHours(23, 59, 59, 999);
7669
+ periodString = `${year}-Q${quarter + 1}`;
7670
+ break;
7671
+ }
7672
+ case "year": {
7673
+ startDate = new Date(year, 0, 1);
7674
+ startDate.setHours(0, 0, 0, 0);
7675
+ endDate = new Date(year, 11, 31);
7676
+ endDate.setHours(23, 59, 59, 999);
7677
+ periodString = `${year}`;
7678
+ break;
7679
+ }
7680
+ }
7681
+ return {
7682
+ period: periodString,
7683
+ startDate,
7684
+ endDate
7685
+ };
7686
+ }
7687
+ function getWeekNumber(date) {
7688
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
7689
+ const dayNum = d.getUTCDay() || 7;
7690
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
7691
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
7692
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
7693
+ }
7694
+ function groupAppointmentsByPeriod(appointments, period) {
7695
+ const periodMap = /* @__PURE__ */ new Map();
7696
+ appointments.forEach((appointment) => {
7697
+ const appointmentDate = appointment.appointmentStartTime.toDate();
7698
+ const periodInfo = getPeriodDates(appointmentDate, period);
7699
+ const periodKey = periodInfo.period;
7700
+ if (!periodMap.has(periodKey)) {
7701
+ periodMap.set(periodKey, []);
7702
+ }
7703
+ periodMap.get(periodKey).push(appointment);
7704
+ });
7705
+ return periodMap;
7706
+ }
7707
+ function generatePeriods(startDate, endDate, period) {
7708
+ const periods = [];
7709
+ const current = new Date(startDate);
7710
+ while (current <= endDate) {
7711
+ const periodInfo = getPeriodDates(current, period);
7712
+ if (periodInfo.endDate >= startDate && periodInfo.startDate <= endDate) {
7713
+ periods.push(periodInfo);
7714
+ }
7715
+ switch (period) {
7716
+ case "week":
7717
+ current.setDate(current.getDate() + 7);
7718
+ break;
7719
+ case "month":
7720
+ current.setMonth(current.getMonth() + 1);
7721
+ break;
7722
+ case "quarter":
7723
+ current.setMonth(current.getMonth() + 3);
7724
+ break;
7725
+ case "year":
7726
+ current.setFullYear(current.getFullYear() + 1);
7727
+ break;
7728
+ }
7729
+ }
7730
+ return periods.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
7731
+ }
7732
+ function calculatePercentageChange(current, previous) {
7733
+ if (previous === 0) {
7734
+ return current > 0 ? 100 : 0;
7735
+ }
7736
+ return (current - previous) / previous * 100;
7737
+ }
7738
+ function getTrendChange(current, previous) {
7739
+ const percentageChange = calculatePercentageChange(current, previous);
7740
+ const direction = percentageChange > 0.01 ? "up" : percentageChange < -0.01 ? "down" : "stable";
7741
+ return {
7742
+ value: current,
7743
+ previousValue: previous,
7744
+ percentageChange: Math.abs(percentageChange),
7745
+ direction
7746
+ };
7747
+ }
7748
+
7749
+ // src/services/analytics/review-analytics.service.ts
7750
+ import { collection, query, where, getDocs, getDoc as getDoc2, doc as doc2, Timestamp as Timestamp2 } from "firebase/firestore";
7751
+ var ReviewAnalyticsService = class extends BaseService {
7752
+ constructor(db, auth, app, appointmentService) {
7753
+ super(db, auth, app);
7754
+ this.appointmentService = appointmentService;
7755
+ }
7756
+ /**
7757
+ * Fetches reviews filtered by date range and optional filters
7758
+ * Properly filters by clinic branch by checking appointment's clinicId
7759
+ */
7760
+ async fetchReviews(dateRange, filters) {
7761
+ let q = query(collection(this.db, REVIEWS_COLLECTION));
7762
+ if (dateRange) {
7763
+ const startTimestamp = Timestamp2.fromDate(dateRange.start);
7764
+ const endTimestamp = Timestamp2.fromDate(dateRange.end);
7765
+ q = query(q, where("createdAt", ">=", startTimestamp), where("createdAt", "<=", endTimestamp));
7766
+ }
7767
+ const snapshot = await getDocs(q);
7768
+ const reviews = snapshot.docs.map((doc3) => {
7769
+ var _a, _b;
7770
+ const data = doc3.data();
7771
+ return {
7772
+ ...data,
7773
+ id: doc3.id,
7774
+ createdAt: ((_a = data.createdAt) == null ? void 0 : _a.toDate) ? data.createdAt.toDate() : new Date(data.createdAt),
7775
+ updatedAt: ((_b = data.updatedAt) == null ? void 0 : _b.toDate) ? data.updatedAt.toDate() : new Date(data.updatedAt)
7776
+ };
7777
+ });
7778
+ console.log(`[ReviewAnalytics] Fetched ${reviews.length} reviews in date range`);
7779
+ if ((filters == null ? void 0 : filters.clinicBranchId) && reviews.length > 0) {
7780
+ const appointmentIds = [...new Set(reviews.map((r) => r.appointmentId))];
7781
+ console.log(`[ReviewAnalytics] Filtering by clinic ${filters.clinicBranchId}, checking ${appointmentIds.length} appointments`);
7782
+ const validAppointmentIds = /* @__PURE__ */ new Set();
7783
+ for (let i = 0; i < appointmentIds.length; i += 10) {
7784
+ const batch = appointmentIds.slice(i, i + 10);
7785
+ const appointmentsQuery = query(
7786
+ collection(this.db, APPOINTMENTS_COLLECTION),
7787
+ where("id", "in", batch)
7788
+ );
7789
+ const appointmentSnapshot = await getDocs(appointmentsQuery);
7790
+ appointmentSnapshot.docs.forEach((doc3) => {
7791
+ const appointment = doc3.data();
7792
+ if (appointment.clinicBranchId === filters.clinicBranchId) {
7793
+ validAppointmentIds.add(doc3.id);
7794
+ }
7795
+ });
7796
+ }
7797
+ const filteredReviews = reviews.filter((review) => validAppointmentIds.has(review.appointmentId));
7798
+ console.log(`[ReviewAnalytics] After clinic filter: ${filteredReviews.length} reviews (from ${validAppointmentIds.size} valid appointments)`);
7799
+ return filteredReviews;
7800
+ }
7801
+ return reviews;
7802
+ }
7803
+ /**
7804
+ * Gets review metrics for a specific entity
7805
+ */
7806
+ async getReviewMetricsByEntity(entityType, entityId, dateRange, filters) {
7807
+ const reviews = await this.fetchReviews(dateRange, filters);
7808
+ let relevantReviews = [];
7809
+ if (entityType === "practitioner") {
7810
+ relevantReviews = reviews.filter((r) => {
7811
+ var _a;
7812
+ return ((_a = r.practitionerReview) == null ? void 0 : _a.practitionerId) === entityId;
7813
+ });
7814
+ } else if (entityType === "procedure") {
7815
+ relevantReviews = reviews.filter((r) => {
7816
+ var _a;
7817
+ return ((_a = r.procedureReview) == null ? void 0 : _a.procedureId) === entityId;
7818
+ });
7819
+ } else if (entityType === "category" || entityType === "subcategory") {
7820
+ relevantReviews = reviews;
7821
+ }
7822
+ if (relevantReviews.length === 0) {
7823
+ return null;
7824
+ }
7825
+ return this.calculateReviewMetrics(relevantReviews, entityType, entityId);
7826
+ }
7827
+ /**
7828
+ * Gets review metrics for multiple entities (grouped)
7829
+ */
7830
+ async getReviewMetricsByEntities(entityType, dateRange, filters) {
7831
+ const reviews = await this.fetchReviews(dateRange, filters);
7832
+ const entityMap = /* @__PURE__ */ new Map();
7833
+ let practitionerNameMap = null;
7834
+ let procedureNameMap = null;
7835
+ let procedureToTechnologyMap = null;
7836
+ if (entityType === "practitioner" || entityType === "procedure" || entityType === "technology") {
7837
+ if (!this.appointmentService) {
7838
+ console.warn(`[ReviewAnalytics] AppointmentService not available for ${entityType} name resolution`);
7839
+ return [];
7840
+ }
7841
+ console.log(`[ReviewAnalytics] Grouping by ${entityType}, fetching appointments for name resolution...`);
7842
+ const searchParams = {
7843
+ ...filters
7844
+ };
7845
+ if (dateRange) {
7846
+ searchParams.startDate = dateRange.start;
7847
+ searchParams.endDate = dateRange.end;
7848
+ }
7849
+ const appointmentsResult = await this.appointmentService.searchAppointments(searchParams);
7850
+ const appointments = appointmentsResult.appointments || [];
7851
+ console.log(`[ReviewAnalytics] Found ${appointments.length} appointments for name resolution`);
7852
+ practitionerNameMap = /* @__PURE__ */ new Map();
7853
+ procedureNameMap = /* @__PURE__ */ new Map();
7854
+ procedureToTechnologyMap = /* @__PURE__ */ new Map();
7855
+ appointments.forEach((appointment) => {
7856
+ var _a, _b, _c, _d, _e, _f;
7857
+ if (appointment.practitionerId && ((_a = appointment.practitionerInfo) == null ? void 0 : _a.name)) {
7858
+ practitionerNameMap.set(appointment.practitionerId, appointment.practitionerInfo.name);
7859
+ }
7860
+ if (appointment.procedureId) {
7861
+ if ((_b = appointment.procedureInfo) == null ? void 0 : _b.name) {
7862
+ procedureNameMap.set(appointment.procedureId, appointment.procedureInfo.name);
7863
+ }
7864
+ const mainTechnologyId = ((_c = appointment.procedureExtendedInfo) == null ? void 0 : _c.procedureTechnologyId) || "unknown-technology";
7865
+ const mainTechnologyName = ((_d = appointment.procedureExtendedInfo) == null ? void 0 : _d.procedureTechnologyName) || ((_e = appointment.procedureInfo) == null ? void 0 : _e.name) || "Unknown Technology";
7866
+ procedureToTechnologyMap.set(appointment.procedureId, {
7867
+ id: mainTechnologyId,
7868
+ name: mainTechnologyName
7869
+ });
7870
+ }
7871
+ if ((_f = appointment.metadata) == null ? void 0 : _f.extendedProcedures) {
7872
+ appointment.metadata.extendedProcedures.forEach((extendedProc) => {
7873
+ if (extendedProc.procedureId) {
7874
+ if (extendedProc.procedureName) {
7875
+ procedureNameMap.set(extendedProc.procedureId, extendedProc.procedureName);
7876
+ }
7877
+ const extTechnologyId = extendedProc.procedureTechnologyId || "unknown-technology";
7878
+ const extTechnologyName = extendedProc.procedureTechnologyName || "Unknown Technology";
7879
+ procedureToTechnologyMap.set(extendedProc.procedureId, {
7880
+ id: extTechnologyId,
7881
+ name: extTechnologyName
7882
+ });
7883
+ }
7884
+ });
7885
+ }
7886
+ });
7887
+ console.log(`[ReviewAnalytics] Built name maps: ${practitionerNameMap.size} practitioners, ${procedureNameMap.size} procedures, ${procedureToTechnologyMap.size} technologies`);
7888
+ }
7889
+ if (entityType === "technology" && procedureToTechnologyMap) {
7890
+ let processedReviewCount = 0;
7891
+ reviews.forEach((review) => {
7892
+ var _a;
7893
+ if ((_a = review.procedureReview) == null ? void 0 : _a.procedureId) {
7894
+ const techInfo = procedureToTechnologyMap.get(review.procedureReview.procedureId);
7895
+ if (techInfo) {
7896
+ if (!entityMap.has(techInfo.id)) {
7897
+ entityMap.set(techInfo.id, { reviews: [], name: techInfo.name });
7898
+ }
7899
+ entityMap.get(techInfo.id).reviews.push(review);
7900
+ processedReviewCount++;
7901
+ }
7902
+ }
7903
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
7904
+ review.extendedProcedureReviews.forEach((extendedReview) => {
7905
+ if (extendedReview.procedureId) {
7906
+ const techInfo = procedureToTechnologyMap.get(extendedReview.procedureId);
7907
+ if (techInfo) {
7908
+ if (!entityMap.has(techInfo.id)) {
7909
+ entityMap.set(techInfo.id, { reviews: [], name: techInfo.name });
7910
+ }
7911
+ const reviewWithExtendedOnly = {
7912
+ ...review,
7913
+ procedureReview: extendedReview,
7914
+ extendedProcedureReviews: void 0
7915
+ };
7916
+ entityMap.get(techInfo.id).reviews.push(reviewWithExtendedOnly);
7917
+ processedReviewCount++;
7918
+ }
7919
+ }
7920
+ });
7921
+ }
7922
+ });
7923
+ console.log(`[ReviewAnalytics] Processed ${processedReviewCount} procedure reviews into ${entityMap.size} technology groups`);
7924
+ entityMap.forEach((data, techId) => {
7925
+ console.log(`[ReviewAnalytics] - ${data.name} (${techId}): ${data.reviews.length} reviews`);
7926
+ });
7927
+ } else if (entityType === "procedure" && procedureNameMap) {
7928
+ let processedReviewCount = 0;
7929
+ reviews.forEach((review) => {
7930
+ if (review.procedureReview) {
7931
+ const procedureId = review.procedureReview.procedureId;
7932
+ const procedureName = procedureId && procedureNameMap.get(procedureId) || review.procedureReview.procedureName || "Unknown Procedure";
7933
+ if (procedureId) {
7934
+ if (!entityMap.has(procedureId)) {
7935
+ entityMap.set(procedureId, { reviews: [], name: procedureName });
7936
+ }
7937
+ entityMap.get(procedureId).reviews.push(review);
7938
+ processedReviewCount++;
7939
+ }
7940
+ }
7941
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
7942
+ review.extendedProcedureReviews.forEach((extendedReview) => {
7943
+ const procedureId = extendedReview.procedureId;
7944
+ const procedureName = procedureId && procedureNameMap.get(procedureId) || extendedReview.procedureName || "Unknown Procedure";
7945
+ if (procedureId) {
7946
+ if (!entityMap.has(procedureId)) {
7947
+ entityMap.set(procedureId, { reviews: [], name: procedureName });
7948
+ }
7949
+ const reviewWithExtendedOnly = {
7950
+ ...review,
7951
+ procedureReview: extendedReview,
7952
+ extendedProcedureReviews: void 0
7953
+ };
7954
+ entityMap.get(procedureId).reviews.push(reviewWithExtendedOnly);
7955
+ processedReviewCount++;
7956
+ }
7957
+ });
7958
+ }
7959
+ });
7960
+ console.log(`[ReviewAnalytics] Processed ${processedReviewCount} procedure reviews into ${entityMap.size} procedure groups`);
7961
+ entityMap.forEach((data, procId) => {
7962
+ console.log(`[ReviewAnalytics] - ${data.name} (${procId}): ${data.reviews.length} reviews`);
7963
+ });
7964
+ } else if (entityType === "practitioner" && practitionerNameMap) {
7965
+ reviews.forEach((review) => {
7966
+ if (review.practitionerReview) {
7967
+ const practitionerId = review.practitionerReview.practitionerId;
7968
+ const practitionerName = practitionerId && practitionerNameMap.get(practitionerId) || review.practitionerReview.practitionerName || "Unknown Practitioner";
7969
+ if (practitionerId) {
7970
+ if (!entityMap.has(practitionerId)) {
7971
+ entityMap.set(practitionerId, { reviews: [], name: practitionerName });
7972
+ }
7973
+ entityMap.get(practitionerId).reviews.push(review);
7974
+ }
7975
+ }
7976
+ });
7977
+ console.log(`[ReviewAnalytics] Processed ${reviews.length} reviews into ${entityMap.size} practitioner groups`);
7978
+ entityMap.forEach((data, practId) => {
7979
+ console.log(`[ReviewAnalytics] - ${data.name} (${practId}): ${data.reviews.length} reviews`);
7980
+ });
7981
+ } else {
7982
+ reviews.forEach((review) => {
7983
+ let entityId;
7984
+ let entityName;
7985
+ if (entityId) {
7986
+ if (!entityMap.has(entityId)) {
7987
+ entityMap.set(entityId, { reviews: [], name: entityName || entityId });
7988
+ }
7989
+ entityMap.get(entityId).reviews.push(review);
7990
+ }
7991
+ });
7992
+ }
7993
+ const metrics = [];
7994
+ for (const [entityId, data] of entityMap.entries()) {
7995
+ const metric = this.calculateReviewMetrics(data.reviews, entityType, entityId);
7996
+ if (metric) {
7997
+ metric.entityName = data.name;
7998
+ metrics.push(metric);
7999
+ }
8000
+ }
8001
+ return metrics;
8002
+ }
8003
+ /**
8004
+ * Calculates review metrics from a list of reviews
8005
+ */
8006
+ calculateReviewMetrics(reviews, entityType, entityId) {
8007
+ if (reviews.length === 0) {
8008
+ return null;
8009
+ }
8010
+ let totalRating = 0;
8011
+ let recommendationCount = 0;
8012
+ let practitionerMetrics;
8013
+ let procedureMetrics;
8014
+ let entityName = entityId;
8015
+ if (entityType === "practitioner") {
8016
+ const practitionerReviews = reviews.filter((r) => r.practitionerReview).map((r) => r.practitionerReview);
8017
+ if (practitionerReviews.length === 0) {
8018
+ return null;
8019
+ }
8020
+ entityName = practitionerReviews[0].practitionerName || entityId;
8021
+ totalRating = practitionerReviews.reduce((sum, r) => sum + r.overallRating, 0);
8022
+ recommendationCount = practitionerReviews.filter((r) => r.wouldRecommend).length;
8023
+ practitionerMetrics = {
8024
+ averageKnowledgeAndExpertise: this.calculateAverage(practitionerReviews.map((r) => r.knowledgeAndExpertise)),
8025
+ averageCommunicationSkills: this.calculateAverage(practitionerReviews.map((r) => r.communicationSkills)),
8026
+ averageBedSideManner: this.calculateAverage(practitionerReviews.map((r) => r.bedSideManner)),
8027
+ averageThoroughness: this.calculateAverage(practitionerReviews.map((r) => r.thoroughness)),
8028
+ averageTrustworthiness: this.calculateAverage(practitionerReviews.map((r) => r.trustworthiness))
8029
+ };
8030
+ } else if (entityType === "procedure" || entityType === "technology") {
8031
+ const procedureReviews = reviews.filter((r) => r.procedureReview).map((r) => r.procedureReview);
8032
+ if (procedureReviews.length === 0) {
8033
+ return null;
8034
+ }
8035
+ if (entityType === "procedure") {
8036
+ entityName = procedureReviews[0].procedureName || entityId;
8037
+ }
8038
+ totalRating = procedureReviews.reduce((sum, r) => sum + r.overallRating, 0);
8039
+ recommendationCount = procedureReviews.filter((r) => r.wouldRecommend).length;
8040
+ procedureMetrics = {
8041
+ averageEffectiveness: this.calculateAverage(procedureReviews.map((r) => r.effectivenessOfTreatment)),
8042
+ averageOutcomeExplanation: this.calculateAverage(procedureReviews.map((r) => r.outcomeExplanation)),
8043
+ averagePainManagement: this.calculateAverage(procedureReviews.map((r) => r.painManagement)),
8044
+ averageFollowUpCare: this.calculateAverage(procedureReviews.map((r) => r.followUpCare)),
8045
+ averageValueForMoney: this.calculateAverage(procedureReviews.map((r) => r.valueForMoney))
8046
+ };
8047
+ }
8048
+ const averageRating = totalRating / reviews.length;
8049
+ const recommendationRate = recommendationCount / reviews.length * 100;
8050
+ const result = {
8051
+ entityId,
8052
+ entityName,
8053
+ entityType,
8054
+ totalReviews: reviews.length,
8055
+ averageRating,
8056
+ recommendationRate,
8057
+ practitionerMetrics,
8058
+ procedureMetrics,
8059
+ comparisonToOverall: {
8060
+ ratingDifference: 0,
8061
+ // Will be calculated when comparing to overall
8062
+ recommendationDifference: 0
8063
+ }
8064
+ };
8065
+ return result;
8066
+ }
8067
+ /**
8068
+ * Gets overall review averages for comparison
8069
+ */
8070
+ async getOverallReviewAverages(dateRange, filters) {
8071
+ const reviews = await this.fetchReviews(dateRange, filters);
8072
+ const practitionerReviews = reviews.filter((r) => r.practitionerReview).map((r) => r.practitionerReview);
8073
+ const procedureReviews = reviews.filter((r) => r.procedureReview).map((r) => r.procedureReview);
8074
+ return {
8075
+ practitionerAverage: {
8076
+ totalReviews: practitionerReviews.length,
8077
+ averageRating: practitionerReviews.length > 0 ? this.calculateAverage(practitionerReviews.map((r) => r.overallRating)) : 0,
8078
+ recommendationRate: practitionerReviews.length > 0 ? practitionerReviews.filter((r) => r.wouldRecommend).length / practitionerReviews.length * 100 : 0,
8079
+ averageKnowledgeAndExpertise: this.calculateAverage(practitionerReviews.map((r) => r.knowledgeAndExpertise)),
8080
+ averageCommunicationSkills: this.calculateAverage(practitionerReviews.map((r) => r.communicationSkills)),
8081
+ averageBedSideManner: this.calculateAverage(practitionerReviews.map((r) => r.bedSideManner)),
8082
+ averageThoroughness: this.calculateAverage(practitionerReviews.map((r) => r.thoroughness)),
8083
+ averageTrustworthiness: this.calculateAverage(practitionerReviews.map((r) => r.trustworthiness))
8084
+ },
8085
+ procedureAverage: {
8086
+ totalReviews: procedureReviews.length,
8087
+ averageRating: procedureReviews.length > 0 ? this.calculateAverage(procedureReviews.map((r) => r.overallRating)) : 0,
8088
+ recommendationRate: procedureReviews.length > 0 ? procedureReviews.filter((r) => r.wouldRecommend).length / procedureReviews.length * 100 : 0,
8089
+ averageEffectiveness: this.calculateAverage(procedureReviews.map((r) => r.effectivenessOfTreatment)),
8090
+ averageOutcomeExplanation: this.calculateAverage(procedureReviews.map((r) => r.outcomeExplanation)),
8091
+ averagePainManagement: this.calculateAverage(procedureReviews.map((r) => r.painManagement)),
8092
+ averageFollowUpCare: this.calculateAverage(procedureReviews.map((r) => r.followUpCare)),
8093
+ averageValueForMoney: this.calculateAverage(procedureReviews.map((r) => r.valueForMoney))
8094
+ }
8095
+ };
8096
+ }
8097
+ /**
8098
+ * Gets review details for a specific entity
8099
+ */
8100
+ async getReviewDetails(entityType, entityId, dateRange, filters) {
8101
+ var _a, _b, _c;
8102
+ const reviews = await this.fetchReviews(dateRange, filters);
8103
+ let relevantReviews = [];
8104
+ if (entityType === "practitioner") {
8105
+ relevantReviews = reviews.filter((r) => {
8106
+ var _a2;
8107
+ return ((_a2 = r.practitionerReview) == null ? void 0 : _a2.practitionerId) === entityId;
8108
+ });
8109
+ } else if (entityType === "procedure") {
8110
+ relevantReviews = reviews.filter((r) => {
8111
+ var _a2;
8112
+ return ((_a2 = r.procedureReview) == null ? void 0 : _a2.procedureId) === entityId;
8113
+ });
8114
+ }
8115
+ const details = [];
8116
+ for (const review of relevantReviews) {
8117
+ try {
8118
+ const appointmentDocRef = doc2(this.db, APPOINTMENTS_COLLECTION, review.appointmentId);
8119
+ const appointmentDoc = await getDoc2(appointmentDocRef);
8120
+ let appointment = null;
8121
+ if (appointmentDoc.exists()) {
8122
+ appointment = appointmentDoc.data();
8123
+ }
8124
+ const createdAt = review.createdAt instanceof Timestamp2 ? review.createdAt.toDate() : new Date(review.createdAt);
8125
+ const appointmentDate = (appointment == null ? void 0 : appointment.appointmentStartTime) ? appointment.appointmentStartTime instanceof Timestamp2 ? appointment.appointmentStartTime.toDate() : appointment.appointmentStartTime : createdAt;
8126
+ details.push({
8127
+ reviewId: review.id,
8128
+ appointmentId: review.appointmentId,
8129
+ patientId: review.patientId,
8130
+ patientName: review.patientName || ((_a = appointment == null ? void 0 : appointment.patientInfo) == null ? void 0 : _a.fullName),
8131
+ createdAt,
8132
+ practitionerReview: review.practitionerReview,
8133
+ procedureReview: review.procedureReview,
8134
+ procedureName: (_b = appointment == null ? void 0 : appointment.procedureInfo) == null ? void 0 : _b.name,
8135
+ practitionerName: (_c = appointment == null ? void 0 : appointment.practitionerInfo) == null ? void 0 : _c.name,
8136
+ appointmentDate
8137
+ });
8138
+ } catch (error) {
8139
+ console.warn(`Failed to enhance review ${review.id}:`, error);
8140
+ }
8141
+ }
8142
+ return details;
8143
+ }
8144
+ /**
8145
+ * Helper method to calculate average
8146
+ */
8147
+ calculateAverage(values) {
8148
+ if (values.length === 0) return 0;
8149
+ const sum = values.reduce((acc, val) => acc + val, 0);
8150
+ return sum / values.length;
8151
+ }
8152
+ /**
8153
+ * Calculate review trends over time
8154
+ * Groups reviews by period and calculates rating and recommendation metrics
8155
+ *
8156
+ * @param dateRange - Date range for trend analysis (must align with period boundaries)
8157
+ * @param period - Period type (week, month, quarter, year)
8158
+ * @param filters - Optional filters for clinic, practitioner, procedure
8159
+ * @param entityType - Optional entity type to group trends by (practitioner, procedure, technology)
8160
+ * @returns Array of review trends with percentage changes
8161
+ */
8162
+ async getReviewTrends(dateRange, period, filters, entityType) {
8163
+ const reviews = await this.fetchReviews(dateRange, filters);
8164
+ if (reviews.length === 0) {
8165
+ return [];
8166
+ }
8167
+ if (entityType) {
8168
+ return this.getGroupedReviewTrends(reviews, dateRange, period, entityType, filters);
8169
+ }
8170
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
8171
+ const trends = [];
8172
+ let previousAvgRating = 0;
8173
+ let previousRecRate = 0;
8174
+ periods.forEach((periodInfo) => {
8175
+ const periodReviews = reviews.filter((review) => {
8176
+ const reviewDate = review.createdAt instanceof Date ? review.createdAt : review.createdAt.toDate();
8177
+ return reviewDate >= periodInfo.startDate && reviewDate <= periodInfo.endDate;
8178
+ });
8179
+ if (periodReviews.length === 0) {
8180
+ trends.push({
8181
+ period: periodInfo.period,
8182
+ startDate: periodInfo.startDate,
8183
+ endDate: periodInfo.endDate,
8184
+ averageRating: 0,
8185
+ recommendationRate: 0,
8186
+ totalReviews: 0,
8187
+ previousPeriod: void 0
8188
+ });
8189
+ previousAvgRating = 0;
8190
+ previousRecRate = 0;
8191
+ return;
8192
+ }
8193
+ let totalRatingSum = 0;
8194
+ let totalRatingCount = 0;
8195
+ let totalRecommendations = 0;
8196
+ let totalRecommendationCount = 0;
8197
+ periodReviews.forEach((review) => {
8198
+ if (review.practitionerReview) {
8199
+ totalRatingSum += review.practitionerReview.overallRating;
8200
+ totalRatingCount++;
8201
+ if (review.practitionerReview.wouldRecommend) {
8202
+ totalRecommendations++;
8203
+ }
8204
+ totalRecommendationCount++;
8205
+ }
8206
+ if (review.procedureReview) {
8207
+ totalRatingSum += review.procedureReview.overallRating;
8208
+ totalRatingCount++;
8209
+ if (review.procedureReview.wouldRecommend) {
8210
+ totalRecommendations++;
8211
+ }
8212
+ totalRecommendationCount++;
8213
+ }
8214
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8215
+ review.extendedProcedureReviews.forEach((extReview) => {
8216
+ totalRatingSum += extReview.overallRating;
8217
+ totalRatingCount++;
8218
+ if (extReview.wouldRecommend) {
8219
+ totalRecommendations++;
8220
+ }
8221
+ totalRecommendationCount++;
8222
+ });
8223
+ }
8224
+ });
8225
+ const currentAvgRating = totalRatingCount > 0 ? totalRatingSum / totalRatingCount : 0;
8226
+ const currentRecRate = totalRecommendationCount > 0 ? totalRecommendations / totalRecommendationCount * 100 : 0;
8227
+ const trendChange = getTrendChange(currentAvgRating, previousAvgRating);
8228
+ trends.push({
8229
+ period: periodInfo.period,
8230
+ startDate: periodInfo.startDate,
8231
+ endDate: periodInfo.endDate,
8232
+ averageRating: currentAvgRating,
8233
+ recommendationRate: currentRecRate,
8234
+ totalReviews: periodReviews.length,
8235
+ previousPeriod: previousAvgRating > 0 ? {
8236
+ averageRating: previousAvgRating,
8237
+ recommendationRate: previousRecRate,
8238
+ percentageChange: Math.abs(trendChange.percentageChange),
8239
+ direction: trendChange.direction
8240
+ } : void 0
8241
+ });
8242
+ previousAvgRating = currentAvgRating;
8243
+ previousRecRate = currentRecRate;
8244
+ });
8245
+ return trends;
8246
+ }
8247
+ /**
8248
+ * Calculate grouped review trends (by practitioner, procedure, or technology)
8249
+ * Returns the AVERAGE across all entities of that type for each period
8250
+ * @private
8251
+ */
8252
+ async getGroupedReviewTrends(reviews, dateRange, period, entityType, filters) {
8253
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
8254
+ const trends = [];
8255
+ let appointments = [];
8256
+ let procedureToTechnologyMap = /* @__PURE__ */ new Map();
8257
+ if (entityType === "technology" && this.appointmentService) {
8258
+ const searchParams = { ...filters };
8259
+ if (dateRange) {
8260
+ searchParams.startDate = dateRange.start;
8261
+ searchParams.endDate = dateRange.end;
8262
+ }
8263
+ const appointmentsResult = await this.appointmentService.searchAppointments(searchParams);
8264
+ appointments = appointmentsResult.appointments || [];
8265
+ appointments.forEach((appointment) => {
8266
+ var _a, _b;
8267
+ if (appointment.procedureId && ((_a = appointment.procedureExtendedInfo) == null ? void 0 : _a.procedureTechnologyId)) {
8268
+ procedureToTechnologyMap.set(appointment.procedureId, {
8269
+ id: appointment.procedureExtendedInfo.procedureTechnologyId,
8270
+ name: appointment.procedureExtendedInfo.procedureTechnologyName || "Unknown Technology"
8271
+ });
8272
+ }
8273
+ if ((_b = appointment.metadata) == null ? void 0 : _b.extendedProcedures) {
8274
+ appointment.metadata.extendedProcedures.forEach((extProc) => {
8275
+ if (extProc.procedureId && extProc.procedureTechnologyId) {
8276
+ procedureToTechnologyMap.set(extProc.procedureId, {
8277
+ id: extProc.procedureTechnologyId,
8278
+ name: extProc.procedureTechnologyName || "Unknown Technology"
8279
+ });
8280
+ }
8281
+ });
8282
+ }
8283
+ });
8284
+ }
8285
+ let previousAvgRating = 0;
8286
+ let previousRecRate = 0;
8287
+ periods.forEach((periodInfo) => {
8288
+ const periodReviews = reviews.filter((review) => {
8289
+ const reviewDate = review.createdAt instanceof Date ? review.createdAt : review.createdAt.toDate();
8290
+ return reviewDate >= periodInfo.startDate && reviewDate <= periodInfo.endDate;
8291
+ });
8292
+ if (periodReviews.length === 0) {
8293
+ trends.push({
8294
+ period: periodInfo.period,
8295
+ startDate: periodInfo.startDate,
8296
+ endDate: periodInfo.endDate,
8297
+ averageRating: 0,
8298
+ recommendationRate: 0,
8299
+ totalReviews: 0,
8300
+ previousPeriod: void 0
8301
+ });
8302
+ previousAvgRating = 0;
8303
+ previousRecRate = 0;
8304
+ return;
8305
+ }
8306
+ let totalRatingSum = 0;
8307
+ let totalRatingCount = 0;
8308
+ let totalRecommendations = 0;
8309
+ let totalRecommendationCount = 0;
8310
+ periodReviews.forEach((review) => {
8311
+ var _a;
8312
+ if (entityType === "practitioner" && review.practitionerReview) {
8313
+ totalRatingSum += review.practitionerReview.overallRating;
8314
+ totalRatingCount++;
8315
+ if (review.practitionerReview.wouldRecommend) {
8316
+ totalRecommendations++;
8317
+ }
8318
+ totalRecommendationCount++;
8319
+ } else if (entityType === "procedure" && review.procedureReview) {
8320
+ totalRatingSum += review.procedureReview.overallRating;
8321
+ totalRatingCount++;
8322
+ if (review.procedureReview.wouldRecommend) {
8323
+ totalRecommendations++;
8324
+ }
8325
+ totalRecommendationCount++;
8326
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8327
+ review.extendedProcedureReviews.forEach((extReview) => {
8328
+ totalRatingSum += extReview.overallRating;
8329
+ totalRatingCount++;
8330
+ if (extReview.wouldRecommend) {
8331
+ totalRecommendations++;
8332
+ }
8333
+ totalRecommendationCount++;
8334
+ });
8335
+ }
8336
+ } else if (entityType === "technology") {
8337
+ if ((_a = review.procedureReview) == null ? void 0 : _a.procedureId) {
8338
+ const tech = procedureToTechnologyMap.get(review.procedureReview.procedureId);
8339
+ if (tech) {
8340
+ totalRatingSum += review.procedureReview.overallRating;
8341
+ totalRatingCount++;
8342
+ if (review.procedureReview.wouldRecommend) {
8343
+ totalRecommendations++;
8344
+ }
8345
+ totalRecommendationCount++;
8346
+ }
8347
+ }
8348
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8349
+ review.extendedProcedureReviews.forEach((extReview) => {
8350
+ if (extReview.procedureId) {
8351
+ const tech = procedureToTechnologyMap.get(extReview.procedureId);
8352
+ if (tech) {
8353
+ totalRatingSum += extReview.overallRating;
8354
+ totalRatingCount++;
8355
+ if (extReview.wouldRecommend) {
8356
+ totalRecommendations++;
8357
+ }
8358
+ totalRecommendationCount++;
8359
+ }
8360
+ }
8361
+ });
8362
+ }
8363
+ }
8364
+ });
8365
+ const currentAvgRating = totalRatingCount > 0 ? totalRatingSum / totalRatingCount : 0;
8366
+ const currentRecRate = totalRecommendationCount > 0 ? totalRecommendations / totalRecommendationCount * 100 : 0;
8367
+ const trendChange = getTrendChange(currentAvgRating, previousAvgRating);
8368
+ trends.push({
8369
+ period: periodInfo.period,
8370
+ startDate: periodInfo.startDate,
8371
+ endDate: periodInfo.endDate,
8372
+ averageRating: currentAvgRating,
8373
+ recommendationRate: currentRecRate,
8374
+ totalReviews: totalRatingCount,
8375
+ // Count of reviews for this entity type
8376
+ previousPeriod: previousAvgRating > 0 ? {
8377
+ averageRating: previousAvgRating,
8378
+ recommendationRate: previousRecRate,
8379
+ percentageChange: Math.abs(trendChange.percentageChange),
8380
+ direction: trendChange.direction
8381
+ } : void 0
8382
+ });
8383
+ previousAvgRating = currentAvgRating;
8384
+ previousRecRate = currentRecRate;
8385
+ });
8386
+ return trends;
8387
+ }
8388
+ };
8389
+
7590
8390
  // src/services/analytics/analytics.service.ts
7591
8391
  var AnalyticsService = class extends BaseService {
7592
8392
  /**
@@ -7600,6 +8400,7 @@ var AnalyticsService = class extends BaseService {
7600
8400
  constructor(db, auth, app, appointmentService) {
7601
8401
  super(db, auth, app);
7602
8402
  this.appointmentService = appointmentService;
8403
+ this.reviewAnalyticsService = new ReviewAnalyticsService(db, auth, app, appointmentService);
7603
8404
  }
7604
8405
  /**
7605
8406
  * Fetches appointments with optional filters
@@ -7612,20 +8413,20 @@ var AnalyticsService = class extends BaseService {
7612
8413
  try {
7613
8414
  const constraints = [];
7614
8415
  if (filters == null ? void 0 : filters.clinicBranchId) {
7615
- constraints.push(where("clinicBranchId", "==", filters.clinicBranchId));
8416
+ constraints.push(where2("clinicBranchId", "==", filters.clinicBranchId));
7616
8417
  }
7617
8418
  if (filters == null ? void 0 : filters.practitionerId) {
7618
- constraints.push(where("practitionerId", "==", filters.practitionerId));
8419
+ constraints.push(where2("practitionerId", "==", filters.practitionerId));
7619
8420
  }
7620
8421
  if (filters == null ? void 0 : filters.procedureId) {
7621
- constraints.push(where("procedureId", "==", filters.procedureId));
8422
+ constraints.push(where2("procedureId", "==", filters.procedureId));
7622
8423
  }
7623
8424
  if (filters == null ? void 0 : filters.patientId) {
7624
- constraints.push(where("patientId", "==", filters.patientId));
8425
+ constraints.push(where2("patientId", "==", filters.patientId));
7625
8426
  }
7626
8427
  if (dateRange) {
7627
- constraints.push(where("appointmentStartTime", ">=", Timestamp2.fromDate(dateRange.start)));
7628
- constraints.push(where("appointmentStartTime", "<=", Timestamp2.fromDate(dateRange.end)));
8428
+ constraints.push(where2("appointmentStartTime", ">=", Timestamp3.fromDate(dateRange.start)));
8429
+ constraints.push(where2("appointmentStartTime", "<=", Timestamp3.fromDate(dateRange.end)));
7629
8430
  }
7630
8431
  const searchParams = {};
7631
8432
  if (filters == null ? void 0 : filters.clinicBranchId) searchParams.clinicBranchId = filters.clinicBranchId;
@@ -8107,11 +8908,17 @@ var AnalyticsService = class extends BaseService {
8107
8908
  groupCancellationsByProcedure(canceled, allAppointments) {
8108
8909
  const procedureMap = /* @__PURE__ */ new Map();
8109
8910
  allAppointments.forEach((appointment) => {
8110
- var _a;
8911
+ var _a, _b;
8111
8912
  const procedureId = appointment.procedureId;
8112
8913
  const procedureName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8113
8914
  if (!procedureMap.has(procedureId)) {
8114
- procedureMap.set(procedureId, { name: procedureName, canceled: [], all: [] });
8915
+ procedureMap.set(procedureId, {
8916
+ name: procedureName,
8917
+ canceled: [],
8918
+ all: [],
8919
+ practitionerId: appointment.practitionerId,
8920
+ practitionerName: (_b = appointment.practitionerInfo) == null ? void 0 : _b.name
8921
+ });
8115
8922
  }
8116
8923
  procedureMap.get(procedureId).all.push(appointment);
8117
8924
  });
@@ -8121,15 +8928,20 @@ var AnalyticsService = class extends BaseService {
8121
8928
  procedureMap.get(procedureId).canceled.push(appointment);
8122
8929
  }
8123
8930
  });
8124
- return Array.from(procedureMap.entries()).map(
8125
- ([procedureId, data]) => this.calculateCancellationMetrics(
8931
+ return Array.from(procedureMap.entries()).map(([procedureId, data]) => {
8932
+ const metrics = this.calculateCancellationMetrics(
8126
8933
  procedureId,
8127
8934
  data.name,
8128
8935
  "procedure",
8129
8936
  data.canceled,
8130
8937
  data.all
8131
- )
8132
- );
8938
+ );
8939
+ return {
8940
+ ...metrics,
8941
+ ...data.practitionerId && { practitionerId: data.practitionerId },
8942
+ ...data.practitionerName && { practitionerName: data.practitionerName }
8943
+ };
8944
+ });
8133
8945
  }
8134
8946
  /**
8135
8947
  * Group cancellations by technology
@@ -8334,11 +9146,17 @@ var AnalyticsService = class extends BaseService {
8334
9146
  groupNoShowsByProcedure(noShow, allAppointments) {
8335
9147
  const procedureMap = /* @__PURE__ */ new Map();
8336
9148
  allAppointments.forEach((appointment) => {
8337
- var _a;
9149
+ var _a, _b;
8338
9150
  const procedureId = appointment.procedureId;
8339
9151
  const procedureName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8340
9152
  if (!procedureMap.has(procedureId)) {
8341
- procedureMap.set(procedureId, { name: procedureName, noShow: [], all: [] });
9153
+ procedureMap.set(procedureId, {
9154
+ name: procedureName,
9155
+ noShow: [],
9156
+ all: [],
9157
+ practitionerId: appointment.practitionerId,
9158
+ practitionerName: (_b = appointment.practitionerInfo) == null ? void 0 : _b.name
9159
+ });
8342
9160
  }
8343
9161
  procedureMap.get(procedureId).all.push(appointment);
8344
9162
  });
@@ -8354,7 +9172,9 @@ var AnalyticsService = class extends BaseService {
8354
9172
  entityType: "procedure",
8355
9173
  totalAppointments: data.all.length,
8356
9174
  noShowAppointments: data.noShow.length,
8357
- noShowRate: calculatePercentage(data.noShow.length, data.all.length)
9175
+ noShowRate: calculatePercentage(data.noShow.length, data.all.length),
9176
+ ...data.practitionerId && { practitionerId: data.practitionerId },
9177
+ ...data.practitionerName && { practitionerName: data.practitionerName }
8358
9178
  }));
8359
9179
  }
8360
9180
  /**
@@ -8407,6 +9227,10 @@ var AnalyticsService = class extends BaseService {
8407
9227
  * Get revenue metrics
8408
9228
  * First checks for stored analytics, then calculates if not available or stale
8409
9229
  *
9230
+ * IMPORTANT: Financial calculations only consider COMPLETED appointments.
9231
+ * Confirmed, pending, canceled, and no-show appointments are NOT included in revenue calculations.
9232
+ * Only procedures that have been completed generate revenue.
9233
+ *
8410
9234
  * @param filters - Optional filters
8411
9235
  * @param dateRange - Optional date range filter
8412
9236
  * @param options - Options for reading stored analytics
@@ -8429,11 +9253,8 @@ var AnalyticsService = class extends BaseService {
8429
9253
  const completed = getCompletedAppointments(appointments);
8430
9254
  const { totalRevenue, currency } = calculateTotalRevenue(completed);
8431
9255
  const revenueByStatus = {};
8432
- Object.values(AppointmentStatus).forEach((status) => {
8433
- const statusAppointments = appointments.filter((a) => a.status === status);
8434
- const { totalRevenue: statusRevenue } = calculateTotalRevenue(statusAppointments);
8435
- revenueByStatus[status] = statusRevenue;
8436
- });
9256
+ const { totalRevenue: completedRevenue } = calculateTotalRevenue(completed);
9257
+ revenueByStatus["completed" /* COMPLETED */] = completedRevenue;
8437
9258
  const revenueByPaymentStatus = {};
8438
9259
  Object.values(PaymentStatus).forEach((paymentStatus) => {
8439
9260
  const paymentAppointments = completed.filter((a) => a.paymentStatus === paymentStatus);
@@ -8487,18 +9308,23 @@ var AnalyticsService = class extends BaseService {
8487
9308
  /**
8488
9309
  * Get product usage metrics
8489
9310
  *
9311
+ * IMPORTANT: Only COMPLETED appointments are included in product usage calculations.
9312
+ * Products are only considered "used" when the procedure has been completed.
9313
+ * Confirmed, pending, canceled, and no-show appointments are excluded from product metrics.
9314
+ *
8490
9315
  * @param productId - Optional product ID (if not provided, returns all products)
8491
9316
  * @param dateRange - Optional date range filter
9317
+ * @param filters - Optional filters (e.g., clinicBranchId)
8492
9318
  * @returns Product usage metrics
8493
9319
  */
8494
- async getProductUsageMetrics(productId, dateRange) {
8495
- const appointments = await this.fetchAppointments(void 0, dateRange);
9320
+ async getProductUsageMetrics(productId, dateRange, filters) {
9321
+ const appointments = await this.fetchAppointments(filters, dateRange);
8496
9322
  const completed = getCompletedAppointments(appointments);
8497
9323
  const productMap = /* @__PURE__ */ new Map();
8498
9324
  completed.forEach((appointment) => {
8499
9325
  const products = extractProductUsage(appointment);
9326
+ const productsInThisAppointment = /* @__PURE__ */ new Set();
8500
9327
  products.forEach((product) => {
8501
- var _a;
8502
9328
  if (productId && product.productId !== productId) {
8503
9329
  return;
8504
9330
  }
@@ -8510,25 +9336,37 @@ var AnalyticsService = class extends BaseService {
8510
9336
  quantity: 0,
8511
9337
  revenue: 0,
8512
9338
  usageCount: 0,
9339
+ appointmentIds: /* @__PURE__ */ new Set(),
8513
9340
  procedureMap: /* @__PURE__ */ new Map()
8514
9341
  });
8515
9342
  }
8516
9343
  const productData = productMap.get(product.productId);
8517
9344
  productData.quantity += product.quantity;
8518
9345
  productData.revenue += product.subtotal;
8519
- productData.usageCount++;
8520
- const procId = appointment.procedureId;
8521
- const procName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8522
- if (productData.procedureMap.has(procId)) {
8523
- const procData = productData.procedureMap.get(procId);
8524
- procData.count++;
8525
- procData.quantity += product.quantity;
8526
- } else {
8527
- productData.procedureMap.set(procId, {
8528
- name: procName,
8529
- count: 1,
8530
- quantity: product.quantity
8531
- });
9346
+ productsInThisAppointment.add(product.productId);
9347
+ });
9348
+ productsInThisAppointment.forEach((productId2) => {
9349
+ var _a;
9350
+ const productData = productMap.get(productId2);
9351
+ if (!productData.appointmentIds.has(appointment.id)) {
9352
+ productData.appointmentIds.add(appointment.id);
9353
+ productData.usageCount++;
9354
+ const procId = appointment.procedureId;
9355
+ const procName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
9356
+ if (productData.procedureMap.has(procId)) {
9357
+ const procData = productData.procedureMap.get(procId);
9358
+ procData.count++;
9359
+ const appointmentProducts = products.filter((p) => p.productId === productId2);
9360
+ procData.quantity += appointmentProducts.reduce((sum, p) => sum + p.quantity, 0);
9361
+ } else {
9362
+ const appointmentProducts = products.filter((p) => p.productId === productId2);
9363
+ const totalQuantity = appointmentProducts.reduce((sum, p) => sum + p.quantity, 0);
9364
+ productData.procedureMap.set(procId, {
9365
+ name: procName,
9366
+ count: 1,
9367
+ quantity: totalQuantity
9368
+ });
9369
+ }
8532
9370
  }
8533
9371
  });
8534
9372
  });
@@ -8766,6 +9604,341 @@ var AnalyticsService = class extends BaseService {
8766
9604
  recentActivity
8767
9605
  };
8768
9606
  }
9607
+ /**
9608
+ * Calculate revenue trends over time
9609
+ * Groups appointments by week/month/quarter/year and calculates revenue metrics
9610
+ *
9611
+ * @param dateRange - Date range for trend analysis (must align with period boundaries)
9612
+ * @param period - Period type (week, month, quarter, year)
9613
+ * @param filters - Optional filters for clinic, practitioner, procedure, patient
9614
+ * @param groupBy - Optional entity type to group trends by (clinic, practitioner, procedure, technology, patient)
9615
+ * @returns Array of revenue trends with percentage changes
9616
+ */
9617
+ async getRevenueTrends(dateRange, period, filters, groupBy) {
9618
+ const appointments = await this.fetchAppointments(filters);
9619
+ const filtered = filterByDateRange(appointments, dateRange);
9620
+ if (filtered.length === 0) {
9621
+ return [];
9622
+ }
9623
+ if (groupBy) {
9624
+ return this.getGroupedRevenueTrends(filtered, dateRange, period, groupBy);
9625
+ }
9626
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9627
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9628
+ const trends = [];
9629
+ let previousRevenue = 0;
9630
+ let previousAppointmentCount = 0;
9631
+ periods.forEach((periodInfo) => {
9632
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9633
+ const completed = getCompletedAppointments(periodAppointments);
9634
+ const { totalRevenue, currency } = calculateTotalRevenue(completed);
9635
+ const appointmentCount = completed.length;
9636
+ const averageRevenue = appointmentCount > 0 ? totalRevenue / appointmentCount : 0;
9637
+ const trend = {
9638
+ period: periodInfo.period,
9639
+ startDate: periodInfo.startDate,
9640
+ endDate: periodInfo.endDate,
9641
+ revenue: totalRevenue,
9642
+ appointmentCount,
9643
+ averageRevenue,
9644
+ currency
9645
+ };
9646
+ if (previousRevenue > 0 || previousAppointmentCount > 0) {
9647
+ const revenueChange = getTrendChange(totalRevenue, previousRevenue);
9648
+ trend.previousPeriod = {
9649
+ revenue: previousRevenue,
9650
+ appointmentCount: previousAppointmentCount,
9651
+ percentageChange: revenueChange.percentageChange,
9652
+ direction: revenueChange.direction
9653
+ };
9654
+ }
9655
+ trends.push(trend);
9656
+ previousRevenue = totalRevenue;
9657
+ previousAppointmentCount = appointmentCount;
9658
+ });
9659
+ return trends;
9660
+ }
9661
+ /**
9662
+ * Calculate revenue trends grouped by entity
9663
+ */
9664
+ async getGroupedRevenueTrends(appointments, dateRange, period, groupBy) {
9665
+ const periodMap = groupAppointmentsByPeriod(appointments, period);
9666
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9667
+ const trends = [];
9668
+ periods.forEach((periodInfo) => {
9669
+ var _a;
9670
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9671
+ if (periodAppointments.length === 0) return;
9672
+ const groupedMetrics = calculateGroupedRevenueMetrics(periodAppointments, groupBy);
9673
+ const totalRevenue = groupedMetrics.reduce((sum, m) => sum + m.totalRevenue, 0);
9674
+ const totalAppointments = groupedMetrics.reduce((sum, m) => sum + m.totalAppointments, 0);
9675
+ const currency = ((_a = groupedMetrics[0]) == null ? void 0 : _a.currency) || "CHF";
9676
+ const averageRevenue = totalAppointments > 0 ? totalRevenue / totalAppointments : 0;
9677
+ trends.push({
9678
+ period: periodInfo.period,
9679
+ startDate: periodInfo.startDate,
9680
+ endDate: periodInfo.endDate,
9681
+ revenue: totalRevenue,
9682
+ appointmentCount: totalAppointments,
9683
+ averageRevenue,
9684
+ currency
9685
+ });
9686
+ });
9687
+ for (let i = 1; i < trends.length; i++) {
9688
+ const current = trends[i];
9689
+ const previous = trends[i - 1];
9690
+ const revenueChange = getTrendChange(current.revenue, previous.revenue);
9691
+ current.previousPeriod = {
9692
+ revenue: previous.revenue,
9693
+ appointmentCount: previous.appointmentCount,
9694
+ percentageChange: revenueChange.percentageChange,
9695
+ direction: revenueChange.direction
9696
+ };
9697
+ }
9698
+ return trends;
9699
+ }
9700
+ /**
9701
+ * Calculate duration/efficiency trends over time
9702
+ *
9703
+ * @param dateRange - Date range for trend analysis
9704
+ * @param period - Period type (week, month, quarter, year)
9705
+ * @param filters - Optional filters
9706
+ * @param groupBy - Optional entity type to group trends by
9707
+ * @returns Array of duration trends with percentage changes
9708
+ */
9709
+ async getDurationTrends(dateRange, period, filters, groupBy) {
9710
+ const appointments = await this.fetchAppointments(filters);
9711
+ const filtered = filterByDateRange(appointments, dateRange);
9712
+ if (filtered.length === 0) {
9713
+ return [];
9714
+ }
9715
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9716
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9717
+ const trends = [];
9718
+ let previousEfficiency = 0;
9719
+ let previousBookedDuration = 0;
9720
+ let previousActualDuration = 0;
9721
+ periods.forEach((periodInfo) => {
9722
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9723
+ const completed = getCompletedAppointments(periodAppointments);
9724
+ if (groupBy) {
9725
+ const groupedMetrics = calculateGroupedTimeEfficiencyMetrics(completed, groupBy);
9726
+ if (groupedMetrics.length === 0) return;
9727
+ const totalAppointments = groupedMetrics.reduce((sum, m) => sum + m.totalAppointments, 0);
9728
+ const weightedBooked = groupedMetrics.reduce(
9729
+ (sum, m) => sum + m.averageBookedDuration * m.totalAppointments,
9730
+ 0
9731
+ );
9732
+ const weightedActual = groupedMetrics.reduce(
9733
+ (sum, m) => sum + m.averageActualDuration * m.totalAppointments,
9734
+ 0
9735
+ );
9736
+ const weightedEfficiency = groupedMetrics.reduce(
9737
+ (sum, m) => sum + m.averageEfficiency * m.totalAppointments,
9738
+ 0
9739
+ );
9740
+ const averageBookedDuration = totalAppointments > 0 ? weightedBooked / totalAppointments : 0;
9741
+ const averageActualDuration = totalAppointments > 0 ? weightedActual / totalAppointments : 0;
9742
+ const averageEfficiency = totalAppointments > 0 ? weightedEfficiency / totalAppointments : 0;
9743
+ const trend = {
9744
+ period: periodInfo.period,
9745
+ startDate: periodInfo.startDate,
9746
+ endDate: periodInfo.endDate,
9747
+ averageBookedDuration,
9748
+ averageActualDuration,
9749
+ averageEfficiency,
9750
+ appointmentCount: totalAppointments
9751
+ };
9752
+ if (previousEfficiency > 0) {
9753
+ const efficiencyChange = getTrendChange(averageEfficiency, previousEfficiency);
9754
+ trend.previousPeriod = {
9755
+ averageBookedDuration: previousBookedDuration,
9756
+ averageActualDuration: previousActualDuration,
9757
+ averageEfficiency: previousEfficiency,
9758
+ efficiencyPercentageChange: efficiencyChange.percentageChange,
9759
+ direction: efficiencyChange.direction
9760
+ };
9761
+ }
9762
+ trends.push(trend);
9763
+ previousEfficiency = averageEfficiency;
9764
+ previousBookedDuration = averageBookedDuration;
9765
+ previousActualDuration = averageActualDuration;
9766
+ } else {
9767
+ const timeMetrics = calculateAverageTimeMetrics(completed);
9768
+ const trend = {
9769
+ period: periodInfo.period,
9770
+ startDate: periodInfo.startDate,
9771
+ endDate: periodInfo.endDate,
9772
+ averageBookedDuration: timeMetrics.averageBookedDuration,
9773
+ averageActualDuration: timeMetrics.averageActualDuration,
9774
+ averageEfficiency: timeMetrics.averageEfficiency,
9775
+ appointmentCount: timeMetrics.appointmentsWithActualTime
9776
+ };
9777
+ if (previousEfficiency > 0) {
9778
+ const efficiencyChange = getTrendChange(timeMetrics.averageEfficiency, previousEfficiency);
9779
+ trend.previousPeriod = {
9780
+ averageBookedDuration: previousBookedDuration,
9781
+ averageActualDuration: previousActualDuration,
9782
+ averageEfficiency: previousEfficiency,
9783
+ efficiencyPercentageChange: efficiencyChange.percentageChange,
9784
+ direction: efficiencyChange.direction
9785
+ };
9786
+ }
9787
+ trends.push(trend);
9788
+ previousEfficiency = timeMetrics.averageEfficiency;
9789
+ previousBookedDuration = timeMetrics.averageBookedDuration;
9790
+ previousActualDuration = timeMetrics.averageActualDuration;
9791
+ }
9792
+ });
9793
+ return trends;
9794
+ }
9795
+ /**
9796
+ * Calculate appointment count trends over time
9797
+ *
9798
+ * @param dateRange - Date range for trend analysis
9799
+ * @param period - Period type (week, month, quarter, year)
9800
+ * @param filters - Optional filters
9801
+ * @param groupBy - Optional entity type to group trends by
9802
+ * @returns Array of appointment trends with percentage changes
9803
+ */
9804
+ async getAppointmentTrends(dateRange, period, filters, groupBy) {
9805
+ const appointments = await this.fetchAppointments(filters);
9806
+ const filtered = filterByDateRange(appointments, dateRange);
9807
+ if (filtered.length === 0) {
9808
+ return [];
9809
+ }
9810
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9811
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9812
+ const trends = [];
9813
+ let previousTotal = 0;
9814
+ let previousCompleted = 0;
9815
+ periods.forEach((periodInfo) => {
9816
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9817
+ const completed = getCompletedAppointments(periodAppointments);
9818
+ const canceled = getCanceledAppointments(periodAppointments);
9819
+ const noShow = getNoShowAppointments(periodAppointments);
9820
+ const pending = periodAppointments.filter((a) => a.status === "pending" /* PENDING */);
9821
+ const confirmed = periodAppointments.filter((a) => a.status === "confirmed" /* CONFIRMED */);
9822
+ const trend = {
9823
+ period: periodInfo.period,
9824
+ startDate: periodInfo.startDate,
9825
+ endDate: periodInfo.endDate,
9826
+ totalAppointments: periodAppointments.length,
9827
+ completedAppointments: completed.length,
9828
+ canceledAppointments: canceled.length,
9829
+ noShowAppointments: noShow.length,
9830
+ pendingAppointments: pending.length,
9831
+ confirmedAppointments: confirmed.length
9832
+ };
9833
+ if (previousTotal > 0) {
9834
+ const totalChange = getTrendChange(periodAppointments.length, previousTotal);
9835
+ trend.previousPeriod = {
9836
+ totalAppointments: previousTotal,
9837
+ completedAppointments: previousCompleted,
9838
+ percentageChange: totalChange.percentageChange,
9839
+ direction: totalChange.direction
9840
+ };
9841
+ }
9842
+ trends.push(trend);
9843
+ previousTotal = periodAppointments.length;
9844
+ previousCompleted = completed.length;
9845
+ });
9846
+ return trends;
9847
+ }
9848
+ /**
9849
+ * Calculate cancellation and no-show rate trends over time
9850
+ *
9851
+ * @param dateRange - Date range for trend analysis
9852
+ * @param period - Period type (week, month, quarter, year)
9853
+ * @param filters - Optional filters
9854
+ * @param groupBy - Optional entity type to group trends by
9855
+ * @returns Array of cancellation rate trends with percentage changes
9856
+ */
9857
+ async getCancellationRateTrends(dateRange, period, filters, groupBy) {
9858
+ const appointments = await this.fetchAppointments(filters);
9859
+ const filtered = filterByDateRange(appointments, dateRange);
9860
+ if (filtered.length === 0) {
9861
+ return [];
9862
+ }
9863
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9864
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9865
+ const trends = [];
9866
+ let previousCancellationRate = 0;
9867
+ let previousNoShowRate = 0;
9868
+ periods.forEach((periodInfo) => {
9869
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9870
+ const canceled = getCanceledAppointments(periodAppointments);
9871
+ const noShow = getNoShowAppointments(periodAppointments);
9872
+ const cancellationRate = calculatePercentage(canceled.length, periodAppointments.length);
9873
+ const noShowRate = calculatePercentage(noShow.length, periodAppointments.length);
9874
+ const trend = {
9875
+ period: periodInfo.period,
9876
+ startDate: periodInfo.startDate,
9877
+ endDate: periodInfo.endDate,
9878
+ cancellationRate,
9879
+ noShowRate,
9880
+ totalAppointments: periodAppointments.length,
9881
+ canceledAppointments: canceled.length,
9882
+ noShowAppointments: noShow.length
9883
+ };
9884
+ if (previousCancellationRate > 0 || previousNoShowRate > 0) {
9885
+ const cancellationChange = getTrendChange(cancellationRate, previousCancellationRate);
9886
+ const noShowChange = getTrendChange(noShowRate, previousNoShowRate);
9887
+ trend.previousPeriod = {
9888
+ cancellationRate: previousCancellationRate,
9889
+ noShowRate: previousNoShowRate,
9890
+ cancellationRateChange: cancellationChange.percentageChange,
9891
+ noShowRateChange: noShowChange.percentageChange,
9892
+ direction: cancellationChange.direction
9893
+ // Use cancellation direction as primary
9894
+ };
9895
+ }
9896
+ trends.push(trend);
9897
+ previousCancellationRate = cancellationRate;
9898
+ previousNoShowRate = noShowRate;
9899
+ });
9900
+ return trends;
9901
+ }
9902
+ // ==========================================
9903
+ // Review Analytics Methods
9904
+ // ==========================================
9905
+ /**
9906
+ * Get review metrics for a specific entity (practitioner, procedure, etc.)
9907
+ */
9908
+ async getReviewMetricsByEntity(entityType, entityId, dateRange, filters) {
9909
+ return this.reviewAnalyticsService.getReviewMetricsByEntity(entityType, entityId, dateRange, filters);
9910
+ }
9911
+ /**
9912
+ * Get review metrics for multiple entities (grouped)
9913
+ */
9914
+ async getReviewMetricsByEntities(entityType, dateRange, filters) {
9915
+ return this.reviewAnalyticsService.getReviewMetricsByEntities(entityType, dateRange, filters);
9916
+ }
9917
+ /**
9918
+ * Get overall review averages for comparison
9919
+ */
9920
+ async getOverallReviewAverages(dateRange, filters) {
9921
+ return this.reviewAnalyticsService.getOverallReviewAverages(dateRange, filters);
9922
+ }
9923
+ /**
9924
+ * Get review details for a specific entity
9925
+ */
9926
+ async getReviewDetails(entityType, entityId, dateRange, filters) {
9927
+ return this.reviewAnalyticsService.getReviewDetails(entityType, entityId, dateRange, filters);
9928
+ }
9929
+ /**
9930
+ * Calculate review trends over time
9931
+ * Groups reviews by period and calculates rating and recommendation metrics
9932
+ *
9933
+ * @param dateRange - Date range for trend analysis
9934
+ * @param period - Period type (week, month, quarter, year)
9935
+ * @param filters - Optional filters for clinic, practitioner, procedure
9936
+ * @param entityType - Optional entity type to group trends by
9937
+ * @returns Array of review trends with percentage changes
9938
+ */
9939
+ async getReviewTrends(dateRange, period, filters, entityType) {
9940
+ return this.reviewAnalyticsService.getReviewTrends(dateRange, period, filters, entityType);
9941
+ }
8769
9942
  };
8770
9943
 
8771
9944
  // src/admin/analytics/analytics.admin.service.ts
@@ -8798,33 +9971,33 @@ var AnalyticsAdminService = class {
8798
9971
  createAppointmentServiceAdapter() {
8799
9972
  return {
8800
9973
  searchAppointments: async (params) => {
8801
- let query2 = this.db.collection(APPOINTMENTS_COLLECTION);
9974
+ let query3 = this.db.collection(APPOINTMENTS_COLLECTION);
8802
9975
  if (params.clinicBranchId) {
8803
- query2 = query2.where("clinicBranchId", "==", params.clinicBranchId);
9976
+ query3 = query3.where("clinicBranchId", "==", params.clinicBranchId);
8804
9977
  }
8805
9978
  if (params.practitionerId) {
8806
- query2 = query2.where("practitionerId", "==", params.practitionerId);
9979
+ query3 = query3.where("practitionerId", "==", params.practitionerId);
8807
9980
  }
8808
9981
  if (params.procedureId) {
8809
- query2 = query2.where("procedureId", "==", params.procedureId);
9982
+ query3 = query3.where("procedureId", "==", params.procedureId);
8810
9983
  }
8811
9984
  if (params.patientId) {
8812
- query2 = query2.where("patientId", "==", params.patientId);
9985
+ query3 = query3.where("patientId", "==", params.patientId);
8813
9986
  }
8814
9987
  if (params.startDate) {
8815
9988
  const startDate = params.startDate instanceof Date ? params.startDate : params.startDate.toDate();
8816
9989
  const startTimestamp = admin14.firestore.Timestamp.fromDate(startDate);
8817
- query2 = query2.where("appointmentStartTime", ">=", startTimestamp);
9990
+ query3 = query3.where("appointmentStartTime", ">=", startTimestamp);
8818
9991
  }
8819
9992
  if (params.endDate) {
8820
9993
  const endDate = params.endDate instanceof Date ? params.endDate : params.endDate.toDate();
8821
9994
  const endTimestamp = admin14.firestore.Timestamp.fromDate(endDate);
8822
- query2 = query2.where("appointmentStartTime", "<=", endTimestamp);
9995
+ query3 = query3.where("appointmentStartTime", "<=", endTimestamp);
8823
9996
  }
8824
- const snapshot = await query2.get();
8825
- const appointments = snapshot.docs.map((doc2) => ({
8826
- id: doc2.id,
8827
- ...doc2.data()
9997
+ const snapshot = await query3.get();
9998
+ const appointments = snapshot.docs.map((doc3) => ({
9999
+ id: doc3.id,
10000
+ ...doc3.data()
8828
10001
  }));
8829
10002
  return {
8830
10003
  appointments,
@@ -8921,7 +10094,7 @@ var AnalyticsAdminService = class {
8921
10094
  };
8922
10095
 
8923
10096
  // src/admin/booking/booking.calculator.ts
8924
- import { Timestamp as Timestamp3 } from "firebase/firestore";
10097
+ import { Timestamp as Timestamp4 } from "firebase/firestore";
8925
10098
  import { DateTime as DateTime2 } from "luxon";
8926
10099
  var BookingAvailabilityCalculator = class {
8927
10100
  /**
@@ -9047,8 +10220,8 @@ var BookingAvailabilityCalculator = class {
9047
10220
  const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
9048
10221
  const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
9049
10222
  workingIntervals.push({
9050
- start: Timestamp3.fromMillis(intervalStart.toMillis()),
9051
- end: Timestamp3.fromMillis(intervalEnd.toMillis())
10223
+ start: Timestamp4.fromMillis(intervalStart.toMillis()),
10224
+ end: Timestamp4.fromMillis(intervalEnd.toMillis())
9052
10225
  });
9053
10226
  if (daySchedule.breaks && daySchedule.breaks.length > 0) {
9054
10227
  for (const breakTime of daySchedule.breaks) {
@@ -9068,8 +10241,8 @@ var BookingAvailabilityCalculator = class {
9068
10241
  ...this.subtractInterval(
9069
10242
  workingIntervals[workingIntervals.length - 1],
9070
10243
  {
9071
- start: Timestamp3.fromMillis(breakStart.toMillis()),
9072
- end: Timestamp3.fromMillis(breakEnd.toMillis())
10244
+ start: Timestamp4.fromMillis(breakStart.toMillis()),
10245
+ end: Timestamp4.fromMillis(breakEnd.toMillis())
9073
10246
  }
9074
10247
  )
9075
10248
  );
@@ -9179,8 +10352,8 @@ var BookingAvailabilityCalculator = class {
9179
10352
  const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
9180
10353
  const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
9181
10354
  workingIntervals.push({
9182
- start: Timestamp3.fromMillis(intervalStart.toMillis()),
9183
- end: Timestamp3.fromMillis(intervalEnd.toMillis())
10355
+ start: Timestamp4.fromMillis(intervalStart.toMillis()),
10356
+ end: Timestamp4.fromMillis(intervalEnd.toMillis())
9184
10357
  });
9185
10358
  }
9186
10359
  }
@@ -9260,7 +10433,7 @@ var BookingAvailabilityCalculator = class {
9260
10433
  const isInFuture = slotStart >= earliestBookableTime;
9261
10434
  if (isInFuture && this.isSlotFullyAvailable(slotStart, slotEnd, intervals, tz)) {
9262
10435
  slots.push({
9263
- start: Timestamp3.fromMillis(slotStart.toMillis())
10436
+ start: Timestamp4.fromMillis(slotStart.toMillis())
9264
10437
  });
9265
10438
  }
9266
10439
  slotStart = slotStart.plus({ minutes: intervalMinutes });
@@ -9490,8 +10663,8 @@ var DocumentManagerAdminService = class {
9490
10663
  const templateIds = technologyTemplates.map((t) => t.templateId);
9491
10664
  const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin15.firestore.FieldPath.documentId(), "in", templateIds).get();
9492
10665
  const templatesMap = /* @__PURE__ */ new Map();
9493
- templatesSnapshot.forEach((doc2) => {
9494
- templatesMap.set(doc2.id, doc2.data());
10666
+ templatesSnapshot.forEach((doc3) => {
10667
+ templatesMap.set(doc3.id, doc3.data());
9495
10668
  });
9496
10669
  for (const templateRef of technologyTemplates) {
9497
10670
  const template = templatesMap.get(templateRef.templateId);
@@ -9732,9 +10905,9 @@ var BookingAdmin = class {
9732
10905
  );
9733
10906
  const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
9734
10907
  const snapshot = await eventsRef.get();
9735
- const events = snapshot.docs.map((doc2) => ({
9736
- ...doc2.data(),
9737
- id: doc2.id
10908
+ const events = snapshot.docs.map((doc3) => ({
10909
+ ...doc3.data(),
10910
+ id: doc3.id
9738
10911
  })).filter((event) => {
9739
10912
  return event.eventTime.end.toMillis() > start.toMillis();
9740
10913
  });
@@ -9778,9 +10951,9 @@ var BookingAdmin = class {
9778
10951
  );
9779
10952
  const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
9780
10953
  const snapshot = await eventsRef.get();
9781
- const events = snapshot.docs.map((doc2) => ({
9782
- ...doc2.data(),
9783
- id: doc2.id
10954
+ const events = snapshot.docs.map((doc3) => ({
10955
+ ...doc3.data(),
10956
+ id: doc3.id
9784
10957
  })).filter((event) => {
9785
10958
  return event.eventTime.end.toMillis() > start.toMillis();
9786
10959
  });
@@ -11635,8 +12808,8 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
11635
12808
  */
11636
12809
  async fetchPractitionerById(practitionerId) {
11637
12810
  try {
11638
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
11639
- return doc2.exists ? doc2.data() : null;
12811
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
12812
+ return doc3.exists ? doc3.data() : null;
11640
12813
  } catch (error) {
11641
12814
  Logger.error(
11642
12815
  "[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
@@ -11652,8 +12825,8 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
11652
12825
  */
11653
12826
  async fetchClinicById(clinicId) {
11654
12827
  try {
11655
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
11656
- return doc2.exists ? doc2.data() : null;
12828
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
12829
+ return doc3.exists ? doc3.data() : null;
11657
12830
  } catch (error) {
11658
12831
  Logger.error(
11659
12832
  "[ExistingPractitionerInviteMailingService] Error fetching clinic:",