@blackcode_sa/metaestetics-api 1.14.58 → 1.14.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +42 -1
- package/dist/admin/index.d.ts +42 -1
- package/dist/admin/index.js +572 -19
- package/dist/admin/index.mjs +572 -19
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +28 -6
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +498 -7
- package/src/admin/notifications/notifications.admin.ts +104 -6
- package/src/types/appointment/index.ts +6 -0
- package/src/types/notifications/index.ts +14 -0
package/dist/admin/index.js
CHANGED
|
@@ -505,6 +505,7 @@ var NotificationType = /* @__PURE__ */ ((NotificationType2) => {
|
|
|
505
505
|
NotificationType2["APPOINTMENT_REMINDER"] = "appointmentReminder";
|
|
506
506
|
NotificationType2["APPOINTMENT_STATUS_CHANGE"] = "appointmentStatusChange";
|
|
507
507
|
NotificationType2["APPOINTMENT_RESCHEDULED_PROPOSAL"] = "appointmentRescheduledProposal";
|
|
508
|
+
NotificationType2["APPOINTMENT_RESCHEDULED_REMINDER"] = "appointmentRescheduledReminder";
|
|
508
509
|
NotificationType2["APPOINTMENT_CANCELLED"] = "appointmentCancelled";
|
|
509
510
|
NotificationType2["PRE_REQUIREMENT_INSTRUCTION_DUE"] = "preRequirementInstructionDue";
|
|
510
511
|
NotificationType2["POST_REQUIREMENT_INSTRUCTION_DUE"] = "postRequirementInstructionDue";
|
|
@@ -716,6 +717,37 @@ var NotificationsAdmin = class {
|
|
|
716
717
|
});
|
|
717
718
|
return docRef.id;
|
|
718
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Creates a notification and immediately attempts to send it.
|
|
722
|
+
* If immediate send fails, the notification remains PENDING for cron pickup.
|
|
723
|
+
* Returns the notification ID regardless of send success.
|
|
724
|
+
*/
|
|
725
|
+
async createAndSendNotificationImmediately(notification) {
|
|
726
|
+
const notificationId = await this.createNotification(notification);
|
|
727
|
+
try {
|
|
728
|
+
const fullNotification = {
|
|
729
|
+
...notification,
|
|
730
|
+
id: notificationId,
|
|
731
|
+
status: "pending" /* PENDING */
|
|
732
|
+
};
|
|
733
|
+
const sent = await this.sendPushNotification(fullNotification);
|
|
734
|
+
if (sent) {
|
|
735
|
+
Logger.info(
|
|
736
|
+
`[NotificationsAdmin] Notification ${notificationId} sent immediately`
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
Logger.info(
|
|
740
|
+
`[NotificationsAdmin] Notification ${notificationId} immediate send failed, will be retried by cron`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
Logger.error(
|
|
745
|
+
`[NotificationsAdmin] Error sending notification ${notificationId} immediately, will be retried by cron:`,
|
|
746
|
+
error
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
return notificationId;
|
|
750
|
+
}
|
|
719
751
|
/**
|
|
720
752
|
* Priprema Expo poruku za slanje
|
|
721
753
|
*/
|
|
@@ -982,7 +1014,7 @@ var NotificationsAdmin = class {
|
|
|
982
1014
|
appointmentId: appointment.id
|
|
983
1015
|
};
|
|
984
1016
|
try {
|
|
985
|
-
const notificationId = await this.
|
|
1017
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
986
1018
|
notificationData
|
|
987
1019
|
);
|
|
988
1020
|
console.log(
|
|
@@ -1033,7 +1065,7 @@ var NotificationsAdmin = class {
|
|
|
1033
1065
|
appointmentId: appointment.id
|
|
1034
1066
|
};
|
|
1035
1067
|
try {
|
|
1036
|
-
const notificationId = await this.
|
|
1068
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1037
1069
|
notificationData
|
|
1038
1070
|
);
|
|
1039
1071
|
console.log(
|
|
@@ -1069,7 +1101,7 @@ var NotificationsAdmin = class {
|
|
|
1069
1101
|
appointmentId: appointment.id
|
|
1070
1102
|
};
|
|
1071
1103
|
try {
|
|
1072
|
-
const notificationId = await this.
|
|
1104
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1073
1105
|
notificationData
|
|
1074
1106
|
);
|
|
1075
1107
|
console.log(
|
|
@@ -1106,7 +1138,7 @@ var NotificationsAdmin = class {
|
|
|
1106
1138
|
appointmentId: appointment.id
|
|
1107
1139
|
};
|
|
1108
1140
|
try {
|
|
1109
|
-
const notificationId = await this.
|
|
1141
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1110
1142
|
notificationData
|
|
1111
1143
|
);
|
|
1112
1144
|
console.log(
|
|
@@ -1142,7 +1174,7 @@ var NotificationsAdmin = class {
|
|
|
1142
1174
|
appointmentId: appointment.id
|
|
1143
1175
|
};
|
|
1144
1176
|
try {
|
|
1145
|
-
const notificationId = await this.
|
|
1177
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1146
1178
|
notificationData
|
|
1147
1179
|
);
|
|
1148
1180
|
console.log(
|
|
@@ -1185,7 +1217,7 @@ var NotificationsAdmin = class {
|
|
|
1185
1217
|
appointmentId: appointment.id
|
|
1186
1218
|
};
|
|
1187
1219
|
try {
|
|
1188
|
-
const notificationId = await this.
|
|
1220
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1189
1221
|
notificationData
|
|
1190
1222
|
);
|
|
1191
1223
|
console.log(
|
|
@@ -1200,6 +1232,50 @@ var NotificationsAdmin = class {
|
|
|
1200
1232
|
return null;
|
|
1201
1233
|
}
|
|
1202
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Sends a reminder push notification for a pending reschedule request.
|
|
1237
|
+
* Used when a clinic has proposed a reschedule and the patient hasn't responded.
|
|
1238
|
+
* @param appointment The appointment with pending reschedule.
|
|
1239
|
+
* @param patientUserId The ID of the patient.
|
|
1240
|
+
* @param patientExpoTokens Array of Expo push tokens for the patient.
|
|
1241
|
+
* @param reminderCount Optional count of reminders already sent (for tracking).
|
|
1242
|
+
*/
|
|
1243
|
+
async sendRescheduleReminderPush(appointment, patientUserId, patientExpoTokens, reminderCount) {
|
|
1244
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
1245
|
+
console.log(
|
|
1246
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule reminder. Skipping push.`
|
|
1247
|
+
);
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
const title = "Reminder: Reschedule Request Pending";
|
|
1251
|
+
const body = `You have a pending reschedule request for your ${appointment.procedureInfo.name} appointment. Please respond in the app.`;
|
|
1252
|
+
const notificationTimestampForDb = admin2.firestore.Timestamp.now();
|
|
1253
|
+
const notificationData = {
|
|
1254
|
+
userId: patientUserId,
|
|
1255
|
+
userRole: "patient" /* PATIENT */,
|
|
1256
|
+
notificationType: "appointmentRescheduledReminder" /* APPOINTMENT_RESCHEDULED_REMINDER */,
|
|
1257
|
+
notificationTime: notificationTimestampForDb,
|
|
1258
|
+
notificationTokens: patientExpoTokens,
|
|
1259
|
+
title,
|
|
1260
|
+
body,
|
|
1261
|
+
appointmentId: appointment.id
|
|
1262
|
+
};
|
|
1263
|
+
try {
|
|
1264
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1265
|
+
notificationData
|
|
1266
|
+
);
|
|
1267
|
+
console.log(
|
|
1268
|
+
`[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_REMINDER notification ${notificationId} for patient ${patientUserId}. Reminder count: ${reminderCount != null ? reminderCount : 1}.`
|
|
1269
|
+
);
|
|
1270
|
+
return notificationId;
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
console.error(
|
|
1273
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_REMINDER notification for patient ${patientUserId}:`,
|
|
1274
|
+
error
|
|
1275
|
+
);
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1203
1279
|
};
|
|
1204
1280
|
|
|
1205
1281
|
// src/admin/requirements/patient-requirements.admin.service.ts
|
|
@@ -2363,6 +2439,305 @@ var clinicAppointmentRequestedTemplate = `
|
|
|
2363
2439
|
</body>
|
|
2364
2440
|
</html>
|
|
2365
2441
|
`;
|
|
2442
|
+
var appointmentCancelledTemplate = `
|
|
2443
|
+
<!DOCTYPE html>
|
|
2444
|
+
<html lang="en">
|
|
2445
|
+
<head>
|
|
2446
|
+
<meta charset="UTF-8">
|
|
2447
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2448
|
+
<title>Appointment Cancelled</title>
|
|
2449
|
+
<style>
|
|
2450
|
+
body {
|
|
2451
|
+
margin: 0;
|
|
2452
|
+
padding: 0;
|
|
2453
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
2454
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2455
|
+
min-height: 100vh;
|
|
2456
|
+
}
|
|
2457
|
+
.email-container {
|
|
2458
|
+
max-width: 600px;
|
|
2459
|
+
margin: 0 auto;
|
|
2460
|
+
background: #ffffff;
|
|
2461
|
+
border-radius: 20px;
|
|
2462
|
+
overflow: hidden;
|
|
2463
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
2464
|
+
margin-top: 40px;
|
|
2465
|
+
margin-bottom: 40px;
|
|
2466
|
+
}
|
|
2467
|
+
.header {
|
|
2468
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2469
|
+
padding: 40px 30px;
|
|
2470
|
+
text-align: center;
|
|
2471
|
+
color: white;
|
|
2472
|
+
}
|
|
2473
|
+
.header h1 {
|
|
2474
|
+
margin: 0;
|
|
2475
|
+
font-size: 28px;
|
|
2476
|
+
font-weight: 300;
|
|
2477
|
+
letter-spacing: 1px;
|
|
2478
|
+
}
|
|
2479
|
+
.header .subtitle {
|
|
2480
|
+
margin: 10px 0 0 0;
|
|
2481
|
+
font-size: 16px;
|
|
2482
|
+
opacity: 0.9;
|
|
2483
|
+
font-weight: 300;
|
|
2484
|
+
}
|
|
2485
|
+
.content {
|
|
2486
|
+
padding: 40px 30px;
|
|
2487
|
+
}
|
|
2488
|
+
.greeting {
|
|
2489
|
+
font-size: 18px;
|
|
2490
|
+
color: #333;
|
|
2491
|
+
margin-bottom: 25px;
|
|
2492
|
+
font-weight: 400;
|
|
2493
|
+
}
|
|
2494
|
+
.cancellation-notice {
|
|
2495
|
+
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
|
2496
|
+
border-radius: 15px;
|
|
2497
|
+
padding: 25px;
|
|
2498
|
+
margin: 25px 0;
|
|
2499
|
+
border-left: 5px solid #d4736c;
|
|
2500
|
+
}
|
|
2501
|
+
.cancellation-notice p {
|
|
2502
|
+
margin: 0;
|
|
2503
|
+
color: #c62828;
|
|
2504
|
+
font-size: 15px;
|
|
2505
|
+
font-weight: 500;
|
|
2506
|
+
line-height: 1.6;
|
|
2507
|
+
}
|
|
2508
|
+
.cancelled-by-info {
|
|
2509
|
+
background: #fafafa;
|
|
2510
|
+
border-radius: 10px;
|
|
2511
|
+
padding: 15px 20px;
|
|
2512
|
+
margin-top: 15px;
|
|
2513
|
+
}
|
|
2514
|
+
.cancelled-by-info .label {
|
|
2515
|
+
font-size: 12px;
|
|
2516
|
+
color: #757575;
|
|
2517
|
+
text-transform: uppercase;
|
|
2518
|
+
letter-spacing: 0.5px;
|
|
2519
|
+
margin-bottom: 5px;
|
|
2520
|
+
}
|
|
2521
|
+
.cancelled-by-info .value {
|
|
2522
|
+
font-size: 14px;
|
|
2523
|
+
color: #424242;
|
|
2524
|
+
font-weight: 500;
|
|
2525
|
+
}
|
|
2526
|
+
.reason-box {
|
|
2527
|
+
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
|
2528
|
+
border-radius: 15px;
|
|
2529
|
+
padding: 20px;
|
|
2530
|
+
margin: 20px 0;
|
|
2531
|
+
border-left: 5px solid #ffa000;
|
|
2532
|
+
}
|
|
2533
|
+
.reason-box .label {
|
|
2534
|
+
font-size: 14px;
|
|
2535
|
+
font-weight: 600;
|
|
2536
|
+
color: #e65100;
|
|
2537
|
+
margin-bottom: 8px;
|
|
2538
|
+
}
|
|
2539
|
+
.reason-box .reason-text {
|
|
2540
|
+
font-size: 15px;
|
|
2541
|
+
color: #424242;
|
|
2542
|
+
line-height: 1.6;
|
|
2543
|
+
font-style: italic;
|
|
2544
|
+
}
|
|
2545
|
+
.appointment-card {
|
|
2546
|
+
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
|
2547
|
+
border-radius: 15px;
|
|
2548
|
+
padding: 30px;
|
|
2549
|
+
margin: 25px 0;
|
|
2550
|
+
border-left: 5px solid #9e9e9e;
|
|
2551
|
+
opacity: 0.9;
|
|
2552
|
+
}
|
|
2553
|
+
.appointment-title {
|
|
2554
|
+
font-size: 20px;
|
|
2555
|
+
color: #757575;
|
|
2556
|
+
margin-bottom: 20px;
|
|
2557
|
+
font-weight: 600;
|
|
2558
|
+
}
|
|
2559
|
+
.appointment-details {
|
|
2560
|
+
display: grid;
|
|
2561
|
+
gap: 15px;
|
|
2562
|
+
}
|
|
2563
|
+
.detail-row {
|
|
2564
|
+
display: flex;
|
|
2565
|
+
align-items: center;
|
|
2566
|
+
padding: 8px 0;
|
|
2567
|
+
}
|
|
2568
|
+
.detail-label {
|
|
2569
|
+
font-weight: 600;
|
|
2570
|
+
color: #757575;
|
|
2571
|
+
min-width: 120px;
|
|
2572
|
+
font-size: 14px;
|
|
2573
|
+
}
|
|
2574
|
+
.detail-value {
|
|
2575
|
+
color: #616161;
|
|
2576
|
+
font-size: 16px;
|
|
2577
|
+
font-weight: 500;
|
|
2578
|
+
text-decoration: line-through;
|
|
2579
|
+
}
|
|
2580
|
+
.procedure-name {
|
|
2581
|
+
color: #757575;
|
|
2582
|
+
font-weight: 600;
|
|
2583
|
+
}
|
|
2584
|
+
.clinic-name {
|
|
2585
|
+
color: #9e9e9e;
|
|
2586
|
+
font-weight: 600;
|
|
2587
|
+
}
|
|
2588
|
+
.rebook-section {
|
|
2589
|
+
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
2590
|
+
border-radius: 15px;
|
|
2591
|
+
padding: 25px;
|
|
2592
|
+
margin: 30px 0;
|
|
2593
|
+
text-align: center;
|
|
2594
|
+
border-left: 5px solid #4caf50;
|
|
2595
|
+
}
|
|
2596
|
+
.rebook-section h3 {
|
|
2597
|
+
margin: 0 0 15px 0;
|
|
2598
|
+
color: #2e7d32;
|
|
2599
|
+
font-weight: 600;
|
|
2600
|
+
font-size: 18px;
|
|
2601
|
+
}
|
|
2602
|
+
.rebook-section p {
|
|
2603
|
+
margin: 0;
|
|
2604
|
+
color: #555;
|
|
2605
|
+
font-size: 15px;
|
|
2606
|
+
line-height: 1.6;
|
|
2607
|
+
}
|
|
2608
|
+
.support-section {
|
|
2609
|
+
background: #f8f9fa;
|
|
2610
|
+
border-radius: 15px;
|
|
2611
|
+
padding: 20px;
|
|
2612
|
+
margin: 25px 0;
|
|
2613
|
+
text-align: center;
|
|
2614
|
+
}
|
|
2615
|
+
.support-section h4 {
|
|
2616
|
+
margin: 0 0 10px 0;
|
|
2617
|
+
color: #555;
|
|
2618
|
+
font-weight: 600;
|
|
2619
|
+
font-size: 16px;
|
|
2620
|
+
}
|
|
2621
|
+
.support-section p {
|
|
2622
|
+
margin: 0;
|
|
2623
|
+
color: #757575;
|
|
2624
|
+
font-size: 14px;
|
|
2625
|
+
line-height: 1.6;
|
|
2626
|
+
}
|
|
2627
|
+
.footer {
|
|
2628
|
+
background: #f8f9fa;
|
|
2629
|
+
padding: 25px 30px;
|
|
2630
|
+
text-align: center;
|
|
2631
|
+
color: #666;
|
|
2632
|
+
font-size: 14px;
|
|
2633
|
+
border-top: 1px solid #eee;
|
|
2634
|
+
}
|
|
2635
|
+
.logo {
|
|
2636
|
+
font-size: 24px;
|
|
2637
|
+
font-weight: 700;
|
|
2638
|
+
color: white;
|
|
2639
|
+
margin-bottom: 5px;
|
|
2640
|
+
}
|
|
2641
|
+
.divider {
|
|
2642
|
+
height: 2px;
|
|
2643
|
+
background: linear-gradient(90deg, #d4736c, #b85450);
|
|
2644
|
+
margin: 25px 0;
|
|
2645
|
+
border-radius: 1px;
|
|
2646
|
+
}
|
|
2647
|
+
.icon {
|
|
2648
|
+
text-align: center;
|
|
2649
|
+
margin: 20px 0;
|
|
2650
|
+
font-size: 48px;
|
|
2651
|
+
}
|
|
2652
|
+
</style>
|
|
2653
|
+
</head>
|
|
2654
|
+
<body>
|
|
2655
|
+
<div class="email-container">
|
|
2656
|
+
<div class="header">
|
|
2657
|
+
<div class="logo">MetaEstetics</div>
|
|
2658
|
+
<h1>Appointment Cancelled</h1>
|
|
2659
|
+
<div class="subtitle">We're Sorry to See This Change</div>
|
|
2660
|
+
</div>
|
|
2661
|
+
|
|
2662
|
+
<div class="content">
|
|
2663
|
+
<div class="icon">❌</div>
|
|
2664
|
+
|
|
2665
|
+
<div class="greeting">
|
|
2666
|
+
Dear <strong>{{recipientName}}</strong>,
|
|
2667
|
+
</div>
|
|
2668
|
+
|
|
2669
|
+
<div class="cancellation-notice">
|
|
2670
|
+
<p><strong>Your appointment has been cancelled.</strong> We wanted to let you know that the following appointment is no longer scheduled.</p>
|
|
2671
|
+
<div class="cancelled-by-info">
|
|
2672
|
+
<div class="label">Cancelled By</div>
|
|
2673
|
+
<div class="value">{{cancelledByDisplay}}</div>
|
|
2674
|
+
</div>
|
|
2675
|
+
</div>
|
|
2676
|
+
|
|
2677
|
+
{{#if cancellationReason}}
|
|
2678
|
+
<div class="reason-box">
|
|
2679
|
+
<div class="label">Reason for Cancellation</div>
|
|
2680
|
+
<div class="reason-text">"{{cancellationReason}}"</div>
|
|
2681
|
+
</div>
|
|
2682
|
+
{{/if}}
|
|
2683
|
+
|
|
2684
|
+
<div class="appointment-card">
|
|
2685
|
+
<div class="appointment-title">Cancelled Appointment Details</div>
|
|
2686
|
+
<div class="appointment-details">
|
|
2687
|
+
<div class="detail-row">
|
|
2688
|
+
<div class="detail-label">Procedure:</div>
|
|
2689
|
+
<div class="detail-value procedure-name">{{procedureName}}</div>
|
|
2690
|
+
</div>
|
|
2691
|
+
<div class="detail-row">
|
|
2692
|
+
<div class="detail-label">Date:</div>
|
|
2693
|
+
<div class="detail-value">{{appointmentDate}}</div>
|
|
2694
|
+
</div>
|
|
2695
|
+
<div class="detail-row">
|
|
2696
|
+
<div class="detail-label">Time:</div>
|
|
2697
|
+
<div class="detail-value">{{appointmentTime}}</div>
|
|
2698
|
+
</div>
|
|
2699
|
+
<div class="detail-row">
|
|
2700
|
+
<div class="detail-label">Practitioner:</div>
|
|
2701
|
+
<div class="detail-value">{{practitionerName}}</div>
|
|
2702
|
+
</div>
|
|
2703
|
+
<div class="detail-row">
|
|
2704
|
+
<div class="detail-label">Location:</div>
|
|
2705
|
+
<div class="detail-value clinic-name">{{clinicName}}</div>
|
|
2706
|
+
</div>
|
|
2707
|
+
</div>
|
|
2708
|
+
</div>
|
|
2709
|
+
|
|
2710
|
+
<div class="divider"></div>
|
|
2711
|
+
|
|
2712
|
+
<div class="rebook-section">
|
|
2713
|
+
<h3>Would You Like to Reschedule?</h3>
|
|
2714
|
+
<p>
|
|
2715
|
+
We'd love to see you! If you'd like to book a new appointment,
|
|
2716
|
+
simply open the MetaEstetics app and browse available times that work for you.
|
|
2717
|
+
</p>
|
|
2718
|
+
</div>
|
|
2719
|
+
|
|
2720
|
+
<div class="support-section">
|
|
2721
|
+
<h4>Need Assistance?</h4>
|
|
2722
|
+
<p>
|
|
2723
|
+
If you have any questions about this cancellation or need help rebooking,
|
|
2724
|
+
please contact {{clinicName}} directly through the app or reach out to our support team.
|
|
2725
|
+
</p>
|
|
2726
|
+
</div>
|
|
2727
|
+
</div>
|
|
2728
|
+
|
|
2729
|
+
<div class="footer">
|
|
2730
|
+
<p style="margin: 0 0 10px 0;">
|
|
2731
|
+
<strong>MetaEstetics</strong> - Premium Aesthetic Services
|
|
2732
|
+
</p>
|
|
2733
|
+
<p style="margin: 0; font-size: 12px; color: #999;">
|
|
2734
|
+
This is an automated message. Please do not reply to this email.
|
|
2735
|
+
</p>
|
|
2736
|
+
</div>
|
|
2737
|
+
</div>
|
|
2738
|
+
</body>
|
|
2739
|
+
</html>
|
|
2740
|
+
`;
|
|
2366
2741
|
var appointmentRescheduledProposalTemplate = `
|
|
2367
2742
|
<!DOCTYPE html>
|
|
2368
2743
|
<html lang="en">
|
|
@@ -2523,6 +2898,42 @@ var appointmentRescheduledProposalTemplate = `
|
|
|
2523
2898
|
font-size: 15px;
|
|
2524
2899
|
line-height: 1.6;
|
|
2525
2900
|
}
|
|
2901
|
+
.action-required-box {
|
|
2902
|
+
background: linear-gradient(135deg, #fff3e0 0%, #ffecb3 100%);
|
|
2903
|
+
border: 2px solid #ff9800;
|
|
2904
|
+
border-radius: 15px;
|
|
2905
|
+
padding: 25px;
|
|
2906
|
+
margin: 25px 0;
|
|
2907
|
+
text-align: center;
|
|
2908
|
+
}
|
|
2909
|
+
.action-required-box h3 {
|
|
2910
|
+
margin: 0 0 15px 0;
|
|
2911
|
+
color: #e65100;
|
|
2912
|
+
font-weight: 700;
|
|
2913
|
+
font-size: 18px;
|
|
2914
|
+
}
|
|
2915
|
+
.action-required-box p {
|
|
2916
|
+
margin: 0 0 12px 0;
|
|
2917
|
+
color: #bf360c;
|
|
2918
|
+
font-size: 15px;
|
|
2919
|
+
line-height: 1.6;
|
|
2920
|
+
}
|
|
2921
|
+
.action-required-box p:last-child {
|
|
2922
|
+
margin-bottom: 0;
|
|
2923
|
+
}
|
|
2924
|
+
.pending-notice {
|
|
2925
|
+
background: #fff8e1;
|
|
2926
|
+
border-radius: 8px;
|
|
2927
|
+
padding: 12px 15px;
|
|
2928
|
+
margin-top: 15px;
|
|
2929
|
+
display: inline-block;
|
|
2930
|
+
}
|
|
2931
|
+
.pending-notice p {
|
|
2932
|
+
margin: 0;
|
|
2933
|
+
color: #f57c00;
|
|
2934
|
+
font-size: 14px;
|
|
2935
|
+
font-weight: 600;
|
|
2936
|
+
}
|
|
2526
2937
|
.footer {
|
|
2527
2938
|
background: #f8f9fa;
|
|
2528
2939
|
padding: 25px 30px;
|
|
@@ -2614,16 +3025,29 @@ var appointmentRescheduledProposalTemplate = `
|
|
|
2614
3025
|
</div>
|
|
2615
3026
|
|
|
2616
3027
|
<div class="divider"></div>
|
|
2617
|
-
|
|
3028
|
+
|
|
3029
|
+
<div class="action-required-box">
|
|
3030
|
+
<h3>Your Response is Required</h3>
|
|
3031
|
+
<p>
|
|
3032
|
+
<strong>Missed our notification?</strong> Please open the MetaEstetics app to confirm or reject this reschedule request.
|
|
3033
|
+
</p>
|
|
3034
|
+
<p>
|
|
3035
|
+
Please respond as soon as possible so we can finalize your appointment.
|
|
3036
|
+
</p>
|
|
3037
|
+
<div class="pending-notice">
|
|
3038
|
+
<p>Your appointment will remain pending until you respond.</p>
|
|
3039
|
+
</div>
|
|
3040
|
+
</div>
|
|
3041
|
+
|
|
2618
3042
|
<div class="action-section">
|
|
2619
|
-
<h3>
|
|
3043
|
+
<h3>How to Respond</h3>
|
|
2620
3044
|
<p>
|
|
2621
|
-
|
|
2622
|
-
If the new time works for you, simply tap "Accept Reschedule".
|
|
3045
|
+
Open the MetaEstetics app and navigate to your appointments.
|
|
3046
|
+
If the new time works for you, simply tap "Accept Reschedule".
|
|
2623
3047
|
If not, you can reject it and we'll work with you to find an alternative time.
|
|
2624
3048
|
</p>
|
|
2625
3049
|
</div>
|
|
2626
|
-
|
|
3050
|
+
|
|
2627
3051
|
<p style="color: #555; font-size: 14px; line-height: 1.6; margin-top: 25px;">
|
|
2628
3052
|
<strong>Need Help?</strong> If you have any questions or concerns about this reschedule, please contact us directly through the app or reach out to {{clinicName}}.
|
|
2629
3053
|
</p>
|
|
@@ -2839,11 +3263,121 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2839
3263
|
throw error;
|
|
2840
3264
|
}
|
|
2841
3265
|
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Gets a user-friendly display text for who cancelled the appointment
|
|
3268
|
+
* @param cancelledBy - The entity that cancelled the appointment
|
|
3269
|
+
* @param clinicName - The clinic name for context
|
|
3270
|
+
* @returns User-friendly cancellation source text
|
|
3271
|
+
*/
|
|
3272
|
+
getCancelledByDisplayText(cancelledBy, clinicName) {
|
|
3273
|
+
switch (cancelledBy) {
|
|
3274
|
+
case "patient":
|
|
3275
|
+
return "Patient Request";
|
|
3276
|
+
case "clinic":
|
|
3277
|
+
return `${clinicName} (Clinic)`;
|
|
3278
|
+
case "practitioner":
|
|
3279
|
+
return "Your Practitioner";
|
|
3280
|
+
case "system":
|
|
3281
|
+
return "System (Automatic)";
|
|
3282
|
+
default:
|
|
3283
|
+
return "Unknown";
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Sends an appointment cancellation email to the recipient
|
|
3288
|
+
* @param data - Appointment cancellation email data
|
|
3289
|
+
* @returns Promise with the sending result
|
|
3290
|
+
*/
|
|
2842
3291
|
async sendAppointmentCancelledEmail(data) {
|
|
3292
|
+
var _a, _b, _c, _d;
|
|
2843
3293
|
Logger.info(
|
|
2844
|
-
`[AppointmentMailingService]
|
|
3294
|
+
`[AppointmentMailingService] Preparing to send appointment cancellation email to ${data.recipientRole}: ${data.recipientProfile.id}`
|
|
2845
3295
|
);
|
|
2846
|
-
|
|
3296
|
+
const recipientEmail = data.recipientProfile.email;
|
|
3297
|
+
if (!recipientEmail) {
|
|
3298
|
+
Logger.error("[AppointmentMailingService] Recipient email not found for cancellation.", {
|
|
3299
|
+
recipientId: data.recipientProfile.id,
|
|
3300
|
+
role: data.recipientRole
|
|
3301
|
+
});
|
|
3302
|
+
throw new Error("Recipient email address is missing.");
|
|
3303
|
+
}
|
|
3304
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3305
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for cancellation", {
|
|
3306
|
+
clinicTimezone,
|
|
3307
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3308
|
+
});
|
|
3309
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
3310
|
+
data.appointment.appointmentStartTime,
|
|
3311
|
+
clinicTimezone,
|
|
3312
|
+
"time"
|
|
3313
|
+
);
|
|
3314
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3315
|
+
const cancelledBy = data.appointment.canceledBy || "system";
|
|
3316
|
+
const cancelledByDisplay = this.getCancelledByDisplayText(
|
|
3317
|
+
cancelledBy,
|
|
3318
|
+
data.appointment.clinicInfo.name
|
|
3319
|
+
);
|
|
3320
|
+
const recipientName = data.recipientRole === "patient" ? data.appointment.patientInfo.fullName : data.appointment.practitionerInfo.name;
|
|
3321
|
+
const templateVariables = {
|
|
3322
|
+
recipientName,
|
|
3323
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3324
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
3325
|
+
data.appointment.appointmentStartTime,
|
|
3326
|
+
clinicTimezone,
|
|
3327
|
+
"date"
|
|
3328
|
+
),
|
|
3329
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
3330
|
+
practitionerName: data.appointment.practitionerInfo.name,
|
|
3331
|
+
clinicName: data.appointment.clinicInfo.name,
|
|
3332
|
+
cancelledByDisplay
|
|
3333
|
+
};
|
|
3334
|
+
const cancellationReason = data.cancellationReason || data.appointment.cancellationReason;
|
|
3335
|
+
let html = appointmentCancelledTemplate;
|
|
3336
|
+
if (cancellationReason) {
|
|
3337
|
+
templateVariables.cancellationReason = cancellationReason;
|
|
3338
|
+
html = html.replace(
|
|
3339
|
+
/\{\{#if cancellationReason\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
3340
|
+
"$1"
|
|
3341
|
+
);
|
|
3342
|
+
} else {
|
|
3343
|
+
html = html.replace(/\{\{#if cancellationReason\}\}[\s\S]*?\{\{\/if\}\}/g, "");
|
|
3344
|
+
}
|
|
3345
|
+
html = this.renderTemplate(html, templateVariables);
|
|
3346
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Appointment Cancelled: ${data.appointment.procedureInfo.name}`;
|
|
3347
|
+
const fromAddress = ((_b = data.options) == null ? void 0 : _b.fromAddress) || `MetaEstetics <no-reply@${((_c = data.options) == null ? void 0 : _c.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN}>`;
|
|
3348
|
+
const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3349
|
+
const mailgunSendData = {
|
|
3350
|
+
to: recipientEmail,
|
|
3351
|
+
from: fromAddress,
|
|
3352
|
+
subject,
|
|
3353
|
+
html
|
|
3354
|
+
};
|
|
3355
|
+
try {
|
|
3356
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3357
|
+
await this.logEmailAttempt(
|
|
3358
|
+
{ to: recipientEmail, subject, templateName: "appointment_cancelled" },
|
|
3359
|
+
true
|
|
3360
|
+
);
|
|
3361
|
+
Logger.info(
|
|
3362
|
+
`[AppointmentMailingService] Successfully sent cancellation email to ${recipientEmail}`
|
|
3363
|
+
);
|
|
3364
|
+
return result;
|
|
3365
|
+
} catch (error) {
|
|
3366
|
+
await this.logEmailAttempt(
|
|
3367
|
+
{
|
|
3368
|
+
to: recipientEmail,
|
|
3369
|
+
subject,
|
|
3370
|
+
templateName: "appointment_cancelled"
|
|
3371
|
+
},
|
|
3372
|
+
false,
|
|
3373
|
+
error
|
|
3374
|
+
);
|
|
3375
|
+
Logger.error(
|
|
3376
|
+
`[AppointmentMailingService] Error sending cancellation email to ${recipientEmail}:`,
|
|
3377
|
+
error
|
|
3378
|
+
);
|
|
3379
|
+
throw error;
|
|
3380
|
+
}
|
|
2847
3381
|
}
|
|
2848
3382
|
/**
|
|
2849
3383
|
* Sends a reschedule proposal email to the patient
|
|
@@ -3232,12 +3766,11 @@ var AppointmentAggregationService = class {
|
|
|
3232
3766
|
const patientCancellationData = {
|
|
3233
3767
|
appointment: after,
|
|
3234
3768
|
recipientProfile: after.patientInfo,
|
|
3235
|
-
recipientRole: "patient"
|
|
3236
|
-
|
|
3769
|
+
recipientRole: "patient",
|
|
3770
|
+
cancellationReason: after.cancellationReason
|
|
3237
3771
|
};
|
|
3238
3772
|
await this.appointmentMailingService.sendAppointmentCancelledEmail(
|
|
3239
3773
|
patientCancellationData
|
|
3240
|
-
// TODO: Properly import types
|
|
3241
3774
|
);
|
|
3242
3775
|
}
|
|
3243
3776
|
if ((_c = practitionerProfile == null ? void 0 : practitionerProfile.basicInfo) == null ? void 0 : _c.email) {
|
|
@@ -3247,12 +3780,22 @@ var AppointmentAggregationService = class {
|
|
|
3247
3780
|
const practitionerCancellationData = {
|
|
3248
3781
|
appointment: after,
|
|
3249
3782
|
recipientProfile: after.practitionerInfo,
|
|
3250
|
-
recipientRole: "practitioner"
|
|
3251
|
-
|
|
3783
|
+
recipientRole: "practitioner",
|
|
3784
|
+
cancellationReason: after.cancellationReason
|
|
3252
3785
|
};
|
|
3253
3786
|
await this.appointmentMailingService.sendAppointmentCancelledEmail(
|
|
3254
3787
|
practitionerCancellationData
|
|
3255
|
-
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
|
|
3791
|
+
Logger.info(
|
|
3792
|
+
`[AggService] Sending cancellation push notification to patient ${after.patientId}`
|
|
3793
|
+
);
|
|
3794
|
+
await this.notificationsAdmin.sendAppointmentCancelledPush(
|
|
3795
|
+
after,
|
|
3796
|
+
after.patientId,
|
|
3797
|
+
patientProfile.expoTokens,
|
|
3798
|
+
"patient" /* PATIENT */
|
|
3256
3799
|
);
|
|
3257
3800
|
}
|
|
3258
3801
|
} else if (after.status === "completed" /* COMPLETED */) {
|
|
@@ -3308,6 +3851,16 @@ var AppointmentAggregationService = class {
|
|
|
3308
3851
|
// TODO: Properly import PatientProfileInfo and types
|
|
3309
3852
|
);
|
|
3310
3853
|
}
|
|
3854
|
+
if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
|
|
3855
|
+
Logger.info(
|
|
3856
|
+
`[AggService] Sending reschedule proposal push notification to patient ${after.patientId}`
|
|
3857
|
+
);
|
|
3858
|
+
await this.notificationsAdmin.sendAppointmentRescheduledProposalPush(
|
|
3859
|
+
after,
|
|
3860
|
+
after.patientId,
|
|
3861
|
+
patientProfile.expoTokens
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3311
3864
|
Logger.info(
|
|
3312
3865
|
`[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`
|
|
3313
3866
|
);
|