@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.
@@ -689,8 +689,8 @@ var NotificationsAdmin = class {
689
689
  * Dohvata notifikaciju po ID-u
690
690
  */
691
691
  async getNotification(id) {
692
- const doc2 = await this.db.collection("notifications").doc(id).get();
693
- return doc2.exists ? { id: doc2.id, ...doc2.data() } : null;
692
+ const doc3 = await this.db.collection("notifications").doc(id).get();
693
+ return doc3.exists ? { id: doc3.id, ...doc3.data() } : null;
694
694
  }
695
695
  /**
696
696
  * Kreira novu notifikaciju
@@ -877,10 +877,10 @@ var NotificationsAdmin = class {
877
877
  return;
878
878
  }
879
879
  const results = await Promise.allSettled(
880
- pendingNotifications.docs.map(async (doc2) => {
880
+ pendingNotifications.docs.map(async (doc3) => {
881
881
  const notification = {
882
- id: doc2.id,
883
- ...doc2.data()
882
+ id: doc3.id,
883
+ ...doc3.data()
884
884
  };
885
885
  Logger.info(
886
886
  `[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
@@ -921,8 +921,8 @@ var NotificationsAdmin = class {
921
921
  break;
922
922
  }
923
923
  const batch = this.db.batch();
924
- oldNotifications.docs.forEach((doc2) => {
925
- batch.delete(doc2.ref);
924
+ oldNotifications.docs.forEach((doc3) => {
925
+ batch.delete(doc3.ref);
926
926
  });
927
927
  await batch.commit();
928
928
  totalDeleted += oldNotifications.size;
@@ -3610,10 +3610,10 @@ var AppointmentAggregationService = class {
3610
3610
  }
3611
3611
  const batch = this.db.batch();
3612
3612
  let instancesUpdatedCount = 0;
3613
- instancesSnapshot.docs.forEach((doc2) => {
3614
- const instance = doc2.data();
3613
+ instancesSnapshot.docs.forEach((doc3) => {
3614
+ const instance = doc3.data();
3615
3615
  if (instance.overallStatus !== newOverallStatus && instance.overallStatus !== "failedToProcess" /* FAILED_TO_PROCESS */) {
3616
- batch.update(doc2.ref, {
3616
+ batch.update(doc3.ref, {
3617
3617
  overallStatus: newOverallStatus,
3618
3618
  updatedAt: admin6.firestore.FieldValue.serverTimestamp()
3619
3619
  // Cast for now
@@ -3622,7 +3622,7 @@ var AppointmentAggregationService = class {
3622
3622
  });
3623
3623
  instancesUpdatedCount++;
3624
3624
  Logger.debug(
3625
- `[AggService] Added update for PatientRequirementInstance ${doc2.id} to batch. New status: ${newOverallStatus}`
3625
+ `[AggService] Added update for PatientRequirementInstance ${doc3.id} to batch. New status: ${newOverallStatus}`
3626
3626
  );
3627
3627
  }
3628
3628
  });
@@ -3797,8 +3797,8 @@ var AppointmentAggregationService = class {
3797
3797
  // --- Data Fetching Helpers (Consider moving to a data access layer or using existing services if available) ---
3798
3798
  async fetchPatientProfile(patientId) {
3799
3799
  try {
3800
- const doc2 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).get();
3801
- return doc2.exists ? doc2.data() : null;
3800
+ const doc3 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).get();
3801
+ return doc3.exists ? doc3.data() : null;
3802
3802
  } catch (error) {
3803
3803
  Logger.error(`[AggService] Error fetching patient profile ${patientId}:`, error);
3804
3804
  return null;
@@ -3811,12 +3811,12 @@ var AppointmentAggregationService = class {
3811
3811
  */
3812
3812
  async fetchPatientSensitiveInfo(patientId) {
3813
3813
  try {
3814
- const doc2 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(patientId).get();
3815
- if (!doc2.exists) {
3814
+ const doc3 = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(patientId).get();
3815
+ if (!doc3.exists) {
3816
3816
  Logger.warn(`[AggService] No sensitive info found for patient ${patientId}`);
3817
3817
  return null;
3818
3818
  }
3819
- return doc2.data();
3819
+ return doc3.data();
3820
3820
  } catch (error) {
3821
3821
  Logger.error(`[AggService] Error fetching patient sensitive info ${patientId}:`, error);
3822
3822
  return null;
@@ -3833,12 +3833,12 @@ var AppointmentAggregationService = class {
3833
3833
  return null;
3834
3834
  }
3835
3835
  try {
3836
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
3837
- if (!doc2.exists) {
3836
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
3837
+ if (!doc3.exists) {
3838
3838
  Logger.warn(`[AggService] No practitioner profile found for ID ${practitionerId}`);
3839
3839
  return null;
3840
3840
  }
3841
- return doc2.data();
3841
+ return doc3.data();
3842
3842
  } catch (error) {
3843
3843
  Logger.error(`[AggService] Error fetching practitioner profile ${practitionerId}:`, error);
3844
3844
  return null;
@@ -3855,12 +3855,12 @@ var AppointmentAggregationService = class {
3855
3855
  return null;
3856
3856
  }
3857
3857
  try {
3858
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
3859
- if (!doc2.exists) {
3858
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
3859
+ if (!doc3.exists) {
3860
3860
  Logger.warn(`[AggService] No clinic info found for ID ${clinicId}`);
3861
3861
  return null;
3862
3862
  }
3863
- return doc2.data();
3863
+ return doc3.data();
3864
3864
  } catch (error) {
3865
3865
  Logger.error(`[AggService] Error fetching clinic info ${clinicId}:`, error);
3866
3866
  return null;
@@ -4327,11 +4327,11 @@ var ClinicAggregationService = class {
4327
4327
  return;
4328
4328
  }
4329
4329
  const batch = this.db.batch();
4330
- snapshot.docs.forEach((doc2) => {
4330
+ snapshot.docs.forEach((doc3) => {
4331
4331
  console.log(
4332
- `[ClinicAggregationService] Updating location for calendar event ${doc2.ref.path}`
4332
+ `[ClinicAggregationService] Updating location for calendar event ${doc3.ref.path}`
4333
4333
  );
4334
- batch.update(doc2.ref, {
4334
+ batch.update(doc3.ref, {
4335
4335
  eventLocation: newLocation,
4336
4336
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
4337
4337
  });
@@ -4374,11 +4374,11 @@ var ClinicAggregationService = class {
4374
4374
  return;
4375
4375
  }
4376
4376
  const batch = this.db.batch();
4377
- snapshot.docs.forEach((doc2) => {
4377
+ snapshot.docs.forEach((doc3) => {
4378
4378
  console.log(
4379
- `[ClinicAggregationService] Updating clinic info for calendar event ${doc2.ref.path}`
4379
+ `[ClinicAggregationService] Updating clinic info for calendar event ${doc3.ref.path}`
4380
4380
  );
4381
- batch.update(doc2.ref, {
4381
+ batch.update(doc3.ref, {
4382
4382
  clinicInfo,
4383
4383
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
4384
4384
  });
@@ -4589,11 +4589,11 @@ var ClinicAggregationService = class {
4589
4589
  return;
4590
4590
  }
4591
4591
  const batch = this.db.batch();
4592
- snapshot.docs.forEach((doc2) => {
4592
+ snapshot.docs.forEach((doc3) => {
4593
4593
  console.log(
4594
- `[ClinicAggregationService] Canceling calendar event ${doc2.ref.path}`
4594
+ `[ClinicAggregationService] Canceling calendar event ${doc3.ref.path}`
4595
4595
  );
4596
- batch.update(doc2.ref, {
4596
+ batch.update(doc3.ref, {
4597
4597
  status: "CANCELED",
4598
4598
  cancelReason: "Clinic deleted",
4599
4599
  updatedAt: admin7.firestore.FieldValue.serverTimestamp()
@@ -4861,11 +4861,11 @@ var PatientAggregationService = class {
4861
4861
  return;
4862
4862
  }
4863
4863
  const batch = this.db.batch();
4864
- snapshot.docs.forEach((doc2) => {
4864
+ snapshot.docs.forEach((doc3) => {
4865
4865
  console.log(
4866
- `[PatientAggregationService] Updating patient info for calendar event ${doc2.ref.path}`
4866
+ `[PatientAggregationService] Updating patient info for calendar event ${doc3.ref.path}`
4867
4867
  );
4868
- batch.update(doc2.ref, {
4868
+ batch.update(doc3.ref, {
4869
4869
  patientInfo,
4870
4870
  updatedAt: admin9.firestore.FieldValue.serverTimestamp()
4871
4871
  });
@@ -4909,11 +4909,11 @@ var PatientAggregationService = class {
4909
4909
  return;
4910
4910
  }
4911
4911
  const batch = this.db.batch();
4912
- snapshot.docs.forEach((doc2) => {
4912
+ snapshot.docs.forEach((doc3) => {
4913
4913
  console.log(
4914
- `[PatientAggregationService] Canceling calendar event ${doc2.ref.path}`
4914
+ `[PatientAggregationService] Canceling calendar event ${doc3.ref.path}`
4915
4915
  );
4916
- batch.update(doc2.ref, {
4916
+ batch.update(doc3.ref, {
4917
4917
  status: "CANCELED",
4918
4918
  cancelReason: "Patient deleted",
4919
4919
  updatedAt: admin9.firestore.FieldValue.serverTimestamp()
@@ -5084,11 +5084,11 @@ var PractitionerAggregationService = class {
5084
5084
  return;
5085
5085
  }
5086
5086
  const batch = this.db.batch();
5087
- snapshot.docs.forEach((doc2) => {
5087
+ snapshot.docs.forEach((doc3) => {
5088
5088
  console.log(
5089
- `[PractitionerAggregationService] Updating practitioner info for calendar event ${doc2.ref.path}`
5089
+ `[PractitionerAggregationService] Updating practitioner info for calendar event ${doc3.ref.path}`
5090
5090
  );
5091
- batch.update(doc2.ref, {
5091
+ batch.update(doc3.ref, {
5092
5092
  practitionerInfo,
5093
5093
  updatedAt: admin10.firestore.FieldValue.serverTimestamp()
5094
5094
  });
@@ -5172,11 +5172,11 @@ var PractitionerAggregationService = class {
5172
5172
  return;
5173
5173
  }
5174
5174
  const batch = this.db.batch();
5175
- snapshot.docs.forEach((doc2) => {
5175
+ snapshot.docs.forEach((doc3) => {
5176
5176
  console.log(
5177
- `[PractitionerAggregationService] Canceling calendar event ${doc2.ref.path}`
5177
+ `[PractitionerAggregationService] Canceling calendar event ${doc3.ref.path}`
5178
5178
  );
5179
- batch.update(doc2.ref, {
5179
+ batch.update(doc3.ref, {
5180
5180
  status: "CANCELED",
5181
5181
  cancelReason: "Practitioner deleted",
5182
5182
  updatedAt: admin10.firestore.FieldValue.serverTimestamp()
@@ -5728,8 +5728,8 @@ var PractitionerInviteAggregationService = class {
5728
5728
  */
5729
5729
  async fetchClinicAdminById(adminId) {
5730
5730
  try {
5731
- const doc2 = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
5732
- return doc2.exists ? doc2.data() : null;
5731
+ const doc3 = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
5732
+ return doc3.exists ? doc3.data() : null;
5733
5733
  } catch (error) {
5734
5734
  Logger.error(
5735
5735
  `[PractitionerInviteAggService] Error fetching clinic admin ${adminId}:`,
@@ -5745,8 +5745,8 @@ var PractitionerInviteAggregationService = class {
5745
5745
  */
5746
5746
  async fetchPractitionerById(practitionerId) {
5747
5747
  try {
5748
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
5749
- return doc2.exists ? doc2.data() : null;
5748
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
5749
+ return doc3.exists ? doc3.data() : null;
5750
5750
  } catch (error) {
5751
5751
  Logger.error(
5752
5752
  `[PractitionerInviteAggService] Error fetching practitioner ${practitionerId}:`,
@@ -5762,8 +5762,8 @@ var PractitionerInviteAggregationService = class {
5762
5762
  */
5763
5763
  async fetchClinicById(clinicId) {
5764
5764
  try {
5765
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
5766
- return doc2.exists ? doc2.data() : null;
5765
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
5766
+ return doc3.exists ? doc3.data() : null;
5767
5767
  } catch (error) {
5768
5768
  Logger.error(
5769
5769
  `[PractitionerInviteAggService] Error fetching clinic ${clinicId}:`,
@@ -6198,11 +6198,11 @@ var ProcedureAggregationService = class {
6198
6198
  return;
6199
6199
  }
6200
6200
  const batch = this.db.batch();
6201
- snapshot.docs.forEach((doc2) => {
6201
+ snapshot.docs.forEach((doc3) => {
6202
6202
  console.log(
6203
- `[ProcedureAggregationService] Updating procedure info for calendar event ${doc2.ref.path}`
6203
+ `[ProcedureAggregationService] Updating procedure info for calendar event ${doc3.ref.path}`
6204
6204
  );
6205
- batch.update(doc2.ref, {
6205
+ batch.update(doc3.ref, {
6206
6206
  procedureInfo,
6207
6207
  updatedAt: admin12.firestore.FieldValue.serverTimestamp()
6208
6208
  });
@@ -6245,11 +6245,11 @@ var ProcedureAggregationService = class {
6245
6245
  return;
6246
6246
  }
6247
6247
  const batch = this.db.batch();
6248
- snapshot.docs.forEach((doc2) => {
6248
+ snapshot.docs.forEach((doc3) => {
6249
6249
  console.log(
6250
- `[ProcedureAggregationService] Canceling calendar event ${doc2.ref.path}`
6250
+ `[ProcedureAggregationService] Canceling calendar event ${doc3.ref.path}`
6251
6251
  );
6252
- batch.update(doc2.ref, {
6252
+ batch.update(doc3.ref, {
6253
6253
  status: "CANCELED",
6254
6254
  cancelReason: "Procedure deleted or inactivated",
6255
6255
  updatedAt: admin12.firestore.FieldValue.serverTimestamp()
@@ -6630,7 +6630,7 @@ var ReviewsAggregationService = class {
6630
6630
  );
6631
6631
  return updatedReviewInfo2;
6632
6632
  }
6633
- const reviews = reviewsQuery.docs.map((doc2) => doc2.data());
6633
+ const reviews = reviewsQuery.docs.map((doc3) => doc3.data());
6634
6634
  const clinicReviews = reviews.map((review) => review.clinicReview).filter((review) => review !== void 0);
6635
6635
  let totalRating = 0;
6636
6636
  let totalCleanliness = 0;
@@ -6720,7 +6720,7 @@ var ReviewsAggregationService = class {
6720
6720
  );
6721
6721
  return updatedReviewInfo2;
6722
6722
  }
6723
- const reviews = reviewsQuery.docs.map((doc2) => doc2.data());
6723
+ const reviews = reviewsQuery.docs.map((doc3) => doc3.data());
6724
6724
  const practitionerReviews = reviews.map((review) => review.practitionerReview).filter((review) => review !== void 0);
6725
6725
  let totalRating = 0;
6726
6726
  let totalKnowledgeAndExpertise = 0;
@@ -6793,7 +6793,7 @@ var ReviewsAggregationService = class {
6793
6793
  recommendationPercentage: 0
6794
6794
  };
6795
6795
  const allReviewsQuery = await this.db.collection(REVIEWS_COLLECTION).get();
6796
- const reviews = allReviewsQuery.docs.map((doc2) => doc2.data());
6796
+ const reviews = allReviewsQuery.docs.map((doc3) => doc3.data());
6797
6797
  const procedureReviews = [];
6798
6798
  reviews.forEach((review) => {
6799
6799
  if (review.procedureReview && review.procedureReview.procedureId === procedureId) {
@@ -6963,7 +6963,7 @@ var ReviewsAggregationService = class {
6963
6963
  var admin14 = __toESM(require("firebase-admin"));
6964
6964
 
6965
6965
  // src/services/analytics/analytics.service.ts
6966
- var import_firestore3 = require("firebase/firestore");
6966
+ var import_firestore4 = require("firebase/firestore");
6967
6967
 
6968
6968
  // src/services/base.service.ts
6969
6969
  var import_storage = require("firebase/storage");
@@ -7063,7 +7063,9 @@ function extractProductUsage(appointment) {
7063
7063
  if (item.type === "item" && item.productId) {
7064
7064
  const price = item.priceOverrideAmount || item.price || 0;
7065
7065
  const quantity = item.quantity || 1;
7066
- const subtotal = item.subtotal || price * quantity;
7066
+ const calculatedSubtotal = price * quantity;
7067
+ const storedSubtotal = item.subtotal || 0;
7068
+ const subtotal = Math.abs(storedSubtotal - calculatedSubtotal) < 0.01 ? storedSubtotal : calculatedSubtotal;
7067
7069
  products.push({
7068
7070
  productId: item.productId,
7069
7071
  productName: item.productName || "Unknown Product",
@@ -7192,6 +7194,17 @@ function calculateCancellationLeadTime(appointment) {
7192
7194
  }
7193
7195
 
7194
7196
  // src/services/analytics/utils/appointment-filtering.utils.ts
7197
+ function filterByDateRange(appointments, dateRange) {
7198
+ if (!dateRange) {
7199
+ return appointments;
7200
+ }
7201
+ const startTime = dateRange.start.getTime();
7202
+ const endTime = dateRange.end.getTime();
7203
+ return appointments.filter((appointment) => {
7204
+ const appointmentTime = appointment.appointmentStartTime.toMillis();
7205
+ return appointmentTime >= startTime && appointmentTime <= endTime;
7206
+ });
7207
+ }
7195
7208
  function filterAppointments(appointments, filters) {
7196
7209
  if (!filters) {
7197
7210
  return appointments;
@@ -7467,6 +7480,7 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7467
7480
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7468
7481
  const completed = getCompletedAppointments(appointments);
7469
7482
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7483
+ var _a;
7470
7484
  const entityAppointments = data.appointments;
7471
7485
  const entityCompleted = entityAppointments.filter(
7472
7486
  (a) => completed.some((c) => c.id === a.id)
@@ -7490,6 +7504,13 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7490
7504
  refundedRevenue += costData.cost;
7491
7505
  }
7492
7506
  });
7507
+ let practitionerId;
7508
+ let practitionerName;
7509
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7510
+ const firstAppointment = entityAppointments[0];
7511
+ practitionerId = firstAppointment.practitionerId;
7512
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7513
+ }
7493
7514
  return {
7494
7515
  entityId,
7495
7516
  entityName: data.name,
@@ -7502,7 +7523,9 @@ function calculateGroupedRevenueMetrics(appointments, entityType) {
7502
7523
  unpaidRevenue,
7503
7524
  refundedRevenue,
7504
7525
  totalTax,
7505
- totalSubtotal
7526
+ totalSubtotal,
7527
+ ...practitionerId && { practitionerId },
7528
+ ...practitionerName && { practitionerName }
7506
7529
  };
7507
7530
  });
7508
7531
  }
@@ -7510,6 +7533,7 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7510
7533
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7511
7534
  const completed = getCompletedAppointments(appointments);
7512
7535
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7536
+ var _a;
7513
7537
  const entityAppointments = data.appointments;
7514
7538
  const entityCompleted = entityAppointments.filter(
7515
7539
  (a) => completed.some((c) => c.id === a.id)
@@ -7544,6 +7568,13 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7544
7568
  })).sort((a, b) => b.totalRevenue - a.totalRevenue).slice(0, 10);
7545
7569
  const totalProductRevenue = topProducts.reduce((sum, p) => sum + p.totalRevenue, 0);
7546
7570
  const totalProductQuantity = topProducts.reduce((sum, p) => sum + p.totalQuantity, 0);
7571
+ let practitionerId;
7572
+ let practitionerName;
7573
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7574
+ const firstAppointment = entityAppointments[0];
7575
+ practitionerId = firstAppointment.practitionerId;
7576
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7577
+ }
7547
7578
  return {
7548
7579
  entityId,
7549
7580
  entityName: data.name,
@@ -7553,7 +7584,9 @@ function calculateGroupedProductUsageMetrics(appointments, entityType) {
7553
7584
  totalProductRevenue,
7554
7585
  totalProductQuantity,
7555
7586
  averageProductsPerAppointment: entityCompleted.length > 0 ? productMap.size / entityCompleted.length : 0,
7556
- topProducts
7587
+ topProducts,
7588
+ ...practitionerId && { practitionerId },
7589
+ ...practitionerName && { practitionerName }
7557
7590
  };
7558
7591
  });
7559
7592
  }
@@ -7561,11 +7594,19 @@ function calculateGroupedTimeEfficiencyMetrics(appointments, entityType) {
7561
7594
  const entityMap = groupAppointmentsByEntity(appointments, entityType);
7562
7595
  const completed = getCompletedAppointments(appointments);
7563
7596
  return Array.from(entityMap.entries()).map(([entityId, data]) => {
7597
+ var _a;
7564
7598
  const entityAppointments = data.appointments;
7565
7599
  const entityCompleted = entityAppointments.filter(
7566
7600
  (a) => completed.some((c) => c.id === a.id)
7567
7601
  );
7568
7602
  const timeMetrics = calculateAverageTimeMetrics(entityCompleted);
7603
+ let practitionerId;
7604
+ let practitionerName;
7605
+ if (entityType === "procedure" && entityAppointments.length > 0) {
7606
+ const firstAppointment = entityAppointments[0];
7607
+ practitionerId = firstAppointment.practitionerId;
7608
+ practitionerName = (_a = firstAppointment.practitionerInfo) == null ? void 0 : _a.name;
7609
+ }
7569
7610
  return {
7570
7611
  entityId,
7571
7612
  entityName: data.name,
@@ -7578,7 +7619,9 @@ function calculateGroupedTimeEfficiencyMetrics(appointments, entityType) {
7578
7619
  totalOverrun: timeMetrics.totalOverrun,
7579
7620
  totalUnderutilization: timeMetrics.totalUnderutilization,
7580
7621
  averageOverrun: timeMetrics.averageOverrun,
7581
- averageUnderutilization: timeMetrics.averageUnderutilization
7622
+ averageUnderutilization: timeMetrics.averageUnderutilization,
7623
+ ...practitionerId && { practitionerId },
7624
+ ...practitionerName && { practitionerName }
7582
7625
  };
7583
7626
  });
7584
7627
  }
@@ -7660,6 +7703,763 @@ function calculateGroupedPatientBehaviorMetrics(appointments, entityType) {
7660
7703
  });
7661
7704
  }
7662
7705
 
7706
+ // src/services/analytics/utils/trend-calculation.utils.ts
7707
+ function getPeriodDates(date, period) {
7708
+ const year = date.getFullYear();
7709
+ const month = date.getMonth();
7710
+ const day = date.getDate();
7711
+ let startDate;
7712
+ let endDate;
7713
+ let periodString;
7714
+ switch (period) {
7715
+ case "week": {
7716
+ const dayOfWeek = date.getDay();
7717
+ const diff = date.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
7718
+ startDate = new Date(year, month, diff);
7719
+ startDate.setHours(0, 0, 0, 0);
7720
+ endDate = new Date(startDate);
7721
+ endDate.setDate(endDate.getDate() + 6);
7722
+ endDate.setHours(23, 59, 59, 999);
7723
+ const weekNumber = getWeekNumber(date);
7724
+ periodString = `${year}-W${weekNumber.toString().padStart(2, "0")}`;
7725
+ break;
7726
+ }
7727
+ case "month": {
7728
+ startDate = new Date(year, month, 1);
7729
+ startDate.setHours(0, 0, 0, 0);
7730
+ endDate = new Date(year, month + 1, 0);
7731
+ endDate.setHours(23, 59, 59, 999);
7732
+ periodString = `${year}-${(month + 1).toString().padStart(2, "0")}`;
7733
+ break;
7734
+ }
7735
+ case "quarter": {
7736
+ const quarter = Math.floor(month / 3);
7737
+ const quarterStartMonth = quarter * 3;
7738
+ startDate = new Date(year, quarterStartMonth, 1);
7739
+ startDate.setHours(0, 0, 0, 0);
7740
+ endDate = new Date(year, quarterStartMonth + 3, 0);
7741
+ endDate.setHours(23, 59, 59, 999);
7742
+ periodString = `${year}-Q${quarter + 1}`;
7743
+ break;
7744
+ }
7745
+ case "year": {
7746
+ startDate = new Date(year, 0, 1);
7747
+ startDate.setHours(0, 0, 0, 0);
7748
+ endDate = new Date(year, 11, 31);
7749
+ endDate.setHours(23, 59, 59, 999);
7750
+ periodString = `${year}`;
7751
+ break;
7752
+ }
7753
+ }
7754
+ return {
7755
+ period: periodString,
7756
+ startDate,
7757
+ endDate
7758
+ };
7759
+ }
7760
+ function getWeekNumber(date) {
7761
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
7762
+ const dayNum = d.getUTCDay() || 7;
7763
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
7764
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
7765
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
7766
+ }
7767
+ function groupAppointmentsByPeriod(appointments, period) {
7768
+ const periodMap = /* @__PURE__ */ new Map();
7769
+ appointments.forEach((appointment) => {
7770
+ const appointmentDate = appointment.appointmentStartTime.toDate();
7771
+ const periodInfo = getPeriodDates(appointmentDate, period);
7772
+ const periodKey = periodInfo.period;
7773
+ if (!periodMap.has(periodKey)) {
7774
+ periodMap.set(periodKey, []);
7775
+ }
7776
+ periodMap.get(periodKey).push(appointment);
7777
+ });
7778
+ return periodMap;
7779
+ }
7780
+ function generatePeriods(startDate, endDate, period) {
7781
+ const periods = [];
7782
+ const current = new Date(startDate);
7783
+ while (current <= endDate) {
7784
+ const periodInfo = getPeriodDates(current, period);
7785
+ if (periodInfo.endDate >= startDate && periodInfo.startDate <= endDate) {
7786
+ periods.push(periodInfo);
7787
+ }
7788
+ switch (period) {
7789
+ case "week":
7790
+ current.setDate(current.getDate() + 7);
7791
+ break;
7792
+ case "month":
7793
+ current.setMonth(current.getMonth() + 1);
7794
+ break;
7795
+ case "quarter":
7796
+ current.setMonth(current.getMonth() + 3);
7797
+ break;
7798
+ case "year":
7799
+ current.setFullYear(current.getFullYear() + 1);
7800
+ break;
7801
+ }
7802
+ }
7803
+ return periods.sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
7804
+ }
7805
+ function calculatePercentageChange(current, previous) {
7806
+ if (previous === 0) {
7807
+ return current > 0 ? 100 : 0;
7808
+ }
7809
+ return (current - previous) / previous * 100;
7810
+ }
7811
+ function getTrendChange(current, previous) {
7812
+ const percentageChange = calculatePercentageChange(current, previous);
7813
+ const direction = percentageChange > 0.01 ? "up" : percentageChange < -0.01 ? "down" : "stable";
7814
+ return {
7815
+ value: current,
7816
+ previousValue: previous,
7817
+ percentageChange: Math.abs(percentageChange),
7818
+ direction
7819
+ };
7820
+ }
7821
+
7822
+ // src/services/analytics/review-analytics.service.ts
7823
+ var import_firestore3 = require("firebase/firestore");
7824
+ var ReviewAnalyticsService = class extends BaseService {
7825
+ constructor(db, auth, app, appointmentService) {
7826
+ super(db, auth, app);
7827
+ this.appointmentService = appointmentService;
7828
+ }
7829
+ /**
7830
+ * Fetches reviews filtered by date range and optional filters
7831
+ * Properly filters by clinic branch by checking appointment's clinicId
7832
+ */
7833
+ async fetchReviews(dateRange, filters) {
7834
+ let q = (0, import_firestore3.query)((0, import_firestore3.collection)(this.db, REVIEWS_COLLECTION));
7835
+ if (dateRange) {
7836
+ const startTimestamp = import_firestore3.Timestamp.fromDate(dateRange.start);
7837
+ const endTimestamp = import_firestore3.Timestamp.fromDate(dateRange.end);
7838
+ q = (0, import_firestore3.query)(q, (0, import_firestore3.where)("createdAt", ">=", startTimestamp), (0, import_firestore3.where)("createdAt", "<=", endTimestamp));
7839
+ }
7840
+ const snapshot = await (0, import_firestore3.getDocs)(q);
7841
+ const reviews = snapshot.docs.map((doc3) => {
7842
+ var _a, _b;
7843
+ const data = doc3.data();
7844
+ return {
7845
+ ...data,
7846
+ id: doc3.id,
7847
+ createdAt: ((_a = data.createdAt) == null ? void 0 : _a.toDate) ? data.createdAt.toDate() : new Date(data.createdAt),
7848
+ updatedAt: ((_b = data.updatedAt) == null ? void 0 : _b.toDate) ? data.updatedAt.toDate() : new Date(data.updatedAt)
7849
+ };
7850
+ });
7851
+ console.log(`[ReviewAnalytics] Fetched ${reviews.length} reviews in date range`);
7852
+ if ((filters == null ? void 0 : filters.clinicBranchId) && reviews.length > 0) {
7853
+ const appointmentIds = [...new Set(reviews.map((r) => r.appointmentId))];
7854
+ console.log(`[ReviewAnalytics] Filtering by clinic ${filters.clinicBranchId}, checking ${appointmentIds.length} appointments`);
7855
+ const validAppointmentIds = /* @__PURE__ */ new Set();
7856
+ for (let i = 0; i < appointmentIds.length; i += 10) {
7857
+ const batch = appointmentIds.slice(i, i + 10);
7858
+ const appointmentsQuery = (0, import_firestore3.query)(
7859
+ (0, import_firestore3.collection)(this.db, APPOINTMENTS_COLLECTION),
7860
+ (0, import_firestore3.where)("id", "in", batch)
7861
+ );
7862
+ const appointmentSnapshot = await (0, import_firestore3.getDocs)(appointmentsQuery);
7863
+ appointmentSnapshot.docs.forEach((doc3) => {
7864
+ const appointment = doc3.data();
7865
+ if (appointment.clinicBranchId === filters.clinicBranchId) {
7866
+ validAppointmentIds.add(doc3.id);
7867
+ }
7868
+ });
7869
+ }
7870
+ const filteredReviews = reviews.filter((review) => validAppointmentIds.has(review.appointmentId));
7871
+ console.log(`[ReviewAnalytics] After clinic filter: ${filteredReviews.length} reviews (from ${validAppointmentIds.size} valid appointments)`);
7872
+ return filteredReviews;
7873
+ }
7874
+ return reviews;
7875
+ }
7876
+ /**
7877
+ * Gets review metrics for a specific entity
7878
+ */
7879
+ async getReviewMetricsByEntity(entityType, entityId, dateRange, filters) {
7880
+ const reviews = await this.fetchReviews(dateRange, filters);
7881
+ let relevantReviews = [];
7882
+ if (entityType === "practitioner") {
7883
+ relevantReviews = reviews.filter((r) => {
7884
+ var _a;
7885
+ return ((_a = r.practitionerReview) == null ? void 0 : _a.practitionerId) === entityId;
7886
+ });
7887
+ } else if (entityType === "procedure") {
7888
+ relevantReviews = reviews.filter((r) => {
7889
+ var _a;
7890
+ return ((_a = r.procedureReview) == null ? void 0 : _a.procedureId) === entityId;
7891
+ });
7892
+ } else if (entityType === "category" || entityType === "subcategory") {
7893
+ relevantReviews = reviews;
7894
+ }
7895
+ if (relevantReviews.length === 0) {
7896
+ return null;
7897
+ }
7898
+ return this.calculateReviewMetrics(relevantReviews, entityType, entityId);
7899
+ }
7900
+ /**
7901
+ * Gets review metrics for multiple entities (grouped)
7902
+ */
7903
+ async getReviewMetricsByEntities(entityType, dateRange, filters) {
7904
+ const reviews = await this.fetchReviews(dateRange, filters);
7905
+ const entityMap = /* @__PURE__ */ new Map();
7906
+ let practitionerNameMap = null;
7907
+ let procedureNameMap = null;
7908
+ let procedureToTechnologyMap = null;
7909
+ if (entityType === "practitioner" || entityType === "procedure" || entityType === "technology") {
7910
+ if (!this.appointmentService) {
7911
+ console.warn(`[ReviewAnalytics] AppointmentService not available for ${entityType} name resolution`);
7912
+ return [];
7913
+ }
7914
+ console.log(`[ReviewAnalytics] Grouping by ${entityType}, fetching appointments for name resolution...`);
7915
+ const searchParams = {
7916
+ ...filters
7917
+ };
7918
+ if (dateRange) {
7919
+ searchParams.startDate = dateRange.start;
7920
+ searchParams.endDate = dateRange.end;
7921
+ }
7922
+ const appointmentsResult = await this.appointmentService.searchAppointments(searchParams);
7923
+ const appointments = appointmentsResult.appointments || [];
7924
+ console.log(`[ReviewAnalytics] Found ${appointments.length} appointments for name resolution`);
7925
+ practitionerNameMap = /* @__PURE__ */ new Map();
7926
+ procedureNameMap = /* @__PURE__ */ new Map();
7927
+ procedureToTechnologyMap = /* @__PURE__ */ new Map();
7928
+ appointments.forEach((appointment) => {
7929
+ var _a, _b, _c, _d, _e, _f;
7930
+ if (appointment.practitionerId && ((_a = appointment.practitionerInfo) == null ? void 0 : _a.name)) {
7931
+ practitionerNameMap.set(appointment.practitionerId, appointment.practitionerInfo.name);
7932
+ }
7933
+ if (appointment.procedureId) {
7934
+ if ((_b = appointment.procedureInfo) == null ? void 0 : _b.name) {
7935
+ procedureNameMap.set(appointment.procedureId, appointment.procedureInfo.name);
7936
+ }
7937
+ const mainTechnologyId = ((_c = appointment.procedureExtendedInfo) == null ? void 0 : _c.procedureTechnologyId) || "unknown-technology";
7938
+ const mainTechnologyName = ((_d = appointment.procedureExtendedInfo) == null ? void 0 : _d.procedureTechnologyName) || ((_e = appointment.procedureInfo) == null ? void 0 : _e.name) || "Unknown Technology";
7939
+ procedureToTechnologyMap.set(appointment.procedureId, {
7940
+ id: mainTechnologyId,
7941
+ name: mainTechnologyName
7942
+ });
7943
+ }
7944
+ if ((_f = appointment.metadata) == null ? void 0 : _f.extendedProcedures) {
7945
+ appointment.metadata.extendedProcedures.forEach((extendedProc) => {
7946
+ if (extendedProc.procedureId) {
7947
+ if (extendedProc.procedureName) {
7948
+ procedureNameMap.set(extendedProc.procedureId, extendedProc.procedureName);
7949
+ }
7950
+ const extTechnologyId = extendedProc.procedureTechnologyId || "unknown-technology";
7951
+ const extTechnologyName = extendedProc.procedureTechnologyName || "Unknown Technology";
7952
+ procedureToTechnologyMap.set(extendedProc.procedureId, {
7953
+ id: extTechnologyId,
7954
+ name: extTechnologyName
7955
+ });
7956
+ }
7957
+ });
7958
+ }
7959
+ });
7960
+ console.log(`[ReviewAnalytics] Built name maps: ${practitionerNameMap.size} practitioners, ${procedureNameMap.size} procedures, ${procedureToTechnologyMap.size} technologies`);
7961
+ }
7962
+ if (entityType === "technology" && procedureToTechnologyMap) {
7963
+ let processedReviewCount = 0;
7964
+ reviews.forEach((review) => {
7965
+ var _a;
7966
+ if ((_a = review.procedureReview) == null ? void 0 : _a.procedureId) {
7967
+ const techInfo = procedureToTechnologyMap.get(review.procedureReview.procedureId);
7968
+ if (techInfo) {
7969
+ if (!entityMap.has(techInfo.id)) {
7970
+ entityMap.set(techInfo.id, { reviews: [], name: techInfo.name });
7971
+ }
7972
+ entityMap.get(techInfo.id).reviews.push(review);
7973
+ processedReviewCount++;
7974
+ }
7975
+ }
7976
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
7977
+ review.extendedProcedureReviews.forEach((extendedReview) => {
7978
+ if (extendedReview.procedureId) {
7979
+ const techInfo = procedureToTechnologyMap.get(extendedReview.procedureId);
7980
+ if (techInfo) {
7981
+ if (!entityMap.has(techInfo.id)) {
7982
+ entityMap.set(techInfo.id, { reviews: [], name: techInfo.name });
7983
+ }
7984
+ const reviewWithExtendedOnly = {
7985
+ ...review,
7986
+ procedureReview: extendedReview,
7987
+ extendedProcedureReviews: void 0
7988
+ };
7989
+ entityMap.get(techInfo.id).reviews.push(reviewWithExtendedOnly);
7990
+ processedReviewCount++;
7991
+ }
7992
+ }
7993
+ });
7994
+ }
7995
+ });
7996
+ console.log(`[ReviewAnalytics] Processed ${processedReviewCount} procedure reviews into ${entityMap.size} technology groups`);
7997
+ entityMap.forEach((data, techId) => {
7998
+ console.log(`[ReviewAnalytics] - ${data.name} (${techId}): ${data.reviews.length} reviews`);
7999
+ });
8000
+ } else if (entityType === "procedure" && procedureNameMap) {
8001
+ let processedReviewCount = 0;
8002
+ reviews.forEach((review) => {
8003
+ if (review.procedureReview) {
8004
+ const procedureId = review.procedureReview.procedureId;
8005
+ const procedureName = procedureId && procedureNameMap.get(procedureId) || review.procedureReview.procedureName || "Unknown Procedure";
8006
+ if (procedureId) {
8007
+ if (!entityMap.has(procedureId)) {
8008
+ entityMap.set(procedureId, { reviews: [], name: procedureName });
8009
+ }
8010
+ entityMap.get(procedureId).reviews.push(review);
8011
+ processedReviewCount++;
8012
+ }
8013
+ }
8014
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8015
+ review.extendedProcedureReviews.forEach((extendedReview) => {
8016
+ const procedureId = extendedReview.procedureId;
8017
+ const procedureName = procedureId && procedureNameMap.get(procedureId) || extendedReview.procedureName || "Unknown Procedure";
8018
+ if (procedureId) {
8019
+ if (!entityMap.has(procedureId)) {
8020
+ entityMap.set(procedureId, { reviews: [], name: procedureName });
8021
+ }
8022
+ const reviewWithExtendedOnly = {
8023
+ ...review,
8024
+ procedureReview: extendedReview,
8025
+ extendedProcedureReviews: void 0
8026
+ };
8027
+ entityMap.get(procedureId).reviews.push(reviewWithExtendedOnly);
8028
+ processedReviewCount++;
8029
+ }
8030
+ });
8031
+ }
8032
+ });
8033
+ console.log(`[ReviewAnalytics] Processed ${processedReviewCount} procedure reviews into ${entityMap.size} procedure groups`);
8034
+ entityMap.forEach((data, procId) => {
8035
+ console.log(`[ReviewAnalytics] - ${data.name} (${procId}): ${data.reviews.length} reviews`);
8036
+ });
8037
+ } else if (entityType === "practitioner" && practitionerNameMap) {
8038
+ reviews.forEach((review) => {
8039
+ if (review.practitionerReview) {
8040
+ const practitionerId = review.practitionerReview.practitionerId;
8041
+ const practitionerName = practitionerId && practitionerNameMap.get(practitionerId) || review.practitionerReview.practitionerName || "Unknown Practitioner";
8042
+ if (practitionerId) {
8043
+ if (!entityMap.has(practitionerId)) {
8044
+ entityMap.set(practitionerId, { reviews: [], name: practitionerName });
8045
+ }
8046
+ entityMap.get(practitionerId).reviews.push(review);
8047
+ }
8048
+ }
8049
+ });
8050
+ console.log(`[ReviewAnalytics] Processed ${reviews.length} reviews into ${entityMap.size} practitioner groups`);
8051
+ entityMap.forEach((data, practId) => {
8052
+ console.log(`[ReviewAnalytics] - ${data.name} (${practId}): ${data.reviews.length} reviews`);
8053
+ });
8054
+ } else {
8055
+ reviews.forEach((review) => {
8056
+ let entityId;
8057
+ let entityName;
8058
+ if (entityId) {
8059
+ if (!entityMap.has(entityId)) {
8060
+ entityMap.set(entityId, { reviews: [], name: entityName || entityId });
8061
+ }
8062
+ entityMap.get(entityId).reviews.push(review);
8063
+ }
8064
+ });
8065
+ }
8066
+ const metrics = [];
8067
+ for (const [entityId, data] of entityMap.entries()) {
8068
+ const metric = this.calculateReviewMetrics(data.reviews, entityType, entityId);
8069
+ if (metric) {
8070
+ metric.entityName = data.name;
8071
+ metrics.push(metric);
8072
+ }
8073
+ }
8074
+ return metrics;
8075
+ }
8076
+ /**
8077
+ * Calculates review metrics from a list of reviews
8078
+ */
8079
+ calculateReviewMetrics(reviews, entityType, entityId) {
8080
+ if (reviews.length === 0) {
8081
+ return null;
8082
+ }
8083
+ let totalRating = 0;
8084
+ let recommendationCount = 0;
8085
+ let practitionerMetrics;
8086
+ let procedureMetrics;
8087
+ let entityName = entityId;
8088
+ if (entityType === "practitioner") {
8089
+ const practitionerReviews = reviews.filter((r) => r.practitionerReview).map((r) => r.practitionerReview);
8090
+ if (practitionerReviews.length === 0) {
8091
+ return null;
8092
+ }
8093
+ entityName = practitionerReviews[0].practitionerName || entityId;
8094
+ totalRating = practitionerReviews.reduce((sum, r) => sum + r.overallRating, 0);
8095
+ recommendationCount = practitionerReviews.filter((r) => r.wouldRecommend).length;
8096
+ practitionerMetrics = {
8097
+ averageKnowledgeAndExpertise: this.calculateAverage(practitionerReviews.map((r) => r.knowledgeAndExpertise)),
8098
+ averageCommunicationSkills: this.calculateAverage(practitionerReviews.map((r) => r.communicationSkills)),
8099
+ averageBedSideManner: this.calculateAverage(practitionerReviews.map((r) => r.bedSideManner)),
8100
+ averageThoroughness: this.calculateAverage(practitionerReviews.map((r) => r.thoroughness)),
8101
+ averageTrustworthiness: this.calculateAverage(practitionerReviews.map((r) => r.trustworthiness))
8102
+ };
8103
+ } else if (entityType === "procedure" || entityType === "technology") {
8104
+ const procedureReviews = reviews.filter((r) => r.procedureReview).map((r) => r.procedureReview);
8105
+ if (procedureReviews.length === 0) {
8106
+ return null;
8107
+ }
8108
+ if (entityType === "procedure") {
8109
+ entityName = procedureReviews[0].procedureName || entityId;
8110
+ }
8111
+ totalRating = procedureReviews.reduce((sum, r) => sum + r.overallRating, 0);
8112
+ recommendationCount = procedureReviews.filter((r) => r.wouldRecommend).length;
8113
+ procedureMetrics = {
8114
+ averageEffectiveness: this.calculateAverage(procedureReviews.map((r) => r.effectivenessOfTreatment)),
8115
+ averageOutcomeExplanation: this.calculateAverage(procedureReviews.map((r) => r.outcomeExplanation)),
8116
+ averagePainManagement: this.calculateAverage(procedureReviews.map((r) => r.painManagement)),
8117
+ averageFollowUpCare: this.calculateAverage(procedureReviews.map((r) => r.followUpCare)),
8118
+ averageValueForMoney: this.calculateAverage(procedureReviews.map((r) => r.valueForMoney))
8119
+ };
8120
+ }
8121
+ const averageRating = totalRating / reviews.length;
8122
+ const recommendationRate = recommendationCount / reviews.length * 100;
8123
+ const result = {
8124
+ entityId,
8125
+ entityName,
8126
+ entityType,
8127
+ totalReviews: reviews.length,
8128
+ averageRating,
8129
+ recommendationRate,
8130
+ practitionerMetrics,
8131
+ procedureMetrics,
8132
+ comparisonToOverall: {
8133
+ ratingDifference: 0,
8134
+ // Will be calculated when comparing to overall
8135
+ recommendationDifference: 0
8136
+ }
8137
+ };
8138
+ return result;
8139
+ }
8140
+ /**
8141
+ * Gets overall review averages for comparison
8142
+ */
8143
+ async getOverallReviewAverages(dateRange, filters) {
8144
+ const reviews = await this.fetchReviews(dateRange, filters);
8145
+ const practitionerReviews = reviews.filter((r) => r.practitionerReview).map((r) => r.practitionerReview);
8146
+ const procedureReviews = reviews.filter((r) => r.procedureReview).map((r) => r.procedureReview);
8147
+ return {
8148
+ practitionerAverage: {
8149
+ totalReviews: practitionerReviews.length,
8150
+ averageRating: practitionerReviews.length > 0 ? this.calculateAverage(practitionerReviews.map((r) => r.overallRating)) : 0,
8151
+ recommendationRate: practitionerReviews.length > 0 ? practitionerReviews.filter((r) => r.wouldRecommend).length / practitionerReviews.length * 100 : 0,
8152
+ averageKnowledgeAndExpertise: this.calculateAverage(practitionerReviews.map((r) => r.knowledgeAndExpertise)),
8153
+ averageCommunicationSkills: this.calculateAverage(practitionerReviews.map((r) => r.communicationSkills)),
8154
+ averageBedSideManner: this.calculateAverage(practitionerReviews.map((r) => r.bedSideManner)),
8155
+ averageThoroughness: this.calculateAverage(practitionerReviews.map((r) => r.thoroughness)),
8156
+ averageTrustworthiness: this.calculateAverage(practitionerReviews.map((r) => r.trustworthiness))
8157
+ },
8158
+ procedureAverage: {
8159
+ totalReviews: procedureReviews.length,
8160
+ averageRating: procedureReviews.length > 0 ? this.calculateAverage(procedureReviews.map((r) => r.overallRating)) : 0,
8161
+ recommendationRate: procedureReviews.length > 0 ? procedureReviews.filter((r) => r.wouldRecommend).length / procedureReviews.length * 100 : 0,
8162
+ averageEffectiveness: this.calculateAverage(procedureReviews.map((r) => r.effectivenessOfTreatment)),
8163
+ averageOutcomeExplanation: this.calculateAverage(procedureReviews.map((r) => r.outcomeExplanation)),
8164
+ averagePainManagement: this.calculateAverage(procedureReviews.map((r) => r.painManagement)),
8165
+ averageFollowUpCare: this.calculateAverage(procedureReviews.map((r) => r.followUpCare)),
8166
+ averageValueForMoney: this.calculateAverage(procedureReviews.map((r) => r.valueForMoney))
8167
+ }
8168
+ };
8169
+ }
8170
+ /**
8171
+ * Gets review details for a specific entity
8172
+ */
8173
+ async getReviewDetails(entityType, entityId, dateRange, filters) {
8174
+ var _a, _b, _c;
8175
+ const reviews = await this.fetchReviews(dateRange, filters);
8176
+ let relevantReviews = [];
8177
+ if (entityType === "practitioner") {
8178
+ relevantReviews = reviews.filter((r) => {
8179
+ var _a2;
8180
+ return ((_a2 = r.practitionerReview) == null ? void 0 : _a2.practitionerId) === entityId;
8181
+ });
8182
+ } else if (entityType === "procedure") {
8183
+ relevantReviews = reviews.filter((r) => {
8184
+ var _a2;
8185
+ return ((_a2 = r.procedureReview) == null ? void 0 : _a2.procedureId) === entityId;
8186
+ });
8187
+ }
8188
+ const details = [];
8189
+ for (const review of relevantReviews) {
8190
+ try {
8191
+ const appointmentDocRef = (0, import_firestore3.doc)(this.db, APPOINTMENTS_COLLECTION, review.appointmentId);
8192
+ const appointmentDoc = await (0, import_firestore3.getDoc)(appointmentDocRef);
8193
+ let appointment = null;
8194
+ if (appointmentDoc.exists()) {
8195
+ appointment = appointmentDoc.data();
8196
+ }
8197
+ const createdAt = review.createdAt instanceof import_firestore3.Timestamp ? review.createdAt.toDate() : new Date(review.createdAt);
8198
+ const appointmentDate = (appointment == null ? void 0 : appointment.appointmentStartTime) ? appointment.appointmentStartTime instanceof import_firestore3.Timestamp ? appointment.appointmentStartTime.toDate() : appointment.appointmentStartTime : createdAt;
8199
+ details.push({
8200
+ reviewId: review.id,
8201
+ appointmentId: review.appointmentId,
8202
+ patientId: review.patientId,
8203
+ patientName: review.patientName || ((_a = appointment == null ? void 0 : appointment.patientInfo) == null ? void 0 : _a.fullName),
8204
+ createdAt,
8205
+ practitionerReview: review.practitionerReview,
8206
+ procedureReview: review.procedureReview,
8207
+ procedureName: (_b = appointment == null ? void 0 : appointment.procedureInfo) == null ? void 0 : _b.name,
8208
+ practitionerName: (_c = appointment == null ? void 0 : appointment.practitionerInfo) == null ? void 0 : _c.name,
8209
+ appointmentDate
8210
+ });
8211
+ } catch (error) {
8212
+ console.warn(`Failed to enhance review ${review.id}:`, error);
8213
+ }
8214
+ }
8215
+ return details;
8216
+ }
8217
+ /**
8218
+ * Helper method to calculate average
8219
+ */
8220
+ calculateAverage(values) {
8221
+ if (values.length === 0) return 0;
8222
+ const sum = values.reduce((acc, val) => acc + val, 0);
8223
+ return sum / values.length;
8224
+ }
8225
+ /**
8226
+ * Calculate review trends over time
8227
+ * Groups reviews by period and calculates rating and recommendation metrics
8228
+ *
8229
+ * @param dateRange - Date range for trend analysis (must align with period boundaries)
8230
+ * @param period - Period type (week, month, quarter, year)
8231
+ * @param filters - Optional filters for clinic, practitioner, procedure
8232
+ * @param entityType - Optional entity type to group trends by (practitioner, procedure, technology)
8233
+ * @returns Array of review trends with percentage changes
8234
+ */
8235
+ async getReviewTrends(dateRange, period, filters, entityType) {
8236
+ const reviews = await this.fetchReviews(dateRange, filters);
8237
+ if (reviews.length === 0) {
8238
+ return [];
8239
+ }
8240
+ if (entityType) {
8241
+ return this.getGroupedReviewTrends(reviews, dateRange, period, entityType, filters);
8242
+ }
8243
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
8244
+ const trends = [];
8245
+ let previousAvgRating = 0;
8246
+ let previousRecRate = 0;
8247
+ periods.forEach((periodInfo) => {
8248
+ const periodReviews = reviews.filter((review) => {
8249
+ const reviewDate = review.createdAt instanceof Date ? review.createdAt : review.createdAt.toDate();
8250
+ return reviewDate >= periodInfo.startDate && reviewDate <= periodInfo.endDate;
8251
+ });
8252
+ if (periodReviews.length === 0) {
8253
+ trends.push({
8254
+ period: periodInfo.period,
8255
+ startDate: periodInfo.startDate,
8256
+ endDate: periodInfo.endDate,
8257
+ averageRating: 0,
8258
+ recommendationRate: 0,
8259
+ totalReviews: 0,
8260
+ previousPeriod: void 0
8261
+ });
8262
+ previousAvgRating = 0;
8263
+ previousRecRate = 0;
8264
+ return;
8265
+ }
8266
+ let totalRatingSum = 0;
8267
+ let totalRatingCount = 0;
8268
+ let totalRecommendations = 0;
8269
+ let totalRecommendationCount = 0;
8270
+ periodReviews.forEach((review) => {
8271
+ if (review.practitionerReview) {
8272
+ totalRatingSum += review.practitionerReview.overallRating;
8273
+ totalRatingCount++;
8274
+ if (review.practitionerReview.wouldRecommend) {
8275
+ totalRecommendations++;
8276
+ }
8277
+ totalRecommendationCount++;
8278
+ }
8279
+ if (review.procedureReview) {
8280
+ totalRatingSum += review.procedureReview.overallRating;
8281
+ totalRatingCount++;
8282
+ if (review.procedureReview.wouldRecommend) {
8283
+ totalRecommendations++;
8284
+ }
8285
+ totalRecommendationCount++;
8286
+ }
8287
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8288
+ review.extendedProcedureReviews.forEach((extReview) => {
8289
+ totalRatingSum += extReview.overallRating;
8290
+ totalRatingCount++;
8291
+ if (extReview.wouldRecommend) {
8292
+ totalRecommendations++;
8293
+ }
8294
+ totalRecommendationCount++;
8295
+ });
8296
+ }
8297
+ });
8298
+ const currentAvgRating = totalRatingCount > 0 ? totalRatingSum / totalRatingCount : 0;
8299
+ const currentRecRate = totalRecommendationCount > 0 ? totalRecommendations / totalRecommendationCount * 100 : 0;
8300
+ const trendChange = getTrendChange(currentAvgRating, previousAvgRating);
8301
+ trends.push({
8302
+ period: periodInfo.period,
8303
+ startDate: periodInfo.startDate,
8304
+ endDate: periodInfo.endDate,
8305
+ averageRating: currentAvgRating,
8306
+ recommendationRate: currentRecRate,
8307
+ totalReviews: periodReviews.length,
8308
+ previousPeriod: previousAvgRating > 0 ? {
8309
+ averageRating: previousAvgRating,
8310
+ recommendationRate: previousRecRate,
8311
+ percentageChange: Math.abs(trendChange.percentageChange),
8312
+ direction: trendChange.direction
8313
+ } : void 0
8314
+ });
8315
+ previousAvgRating = currentAvgRating;
8316
+ previousRecRate = currentRecRate;
8317
+ });
8318
+ return trends;
8319
+ }
8320
+ /**
8321
+ * Calculate grouped review trends (by practitioner, procedure, or technology)
8322
+ * Returns the AVERAGE across all entities of that type for each period
8323
+ * @private
8324
+ */
8325
+ async getGroupedReviewTrends(reviews, dateRange, period, entityType, filters) {
8326
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
8327
+ const trends = [];
8328
+ let appointments = [];
8329
+ let procedureToTechnologyMap = /* @__PURE__ */ new Map();
8330
+ if (entityType === "technology" && this.appointmentService) {
8331
+ const searchParams = { ...filters };
8332
+ if (dateRange) {
8333
+ searchParams.startDate = dateRange.start;
8334
+ searchParams.endDate = dateRange.end;
8335
+ }
8336
+ const appointmentsResult = await this.appointmentService.searchAppointments(searchParams);
8337
+ appointments = appointmentsResult.appointments || [];
8338
+ appointments.forEach((appointment) => {
8339
+ var _a, _b;
8340
+ if (appointment.procedureId && ((_a = appointment.procedureExtendedInfo) == null ? void 0 : _a.procedureTechnologyId)) {
8341
+ procedureToTechnologyMap.set(appointment.procedureId, {
8342
+ id: appointment.procedureExtendedInfo.procedureTechnologyId,
8343
+ name: appointment.procedureExtendedInfo.procedureTechnologyName || "Unknown Technology"
8344
+ });
8345
+ }
8346
+ if ((_b = appointment.metadata) == null ? void 0 : _b.extendedProcedures) {
8347
+ appointment.metadata.extendedProcedures.forEach((extProc) => {
8348
+ if (extProc.procedureId && extProc.procedureTechnologyId) {
8349
+ procedureToTechnologyMap.set(extProc.procedureId, {
8350
+ id: extProc.procedureTechnologyId,
8351
+ name: extProc.procedureTechnologyName || "Unknown Technology"
8352
+ });
8353
+ }
8354
+ });
8355
+ }
8356
+ });
8357
+ }
8358
+ let previousAvgRating = 0;
8359
+ let previousRecRate = 0;
8360
+ periods.forEach((periodInfo) => {
8361
+ const periodReviews = reviews.filter((review) => {
8362
+ const reviewDate = review.createdAt instanceof Date ? review.createdAt : review.createdAt.toDate();
8363
+ return reviewDate >= periodInfo.startDate && reviewDate <= periodInfo.endDate;
8364
+ });
8365
+ if (periodReviews.length === 0) {
8366
+ trends.push({
8367
+ period: periodInfo.period,
8368
+ startDate: periodInfo.startDate,
8369
+ endDate: periodInfo.endDate,
8370
+ averageRating: 0,
8371
+ recommendationRate: 0,
8372
+ totalReviews: 0,
8373
+ previousPeriod: void 0
8374
+ });
8375
+ previousAvgRating = 0;
8376
+ previousRecRate = 0;
8377
+ return;
8378
+ }
8379
+ let totalRatingSum = 0;
8380
+ let totalRatingCount = 0;
8381
+ let totalRecommendations = 0;
8382
+ let totalRecommendationCount = 0;
8383
+ periodReviews.forEach((review) => {
8384
+ var _a;
8385
+ if (entityType === "practitioner" && review.practitionerReview) {
8386
+ totalRatingSum += review.practitionerReview.overallRating;
8387
+ totalRatingCount++;
8388
+ if (review.practitionerReview.wouldRecommend) {
8389
+ totalRecommendations++;
8390
+ }
8391
+ totalRecommendationCount++;
8392
+ } else if (entityType === "procedure" && review.procedureReview) {
8393
+ totalRatingSum += review.procedureReview.overallRating;
8394
+ totalRatingCount++;
8395
+ if (review.procedureReview.wouldRecommend) {
8396
+ totalRecommendations++;
8397
+ }
8398
+ totalRecommendationCount++;
8399
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8400
+ review.extendedProcedureReviews.forEach((extReview) => {
8401
+ totalRatingSum += extReview.overallRating;
8402
+ totalRatingCount++;
8403
+ if (extReview.wouldRecommend) {
8404
+ totalRecommendations++;
8405
+ }
8406
+ totalRecommendationCount++;
8407
+ });
8408
+ }
8409
+ } else if (entityType === "technology") {
8410
+ if ((_a = review.procedureReview) == null ? void 0 : _a.procedureId) {
8411
+ const tech = procedureToTechnologyMap.get(review.procedureReview.procedureId);
8412
+ if (tech) {
8413
+ totalRatingSum += review.procedureReview.overallRating;
8414
+ totalRatingCount++;
8415
+ if (review.procedureReview.wouldRecommend) {
8416
+ totalRecommendations++;
8417
+ }
8418
+ totalRecommendationCount++;
8419
+ }
8420
+ }
8421
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
8422
+ review.extendedProcedureReviews.forEach((extReview) => {
8423
+ if (extReview.procedureId) {
8424
+ const tech = procedureToTechnologyMap.get(extReview.procedureId);
8425
+ if (tech) {
8426
+ totalRatingSum += extReview.overallRating;
8427
+ totalRatingCount++;
8428
+ if (extReview.wouldRecommend) {
8429
+ totalRecommendations++;
8430
+ }
8431
+ totalRecommendationCount++;
8432
+ }
8433
+ }
8434
+ });
8435
+ }
8436
+ }
8437
+ });
8438
+ const currentAvgRating = totalRatingCount > 0 ? totalRatingSum / totalRatingCount : 0;
8439
+ const currentRecRate = totalRecommendationCount > 0 ? totalRecommendations / totalRecommendationCount * 100 : 0;
8440
+ const trendChange = getTrendChange(currentAvgRating, previousAvgRating);
8441
+ trends.push({
8442
+ period: periodInfo.period,
8443
+ startDate: periodInfo.startDate,
8444
+ endDate: periodInfo.endDate,
8445
+ averageRating: currentAvgRating,
8446
+ recommendationRate: currentRecRate,
8447
+ totalReviews: totalRatingCount,
8448
+ // Count of reviews for this entity type
8449
+ previousPeriod: previousAvgRating > 0 ? {
8450
+ averageRating: previousAvgRating,
8451
+ recommendationRate: previousRecRate,
8452
+ percentageChange: Math.abs(trendChange.percentageChange),
8453
+ direction: trendChange.direction
8454
+ } : void 0
8455
+ });
8456
+ previousAvgRating = currentAvgRating;
8457
+ previousRecRate = currentRecRate;
8458
+ });
8459
+ return trends;
8460
+ }
8461
+ };
8462
+
7663
8463
  // src/services/analytics/analytics.service.ts
7664
8464
  var AnalyticsService = class extends BaseService {
7665
8465
  /**
@@ -7673,6 +8473,7 @@ var AnalyticsService = class extends BaseService {
7673
8473
  constructor(db, auth, app, appointmentService) {
7674
8474
  super(db, auth, app);
7675
8475
  this.appointmentService = appointmentService;
8476
+ this.reviewAnalyticsService = new ReviewAnalyticsService(db, auth, app, appointmentService);
7676
8477
  }
7677
8478
  /**
7678
8479
  * Fetches appointments with optional filters
@@ -7685,20 +8486,20 @@ var AnalyticsService = class extends BaseService {
7685
8486
  try {
7686
8487
  const constraints = [];
7687
8488
  if (filters == null ? void 0 : filters.clinicBranchId) {
7688
- constraints.push((0, import_firestore3.where)("clinicBranchId", "==", filters.clinicBranchId));
8489
+ constraints.push((0, import_firestore4.where)("clinicBranchId", "==", filters.clinicBranchId));
7689
8490
  }
7690
8491
  if (filters == null ? void 0 : filters.practitionerId) {
7691
- constraints.push((0, import_firestore3.where)("practitionerId", "==", filters.practitionerId));
8492
+ constraints.push((0, import_firestore4.where)("practitionerId", "==", filters.practitionerId));
7692
8493
  }
7693
8494
  if (filters == null ? void 0 : filters.procedureId) {
7694
- constraints.push((0, import_firestore3.where)("procedureId", "==", filters.procedureId));
8495
+ constraints.push((0, import_firestore4.where)("procedureId", "==", filters.procedureId));
7695
8496
  }
7696
8497
  if (filters == null ? void 0 : filters.patientId) {
7697
- constraints.push((0, import_firestore3.where)("patientId", "==", filters.patientId));
8498
+ constraints.push((0, import_firestore4.where)("patientId", "==", filters.patientId));
7698
8499
  }
7699
8500
  if (dateRange) {
7700
- constraints.push((0, import_firestore3.where)("appointmentStartTime", ">=", import_firestore3.Timestamp.fromDate(dateRange.start)));
7701
- constraints.push((0, import_firestore3.where)("appointmentStartTime", "<=", import_firestore3.Timestamp.fromDate(dateRange.end)));
8501
+ constraints.push((0, import_firestore4.where)("appointmentStartTime", ">=", import_firestore4.Timestamp.fromDate(dateRange.start)));
8502
+ constraints.push((0, import_firestore4.where)("appointmentStartTime", "<=", import_firestore4.Timestamp.fromDate(dateRange.end)));
7702
8503
  }
7703
8504
  const searchParams = {};
7704
8505
  if (filters == null ? void 0 : filters.clinicBranchId) searchParams.clinicBranchId = filters.clinicBranchId;
@@ -8180,11 +8981,17 @@ var AnalyticsService = class extends BaseService {
8180
8981
  groupCancellationsByProcedure(canceled, allAppointments) {
8181
8982
  const procedureMap = /* @__PURE__ */ new Map();
8182
8983
  allAppointments.forEach((appointment) => {
8183
- var _a;
8984
+ var _a, _b;
8184
8985
  const procedureId = appointment.procedureId;
8185
8986
  const procedureName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8186
8987
  if (!procedureMap.has(procedureId)) {
8187
- procedureMap.set(procedureId, { name: procedureName, canceled: [], all: [] });
8988
+ procedureMap.set(procedureId, {
8989
+ name: procedureName,
8990
+ canceled: [],
8991
+ all: [],
8992
+ practitionerId: appointment.practitionerId,
8993
+ practitionerName: (_b = appointment.practitionerInfo) == null ? void 0 : _b.name
8994
+ });
8188
8995
  }
8189
8996
  procedureMap.get(procedureId).all.push(appointment);
8190
8997
  });
@@ -8194,15 +9001,20 @@ var AnalyticsService = class extends BaseService {
8194
9001
  procedureMap.get(procedureId).canceled.push(appointment);
8195
9002
  }
8196
9003
  });
8197
- return Array.from(procedureMap.entries()).map(
8198
- ([procedureId, data]) => this.calculateCancellationMetrics(
9004
+ return Array.from(procedureMap.entries()).map(([procedureId, data]) => {
9005
+ const metrics = this.calculateCancellationMetrics(
8199
9006
  procedureId,
8200
9007
  data.name,
8201
9008
  "procedure",
8202
9009
  data.canceled,
8203
9010
  data.all
8204
- )
8205
- );
9011
+ );
9012
+ return {
9013
+ ...metrics,
9014
+ ...data.practitionerId && { practitionerId: data.practitionerId },
9015
+ ...data.practitionerName && { practitionerName: data.practitionerName }
9016
+ };
9017
+ });
8206
9018
  }
8207
9019
  /**
8208
9020
  * Group cancellations by technology
@@ -8407,11 +9219,17 @@ var AnalyticsService = class extends BaseService {
8407
9219
  groupNoShowsByProcedure(noShow, allAppointments) {
8408
9220
  const procedureMap = /* @__PURE__ */ new Map();
8409
9221
  allAppointments.forEach((appointment) => {
8410
- var _a;
9222
+ var _a, _b;
8411
9223
  const procedureId = appointment.procedureId;
8412
9224
  const procedureName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8413
9225
  if (!procedureMap.has(procedureId)) {
8414
- procedureMap.set(procedureId, { name: procedureName, noShow: [], all: [] });
9226
+ procedureMap.set(procedureId, {
9227
+ name: procedureName,
9228
+ noShow: [],
9229
+ all: [],
9230
+ practitionerId: appointment.practitionerId,
9231
+ practitionerName: (_b = appointment.practitionerInfo) == null ? void 0 : _b.name
9232
+ });
8415
9233
  }
8416
9234
  procedureMap.get(procedureId).all.push(appointment);
8417
9235
  });
@@ -8427,7 +9245,9 @@ var AnalyticsService = class extends BaseService {
8427
9245
  entityType: "procedure",
8428
9246
  totalAppointments: data.all.length,
8429
9247
  noShowAppointments: data.noShow.length,
8430
- noShowRate: calculatePercentage(data.noShow.length, data.all.length)
9248
+ noShowRate: calculatePercentage(data.noShow.length, data.all.length),
9249
+ ...data.practitionerId && { practitionerId: data.practitionerId },
9250
+ ...data.practitionerName && { practitionerName: data.practitionerName }
8431
9251
  }));
8432
9252
  }
8433
9253
  /**
@@ -8480,6 +9300,10 @@ var AnalyticsService = class extends BaseService {
8480
9300
  * Get revenue metrics
8481
9301
  * First checks for stored analytics, then calculates if not available or stale
8482
9302
  *
9303
+ * IMPORTANT: Financial calculations only consider COMPLETED appointments.
9304
+ * Confirmed, pending, canceled, and no-show appointments are NOT included in revenue calculations.
9305
+ * Only procedures that have been completed generate revenue.
9306
+ *
8483
9307
  * @param filters - Optional filters
8484
9308
  * @param dateRange - Optional date range filter
8485
9309
  * @param options - Options for reading stored analytics
@@ -8502,11 +9326,8 @@ var AnalyticsService = class extends BaseService {
8502
9326
  const completed = getCompletedAppointments(appointments);
8503
9327
  const { totalRevenue, currency } = calculateTotalRevenue(completed);
8504
9328
  const revenueByStatus = {};
8505
- Object.values(AppointmentStatus).forEach((status) => {
8506
- const statusAppointments = appointments.filter((a) => a.status === status);
8507
- const { totalRevenue: statusRevenue } = calculateTotalRevenue(statusAppointments);
8508
- revenueByStatus[status] = statusRevenue;
8509
- });
9329
+ const { totalRevenue: completedRevenue } = calculateTotalRevenue(completed);
9330
+ revenueByStatus["completed" /* COMPLETED */] = completedRevenue;
8510
9331
  const revenueByPaymentStatus = {};
8511
9332
  Object.values(PaymentStatus).forEach((paymentStatus) => {
8512
9333
  const paymentAppointments = completed.filter((a) => a.paymentStatus === paymentStatus);
@@ -8560,18 +9381,23 @@ var AnalyticsService = class extends BaseService {
8560
9381
  /**
8561
9382
  * Get product usage metrics
8562
9383
  *
9384
+ * IMPORTANT: Only COMPLETED appointments are included in product usage calculations.
9385
+ * Products are only considered "used" when the procedure has been completed.
9386
+ * Confirmed, pending, canceled, and no-show appointments are excluded from product metrics.
9387
+ *
8563
9388
  * @param productId - Optional product ID (if not provided, returns all products)
8564
9389
  * @param dateRange - Optional date range filter
9390
+ * @param filters - Optional filters (e.g., clinicBranchId)
8565
9391
  * @returns Product usage metrics
8566
9392
  */
8567
- async getProductUsageMetrics(productId, dateRange) {
8568
- const appointments = await this.fetchAppointments(void 0, dateRange);
9393
+ async getProductUsageMetrics(productId, dateRange, filters) {
9394
+ const appointments = await this.fetchAppointments(filters, dateRange);
8569
9395
  const completed = getCompletedAppointments(appointments);
8570
9396
  const productMap = /* @__PURE__ */ new Map();
8571
9397
  completed.forEach((appointment) => {
8572
9398
  const products = extractProductUsage(appointment);
9399
+ const productsInThisAppointment = /* @__PURE__ */ new Set();
8573
9400
  products.forEach((product) => {
8574
- var _a;
8575
9401
  if (productId && product.productId !== productId) {
8576
9402
  return;
8577
9403
  }
@@ -8583,25 +9409,37 @@ var AnalyticsService = class extends BaseService {
8583
9409
  quantity: 0,
8584
9410
  revenue: 0,
8585
9411
  usageCount: 0,
9412
+ appointmentIds: /* @__PURE__ */ new Set(),
8586
9413
  procedureMap: /* @__PURE__ */ new Map()
8587
9414
  });
8588
9415
  }
8589
9416
  const productData = productMap.get(product.productId);
8590
9417
  productData.quantity += product.quantity;
8591
9418
  productData.revenue += product.subtotal;
8592
- productData.usageCount++;
8593
- const procId = appointment.procedureId;
8594
- const procName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
8595
- if (productData.procedureMap.has(procId)) {
8596
- const procData = productData.procedureMap.get(procId);
8597
- procData.count++;
8598
- procData.quantity += product.quantity;
8599
- } else {
8600
- productData.procedureMap.set(procId, {
8601
- name: procName,
8602
- count: 1,
8603
- quantity: product.quantity
8604
- });
9419
+ productsInThisAppointment.add(product.productId);
9420
+ });
9421
+ productsInThisAppointment.forEach((productId2) => {
9422
+ var _a;
9423
+ const productData = productMap.get(productId2);
9424
+ if (!productData.appointmentIds.has(appointment.id)) {
9425
+ productData.appointmentIds.add(appointment.id);
9426
+ productData.usageCount++;
9427
+ const procId = appointment.procedureId;
9428
+ const procName = ((_a = appointment.procedureInfo) == null ? void 0 : _a.name) || "Unknown";
9429
+ if (productData.procedureMap.has(procId)) {
9430
+ const procData = productData.procedureMap.get(procId);
9431
+ procData.count++;
9432
+ const appointmentProducts = products.filter((p) => p.productId === productId2);
9433
+ procData.quantity += appointmentProducts.reduce((sum, p) => sum + p.quantity, 0);
9434
+ } else {
9435
+ const appointmentProducts = products.filter((p) => p.productId === productId2);
9436
+ const totalQuantity = appointmentProducts.reduce((sum, p) => sum + p.quantity, 0);
9437
+ productData.procedureMap.set(procId, {
9438
+ name: procName,
9439
+ count: 1,
9440
+ quantity: totalQuantity
9441
+ });
9442
+ }
8605
9443
  }
8606
9444
  });
8607
9445
  });
@@ -8839,6 +9677,341 @@ var AnalyticsService = class extends BaseService {
8839
9677
  recentActivity
8840
9678
  };
8841
9679
  }
9680
+ /**
9681
+ * Calculate revenue trends over time
9682
+ * Groups appointments by week/month/quarter/year and calculates revenue metrics
9683
+ *
9684
+ * @param dateRange - Date range for trend analysis (must align with period boundaries)
9685
+ * @param period - Period type (week, month, quarter, year)
9686
+ * @param filters - Optional filters for clinic, practitioner, procedure, patient
9687
+ * @param groupBy - Optional entity type to group trends by (clinic, practitioner, procedure, technology, patient)
9688
+ * @returns Array of revenue trends with percentage changes
9689
+ */
9690
+ async getRevenueTrends(dateRange, period, filters, groupBy) {
9691
+ const appointments = await this.fetchAppointments(filters);
9692
+ const filtered = filterByDateRange(appointments, dateRange);
9693
+ if (filtered.length === 0) {
9694
+ return [];
9695
+ }
9696
+ if (groupBy) {
9697
+ return this.getGroupedRevenueTrends(filtered, dateRange, period, groupBy);
9698
+ }
9699
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9700
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9701
+ const trends = [];
9702
+ let previousRevenue = 0;
9703
+ let previousAppointmentCount = 0;
9704
+ periods.forEach((periodInfo) => {
9705
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9706
+ const completed = getCompletedAppointments(periodAppointments);
9707
+ const { totalRevenue, currency } = calculateTotalRevenue(completed);
9708
+ const appointmentCount = completed.length;
9709
+ const averageRevenue = appointmentCount > 0 ? totalRevenue / appointmentCount : 0;
9710
+ const trend = {
9711
+ period: periodInfo.period,
9712
+ startDate: periodInfo.startDate,
9713
+ endDate: periodInfo.endDate,
9714
+ revenue: totalRevenue,
9715
+ appointmentCount,
9716
+ averageRevenue,
9717
+ currency
9718
+ };
9719
+ if (previousRevenue > 0 || previousAppointmentCount > 0) {
9720
+ const revenueChange = getTrendChange(totalRevenue, previousRevenue);
9721
+ trend.previousPeriod = {
9722
+ revenue: previousRevenue,
9723
+ appointmentCount: previousAppointmentCount,
9724
+ percentageChange: revenueChange.percentageChange,
9725
+ direction: revenueChange.direction
9726
+ };
9727
+ }
9728
+ trends.push(trend);
9729
+ previousRevenue = totalRevenue;
9730
+ previousAppointmentCount = appointmentCount;
9731
+ });
9732
+ return trends;
9733
+ }
9734
+ /**
9735
+ * Calculate revenue trends grouped by entity
9736
+ */
9737
+ async getGroupedRevenueTrends(appointments, dateRange, period, groupBy) {
9738
+ const periodMap = groupAppointmentsByPeriod(appointments, period);
9739
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9740
+ const trends = [];
9741
+ periods.forEach((periodInfo) => {
9742
+ var _a;
9743
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9744
+ if (periodAppointments.length === 0) return;
9745
+ const groupedMetrics = calculateGroupedRevenueMetrics(periodAppointments, groupBy);
9746
+ const totalRevenue = groupedMetrics.reduce((sum, m) => sum + m.totalRevenue, 0);
9747
+ const totalAppointments = groupedMetrics.reduce((sum, m) => sum + m.totalAppointments, 0);
9748
+ const currency = ((_a = groupedMetrics[0]) == null ? void 0 : _a.currency) || "CHF";
9749
+ const averageRevenue = totalAppointments > 0 ? totalRevenue / totalAppointments : 0;
9750
+ trends.push({
9751
+ period: periodInfo.period,
9752
+ startDate: periodInfo.startDate,
9753
+ endDate: periodInfo.endDate,
9754
+ revenue: totalRevenue,
9755
+ appointmentCount: totalAppointments,
9756
+ averageRevenue,
9757
+ currency
9758
+ });
9759
+ });
9760
+ for (let i = 1; i < trends.length; i++) {
9761
+ const current = trends[i];
9762
+ const previous = trends[i - 1];
9763
+ const revenueChange = getTrendChange(current.revenue, previous.revenue);
9764
+ current.previousPeriod = {
9765
+ revenue: previous.revenue,
9766
+ appointmentCount: previous.appointmentCount,
9767
+ percentageChange: revenueChange.percentageChange,
9768
+ direction: revenueChange.direction
9769
+ };
9770
+ }
9771
+ return trends;
9772
+ }
9773
+ /**
9774
+ * Calculate duration/efficiency trends over time
9775
+ *
9776
+ * @param dateRange - Date range for trend analysis
9777
+ * @param period - Period type (week, month, quarter, year)
9778
+ * @param filters - Optional filters
9779
+ * @param groupBy - Optional entity type to group trends by
9780
+ * @returns Array of duration trends with percentage changes
9781
+ */
9782
+ async getDurationTrends(dateRange, period, filters, groupBy) {
9783
+ const appointments = await this.fetchAppointments(filters);
9784
+ const filtered = filterByDateRange(appointments, dateRange);
9785
+ if (filtered.length === 0) {
9786
+ return [];
9787
+ }
9788
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9789
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9790
+ const trends = [];
9791
+ let previousEfficiency = 0;
9792
+ let previousBookedDuration = 0;
9793
+ let previousActualDuration = 0;
9794
+ periods.forEach((periodInfo) => {
9795
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9796
+ const completed = getCompletedAppointments(periodAppointments);
9797
+ if (groupBy) {
9798
+ const groupedMetrics = calculateGroupedTimeEfficiencyMetrics(completed, groupBy);
9799
+ if (groupedMetrics.length === 0) return;
9800
+ const totalAppointments = groupedMetrics.reduce((sum, m) => sum + m.totalAppointments, 0);
9801
+ const weightedBooked = groupedMetrics.reduce(
9802
+ (sum, m) => sum + m.averageBookedDuration * m.totalAppointments,
9803
+ 0
9804
+ );
9805
+ const weightedActual = groupedMetrics.reduce(
9806
+ (sum, m) => sum + m.averageActualDuration * m.totalAppointments,
9807
+ 0
9808
+ );
9809
+ const weightedEfficiency = groupedMetrics.reduce(
9810
+ (sum, m) => sum + m.averageEfficiency * m.totalAppointments,
9811
+ 0
9812
+ );
9813
+ const averageBookedDuration = totalAppointments > 0 ? weightedBooked / totalAppointments : 0;
9814
+ const averageActualDuration = totalAppointments > 0 ? weightedActual / totalAppointments : 0;
9815
+ const averageEfficiency = totalAppointments > 0 ? weightedEfficiency / totalAppointments : 0;
9816
+ const trend = {
9817
+ period: periodInfo.period,
9818
+ startDate: periodInfo.startDate,
9819
+ endDate: periodInfo.endDate,
9820
+ averageBookedDuration,
9821
+ averageActualDuration,
9822
+ averageEfficiency,
9823
+ appointmentCount: totalAppointments
9824
+ };
9825
+ if (previousEfficiency > 0) {
9826
+ const efficiencyChange = getTrendChange(averageEfficiency, previousEfficiency);
9827
+ trend.previousPeriod = {
9828
+ averageBookedDuration: previousBookedDuration,
9829
+ averageActualDuration: previousActualDuration,
9830
+ averageEfficiency: previousEfficiency,
9831
+ efficiencyPercentageChange: efficiencyChange.percentageChange,
9832
+ direction: efficiencyChange.direction
9833
+ };
9834
+ }
9835
+ trends.push(trend);
9836
+ previousEfficiency = averageEfficiency;
9837
+ previousBookedDuration = averageBookedDuration;
9838
+ previousActualDuration = averageActualDuration;
9839
+ } else {
9840
+ const timeMetrics = calculateAverageTimeMetrics(completed);
9841
+ const trend = {
9842
+ period: periodInfo.period,
9843
+ startDate: periodInfo.startDate,
9844
+ endDate: periodInfo.endDate,
9845
+ averageBookedDuration: timeMetrics.averageBookedDuration,
9846
+ averageActualDuration: timeMetrics.averageActualDuration,
9847
+ averageEfficiency: timeMetrics.averageEfficiency,
9848
+ appointmentCount: timeMetrics.appointmentsWithActualTime
9849
+ };
9850
+ if (previousEfficiency > 0) {
9851
+ const efficiencyChange = getTrendChange(timeMetrics.averageEfficiency, previousEfficiency);
9852
+ trend.previousPeriod = {
9853
+ averageBookedDuration: previousBookedDuration,
9854
+ averageActualDuration: previousActualDuration,
9855
+ averageEfficiency: previousEfficiency,
9856
+ efficiencyPercentageChange: efficiencyChange.percentageChange,
9857
+ direction: efficiencyChange.direction
9858
+ };
9859
+ }
9860
+ trends.push(trend);
9861
+ previousEfficiency = timeMetrics.averageEfficiency;
9862
+ previousBookedDuration = timeMetrics.averageBookedDuration;
9863
+ previousActualDuration = timeMetrics.averageActualDuration;
9864
+ }
9865
+ });
9866
+ return trends;
9867
+ }
9868
+ /**
9869
+ * Calculate appointment count trends over time
9870
+ *
9871
+ * @param dateRange - Date range for trend analysis
9872
+ * @param period - Period type (week, month, quarter, year)
9873
+ * @param filters - Optional filters
9874
+ * @param groupBy - Optional entity type to group trends by
9875
+ * @returns Array of appointment trends with percentage changes
9876
+ */
9877
+ async getAppointmentTrends(dateRange, period, filters, groupBy) {
9878
+ const appointments = await this.fetchAppointments(filters);
9879
+ const filtered = filterByDateRange(appointments, dateRange);
9880
+ if (filtered.length === 0) {
9881
+ return [];
9882
+ }
9883
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9884
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9885
+ const trends = [];
9886
+ let previousTotal = 0;
9887
+ let previousCompleted = 0;
9888
+ periods.forEach((periodInfo) => {
9889
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9890
+ const completed = getCompletedAppointments(periodAppointments);
9891
+ const canceled = getCanceledAppointments(periodAppointments);
9892
+ const noShow = getNoShowAppointments(periodAppointments);
9893
+ const pending = periodAppointments.filter((a) => a.status === "pending" /* PENDING */);
9894
+ const confirmed = periodAppointments.filter((a) => a.status === "confirmed" /* CONFIRMED */);
9895
+ const trend = {
9896
+ period: periodInfo.period,
9897
+ startDate: periodInfo.startDate,
9898
+ endDate: periodInfo.endDate,
9899
+ totalAppointments: periodAppointments.length,
9900
+ completedAppointments: completed.length,
9901
+ canceledAppointments: canceled.length,
9902
+ noShowAppointments: noShow.length,
9903
+ pendingAppointments: pending.length,
9904
+ confirmedAppointments: confirmed.length
9905
+ };
9906
+ if (previousTotal > 0) {
9907
+ const totalChange = getTrendChange(periodAppointments.length, previousTotal);
9908
+ trend.previousPeriod = {
9909
+ totalAppointments: previousTotal,
9910
+ completedAppointments: previousCompleted,
9911
+ percentageChange: totalChange.percentageChange,
9912
+ direction: totalChange.direction
9913
+ };
9914
+ }
9915
+ trends.push(trend);
9916
+ previousTotal = periodAppointments.length;
9917
+ previousCompleted = completed.length;
9918
+ });
9919
+ return trends;
9920
+ }
9921
+ /**
9922
+ * Calculate cancellation and no-show rate trends over time
9923
+ *
9924
+ * @param dateRange - Date range for trend analysis
9925
+ * @param period - Period type (week, month, quarter, year)
9926
+ * @param filters - Optional filters
9927
+ * @param groupBy - Optional entity type to group trends by
9928
+ * @returns Array of cancellation rate trends with percentage changes
9929
+ */
9930
+ async getCancellationRateTrends(dateRange, period, filters, groupBy) {
9931
+ const appointments = await this.fetchAppointments(filters);
9932
+ const filtered = filterByDateRange(appointments, dateRange);
9933
+ if (filtered.length === 0) {
9934
+ return [];
9935
+ }
9936
+ const periodMap = groupAppointmentsByPeriod(filtered, period);
9937
+ const periods = generatePeriods(dateRange.start, dateRange.end, period);
9938
+ const trends = [];
9939
+ let previousCancellationRate = 0;
9940
+ let previousNoShowRate = 0;
9941
+ periods.forEach((periodInfo) => {
9942
+ const periodAppointments = periodMap.get(periodInfo.period) || [];
9943
+ const canceled = getCanceledAppointments(periodAppointments);
9944
+ const noShow = getNoShowAppointments(periodAppointments);
9945
+ const cancellationRate = calculatePercentage(canceled.length, periodAppointments.length);
9946
+ const noShowRate = calculatePercentage(noShow.length, periodAppointments.length);
9947
+ const trend = {
9948
+ period: periodInfo.period,
9949
+ startDate: periodInfo.startDate,
9950
+ endDate: periodInfo.endDate,
9951
+ cancellationRate,
9952
+ noShowRate,
9953
+ totalAppointments: periodAppointments.length,
9954
+ canceledAppointments: canceled.length,
9955
+ noShowAppointments: noShow.length
9956
+ };
9957
+ if (previousCancellationRate > 0 || previousNoShowRate > 0) {
9958
+ const cancellationChange = getTrendChange(cancellationRate, previousCancellationRate);
9959
+ const noShowChange = getTrendChange(noShowRate, previousNoShowRate);
9960
+ trend.previousPeriod = {
9961
+ cancellationRate: previousCancellationRate,
9962
+ noShowRate: previousNoShowRate,
9963
+ cancellationRateChange: cancellationChange.percentageChange,
9964
+ noShowRateChange: noShowChange.percentageChange,
9965
+ direction: cancellationChange.direction
9966
+ // Use cancellation direction as primary
9967
+ };
9968
+ }
9969
+ trends.push(trend);
9970
+ previousCancellationRate = cancellationRate;
9971
+ previousNoShowRate = noShowRate;
9972
+ });
9973
+ return trends;
9974
+ }
9975
+ // ==========================================
9976
+ // Review Analytics Methods
9977
+ // ==========================================
9978
+ /**
9979
+ * Get review metrics for a specific entity (practitioner, procedure, etc.)
9980
+ */
9981
+ async getReviewMetricsByEntity(entityType, entityId, dateRange, filters) {
9982
+ return this.reviewAnalyticsService.getReviewMetricsByEntity(entityType, entityId, dateRange, filters);
9983
+ }
9984
+ /**
9985
+ * Get review metrics for multiple entities (grouped)
9986
+ */
9987
+ async getReviewMetricsByEntities(entityType, dateRange, filters) {
9988
+ return this.reviewAnalyticsService.getReviewMetricsByEntities(entityType, dateRange, filters);
9989
+ }
9990
+ /**
9991
+ * Get overall review averages for comparison
9992
+ */
9993
+ async getOverallReviewAverages(dateRange, filters) {
9994
+ return this.reviewAnalyticsService.getOverallReviewAverages(dateRange, filters);
9995
+ }
9996
+ /**
9997
+ * Get review details for a specific entity
9998
+ */
9999
+ async getReviewDetails(entityType, entityId, dateRange, filters) {
10000
+ return this.reviewAnalyticsService.getReviewDetails(entityType, entityId, dateRange, filters);
10001
+ }
10002
+ /**
10003
+ * Calculate review trends over time
10004
+ * Groups reviews by period and calculates rating and recommendation metrics
10005
+ *
10006
+ * @param dateRange - Date range for trend analysis
10007
+ * @param period - Period type (week, month, quarter, year)
10008
+ * @param filters - Optional filters for clinic, practitioner, procedure
10009
+ * @param entityType - Optional entity type to group trends by
10010
+ * @returns Array of review trends with percentage changes
10011
+ */
10012
+ async getReviewTrends(dateRange, period, filters, entityType) {
10013
+ return this.reviewAnalyticsService.getReviewTrends(dateRange, period, filters, entityType);
10014
+ }
8842
10015
  };
8843
10016
 
8844
10017
  // src/admin/analytics/analytics.admin.service.ts
@@ -8871,33 +10044,33 @@ var AnalyticsAdminService = class {
8871
10044
  createAppointmentServiceAdapter() {
8872
10045
  return {
8873
10046
  searchAppointments: async (params) => {
8874
- let query2 = this.db.collection(APPOINTMENTS_COLLECTION);
10047
+ let query3 = this.db.collection(APPOINTMENTS_COLLECTION);
8875
10048
  if (params.clinicBranchId) {
8876
- query2 = query2.where("clinicBranchId", "==", params.clinicBranchId);
10049
+ query3 = query3.where("clinicBranchId", "==", params.clinicBranchId);
8877
10050
  }
8878
10051
  if (params.practitionerId) {
8879
- query2 = query2.where("practitionerId", "==", params.practitionerId);
10052
+ query3 = query3.where("practitionerId", "==", params.practitionerId);
8880
10053
  }
8881
10054
  if (params.procedureId) {
8882
- query2 = query2.where("procedureId", "==", params.procedureId);
10055
+ query3 = query3.where("procedureId", "==", params.procedureId);
8883
10056
  }
8884
10057
  if (params.patientId) {
8885
- query2 = query2.where("patientId", "==", params.patientId);
10058
+ query3 = query3.where("patientId", "==", params.patientId);
8886
10059
  }
8887
10060
  if (params.startDate) {
8888
10061
  const startDate = params.startDate instanceof Date ? params.startDate : params.startDate.toDate();
8889
10062
  const startTimestamp = admin14.firestore.Timestamp.fromDate(startDate);
8890
- query2 = query2.where("appointmentStartTime", ">=", startTimestamp);
10063
+ query3 = query3.where("appointmentStartTime", ">=", startTimestamp);
8891
10064
  }
8892
10065
  if (params.endDate) {
8893
10066
  const endDate = params.endDate instanceof Date ? params.endDate : params.endDate.toDate();
8894
10067
  const endTimestamp = admin14.firestore.Timestamp.fromDate(endDate);
8895
- query2 = query2.where("appointmentStartTime", "<=", endTimestamp);
10068
+ query3 = query3.where("appointmentStartTime", "<=", endTimestamp);
8896
10069
  }
8897
- const snapshot = await query2.get();
8898
- const appointments = snapshot.docs.map((doc2) => ({
8899
- id: doc2.id,
8900
- ...doc2.data()
10070
+ const snapshot = await query3.get();
10071
+ const appointments = snapshot.docs.map((doc3) => ({
10072
+ id: doc3.id,
10073
+ ...doc3.data()
8901
10074
  }));
8902
10075
  return {
8903
10076
  appointments,
@@ -8994,7 +10167,7 @@ var AnalyticsAdminService = class {
8994
10167
  };
8995
10168
 
8996
10169
  // src/admin/booking/booking.calculator.ts
8997
- var import_firestore4 = require("firebase/firestore");
10170
+ var import_firestore5 = require("firebase/firestore");
8998
10171
  var import_luxon2 = require("luxon");
8999
10172
  var BookingAvailabilityCalculator = class {
9000
10173
  /**
@@ -9120,8 +10293,8 @@ var BookingAvailabilityCalculator = class {
9120
10293
  const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
9121
10294
  const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
9122
10295
  workingIntervals.push({
9123
- start: import_firestore4.Timestamp.fromMillis(intervalStart.toMillis()),
9124
- end: import_firestore4.Timestamp.fromMillis(intervalEnd.toMillis())
10296
+ start: import_firestore5.Timestamp.fromMillis(intervalStart.toMillis()),
10297
+ end: import_firestore5.Timestamp.fromMillis(intervalEnd.toMillis())
9125
10298
  });
9126
10299
  if (daySchedule.breaks && daySchedule.breaks.length > 0) {
9127
10300
  for (const breakTime of daySchedule.breaks) {
@@ -9141,8 +10314,8 @@ var BookingAvailabilityCalculator = class {
9141
10314
  ...this.subtractInterval(
9142
10315
  workingIntervals[workingIntervals.length - 1],
9143
10316
  {
9144
- start: import_firestore4.Timestamp.fromMillis(breakStart.toMillis()),
9145
- end: import_firestore4.Timestamp.fromMillis(breakEnd.toMillis())
10317
+ start: import_firestore5.Timestamp.fromMillis(breakStart.toMillis()),
10318
+ end: import_firestore5.Timestamp.fromMillis(breakEnd.toMillis())
9146
10319
  }
9147
10320
  )
9148
10321
  );
@@ -9252,8 +10425,8 @@ var BookingAvailabilityCalculator = class {
9252
10425
  const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
9253
10426
  const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
9254
10427
  workingIntervals.push({
9255
- start: import_firestore4.Timestamp.fromMillis(intervalStart.toMillis()),
9256
- end: import_firestore4.Timestamp.fromMillis(intervalEnd.toMillis())
10428
+ start: import_firestore5.Timestamp.fromMillis(intervalStart.toMillis()),
10429
+ end: import_firestore5.Timestamp.fromMillis(intervalEnd.toMillis())
9257
10430
  });
9258
10431
  }
9259
10432
  }
@@ -9333,7 +10506,7 @@ var BookingAvailabilityCalculator = class {
9333
10506
  const isInFuture = slotStart >= earliestBookableTime;
9334
10507
  if (isInFuture && this.isSlotFullyAvailable(slotStart, slotEnd, intervals, tz)) {
9335
10508
  slots.push({
9336
- start: import_firestore4.Timestamp.fromMillis(slotStart.toMillis())
10509
+ start: import_firestore5.Timestamp.fromMillis(slotStart.toMillis())
9337
10510
  });
9338
10511
  }
9339
10512
  slotStart = slotStart.plus({ minutes: intervalMinutes });
@@ -9563,8 +10736,8 @@ var DocumentManagerAdminService = class {
9563
10736
  const templateIds = technologyTemplates.map((t) => t.templateId);
9564
10737
  const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin15.firestore.FieldPath.documentId(), "in", templateIds).get();
9565
10738
  const templatesMap = /* @__PURE__ */ new Map();
9566
- templatesSnapshot.forEach((doc2) => {
9567
- templatesMap.set(doc2.id, doc2.data());
10739
+ templatesSnapshot.forEach((doc3) => {
10740
+ templatesMap.set(doc3.id, doc3.data());
9568
10741
  });
9569
10742
  for (const templateRef of technologyTemplates) {
9570
10743
  const template = templatesMap.get(templateRef.templateId);
@@ -9805,9 +10978,9 @@ var BookingAdmin = class {
9805
10978
  );
9806
10979
  const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
9807
10980
  const snapshot = await eventsRef.get();
9808
- const events = snapshot.docs.map((doc2) => ({
9809
- ...doc2.data(),
9810
- id: doc2.id
10981
+ const events = snapshot.docs.map((doc3) => ({
10982
+ ...doc3.data(),
10983
+ id: doc3.id
9811
10984
  })).filter((event) => {
9812
10985
  return event.eventTime.end.toMillis() > start.toMillis();
9813
10986
  });
@@ -9851,9 +11024,9 @@ var BookingAdmin = class {
9851
11024
  );
9852
11025
  const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", queryStart).where("eventTime.start", "<", end).orderBy("eventTime.start");
9853
11026
  const snapshot = await eventsRef.get();
9854
- const events = snapshot.docs.map((doc2) => ({
9855
- ...doc2.data(),
9856
- id: doc2.id
11027
+ const events = snapshot.docs.map((doc3) => ({
11028
+ ...doc3.data(),
11029
+ id: doc3.id
9857
11030
  })).filter((event) => {
9858
11031
  return event.eventTime.end.toMillis() > start.toMillis();
9859
11032
  });
@@ -11708,8 +12881,8 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
11708
12881
  */
11709
12882
  async fetchPractitionerById(practitionerId) {
11710
12883
  try {
11711
- const doc2 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
11712
- return doc2.exists ? doc2.data() : null;
12884
+ const doc3 = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
12885
+ return doc3.exists ? doc3.data() : null;
11713
12886
  } catch (error) {
11714
12887
  Logger.error(
11715
12888
  "[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
@@ -11725,8 +12898,8 @@ var ExistingPractitionerInviteMailingService = class extends BaseMailingService
11725
12898
  */
11726
12899
  async fetchClinicById(clinicId) {
11727
12900
  try {
11728
- const doc2 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
11729
- return doc2.exists ? doc2.data() : null;
12901
+ const doc3 = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
12902
+ return doc3.exists ? doc3.data() : null;
11730
12903
  } catch (error) {
11731
12904
  Logger.error(
11732
12905
  "[ExistingPractitionerInviteMailingService] Error fetching clinic:",