@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.mjs
CHANGED
|
@@ -429,6 +429,7 @@ var NotificationType = /* @__PURE__ */ ((NotificationType2) => {
|
|
|
429
429
|
NotificationType2["APPOINTMENT_REMINDER"] = "appointmentReminder";
|
|
430
430
|
NotificationType2["APPOINTMENT_STATUS_CHANGE"] = "appointmentStatusChange";
|
|
431
431
|
NotificationType2["APPOINTMENT_RESCHEDULED_PROPOSAL"] = "appointmentRescheduledProposal";
|
|
432
|
+
NotificationType2["APPOINTMENT_RESCHEDULED_REMINDER"] = "appointmentRescheduledReminder";
|
|
432
433
|
NotificationType2["APPOINTMENT_CANCELLED"] = "appointmentCancelled";
|
|
433
434
|
NotificationType2["PRE_REQUIREMENT_INSTRUCTION_DUE"] = "preRequirementInstructionDue";
|
|
434
435
|
NotificationType2["POST_REQUIREMENT_INSTRUCTION_DUE"] = "postRequirementInstructionDue";
|
|
@@ -640,6 +641,37 @@ var NotificationsAdmin = class {
|
|
|
640
641
|
});
|
|
641
642
|
return docRef.id;
|
|
642
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Creates a notification and immediately attempts to send it.
|
|
646
|
+
* If immediate send fails, the notification remains PENDING for cron pickup.
|
|
647
|
+
* Returns the notification ID regardless of send success.
|
|
648
|
+
*/
|
|
649
|
+
async createAndSendNotificationImmediately(notification) {
|
|
650
|
+
const notificationId = await this.createNotification(notification);
|
|
651
|
+
try {
|
|
652
|
+
const fullNotification = {
|
|
653
|
+
...notification,
|
|
654
|
+
id: notificationId,
|
|
655
|
+
status: "pending" /* PENDING */
|
|
656
|
+
};
|
|
657
|
+
const sent = await this.sendPushNotification(fullNotification);
|
|
658
|
+
if (sent) {
|
|
659
|
+
Logger.info(
|
|
660
|
+
`[NotificationsAdmin] Notification ${notificationId} sent immediately`
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
Logger.info(
|
|
664
|
+
`[NotificationsAdmin] Notification ${notificationId} immediate send failed, will be retried by cron`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
} catch (error) {
|
|
668
|
+
Logger.error(
|
|
669
|
+
`[NotificationsAdmin] Error sending notification ${notificationId} immediately, will be retried by cron:`,
|
|
670
|
+
error
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return notificationId;
|
|
674
|
+
}
|
|
643
675
|
/**
|
|
644
676
|
* Priprema Expo poruku za slanje
|
|
645
677
|
*/
|
|
@@ -906,7 +938,7 @@ var NotificationsAdmin = class {
|
|
|
906
938
|
appointmentId: appointment.id
|
|
907
939
|
};
|
|
908
940
|
try {
|
|
909
|
-
const notificationId = await this.
|
|
941
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
910
942
|
notificationData
|
|
911
943
|
);
|
|
912
944
|
console.log(
|
|
@@ -957,7 +989,7 @@ var NotificationsAdmin = class {
|
|
|
957
989
|
appointmentId: appointment.id
|
|
958
990
|
};
|
|
959
991
|
try {
|
|
960
|
-
const notificationId = await this.
|
|
992
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
961
993
|
notificationData
|
|
962
994
|
);
|
|
963
995
|
console.log(
|
|
@@ -993,7 +1025,7 @@ var NotificationsAdmin = class {
|
|
|
993
1025
|
appointmentId: appointment.id
|
|
994
1026
|
};
|
|
995
1027
|
try {
|
|
996
|
-
const notificationId = await this.
|
|
1028
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
997
1029
|
notificationData
|
|
998
1030
|
);
|
|
999
1031
|
console.log(
|
|
@@ -1030,7 +1062,7 @@ var NotificationsAdmin = class {
|
|
|
1030
1062
|
appointmentId: appointment.id
|
|
1031
1063
|
};
|
|
1032
1064
|
try {
|
|
1033
|
-
const notificationId = await this.
|
|
1065
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1034
1066
|
notificationData
|
|
1035
1067
|
);
|
|
1036
1068
|
console.log(
|
|
@@ -1066,7 +1098,7 @@ var NotificationsAdmin = class {
|
|
|
1066
1098
|
appointmentId: appointment.id
|
|
1067
1099
|
};
|
|
1068
1100
|
try {
|
|
1069
|
-
const notificationId = await this.
|
|
1101
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1070
1102
|
notificationData
|
|
1071
1103
|
);
|
|
1072
1104
|
console.log(
|
|
@@ -1109,7 +1141,7 @@ var NotificationsAdmin = class {
|
|
|
1109
1141
|
appointmentId: appointment.id
|
|
1110
1142
|
};
|
|
1111
1143
|
try {
|
|
1112
|
-
const notificationId = await this.
|
|
1144
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1113
1145
|
notificationData
|
|
1114
1146
|
);
|
|
1115
1147
|
console.log(
|
|
@@ -1124,6 +1156,50 @@ var NotificationsAdmin = class {
|
|
|
1124
1156
|
return null;
|
|
1125
1157
|
}
|
|
1126
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Sends a reminder push notification for a pending reschedule request.
|
|
1161
|
+
* Used when a clinic has proposed a reschedule and the patient hasn't responded.
|
|
1162
|
+
* @param appointment The appointment with pending reschedule.
|
|
1163
|
+
* @param patientUserId The ID of the patient.
|
|
1164
|
+
* @param patientExpoTokens Array of Expo push tokens for the patient.
|
|
1165
|
+
* @param reminderCount Optional count of reminders already sent (for tracking).
|
|
1166
|
+
*/
|
|
1167
|
+
async sendRescheduleReminderPush(appointment, patientUserId, patientExpoTokens, reminderCount) {
|
|
1168
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
1169
|
+
console.log(
|
|
1170
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule reminder. Skipping push.`
|
|
1171
|
+
);
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
const title = "Reminder: Reschedule Request Pending";
|
|
1175
|
+
const body = `You have a pending reschedule request for your ${appointment.procedureInfo.name} appointment. Please respond in the app.`;
|
|
1176
|
+
const notificationTimestampForDb = admin2.firestore.Timestamp.now();
|
|
1177
|
+
const notificationData = {
|
|
1178
|
+
userId: patientUserId,
|
|
1179
|
+
userRole: "patient" /* PATIENT */,
|
|
1180
|
+
notificationType: "appointmentRescheduledReminder" /* APPOINTMENT_RESCHEDULED_REMINDER */,
|
|
1181
|
+
notificationTime: notificationTimestampForDb,
|
|
1182
|
+
notificationTokens: patientExpoTokens,
|
|
1183
|
+
title,
|
|
1184
|
+
body,
|
|
1185
|
+
appointmentId: appointment.id
|
|
1186
|
+
};
|
|
1187
|
+
try {
|
|
1188
|
+
const notificationId = await this.createAndSendNotificationImmediately(
|
|
1189
|
+
notificationData
|
|
1190
|
+
);
|
|
1191
|
+
console.log(
|
|
1192
|
+
`[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_REMINDER notification ${notificationId} for patient ${patientUserId}. Reminder count: ${reminderCount != null ? reminderCount : 1}.`
|
|
1193
|
+
);
|
|
1194
|
+
return notificationId;
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
console.error(
|
|
1197
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_REMINDER notification for patient ${patientUserId}:`,
|
|
1198
|
+
error
|
|
1199
|
+
);
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1127
1203
|
};
|
|
1128
1204
|
|
|
1129
1205
|
// src/admin/requirements/patient-requirements.admin.service.ts
|
|
@@ -2287,6 +2363,305 @@ var clinicAppointmentRequestedTemplate = `
|
|
|
2287
2363
|
</body>
|
|
2288
2364
|
</html>
|
|
2289
2365
|
`;
|
|
2366
|
+
var appointmentCancelledTemplate = `
|
|
2367
|
+
<!DOCTYPE html>
|
|
2368
|
+
<html lang="en">
|
|
2369
|
+
<head>
|
|
2370
|
+
<meta charset="UTF-8">
|
|
2371
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2372
|
+
<title>Appointment Cancelled</title>
|
|
2373
|
+
<style>
|
|
2374
|
+
body {
|
|
2375
|
+
margin: 0;
|
|
2376
|
+
padding: 0;
|
|
2377
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
2378
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2379
|
+
min-height: 100vh;
|
|
2380
|
+
}
|
|
2381
|
+
.email-container {
|
|
2382
|
+
max-width: 600px;
|
|
2383
|
+
margin: 0 auto;
|
|
2384
|
+
background: #ffffff;
|
|
2385
|
+
border-radius: 20px;
|
|
2386
|
+
overflow: hidden;
|
|
2387
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
2388
|
+
margin-top: 40px;
|
|
2389
|
+
margin-bottom: 40px;
|
|
2390
|
+
}
|
|
2391
|
+
.header {
|
|
2392
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2393
|
+
padding: 40px 30px;
|
|
2394
|
+
text-align: center;
|
|
2395
|
+
color: white;
|
|
2396
|
+
}
|
|
2397
|
+
.header h1 {
|
|
2398
|
+
margin: 0;
|
|
2399
|
+
font-size: 28px;
|
|
2400
|
+
font-weight: 300;
|
|
2401
|
+
letter-spacing: 1px;
|
|
2402
|
+
}
|
|
2403
|
+
.header .subtitle {
|
|
2404
|
+
margin: 10px 0 0 0;
|
|
2405
|
+
font-size: 16px;
|
|
2406
|
+
opacity: 0.9;
|
|
2407
|
+
font-weight: 300;
|
|
2408
|
+
}
|
|
2409
|
+
.content {
|
|
2410
|
+
padding: 40px 30px;
|
|
2411
|
+
}
|
|
2412
|
+
.greeting {
|
|
2413
|
+
font-size: 18px;
|
|
2414
|
+
color: #333;
|
|
2415
|
+
margin-bottom: 25px;
|
|
2416
|
+
font-weight: 400;
|
|
2417
|
+
}
|
|
2418
|
+
.cancellation-notice {
|
|
2419
|
+
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
|
2420
|
+
border-radius: 15px;
|
|
2421
|
+
padding: 25px;
|
|
2422
|
+
margin: 25px 0;
|
|
2423
|
+
border-left: 5px solid #d4736c;
|
|
2424
|
+
}
|
|
2425
|
+
.cancellation-notice p {
|
|
2426
|
+
margin: 0;
|
|
2427
|
+
color: #c62828;
|
|
2428
|
+
font-size: 15px;
|
|
2429
|
+
font-weight: 500;
|
|
2430
|
+
line-height: 1.6;
|
|
2431
|
+
}
|
|
2432
|
+
.cancelled-by-info {
|
|
2433
|
+
background: #fafafa;
|
|
2434
|
+
border-radius: 10px;
|
|
2435
|
+
padding: 15px 20px;
|
|
2436
|
+
margin-top: 15px;
|
|
2437
|
+
}
|
|
2438
|
+
.cancelled-by-info .label {
|
|
2439
|
+
font-size: 12px;
|
|
2440
|
+
color: #757575;
|
|
2441
|
+
text-transform: uppercase;
|
|
2442
|
+
letter-spacing: 0.5px;
|
|
2443
|
+
margin-bottom: 5px;
|
|
2444
|
+
}
|
|
2445
|
+
.cancelled-by-info .value {
|
|
2446
|
+
font-size: 14px;
|
|
2447
|
+
color: #424242;
|
|
2448
|
+
font-weight: 500;
|
|
2449
|
+
}
|
|
2450
|
+
.reason-box {
|
|
2451
|
+
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
|
2452
|
+
border-radius: 15px;
|
|
2453
|
+
padding: 20px;
|
|
2454
|
+
margin: 20px 0;
|
|
2455
|
+
border-left: 5px solid #ffa000;
|
|
2456
|
+
}
|
|
2457
|
+
.reason-box .label {
|
|
2458
|
+
font-size: 14px;
|
|
2459
|
+
font-weight: 600;
|
|
2460
|
+
color: #e65100;
|
|
2461
|
+
margin-bottom: 8px;
|
|
2462
|
+
}
|
|
2463
|
+
.reason-box .reason-text {
|
|
2464
|
+
font-size: 15px;
|
|
2465
|
+
color: #424242;
|
|
2466
|
+
line-height: 1.6;
|
|
2467
|
+
font-style: italic;
|
|
2468
|
+
}
|
|
2469
|
+
.appointment-card {
|
|
2470
|
+
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
|
2471
|
+
border-radius: 15px;
|
|
2472
|
+
padding: 30px;
|
|
2473
|
+
margin: 25px 0;
|
|
2474
|
+
border-left: 5px solid #9e9e9e;
|
|
2475
|
+
opacity: 0.9;
|
|
2476
|
+
}
|
|
2477
|
+
.appointment-title {
|
|
2478
|
+
font-size: 20px;
|
|
2479
|
+
color: #757575;
|
|
2480
|
+
margin-bottom: 20px;
|
|
2481
|
+
font-weight: 600;
|
|
2482
|
+
}
|
|
2483
|
+
.appointment-details {
|
|
2484
|
+
display: grid;
|
|
2485
|
+
gap: 15px;
|
|
2486
|
+
}
|
|
2487
|
+
.detail-row {
|
|
2488
|
+
display: flex;
|
|
2489
|
+
align-items: center;
|
|
2490
|
+
padding: 8px 0;
|
|
2491
|
+
}
|
|
2492
|
+
.detail-label {
|
|
2493
|
+
font-weight: 600;
|
|
2494
|
+
color: #757575;
|
|
2495
|
+
min-width: 120px;
|
|
2496
|
+
font-size: 14px;
|
|
2497
|
+
}
|
|
2498
|
+
.detail-value {
|
|
2499
|
+
color: #616161;
|
|
2500
|
+
font-size: 16px;
|
|
2501
|
+
font-weight: 500;
|
|
2502
|
+
text-decoration: line-through;
|
|
2503
|
+
}
|
|
2504
|
+
.procedure-name {
|
|
2505
|
+
color: #757575;
|
|
2506
|
+
font-weight: 600;
|
|
2507
|
+
}
|
|
2508
|
+
.clinic-name {
|
|
2509
|
+
color: #9e9e9e;
|
|
2510
|
+
font-weight: 600;
|
|
2511
|
+
}
|
|
2512
|
+
.rebook-section {
|
|
2513
|
+
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
2514
|
+
border-radius: 15px;
|
|
2515
|
+
padding: 25px;
|
|
2516
|
+
margin: 30px 0;
|
|
2517
|
+
text-align: center;
|
|
2518
|
+
border-left: 5px solid #4caf50;
|
|
2519
|
+
}
|
|
2520
|
+
.rebook-section h3 {
|
|
2521
|
+
margin: 0 0 15px 0;
|
|
2522
|
+
color: #2e7d32;
|
|
2523
|
+
font-weight: 600;
|
|
2524
|
+
font-size: 18px;
|
|
2525
|
+
}
|
|
2526
|
+
.rebook-section p {
|
|
2527
|
+
margin: 0;
|
|
2528
|
+
color: #555;
|
|
2529
|
+
font-size: 15px;
|
|
2530
|
+
line-height: 1.6;
|
|
2531
|
+
}
|
|
2532
|
+
.support-section {
|
|
2533
|
+
background: #f8f9fa;
|
|
2534
|
+
border-radius: 15px;
|
|
2535
|
+
padding: 20px;
|
|
2536
|
+
margin: 25px 0;
|
|
2537
|
+
text-align: center;
|
|
2538
|
+
}
|
|
2539
|
+
.support-section h4 {
|
|
2540
|
+
margin: 0 0 10px 0;
|
|
2541
|
+
color: #555;
|
|
2542
|
+
font-weight: 600;
|
|
2543
|
+
font-size: 16px;
|
|
2544
|
+
}
|
|
2545
|
+
.support-section p {
|
|
2546
|
+
margin: 0;
|
|
2547
|
+
color: #757575;
|
|
2548
|
+
font-size: 14px;
|
|
2549
|
+
line-height: 1.6;
|
|
2550
|
+
}
|
|
2551
|
+
.footer {
|
|
2552
|
+
background: #f8f9fa;
|
|
2553
|
+
padding: 25px 30px;
|
|
2554
|
+
text-align: center;
|
|
2555
|
+
color: #666;
|
|
2556
|
+
font-size: 14px;
|
|
2557
|
+
border-top: 1px solid #eee;
|
|
2558
|
+
}
|
|
2559
|
+
.logo {
|
|
2560
|
+
font-size: 24px;
|
|
2561
|
+
font-weight: 700;
|
|
2562
|
+
color: white;
|
|
2563
|
+
margin-bottom: 5px;
|
|
2564
|
+
}
|
|
2565
|
+
.divider {
|
|
2566
|
+
height: 2px;
|
|
2567
|
+
background: linear-gradient(90deg, #d4736c, #b85450);
|
|
2568
|
+
margin: 25px 0;
|
|
2569
|
+
border-radius: 1px;
|
|
2570
|
+
}
|
|
2571
|
+
.icon {
|
|
2572
|
+
text-align: center;
|
|
2573
|
+
margin: 20px 0;
|
|
2574
|
+
font-size: 48px;
|
|
2575
|
+
}
|
|
2576
|
+
</style>
|
|
2577
|
+
</head>
|
|
2578
|
+
<body>
|
|
2579
|
+
<div class="email-container">
|
|
2580
|
+
<div class="header">
|
|
2581
|
+
<div class="logo">MetaEstetics</div>
|
|
2582
|
+
<h1>Appointment Cancelled</h1>
|
|
2583
|
+
<div class="subtitle">We're Sorry to See This Change</div>
|
|
2584
|
+
</div>
|
|
2585
|
+
|
|
2586
|
+
<div class="content">
|
|
2587
|
+
<div class="icon">❌</div>
|
|
2588
|
+
|
|
2589
|
+
<div class="greeting">
|
|
2590
|
+
Dear <strong>{{recipientName}}</strong>,
|
|
2591
|
+
</div>
|
|
2592
|
+
|
|
2593
|
+
<div class="cancellation-notice">
|
|
2594
|
+
<p><strong>Your appointment has been cancelled.</strong> We wanted to let you know that the following appointment is no longer scheduled.</p>
|
|
2595
|
+
<div class="cancelled-by-info">
|
|
2596
|
+
<div class="label">Cancelled By</div>
|
|
2597
|
+
<div class="value">{{cancelledByDisplay}}</div>
|
|
2598
|
+
</div>
|
|
2599
|
+
</div>
|
|
2600
|
+
|
|
2601
|
+
{{#if cancellationReason}}
|
|
2602
|
+
<div class="reason-box">
|
|
2603
|
+
<div class="label">Reason for Cancellation</div>
|
|
2604
|
+
<div class="reason-text">"{{cancellationReason}}"</div>
|
|
2605
|
+
</div>
|
|
2606
|
+
{{/if}}
|
|
2607
|
+
|
|
2608
|
+
<div class="appointment-card">
|
|
2609
|
+
<div class="appointment-title">Cancelled Appointment Details</div>
|
|
2610
|
+
<div class="appointment-details">
|
|
2611
|
+
<div class="detail-row">
|
|
2612
|
+
<div class="detail-label">Procedure:</div>
|
|
2613
|
+
<div class="detail-value procedure-name">{{procedureName}}</div>
|
|
2614
|
+
</div>
|
|
2615
|
+
<div class="detail-row">
|
|
2616
|
+
<div class="detail-label">Date:</div>
|
|
2617
|
+
<div class="detail-value">{{appointmentDate}}</div>
|
|
2618
|
+
</div>
|
|
2619
|
+
<div class="detail-row">
|
|
2620
|
+
<div class="detail-label">Time:</div>
|
|
2621
|
+
<div class="detail-value">{{appointmentTime}}</div>
|
|
2622
|
+
</div>
|
|
2623
|
+
<div class="detail-row">
|
|
2624
|
+
<div class="detail-label">Practitioner:</div>
|
|
2625
|
+
<div class="detail-value">{{practitionerName}}</div>
|
|
2626
|
+
</div>
|
|
2627
|
+
<div class="detail-row">
|
|
2628
|
+
<div class="detail-label">Location:</div>
|
|
2629
|
+
<div class="detail-value clinic-name">{{clinicName}}</div>
|
|
2630
|
+
</div>
|
|
2631
|
+
</div>
|
|
2632
|
+
</div>
|
|
2633
|
+
|
|
2634
|
+
<div class="divider"></div>
|
|
2635
|
+
|
|
2636
|
+
<div class="rebook-section">
|
|
2637
|
+
<h3>Would You Like to Reschedule?</h3>
|
|
2638
|
+
<p>
|
|
2639
|
+
We'd love to see you! If you'd like to book a new appointment,
|
|
2640
|
+
simply open the MetaEstetics app and browse available times that work for you.
|
|
2641
|
+
</p>
|
|
2642
|
+
</div>
|
|
2643
|
+
|
|
2644
|
+
<div class="support-section">
|
|
2645
|
+
<h4>Need Assistance?</h4>
|
|
2646
|
+
<p>
|
|
2647
|
+
If you have any questions about this cancellation or need help rebooking,
|
|
2648
|
+
please contact {{clinicName}} directly through the app or reach out to our support team.
|
|
2649
|
+
</p>
|
|
2650
|
+
</div>
|
|
2651
|
+
</div>
|
|
2652
|
+
|
|
2653
|
+
<div class="footer">
|
|
2654
|
+
<p style="margin: 0 0 10px 0;">
|
|
2655
|
+
<strong>MetaEstetics</strong> - Premium Aesthetic Services
|
|
2656
|
+
</p>
|
|
2657
|
+
<p style="margin: 0; font-size: 12px; color: #999;">
|
|
2658
|
+
This is an automated message. Please do not reply to this email.
|
|
2659
|
+
</p>
|
|
2660
|
+
</div>
|
|
2661
|
+
</div>
|
|
2662
|
+
</body>
|
|
2663
|
+
</html>
|
|
2664
|
+
`;
|
|
2290
2665
|
var appointmentRescheduledProposalTemplate = `
|
|
2291
2666
|
<!DOCTYPE html>
|
|
2292
2667
|
<html lang="en">
|
|
@@ -2447,6 +2822,42 @@ var appointmentRescheduledProposalTemplate = `
|
|
|
2447
2822
|
font-size: 15px;
|
|
2448
2823
|
line-height: 1.6;
|
|
2449
2824
|
}
|
|
2825
|
+
.action-required-box {
|
|
2826
|
+
background: linear-gradient(135deg, #fff3e0 0%, #ffecb3 100%);
|
|
2827
|
+
border: 2px solid #ff9800;
|
|
2828
|
+
border-radius: 15px;
|
|
2829
|
+
padding: 25px;
|
|
2830
|
+
margin: 25px 0;
|
|
2831
|
+
text-align: center;
|
|
2832
|
+
}
|
|
2833
|
+
.action-required-box h3 {
|
|
2834
|
+
margin: 0 0 15px 0;
|
|
2835
|
+
color: #e65100;
|
|
2836
|
+
font-weight: 700;
|
|
2837
|
+
font-size: 18px;
|
|
2838
|
+
}
|
|
2839
|
+
.action-required-box p {
|
|
2840
|
+
margin: 0 0 12px 0;
|
|
2841
|
+
color: #bf360c;
|
|
2842
|
+
font-size: 15px;
|
|
2843
|
+
line-height: 1.6;
|
|
2844
|
+
}
|
|
2845
|
+
.action-required-box p:last-child {
|
|
2846
|
+
margin-bottom: 0;
|
|
2847
|
+
}
|
|
2848
|
+
.pending-notice {
|
|
2849
|
+
background: #fff8e1;
|
|
2850
|
+
border-radius: 8px;
|
|
2851
|
+
padding: 12px 15px;
|
|
2852
|
+
margin-top: 15px;
|
|
2853
|
+
display: inline-block;
|
|
2854
|
+
}
|
|
2855
|
+
.pending-notice p {
|
|
2856
|
+
margin: 0;
|
|
2857
|
+
color: #f57c00;
|
|
2858
|
+
font-size: 14px;
|
|
2859
|
+
font-weight: 600;
|
|
2860
|
+
}
|
|
2450
2861
|
.footer {
|
|
2451
2862
|
background: #f8f9fa;
|
|
2452
2863
|
padding: 25px 30px;
|
|
@@ -2538,16 +2949,29 @@ var appointmentRescheduledProposalTemplate = `
|
|
|
2538
2949
|
</div>
|
|
2539
2950
|
|
|
2540
2951
|
<div class="divider"></div>
|
|
2541
|
-
|
|
2952
|
+
|
|
2953
|
+
<div class="action-required-box">
|
|
2954
|
+
<h3>Your Response is Required</h3>
|
|
2955
|
+
<p>
|
|
2956
|
+
<strong>Missed our notification?</strong> Please open the MetaEstetics app to confirm or reject this reschedule request.
|
|
2957
|
+
</p>
|
|
2958
|
+
<p>
|
|
2959
|
+
Please respond as soon as possible so we can finalize your appointment.
|
|
2960
|
+
</p>
|
|
2961
|
+
<div class="pending-notice">
|
|
2962
|
+
<p>Your appointment will remain pending until you respond.</p>
|
|
2963
|
+
</div>
|
|
2964
|
+
</div>
|
|
2965
|
+
|
|
2542
2966
|
<div class="action-section">
|
|
2543
|
-
<h3>
|
|
2967
|
+
<h3>How to Respond</h3>
|
|
2544
2968
|
<p>
|
|
2545
|
-
|
|
2546
|
-
If the new time works for you, simply tap "Accept Reschedule".
|
|
2969
|
+
Open the MetaEstetics app and navigate to your appointments.
|
|
2970
|
+
If the new time works for you, simply tap "Accept Reschedule".
|
|
2547
2971
|
If not, you can reject it and we'll work with you to find an alternative time.
|
|
2548
2972
|
</p>
|
|
2549
2973
|
</div>
|
|
2550
|
-
|
|
2974
|
+
|
|
2551
2975
|
<p style="color: #555; font-size: 14px; line-height: 1.6; margin-top: 25px;">
|
|
2552
2976
|
<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}}.
|
|
2553
2977
|
</p>
|
|
@@ -2763,11 +3187,121 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2763
3187
|
throw error;
|
|
2764
3188
|
}
|
|
2765
3189
|
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Gets a user-friendly display text for who cancelled the appointment
|
|
3192
|
+
* @param cancelledBy - The entity that cancelled the appointment
|
|
3193
|
+
* @param clinicName - The clinic name for context
|
|
3194
|
+
* @returns User-friendly cancellation source text
|
|
3195
|
+
*/
|
|
3196
|
+
getCancelledByDisplayText(cancelledBy, clinicName) {
|
|
3197
|
+
switch (cancelledBy) {
|
|
3198
|
+
case "patient":
|
|
3199
|
+
return "Patient Request";
|
|
3200
|
+
case "clinic":
|
|
3201
|
+
return `${clinicName} (Clinic)`;
|
|
3202
|
+
case "practitioner":
|
|
3203
|
+
return "Your Practitioner";
|
|
3204
|
+
case "system":
|
|
3205
|
+
return "System (Automatic)";
|
|
3206
|
+
default:
|
|
3207
|
+
return "Unknown";
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Sends an appointment cancellation email to the recipient
|
|
3212
|
+
* @param data - Appointment cancellation email data
|
|
3213
|
+
* @returns Promise with the sending result
|
|
3214
|
+
*/
|
|
2766
3215
|
async sendAppointmentCancelledEmail(data) {
|
|
3216
|
+
var _a, _b, _c, _d;
|
|
2767
3217
|
Logger.info(
|
|
2768
|
-
`[AppointmentMailingService]
|
|
3218
|
+
`[AppointmentMailingService] Preparing to send appointment cancellation email to ${data.recipientRole}: ${data.recipientProfile.id}`
|
|
2769
3219
|
);
|
|
2770
|
-
|
|
3220
|
+
const recipientEmail = data.recipientProfile.email;
|
|
3221
|
+
if (!recipientEmail) {
|
|
3222
|
+
Logger.error("[AppointmentMailingService] Recipient email not found for cancellation.", {
|
|
3223
|
+
recipientId: data.recipientProfile.id,
|
|
3224
|
+
role: data.recipientRole
|
|
3225
|
+
});
|
|
3226
|
+
throw new Error("Recipient email address is missing.");
|
|
3227
|
+
}
|
|
3228
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3229
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for cancellation", {
|
|
3230
|
+
clinicTimezone,
|
|
3231
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3232
|
+
});
|
|
3233
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
3234
|
+
data.appointment.appointmentStartTime,
|
|
3235
|
+
clinicTimezone,
|
|
3236
|
+
"time"
|
|
3237
|
+
);
|
|
3238
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3239
|
+
const cancelledBy = data.appointment.canceledBy || "system";
|
|
3240
|
+
const cancelledByDisplay = this.getCancelledByDisplayText(
|
|
3241
|
+
cancelledBy,
|
|
3242
|
+
data.appointment.clinicInfo.name
|
|
3243
|
+
);
|
|
3244
|
+
const recipientName = data.recipientRole === "patient" ? data.appointment.patientInfo.fullName : data.appointment.practitionerInfo.name;
|
|
3245
|
+
const templateVariables = {
|
|
3246
|
+
recipientName,
|
|
3247
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3248
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
3249
|
+
data.appointment.appointmentStartTime,
|
|
3250
|
+
clinicTimezone,
|
|
3251
|
+
"date"
|
|
3252
|
+
),
|
|
3253
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
3254
|
+
practitionerName: data.appointment.practitionerInfo.name,
|
|
3255
|
+
clinicName: data.appointment.clinicInfo.name,
|
|
3256
|
+
cancelledByDisplay
|
|
3257
|
+
};
|
|
3258
|
+
const cancellationReason = data.cancellationReason || data.appointment.cancellationReason;
|
|
3259
|
+
let html = appointmentCancelledTemplate;
|
|
3260
|
+
if (cancellationReason) {
|
|
3261
|
+
templateVariables.cancellationReason = cancellationReason;
|
|
3262
|
+
html = html.replace(
|
|
3263
|
+
/\{\{#if cancellationReason\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
3264
|
+
"$1"
|
|
3265
|
+
);
|
|
3266
|
+
} else {
|
|
3267
|
+
html = html.replace(/\{\{#if cancellationReason\}\}[\s\S]*?\{\{\/if\}\}/g, "");
|
|
3268
|
+
}
|
|
3269
|
+
html = this.renderTemplate(html, templateVariables);
|
|
3270
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Appointment Cancelled: ${data.appointment.procedureInfo.name}`;
|
|
3271
|
+
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}>`;
|
|
3272
|
+
const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3273
|
+
const mailgunSendData = {
|
|
3274
|
+
to: recipientEmail,
|
|
3275
|
+
from: fromAddress,
|
|
3276
|
+
subject,
|
|
3277
|
+
html
|
|
3278
|
+
};
|
|
3279
|
+
try {
|
|
3280
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3281
|
+
await this.logEmailAttempt(
|
|
3282
|
+
{ to: recipientEmail, subject, templateName: "appointment_cancelled" },
|
|
3283
|
+
true
|
|
3284
|
+
);
|
|
3285
|
+
Logger.info(
|
|
3286
|
+
`[AppointmentMailingService] Successfully sent cancellation email to ${recipientEmail}`
|
|
3287
|
+
);
|
|
3288
|
+
return result;
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
await this.logEmailAttempt(
|
|
3291
|
+
{
|
|
3292
|
+
to: recipientEmail,
|
|
3293
|
+
subject,
|
|
3294
|
+
templateName: "appointment_cancelled"
|
|
3295
|
+
},
|
|
3296
|
+
false,
|
|
3297
|
+
error
|
|
3298
|
+
);
|
|
3299
|
+
Logger.error(
|
|
3300
|
+
`[AppointmentMailingService] Error sending cancellation email to ${recipientEmail}:`,
|
|
3301
|
+
error
|
|
3302
|
+
);
|
|
3303
|
+
throw error;
|
|
3304
|
+
}
|
|
2771
3305
|
}
|
|
2772
3306
|
/**
|
|
2773
3307
|
* Sends a reschedule proposal email to the patient
|
|
@@ -3156,12 +3690,11 @@ var AppointmentAggregationService = class {
|
|
|
3156
3690
|
const patientCancellationData = {
|
|
3157
3691
|
appointment: after,
|
|
3158
3692
|
recipientProfile: after.patientInfo,
|
|
3159
|
-
recipientRole: "patient"
|
|
3160
|
-
|
|
3693
|
+
recipientRole: "patient",
|
|
3694
|
+
cancellationReason: after.cancellationReason
|
|
3161
3695
|
};
|
|
3162
3696
|
await this.appointmentMailingService.sendAppointmentCancelledEmail(
|
|
3163
3697
|
patientCancellationData
|
|
3164
|
-
// TODO: Properly import types
|
|
3165
3698
|
);
|
|
3166
3699
|
}
|
|
3167
3700
|
if ((_c = practitionerProfile == null ? void 0 : practitionerProfile.basicInfo) == null ? void 0 : _c.email) {
|
|
@@ -3171,12 +3704,22 @@ var AppointmentAggregationService = class {
|
|
|
3171
3704
|
const practitionerCancellationData = {
|
|
3172
3705
|
appointment: after,
|
|
3173
3706
|
recipientProfile: after.practitionerInfo,
|
|
3174
|
-
recipientRole: "practitioner"
|
|
3175
|
-
|
|
3707
|
+
recipientRole: "practitioner",
|
|
3708
|
+
cancellationReason: after.cancellationReason
|
|
3176
3709
|
};
|
|
3177
3710
|
await this.appointmentMailingService.sendAppointmentCancelledEmail(
|
|
3178
3711
|
practitionerCancellationData
|
|
3179
|
-
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3714
|
+
if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
|
|
3715
|
+
Logger.info(
|
|
3716
|
+
`[AggService] Sending cancellation push notification to patient ${after.patientId}`
|
|
3717
|
+
);
|
|
3718
|
+
await this.notificationsAdmin.sendAppointmentCancelledPush(
|
|
3719
|
+
after,
|
|
3720
|
+
after.patientId,
|
|
3721
|
+
patientProfile.expoTokens,
|
|
3722
|
+
"patient" /* PATIENT */
|
|
3180
3723
|
);
|
|
3181
3724
|
}
|
|
3182
3725
|
} else if (after.status === "completed" /* COMPLETED */) {
|
|
@@ -3232,6 +3775,16 @@ var AppointmentAggregationService = class {
|
|
|
3232
3775
|
// TODO: Properly import PatientProfileInfo and types
|
|
3233
3776
|
);
|
|
3234
3777
|
}
|
|
3778
|
+
if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
|
|
3779
|
+
Logger.info(
|
|
3780
|
+
`[AggService] Sending reschedule proposal push notification to patient ${after.patientId}`
|
|
3781
|
+
);
|
|
3782
|
+
await this.notificationsAdmin.sendAppointmentRescheduledProposalPush(
|
|
3783
|
+
after,
|
|
3784
|
+
after.patientId,
|
|
3785
|
+
patientProfile.expoTokens
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3235
3788
|
Logger.info(
|
|
3236
3789
|
`[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`
|
|
3237
3790
|
);
|