@blackcode_sa/metaestetics-api 1.14.57 → 1.14.59
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 +41 -1
- package/dist/admin/index.d.ts +41 -1
- package/dist/admin/index.js +1104 -227
- package/dist/admin/index.mjs +1104 -227
- package/dist/index.d.mts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +95 -27
- package/dist/index.mjs +96 -27
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +12 -0
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +876 -4
- package/src/admin/notifications/notifications.admin.ts +57 -0
- package/src/services/procedure/procedure.service.ts +137 -40
- 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";
|
|
@@ -1200,6 +1201,50 @@ var NotificationsAdmin = class {
|
|
|
1200
1201
|
return null;
|
|
1201
1202
|
}
|
|
1202
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Sends a reminder push notification for a pending reschedule request.
|
|
1206
|
+
* Used when a clinic has proposed a reschedule and the patient hasn't responded.
|
|
1207
|
+
* @param appointment The appointment with pending reschedule.
|
|
1208
|
+
* @param patientUserId The ID of the patient.
|
|
1209
|
+
* @param patientExpoTokens Array of Expo push tokens for the patient.
|
|
1210
|
+
* @param reminderCount Optional count of reminders already sent (for tracking).
|
|
1211
|
+
*/
|
|
1212
|
+
async sendRescheduleReminderPush(appointment, patientUserId, patientExpoTokens, reminderCount) {
|
|
1213
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
1214
|
+
console.log(
|
|
1215
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule reminder. Skipping push.`
|
|
1216
|
+
);
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const title = "Reminder: Reschedule Request Pending";
|
|
1220
|
+
const body = `You have a pending reschedule request for your ${appointment.procedureInfo.name} appointment. Please respond in the app.`;
|
|
1221
|
+
const notificationTimestampForDb = admin2.firestore.Timestamp.now();
|
|
1222
|
+
const notificationData = {
|
|
1223
|
+
userId: patientUserId,
|
|
1224
|
+
userRole: "patient" /* PATIENT */,
|
|
1225
|
+
notificationType: "appointmentRescheduledReminder" /* APPOINTMENT_RESCHEDULED_REMINDER */,
|
|
1226
|
+
notificationTime: notificationTimestampForDb,
|
|
1227
|
+
notificationTokens: patientExpoTokens,
|
|
1228
|
+
title,
|
|
1229
|
+
body,
|
|
1230
|
+
appointmentId: appointment.id
|
|
1231
|
+
};
|
|
1232
|
+
try {
|
|
1233
|
+
const notificationId = await this.createNotification(
|
|
1234
|
+
notificationData
|
|
1235
|
+
);
|
|
1236
|
+
console.log(
|
|
1237
|
+
`[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_REMINDER notification ${notificationId} for patient ${patientUserId}. Reminder count: ${reminderCount != null ? reminderCount : 1}.`
|
|
1238
|
+
);
|
|
1239
|
+
return notificationId;
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
console.error(
|
|
1242
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_REMINDER notification for patient ${patientUserId}:`,
|
|
1243
|
+
error
|
|
1244
|
+
);
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1203
1248
|
};
|
|
1204
1249
|
|
|
1205
1250
|
// src/admin/requirements/patient-requirements.admin.service.ts
|
|
@@ -2363,233 +2408,1055 @@ var clinicAppointmentRequestedTemplate = `
|
|
|
2363
2408
|
</body>
|
|
2364
2409
|
</html>
|
|
2365
2410
|
`;
|
|
2366
|
-
var
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
{
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
{
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2411
|
+
var appointmentCancelledTemplate = `
|
|
2412
|
+
<!DOCTYPE html>
|
|
2413
|
+
<html lang="en">
|
|
2414
|
+
<head>
|
|
2415
|
+
<meta charset="UTF-8">
|
|
2416
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2417
|
+
<title>Appointment Cancelled</title>
|
|
2418
|
+
<style>
|
|
2419
|
+
body {
|
|
2420
|
+
margin: 0;
|
|
2421
|
+
padding: 0;
|
|
2422
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
2423
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2424
|
+
min-height: 100vh;
|
|
2425
|
+
}
|
|
2426
|
+
.email-container {
|
|
2427
|
+
max-width: 600px;
|
|
2428
|
+
margin: 0 auto;
|
|
2429
|
+
background: #ffffff;
|
|
2430
|
+
border-radius: 20px;
|
|
2431
|
+
overflow: hidden;
|
|
2432
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
2433
|
+
margin-top: 40px;
|
|
2434
|
+
margin-bottom: 40px;
|
|
2435
|
+
}
|
|
2436
|
+
.header {
|
|
2437
|
+
background: linear-gradient(135deg, #d4736c 0%, #b85450 100%);
|
|
2438
|
+
padding: 40px 30px;
|
|
2439
|
+
text-align: center;
|
|
2440
|
+
color: white;
|
|
2441
|
+
}
|
|
2442
|
+
.header h1 {
|
|
2443
|
+
margin: 0;
|
|
2444
|
+
font-size: 28px;
|
|
2445
|
+
font-weight: 300;
|
|
2446
|
+
letter-spacing: 1px;
|
|
2447
|
+
}
|
|
2448
|
+
.header .subtitle {
|
|
2449
|
+
margin: 10px 0 0 0;
|
|
2450
|
+
font-size: 16px;
|
|
2451
|
+
opacity: 0.9;
|
|
2452
|
+
font-weight: 300;
|
|
2453
|
+
}
|
|
2454
|
+
.content {
|
|
2455
|
+
padding: 40px 30px;
|
|
2456
|
+
}
|
|
2457
|
+
.greeting {
|
|
2458
|
+
font-size: 18px;
|
|
2459
|
+
color: #333;
|
|
2460
|
+
margin-bottom: 25px;
|
|
2461
|
+
font-weight: 400;
|
|
2462
|
+
}
|
|
2463
|
+
.cancellation-notice {
|
|
2464
|
+
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
|
2465
|
+
border-radius: 15px;
|
|
2466
|
+
padding: 25px;
|
|
2467
|
+
margin: 25px 0;
|
|
2468
|
+
border-left: 5px solid #d4736c;
|
|
2469
|
+
}
|
|
2470
|
+
.cancellation-notice p {
|
|
2471
|
+
margin: 0;
|
|
2472
|
+
color: #c62828;
|
|
2473
|
+
font-size: 15px;
|
|
2474
|
+
font-weight: 500;
|
|
2475
|
+
line-height: 1.6;
|
|
2476
|
+
}
|
|
2477
|
+
.cancelled-by-info {
|
|
2478
|
+
background: #fafafa;
|
|
2479
|
+
border-radius: 10px;
|
|
2480
|
+
padding: 15px 20px;
|
|
2481
|
+
margin-top: 15px;
|
|
2482
|
+
}
|
|
2483
|
+
.cancelled-by-info .label {
|
|
2484
|
+
font-size: 12px;
|
|
2485
|
+
color: #757575;
|
|
2486
|
+
text-transform: uppercase;
|
|
2487
|
+
letter-spacing: 0.5px;
|
|
2488
|
+
margin-bottom: 5px;
|
|
2489
|
+
}
|
|
2490
|
+
.cancelled-by-info .value {
|
|
2491
|
+
font-size: 14px;
|
|
2492
|
+
color: #424242;
|
|
2493
|
+
font-weight: 500;
|
|
2494
|
+
}
|
|
2495
|
+
.reason-box {
|
|
2496
|
+
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
|
2497
|
+
border-radius: 15px;
|
|
2498
|
+
padding: 20px;
|
|
2499
|
+
margin: 20px 0;
|
|
2500
|
+
border-left: 5px solid #ffa000;
|
|
2501
|
+
}
|
|
2502
|
+
.reason-box .label {
|
|
2503
|
+
font-size: 14px;
|
|
2504
|
+
font-weight: 600;
|
|
2505
|
+
color: #e65100;
|
|
2506
|
+
margin-bottom: 8px;
|
|
2507
|
+
}
|
|
2508
|
+
.reason-box .reason-text {
|
|
2509
|
+
font-size: 15px;
|
|
2510
|
+
color: #424242;
|
|
2511
|
+
line-height: 1.6;
|
|
2512
|
+
font-style: italic;
|
|
2513
|
+
}
|
|
2514
|
+
.appointment-card {
|
|
2515
|
+
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
|
2516
|
+
border-radius: 15px;
|
|
2517
|
+
padding: 30px;
|
|
2518
|
+
margin: 25px 0;
|
|
2519
|
+
border-left: 5px solid #9e9e9e;
|
|
2520
|
+
opacity: 0.9;
|
|
2521
|
+
}
|
|
2522
|
+
.appointment-title {
|
|
2523
|
+
font-size: 20px;
|
|
2524
|
+
color: #757575;
|
|
2525
|
+
margin-bottom: 20px;
|
|
2526
|
+
font-weight: 600;
|
|
2527
|
+
}
|
|
2528
|
+
.appointment-details {
|
|
2529
|
+
display: grid;
|
|
2530
|
+
gap: 15px;
|
|
2531
|
+
}
|
|
2532
|
+
.detail-row {
|
|
2533
|
+
display: flex;
|
|
2534
|
+
align-items: center;
|
|
2535
|
+
padding: 8px 0;
|
|
2536
|
+
}
|
|
2537
|
+
.detail-label {
|
|
2538
|
+
font-weight: 600;
|
|
2539
|
+
color: #757575;
|
|
2540
|
+
min-width: 120px;
|
|
2541
|
+
font-size: 14px;
|
|
2542
|
+
}
|
|
2543
|
+
.detail-value {
|
|
2544
|
+
color: #616161;
|
|
2545
|
+
font-size: 16px;
|
|
2546
|
+
font-weight: 500;
|
|
2547
|
+
text-decoration: line-through;
|
|
2548
|
+
}
|
|
2549
|
+
.procedure-name {
|
|
2550
|
+
color: #757575;
|
|
2551
|
+
font-weight: 600;
|
|
2552
|
+
}
|
|
2553
|
+
.clinic-name {
|
|
2554
|
+
color: #9e9e9e;
|
|
2555
|
+
font-weight: 600;
|
|
2556
|
+
}
|
|
2557
|
+
.rebook-section {
|
|
2558
|
+
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
2559
|
+
border-radius: 15px;
|
|
2560
|
+
padding: 25px;
|
|
2561
|
+
margin: 30px 0;
|
|
2562
|
+
text-align: center;
|
|
2563
|
+
border-left: 5px solid #4caf50;
|
|
2564
|
+
}
|
|
2565
|
+
.rebook-section h3 {
|
|
2566
|
+
margin: 0 0 15px 0;
|
|
2567
|
+
color: #2e7d32;
|
|
2568
|
+
font-weight: 600;
|
|
2569
|
+
font-size: 18px;
|
|
2570
|
+
}
|
|
2571
|
+
.rebook-section p {
|
|
2572
|
+
margin: 0;
|
|
2573
|
+
color: #555;
|
|
2574
|
+
font-size: 15px;
|
|
2575
|
+
line-height: 1.6;
|
|
2576
|
+
}
|
|
2577
|
+
.support-section {
|
|
2578
|
+
background: #f8f9fa;
|
|
2579
|
+
border-radius: 15px;
|
|
2580
|
+
padding: 20px;
|
|
2581
|
+
margin: 25px 0;
|
|
2582
|
+
text-align: center;
|
|
2583
|
+
}
|
|
2584
|
+
.support-section h4 {
|
|
2585
|
+
margin: 0 0 10px 0;
|
|
2586
|
+
color: #555;
|
|
2587
|
+
font-weight: 600;
|
|
2588
|
+
font-size: 16px;
|
|
2589
|
+
}
|
|
2590
|
+
.support-section p {
|
|
2591
|
+
margin: 0;
|
|
2592
|
+
color: #757575;
|
|
2593
|
+
font-size: 14px;
|
|
2594
|
+
line-height: 1.6;
|
|
2595
|
+
}
|
|
2596
|
+
.footer {
|
|
2597
|
+
background: #f8f9fa;
|
|
2598
|
+
padding: 25px 30px;
|
|
2599
|
+
text-align: center;
|
|
2600
|
+
color: #666;
|
|
2601
|
+
font-size: 14px;
|
|
2602
|
+
border-top: 1px solid #eee;
|
|
2603
|
+
}
|
|
2604
|
+
.logo {
|
|
2605
|
+
font-size: 24px;
|
|
2606
|
+
font-weight: 700;
|
|
2607
|
+
color: white;
|
|
2608
|
+
margin-bottom: 5px;
|
|
2609
|
+
}
|
|
2610
|
+
.divider {
|
|
2611
|
+
height: 2px;
|
|
2612
|
+
background: linear-gradient(90deg, #d4736c, #b85450);
|
|
2613
|
+
margin: 25px 0;
|
|
2614
|
+
border-radius: 1px;
|
|
2615
|
+
}
|
|
2616
|
+
.icon {
|
|
2617
|
+
text-align: center;
|
|
2618
|
+
margin: 20px 0;
|
|
2619
|
+
font-size: 48px;
|
|
2620
|
+
}
|
|
2621
|
+
</style>
|
|
2622
|
+
</head>
|
|
2623
|
+
<body>
|
|
2624
|
+
<div class="email-container">
|
|
2625
|
+
<div class="header">
|
|
2626
|
+
<div class="logo">MetaEstetics</div>
|
|
2627
|
+
<h1>Appointment Cancelled</h1>
|
|
2628
|
+
<div class="subtitle">We're Sorry to See This Change</div>
|
|
2629
|
+
</div>
|
|
2630
|
+
|
|
2631
|
+
<div class="content">
|
|
2632
|
+
<div class="icon">❌</div>
|
|
2633
|
+
|
|
2634
|
+
<div class="greeting">
|
|
2635
|
+
Dear <strong>{{recipientName}}</strong>,
|
|
2636
|
+
</div>
|
|
2637
|
+
|
|
2638
|
+
<div class="cancellation-notice">
|
|
2639
|
+
<p><strong>Your appointment has been cancelled.</strong> We wanted to let you know that the following appointment is no longer scheduled.</p>
|
|
2640
|
+
<div class="cancelled-by-info">
|
|
2641
|
+
<div class="label">Cancelled By</div>
|
|
2642
|
+
<div class="value">{{cancelledByDisplay}}</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
</div>
|
|
2645
|
+
|
|
2646
|
+
{{#if cancellationReason}}
|
|
2647
|
+
<div class="reason-box">
|
|
2648
|
+
<div class="label">Reason for Cancellation</div>
|
|
2649
|
+
<div class="reason-text">"{{cancellationReason}}"</div>
|
|
2650
|
+
</div>
|
|
2651
|
+
{{/if}}
|
|
2652
|
+
|
|
2653
|
+
<div class="appointment-card">
|
|
2654
|
+
<div class="appointment-title">Cancelled Appointment Details</div>
|
|
2655
|
+
<div class="appointment-details">
|
|
2656
|
+
<div class="detail-row">
|
|
2657
|
+
<div class="detail-label">Procedure:</div>
|
|
2658
|
+
<div class="detail-value procedure-name">{{procedureName}}</div>
|
|
2659
|
+
</div>
|
|
2660
|
+
<div class="detail-row">
|
|
2661
|
+
<div class="detail-label">Date:</div>
|
|
2662
|
+
<div class="detail-value">{{appointmentDate}}</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
<div class="detail-row">
|
|
2665
|
+
<div class="detail-label">Time:</div>
|
|
2666
|
+
<div class="detail-value">{{appointmentTime}}</div>
|
|
2667
|
+
</div>
|
|
2668
|
+
<div class="detail-row">
|
|
2669
|
+
<div class="detail-label">Practitioner:</div>
|
|
2670
|
+
<div class="detail-value">{{practitionerName}}</div>
|
|
2671
|
+
</div>
|
|
2672
|
+
<div class="detail-row">
|
|
2673
|
+
<div class="detail-label">Location:</div>
|
|
2674
|
+
<div class="detail-value clinic-name">{{clinicName}}</div>
|
|
2675
|
+
</div>
|
|
2676
|
+
</div>
|
|
2677
|
+
</div>
|
|
2678
|
+
|
|
2679
|
+
<div class="divider"></div>
|
|
2680
|
+
|
|
2681
|
+
<div class="rebook-section">
|
|
2682
|
+
<h3>Would You Like to Reschedule?</h3>
|
|
2683
|
+
<p>
|
|
2684
|
+
We'd love to see you! If you'd like to book a new appointment,
|
|
2685
|
+
simply open the MetaEstetics app and browse available times that work for you.
|
|
2686
|
+
</p>
|
|
2687
|
+
</div>
|
|
2688
|
+
|
|
2689
|
+
<div class="support-section">
|
|
2690
|
+
<h4>Need Assistance?</h4>
|
|
2691
|
+
<p>
|
|
2692
|
+
If you have any questions about this cancellation or need help rebooking,
|
|
2693
|
+
please contact {{clinicName}} directly through the app or reach out to our support team.
|
|
2694
|
+
</p>
|
|
2695
|
+
</div>
|
|
2696
|
+
</div>
|
|
2697
|
+
|
|
2698
|
+
<div class="footer">
|
|
2699
|
+
<p style="margin: 0 0 10px 0;">
|
|
2700
|
+
<strong>MetaEstetics</strong> - Premium Aesthetic Services
|
|
2701
|
+
</p>
|
|
2702
|
+
<p style="margin: 0; font-size: 12px; color: #999;">
|
|
2703
|
+
This is an automated message. Please do not reply to this email.
|
|
2704
|
+
</p>
|
|
2705
|
+
</div>
|
|
2706
|
+
</div>
|
|
2707
|
+
</body>
|
|
2708
|
+
</html>
|
|
2709
|
+
`;
|
|
2710
|
+
var appointmentRescheduledProposalTemplate = `
|
|
2711
|
+
<!DOCTYPE html>
|
|
2712
|
+
<html lang="en">
|
|
2713
|
+
<head>
|
|
2714
|
+
<meta charset="UTF-8">
|
|
2715
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2716
|
+
<title>Appointment Reschedule Proposal</title>
|
|
2717
|
+
<style>
|
|
2718
|
+
body {
|
|
2719
|
+
margin: 0;
|
|
2720
|
+
padding: 0;
|
|
2721
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
2722
|
+
background: linear-gradient(135deg, #a48a76 0%, #67574A 100%);
|
|
2723
|
+
min-height: 100vh;
|
|
2724
|
+
}
|
|
2725
|
+
.email-container {
|
|
2726
|
+
max-width: 600px;
|
|
2727
|
+
margin: 0 auto;
|
|
2728
|
+
background: #ffffff;
|
|
2729
|
+
border-radius: 20px;
|
|
2730
|
+
overflow: hidden;
|
|
2731
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
2732
|
+
margin-top: 40px;
|
|
2733
|
+
margin-bottom: 40px;
|
|
2734
|
+
}
|
|
2735
|
+
.header {
|
|
2736
|
+
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
|
2737
|
+
padding: 40px 30px;
|
|
2738
|
+
text-align: center;
|
|
2739
|
+
color: white;
|
|
2740
|
+
}
|
|
2741
|
+
.header h1 {
|
|
2742
|
+
margin: 0;
|
|
2743
|
+
font-size: 28px;
|
|
2744
|
+
font-weight: 300;
|
|
2745
|
+
letter-spacing: 1px;
|
|
2746
|
+
}
|
|
2747
|
+
.header .subtitle {
|
|
2748
|
+
margin: 10px 0 0 0;
|
|
2749
|
+
font-size: 16px;
|
|
2750
|
+
opacity: 0.9;
|
|
2751
|
+
font-weight: 300;
|
|
2752
|
+
}
|
|
2753
|
+
.content {
|
|
2754
|
+
padding: 40px 30px;
|
|
2755
|
+
}
|
|
2756
|
+
.greeting {
|
|
2757
|
+
font-size: 18px;
|
|
2758
|
+
color: #333;
|
|
2759
|
+
margin-bottom: 25px;
|
|
2760
|
+
font-weight: 400;
|
|
2761
|
+
}
|
|
2762
|
+
.info-box {
|
|
2763
|
+
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
|
2764
|
+
border-radius: 15px;
|
|
2765
|
+
padding: 25px;
|
|
2766
|
+
margin: 25px 0;
|
|
2767
|
+
border-left: 5px solid #ff9800;
|
|
2768
|
+
}
|
|
2769
|
+
.info-box p {
|
|
2770
|
+
margin: 0;
|
|
2771
|
+
color: #e65100;
|
|
2772
|
+
font-size: 15px;
|
|
2773
|
+
font-weight: 500;
|
|
2774
|
+
line-height: 1.6;
|
|
2775
|
+
}
|
|
2776
|
+
.time-comparison {
|
|
2777
|
+
display: grid;
|
|
2778
|
+
gap: 20px;
|
|
2779
|
+
margin: 25px 0;
|
|
2780
|
+
}
|
|
2781
|
+
.time-card {
|
|
2782
|
+
background: linear-gradient(135deg, #f8f6f5 0%, #f5f3f2 100%);
|
|
2783
|
+
border-radius: 15px;
|
|
2784
|
+
padding: 25px;
|
|
2785
|
+
border-left: 5px solid #a48a76;
|
|
2786
|
+
}
|
|
2787
|
+
.time-card.old-time {
|
|
2788
|
+
border-left-color: #9e9e9e;
|
|
2789
|
+
opacity: 0.8;
|
|
2790
|
+
}
|
|
2791
|
+
.time-card.new-time {
|
|
2792
|
+
border-left-color: #ff9800;
|
|
2793
|
+
background: linear-gradient(135deg, #fff8e1 0%, #ffe0b2 100%);
|
|
2794
|
+
}
|
|
2795
|
+
.time-label {
|
|
2796
|
+
font-size: 14px;
|
|
2797
|
+
font-weight: 600;
|
|
2798
|
+
color: #666;
|
|
2799
|
+
text-transform: uppercase;
|
|
2800
|
+
letter-spacing: 0.5px;
|
|
2801
|
+
margin-bottom: 10px;
|
|
2802
|
+
}
|
|
2803
|
+
.time-label.old {
|
|
2804
|
+
color: #757575;
|
|
2805
|
+
}
|
|
2806
|
+
.time-label.new {
|
|
2807
|
+
color: #f57c00;
|
|
2808
|
+
}
|
|
2809
|
+
.appointment-card {
|
|
2810
|
+
background: linear-gradient(135deg, #f8f6f5 0%, #f5f3f2 100%);
|
|
2811
|
+
border-radius: 15px;
|
|
2812
|
+
padding: 30px;
|
|
2813
|
+
margin: 25px 0;
|
|
2814
|
+
border-left: 5px solid #a48a76;
|
|
2815
|
+
}
|
|
2816
|
+
.appointment-title {
|
|
2817
|
+
font-size: 20px;
|
|
2818
|
+
color: #a48a76;
|
|
2819
|
+
margin-bottom: 20px;
|
|
2820
|
+
font-weight: 600;
|
|
2821
|
+
}
|
|
2822
|
+
.appointment-details {
|
|
2823
|
+
display: grid;
|
|
2824
|
+
gap: 15px;
|
|
2825
|
+
}
|
|
2826
|
+
.detail-row {
|
|
2827
|
+
display: flex;
|
|
2828
|
+
align-items: center;
|
|
2829
|
+
padding: 8px 0;
|
|
2830
|
+
}
|
|
2831
|
+
.detail-label {
|
|
2832
|
+
font-weight: 600;
|
|
2833
|
+
color: #555;
|
|
2834
|
+
min-width: 120px;
|
|
2835
|
+
font-size: 14px;
|
|
2836
|
+
}
|
|
2837
|
+
.detail-value {
|
|
2838
|
+
color: #333;
|
|
2839
|
+
font-size: 16px;
|
|
2840
|
+
font-weight: 500;
|
|
2841
|
+
}
|
|
2842
|
+
.procedure-name {
|
|
2843
|
+
color: #67574A;
|
|
2844
|
+
font-weight: 600;
|
|
2845
|
+
}
|
|
2846
|
+
.clinic-name {
|
|
2847
|
+
color: #a48a76;
|
|
2848
|
+
font-weight: 600;
|
|
2849
|
+
}
|
|
2850
|
+
.action-section {
|
|
2851
|
+
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
2852
|
+
border-radius: 15px;
|
|
2853
|
+
padding: 30px;
|
|
2854
|
+
margin: 30px 0;
|
|
2855
|
+
text-align: center;
|
|
2856
|
+
border-left: 5px solid #4caf50;
|
|
2857
|
+
}
|
|
2858
|
+
.action-section h3 {
|
|
2859
|
+
margin: 0 0 15px 0;
|
|
2860
|
+
color: #2e7d32;
|
|
2861
|
+
font-weight: 600;
|
|
2862
|
+
font-size: 18px;
|
|
2863
|
+
}
|
|
2864
|
+
.action-section p {
|
|
2865
|
+
margin: 0 0 20px 0;
|
|
2866
|
+
color: #555;
|
|
2867
|
+
font-size: 15px;
|
|
2868
|
+
line-height: 1.6;
|
|
2869
|
+
}
|
|
2870
|
+
.action-required-box {
|
|
2871
|
+
background: linear-gradient(135deg, #fff3e0 0%, #ffecb3 100%);
|
|
2872
|
+
border: 2px solid #ff9800;
|
|
2873
|
+
border-radius: 15px;
|
|
2874
|
+
padding: 25px;
|
|
2875
|
+
margin: 25px 0;
|
|
2876
|
+
text-align: center;
|
|
2877
|
+
}
|
|
2878
|
+
.action-required-box h3 {
|
|
2879
|
+
margin: 0 0 15px 0;
|
|
2880
|
+
color: #e65100;
|
|
2881
|
+
font-weight: 700;
|
|
2882
|
+
font-size: 18px;
|
|
2883
|
+
}
|
|
2884
|
+
.action-required-box p {
|
|
2885
|
+
margin: 0 0 12px 0;
|
|
2886
|
+
color: #bf360c;
|
|
2887
|
+
font-size: 15px;
|
|
2888
|
+
line-height: 1.6;
|
|
2889
|
+
}
|
|
2890
|
+
.action-required-box p:last-child {
|
|
2891
|
+
margin-bottom: 0;
|
|
2892
|
+
}
|
|
2893
|
+
.pending-notice {
|
|
2894
|
+
background: #fff8e1;
|
|
2895
|
+
border-radius: 8px;
|
|
2896
|
+
padding: 12px 15px;
|
|
2897
|
+
margin-top: 15px;
|
|
2898
|
+
display: inline-block;
|
|
2899
|
+
}
|
|
2900
|
+
.pending-notice p {
|
|
2901
|
+
margin: 0;
|
|
2902
|
+
color: #f57c00;
|
|
2903
|
+
font-size: 14px;
|
|
2904
|
+
font-weight: 600;
|
|
2905
|
+
}
|
|
2906
|
+
.footer {
|
|
2907
|
+
background: #f8f9fa;
|
|
2908
|
+
padding: 25px 30px;
|
|
2909
|
+
text-align: center;
|
|
2910
|
+
color: #666;
|
|
2911
|
+
font-size: 14px;
|
|
2912
|
+
border-top: 1px solid #eee;
|
|
2913
|
+
}
|
|
2914
|
+
.logo {
|
|
2915
|
+
font-size: 24px;
|
|
2916
|
+
font-weight: 700;
|
|
2917
|
+
color: white;
|
|
2918
|
+
margin-bottom: 5px;
|
|
2919
|
+
}
|
|
2920
|
+
.divider {
|
|
2921
|
+
height: 2px;
|
|
2922
|
+
background: linear-gradient(90deg, #a48a76, #67574A);
|
|
2923
|
+
margin: 25px 0;
|
|
2924
|
+
border-radius: 1px;
|
|
2925
|
+
}
|
|
2926
|
+
.icon {
|
|
2927
|
+
text-align: center;
|
|
2928
|
+
margin: 20px 0;
|
|
2929
|
+
font-size: 48px;
|
|
2930
|
+
}
|
|
2931
|
+
.arrow {
|
|
2932
|
+
text-align: center;
|
|
2933
|
+
font-size: 32px;
|
|
2934
|
+
color: #ff9800;
|
|
2935
|
+
margin: 10px 0;
|
|
2936
|
+
}
|
|
2937
|
+
</style>
|
|
2938
|
+
</head>
|
|
2939
|
+
<body>
|
|
2940
|
+
<div class="email-container">
|
|
2941
|
+
<div class="header">
|
|
2942
|
+
<div class="logo">MetaEstetics</div>
|
|
2943
|
+
<h1>Appointment Reschedule Proposal</h1>
|
|
2944
|
+
<div class="subtitle">Action Required</div>
|
|
2945
|
+
</div>
|
|
2946
|
+
|
|
2947
|
+
<div class="content">
|
|
2948
|
+
<div class="icon">\u{1F4C5}</div>
|
|
2949
|
+
|
|
2950
|
+
<div class="greeting">
|
|
2951
|
+
Dear <strong>{{patientName}}</strong>,
|
|
2952
|
+
</div>
|
|
2953
|
+
|
|
2954
|
+
<p style="color: #555; font-size: 16px; line-height: 1.6; margin-bottom: 25px;">
|
|
2955
|
+
We hope this message finds you well. We need to propose a new time for your upcoming appointment. Please review the details below and confirm if the new time works for you.
|
|
2956
|
+
</p>
|
|
2957
|
+
|
|
2958
|
+
<div class="info-box">
|
|
2959
|
+
<p><strong>\u26A0\uFE0F Important:</strong> Please respond to this reschedule proposal as soon as possible. Your appointment will remain pending until you confirm or reject the new time.</p>
|
|
2960
|
+
</div>
|
|
2961
|
+
|
|
2962
|
+
<div class="appointment-card">
|
|
2963
|
+
<div class="appointment-title">\u{1F4CB} Appointment Details</div>
|
|
2964
|
+
<div class="appointment-details">
|
|
2965
|
+
<div class="detail-row">
|
|
2966
|
+
<div class="detail-label">Procedure:</div>
|
|
2967
|
+
<div class="detail-value procedure-name">{{procedureName}}</div>
|
|
2968
|
+
</div>
|
|
2969
|
+
<div class="detail-row">
|
|
2970
|
+
<div class="detail-label">Practitioner:</div>
|
|
2971
|
+
<div class="detail-value">{{practitionerName}}</div>
|
|
2972
|
+
</div>
|
|
2973
|
+
<div class="detail-row">
|
|
2974
|
+
<div class="detail-label">Location:</div>
|
|
2975
|
+
<div class="detail-value clinic-name">{{clinicName}}</div>
|
|
2976
|
+
</div>
|
|
2977
|
+
</div>
|
|
2978
|
+
</div>
|
|
2979
|
+
|
|
2980
|
+
<div class="time-comparison">
|
|
2981
|
+
<div class="time-card old-time">
|
|
2982
|
+
<div class="time-label old">Previous Time</div>
|
|
2983
|
+
<div style="font-size: 18px; font-weight: 600; color: #424242; margin-bottom: 8px;">{{previousDate}}</div>
|
|
2984
|
+
<div style="font-size: 16px; color: #616161;">{{previousTime}}</div>
|
|
2985
|
+
</div>
|
|
2986
|
+
|
|
2987
|
+
<div class="arrow">\u2193</div>
|
|
2988
|
+
|
|
2989
|
+
<div class="time-card new-time">
|
|
2990
|
+
<div class="time-label new">Proposed New Time</div>
|
|
2991
|
+
<div style="font-size: 18px; font-weight: 600; color: #e65100; margin-bottom: 8px;">{{newDate}}</div>
|
|
2992
|
+
<div style="font-size: 16px; color: #f57c00; font-weight: 500;">{{newTime}}</div>
|
|
2993
|
+
</div>
|
|
2994
|
+
</div>
|
|
2995
|
+
|
|
2996
|
+
<div class="divider"></div>
|
|
2997
|
+
|
|
2998
|
+
<div class="action-required-box">
|
|
2999
|
+
<h3>Your Response is Required</h3>
|
|
3000
|
+
<p>
|
|
3001
|
+
<strong>Missed our notification?</strong> Please open the MetaEstetics app to confirm or reject this reschedule request.
|
|
3002
|
+
</p>
|
|
3003
|
+
<p>
|
|
3004
|
+
Please respond as soon as possible so we can finalize your appointment.
|
|
3005
|
+
</p>
|
|
3006
|
+
<div class="pending-notice">
|
|
3007
|
+
<p>Your appointment will remain pending until you respond.</p>
|
|
3008
|
+
</div>
|
|
3009
|
+
</div>
|
|
3010
|
+
|
|
3011
|
+
<div class="action-section">
|
|
3012
|
+
<h3>How to Respond</h3>
|
|
3013
|
+
<p>
|
|
3014
|
+
Open the MetaEstetics app and navigate to your appointments.
|
|
3015
|
+
If the new time works for you, simply tap "Accept Reschedule".
|
|
3016
|
+
If not, you can reject it and we'll work with you to find an alternative time.
|
|
3017
|
+
</p>
|
|
3018
|
+
</div>
|
|
3019
|
+
|
|
3020
|
+
<p style="color: #555; font-size: 14px; line-height: 1.6; margin-top: 25px;">
|
|
3021
|
+
<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}}.
|
|
3022
|
+
</p>
|
|
3023
|
+
</div>
|
|
3024
|
+
|
|
3025
|
+
<div class="footer">
|
|
3026
|
+
<p style="margin: 0 0 10px 0;">
|
|
3027
|
+
<strong>MetaEstetics</strong> - Premium Aesthetic Services
|
|
3028
|
+
</p>
|
|
3029
|
+
<p style="margin: 0; font-size: 12px; color: #999;">
|
|
3030
|
+
This is an automated message. Please do not reply to this email.
|
|
3031
|
+
</p>
|
|
3032
|
+
</div>
|
|
3033
|
+
</div>
|
|
3034
|
+
</body>
|
|
3035
|
+
</html>
|
|
3036
|
+
`;
|
|
3037
|
+
var AppointmentMailingService = class extends BaseMailingService {
|
|
3038
|
+
constructor(firestore19, mailgunClient) {
|
|
3039
|
+
super(firestore19, mailgunClient);
|
|
3040
|
+
this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
3041
|
+
Logger.info("[AppointmentMailingService] Initialized.");
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
3045
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
3046
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3047
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
3048
|
+
* @returns Formatted string in clinic's local timezone
|
|
3049
|
+
*/
|
|
3050
|
+
formatTimestampInClinicTimezone(timestamp, clinicTimezone, format = "datetime") {
|
|
3051
|
+
try {
|
|
3052
|
+
const dateTimeInClinicTz = import_luxon.DateTime.fromMillis(timestamp.toMillis(), {
|
|
3053
|
+
zone: clinicTimezone
|
|
3054
|
+
});
|
|
3055
|
+
switch (format) {
|
|
3056
|
+
case "date":
|
|
3057
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATE_FULL);
|
|
3058
|
+
case "time":
|
|
3059
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.TIME_SIMPLE);
|
|
3060
|
+
case "datetime":
|
|
3061
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
|
|
3062
|
+
default:
|
|
3063
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
|
|
3064
|
+
}
|
|
3065
|
+
} catch (error) {
|
|
3066
|
+
Logger.error("[AppointmentMailingService] Error formatting timestamp in clinic timezone:", {
|
|
3067
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3068
|
+
clinicTimezone
|
|
3069
|
+
});
|
|
3070
|
+
return timestamp.toDate().toLocaleString();
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Gets a user-friendly display name for a timezone
|
|
3075
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3076
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
3077
|
+
*/
|
|
3078
|
+
getTimezoneDisplayName(timezone) {
|
|
3079
|
+
try {
|
|
3080
|
+
const timezoneMap = {
|
|
3081
|
+
"Europe/Zurich": "Clinic Time - Central European Time",
|
|
3082
|
+
"Europe/London": "Clinic Time - GMT/BST",
|
|
3083
|
+
"America/New_York": "Clinic Time - Eastern Time",
|
|
3084
|
+
"America/Los_Angeles": "Clinic Time - Pacific Time",
|
|
3085
|
+
"Asia/Dubai": "Clinic Time - Gulf Standard Time",
|
|
3086
|
+
"Asia/Karachi": "Clinic Time - Pakistan Standard Time",
|
|
3087
|
+
"Asia/Kolkata": "Clinic Time - India Standard Time",
|
|
3088
|
+
"Australia/Sydney": "Clinic Time - Australian Eastern Time"
|
|
3089
|
+
};
|
|
3090
|
+
return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
Logger.error("[AppointmentMailingService] Error getting timezone display name:", {
|
|
3093
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3094
|
+
timezone
|
|
3095
|
+
});
|
|
3096
|
+
return `Clinic Time - ${timezone}`;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
async sendAppointmentConfirmedEmail(data) {
|
|
3100
|
+
var _a, _b, _c, _d, _e;
|
|
3101
|
+
Logger.info(
|
|
3102
|
+
`[AppointmentMailingService] Preparing to send appointment confirmation email to ${data.recipientRole}: ${data.recipientProfile.id}`
|
|
3103
|
+
);
|
|
3104
|
+
const recipientEmail = data.recipientProfile.email;
|
|
3105
|
+
if (!recipientEmail) {
|
|
3106
|
+
Logger.error("[AppointmentMailingService] Recipient email not found for confirmation.", {
|
|
3107
|
+
recipientId: data.recipientProfile.id,
|
|
3108
|
+
role: data.recipientRole
|
|
3109
|
+
});
|
|
3110
|
+
throw new Error("Recipient email address is missing.");
|
|
3111
|
+
}
|
|
3112
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3113
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time", {
|
|
3114
|
+
clinicTimezone,
|
|
3115
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3116
|
+
});
|
|
3117
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
3118
|
+
data.appointment.appointmentStartTime,
|
|
3119
|
+
clinicTimezone,
|
|
3120
|
+
"time"
|
|
3121
|
+
);
|
|
3122
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3123
|
+
const templateVariables = {
|
|
3124
|
+
patientName: data.appointment.patientInfo.fullName,
|
|
3125
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3126
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
3127
|
+
data.appointment.appointmentStartTime,
|
|
3128
|
+
clinicTimezone,
|
|
3129
|
+
"date"
|
|
3130
|
+
),
|
|
3131
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
3132
|
+
practitionerName: data.appointment.practitionerInfo.name,
|
|
3133
|
+
clinicName: data.appointment.clinicInfo.name
|
|
3134
|
+
};
|
|
3135
|
+
const html = this.renderTemplate(patientAppointmentConfirmedTemplate, templateVariables);
|
|
3136
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || "Your Appointment is Confirmed!";
|
|
3137
|
+
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}>`;
|
|
3138
|
+
const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3139
|
+
const mailgunSendData = {
|
|
3140
|
+
to: recipientEmail,
|
|
3141
|
+
from: fromAddress,
|
|
3142
|
+
subject,
|
|
3143
|
+
html
|
|
3144
|
+
};
|
|
3145
|
+
try {
|
|
3146
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3147
|
+
await this.logEmailAttempt(
|
|
3148
|
+
{ to: recipientEmail, subject, templateName: "appointment_confirmed" },
|
|
3149
|
+
true
|
|
3150
|
+
);
|
|
3151
|
+
return result;
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
await this.logEmailAttempt(
|
|
3154
|
+
{
|
|
3155
|
+
to: recipientEmail,
|
|
3156
|
+
subject: ((_e = data.options) == null ? void 0 : _e.customSubject) || "Your Appointment is Confirmed!",
|
|
3157
|
+
templateName: "appointment_confirmed"
|
|
3158
|
+
},
|
|
3159
|
+
false,
|
|
3160
|
+
error
|
|
3161
|
+
);
|
|
3162
|
+
throw error;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
async sendAppointmentRequestedEmailToClinic(data) {
|
|
3166
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3167
|
+
Logger.info(
|
|
3168
|
+
`[AppointmentMailingService] Preparing to send appointment requested email to clinic: ${data.clinicProfile.id}`
|
|
3169
|
+
);
|
|
3170
|
+
const clinicEmail = (_a = data.clinicProfile.contactInfo) == null ? void 0 : _a.email;
|
|
3171
|
+
if (!clinicEmail) {
|
|
3172
|
+
Logger.error(
|
|
3173
|
+
"[AppointmentMailingService] Clinic contact email not found for request notification.",
|
|
3174
|
+
{ clinicId: data.clinicProfile.id }
|
|
3175
|
+
);
|
|
3176
|
+
throw new Error("Clinic contact email address is missing.");
|
|
3177
|
+
}
|
|
3178
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3179
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for clinic", {
|
|
3180
|
+
clinicTimezone,
|
|
3181
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3182
|
+
});
|
|
3183
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
3184
|
+
data.appointment.appointmentStartTime,
|
|
3185
|
+
clinicTimezone,
|
|
3186
|
+
"time"
|
|
3187
|
+
);
|
|
3188
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3189
|
+
const templateVariables = {
|
|
3190
|
+
clinicName: data.clinicProfile.name,
|
|
3191
|
+
patientName: data.appointment.patientInfo.fullName,
|
|
3192
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3193
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
3194
|
+
data.appointment.appointmentStartTime,
|
|
3195
|
+
clinicTimezone,
|
|
3196
|
+
"date"
|
|
3197
|
+
),
|
|
3198
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
3199
|
+
practitionerName: data.appointment.practitionerInfo.name
|
|
3200
|
+
};
|
|
3201
|
+
const html = this.renderTemplate(clinicAppointmentRequestedTemplate, templateVariables);
|
|
3202
|
+
const subject = ((_b = data.options) == null ? void 0 : _b.customSubject) || "New Appointment Request Received";
|
|
3203
|
+
const fromAddress = ((_c = data.options) == null ? void 0 : _c.fromAddress) || `MetaEstetics <no-reply@${((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN}>`;
|
|
3204
|
+
const domainToSendFrom = ((_e = data.options) == null ? void 0 : _e.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3205
|
+
const mailgunSendData = {
|
|
3206
|
+
to: clinicEmail,
|
|
3207
|
+
from: fromAddress,
|
|
3208
|
+
subject,
|
|
3209
|
+
html
|
|
3210
|
+
};
|
|
3211
|
+
try {
|
|
3212
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3213
|
+
await this.logEmailAttempt(
|
|
3214
|
+
{
|
|
3215
|
+
to: clinicEmail,
|
|
3216
|
+
subject,
|
|
3217
|
+
templateName: "appointment_requested_clinic"
|
|
3218
|
+
},
|
|
3219
|
+
true
|
|
3220
|
+
);
|
|
3221
|
+
return result;
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
await this.logEmailAttempt(
|
|
3224
|
+
{
|
|
3225
|
+
to: clinicEmail,
|
|
3226
|
+
subject: ((_f = data.options) == null ? void 0 : _f.customSubject) || "New Appointment Request Received",
|
|
3227
|
+
templateName: "appointment_requested_clinic"
|
|
3228
|
+
},
|
|
3229
|
+
false,
|
|
3230
|
+
error
|
|
3231
|
+
);
|
|
3232
|
+
throw error;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Gets a user-friendly display text for who cancelled the appointment
|
|
3237
|
+
* @param cancelledBy - The entity that cancelled the appointment
|
|
3238
|
+
* @param clinicName - The clinic name for context
|
|
3239
|
+
* @returns User-friendly cancellation source text
|
|
3240
|
+
*/
|
|
3241
|
+
getCancelledByDisplayText(cancelledBy, clinicName) {
|
|
3242
|
+
switch (cancelledBy) {
|
|
3243
|
+
case "patient":
|
|
3244
|
+
return "Patient Request";
|
|
3245
|
+
case "clinic":
|
|
3246
|
+
return `${clinicName} (Clinic)`;
|
|
3247
|
+
case "practitioner":
|
|
3248
|
+
return "Your Practitioner";
|
|
3249
|
+
case "system":
|
|
3250
|
+
return "System (Automatic)";
|
|
3251
|
+
default:
|
|
3252
|
+
return "Unknown";
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Sends an appointment cancellation email to the recipient
|
|
3257
|
+
* @param data - Appointment cancellation email data
|
|
3258
|
+
* @returns Promise with the sending result
|
|
3259
|
+
*/
|
|
3260
|
+
async sendAppointmentCancelledEmail(data) {
|
|
3261
|
+
var _a, _b, _c, _d;
|
|
3262
|
+
Logger.info(
|
|
3263
|
+
`[AppointmentMailingService] Preparing to send appointment cancellation email to ${data.recipientRole}: ${data.recipientProfile.id}`
|
|
3264
|
+
);
|
|
3265
|
+
const recipientEmail = data.recipientProfile.email;
|
|
3266
|
+
if (!recipientEmail) {
|
|
3267
|
+
Logger.error("[AppointmentMailingService] Recipient email not found for cancellation.", {
|
|
3268
|
+
recipientId: data.recipientProfile.id,
|
|
3269
|
+
role: data.recipientRole
|
|
3270
|
+
});
|
|
3271
|
+
throw new Error("Recipient email address is missing.");
|
|
3272
|
+
}
|
|
3273
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3274
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for cancellation", {
|
|
3275
|
+
clinicTimezone,
|
|
3276
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3277
|
+
});
|
|
3278
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
3279
|
+
data.appointment.appointmentStartTime,
|
|
3280
|
+
clinicTimezone,
|
|
3281
|
+
"time"
|
|
3282
|
+
);
|
|
3283
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3284
|
+
const cancelledBy = data.appointment.canceledBy || "system";
|
|
3285
|
+
const cancelledByDisplay = this.getCancelledByDisplayText(
|
|
3286
|
+
cancelledBy,
|
|
3287
|
+
data.appointment.clinicInfo.name
|
|
3288
|
+
);
|
|
3289
|
+
const recipientName = data.recipientRole === "patient" ? data.appointment.patientInfo.fullName : data.appointment.practitionerInfo.name;
|
|
3290
|
+
const templateVariables = {
|
|
3291
|
+
recipientName,
|
|
3292
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3293
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
3294
|
+
data.appointment.appointmentStartTime,
|
|
3295
|
+
clinicTimezone,
|
|
3296
|
+
"date"
|
|
3297
|
+
),
|
|
3298
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
3299
|
+
practitionerName: data.appointment.practitionerInfo.name,
|
|
3300
|
+
clinicName: data.appointment.clinicInfo.name,
|
|
3301
|
+
cancelledByDisplay
|
|
3302
|
+
};
|
|
3303
|
+
const cancellationReason = data.cancellationReason || data.appointment.cancellationReason;
|
|
3304
|
+
let html = appointmentCancelledTemplate;
|
|
3305
|
+
if (cancellationReason) {
|
|
3306
|
+
templateVariables.cancellationReason = cancellationReason;
|
|
3307
|
+
html = html.replace(
|
|
3308
|
+
/\{\{#if cancellationReason\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
3309
|
+
"$1"
|
|
3310
|
+
);
|
|
3311
|
+
} else {
|
|
3312
|
+
html = html.replace(/\{\{#if cancellationReason\}\}[\s\S]*?\{\{\/if\}\}/g, "");
|
|
3313
|
+
}
|
|
3314
|
+
html = this.renderTemplate(html, templateVariables);
|
|
3315
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Appointment Cancelled: ${data.appointment.procedureInfo.name}`;
|
|
3316
|
+
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}>`;
|
|
3317
|
+
const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3318
|
+
const mailgunSendData = {
|
|
3319
|
+
to: recipientEmail,
|
|
3320
|
+
from: fromAddress,
|
|
3321
|
+
subject,
|
|
3322
|
+
html
|
|
3323
|
+
};
|
|
3324
|
+
try {
|
|
3325
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3326
|
+
await this.logEmailAttempt(
|
|
3327
|
+
{ to: recipientEmail, subject, templateName: "appointment_cancelled" },
|
|
3328
|
+
true
|
|
3329
|
+
);
|
|
3330
|
+
Logger.info(
|
|
3331
|
+
`[AppointmentMailingService] Successfully sent cancellation email to ${recipientEmail}`
|
|
3332
|
+
);
|
|
3333
|
+
return result;
|
|
3334
|
+
} catch (error) {
|
|
3335
|
+
await this.logEmailAttempt(
|
|
3336
|
+
{
|
|
3337
|
+
to: recipientEmail,
|
|
3338
|
+
subject,
|
|
3339
|
+
templateName: "appointment_cancelled"
|
|
3340
|
+
},
|
|
3341
|
+
false,
|
|
3342
|
+
error
|
|
3343
|
+
);
|
|
3344
|
+
Logger.error(
|
|
3345
|
+
`[AppointmentMailingService] Error sending cancellation email to ${recipientEmail}:`,
|
|
3346
|
+
error
|
|
3347
|
+
);
|
|
3348
|
+
throw error;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Sends a reschedule proposal email to the patient
|
|
3353
|
+
* @param data - Appointment reschedule proposal email data
|
|
3354
|
+
* @returns Promise with the sending result
|
|
3355
|
+
*/
|
|
3356
|
+
async sendAppointmentRescheduledProposalEmail(data) {
|
|
3357
|
+
var _a, _b, _c, _d;
|
|
3358
|
+
Logger.info(
|
|
3359
|
+
`[AppointmentMailingService] Preparing to send reschedule proposal email to patient: ${data.patientProfile.id}`
|
|
3360
|
+
);
|
|
3361
|
+
const recipientEmail = data.patientProfile.email;
|
|
3362
|
+
if (!recipientEmail) {
|
|
3363
|
+
Logger.error("[AppointmentMailingService] Patient email not found for reschedule proposal.", {
|
|
3364
|
+
patientId: data.patientProfile.id
|
|
3365
|
+
});
|
|
3366
|
+
throw new Error("Patient email address is missing.");
|
|
3367
|
+
}
|
|
3368
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
3369
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment times for reschedule", {
|
|
3370
|
+
clinicTimezone,
|
|
3371
|
+
previousTime: data.previousStartTime.toDate().toISOString(),
|
|
3372
|
+
newTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
3373
|
+
});
|
|
3374
|
+
const previousFormattedTime = this.formatTimestampInClinicTimezone(
|
|
3375
|
+
data.previousStartTime,
|
|
3376
|
+
clinicTimezone,
|
|
3377
|
+
"time"
|
|
3378
|
+
);
|
|
3379
|
+
const previousFormattedDate = this.formatTimestampInClinicTimezone(
|
|
3380
|
+
data.previousStartTime,
|
|
3381
|
+
clinicTimezone,
|
|
3382
|
+
"date"
|
|
3383
|
+
);
|
|
3384
|
+
const previousTimezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3385
|
+
const newFormattedTime = this.formatTimestampInClinicTimezone(
|
|
3386
|
+
data.appointment.appointmentStartTime,
|
|
3387
|
+
clinicTimezone,
|
|
3388
|
+
"time"
|
|
3389
|
+
);
|
|
3390
|
+
const newFormattedDate = this.formatTimestampInClinicTimezone(
|
|
3391
|
+
data.appointment.appointmentStartTime,
|
|
3392
|
+
clinicTimezone,
|
|
3393
|
+
"date"
|
|
3394
|
+
);
|
|
3395
|
+
const newTimezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
3396
|
+
const templateVariables = {
|
|
3397
|
+
patientName: data.appointment.patientInfo.fullName,
|
|
3398
|
+
procedureName: data.appointment.procedureInfo.name,
|
|
3399
|
+
practitionerName: data.appointment.practitionerInfo.name,
|
|
3400
|
+
clinicName: data.appointment.clinicInfo.name,
|
|
3401
|
+
previousDate: previousFormattedDate,
|
|
3402
|
+
previousTime: `${previousFormattedTime} (${previousTimezoneName})`,
|
|
3403
|
+
newDate: newFormattedDate,
|
|
3404
|
+
newTime: `${newFormattedTime} (${newTimezoneName})`
|
|
3405
|
+
};
|
|
3406
|
+
const html = this.renderTemplate(appointmentRescheduledProposalTemplate, templateVariables);
|
|
3407
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Action Required: Reschedule Proposal for Your ${data.appointment.procedureInfo.name} Appointment`;
|
|
3408
|
+
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}>`;
|
|
3409
|
+
const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
3410
|
+
const mailgunSendData = {
|
|
3411
|
+
to: recipientEmail,
|
|
3412
|
+
from: fromAddress,
|
|
3413
|
+
subject,
|
|
3414
|
+
html
|
|
3415
|
+
};
|
|
3416
|
+
try {
|
|
3417
|
+
const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
|
|
3418
|
+
await this.logEmailAttempt(
|
|
3419
|
+
{ to: recipientEmail, subject, templateName: "appointment_rescheduled_proposal" },
|
|
3420
|
+
true
|
|
3421
|
+
);
|
|
3422
|
+
Logger.info(
|
|
3423
|
+
`[AppointmentMailingService] Successfully sent reschedule proposal email to ${recipientEmail}`
|
|
3424
|
+
);
|
|
3425
|
+
return result;
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
await this.logEmailAttempt(
|
|
3428
|
+
{
|
|
3429
|
+
to: recipientEmail,
|
|
3430
|
+
subject,
|
|
3431
|
+
templateName: "appointment_rescheduled_proposal"
|
|
3432
|
+
},
|
|
3433
|
+
false,
|
|
3434
|
+
error
|
|
3435
|
+
);
|
|
3436
|
+
Logger.error(
|
|
3437
|
+
`[AppointmentMailingService] Error sending reschedule proposal email to ${recipientEmail}:`,
|
|
3438
|
+
error
|
|
3439
|
+
);
|
|
3440
|
+
throw error;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
async sendReviewRequestEmail(data) {
|
|
3444
|
+
Logger.info(
|
|
3445
|
+
`[AppointmentMailingService] Placeholder for sendReviewRequestEmail to patient: ${data.patientProfile.id}`
|
|
3446
|
+
);
|
|
3447
|
+
return Promise.resolve();
|
|
3448
|
+
}
|
|
3449
|
+
async sendReviewAddedEmail(data) {
|
|
3450
|
+
Logger.info(
|
|
3451
|
+
`[AppointmentMailingService] Placeholder for sendReviewAddedEmail to ${data.recipientRole}: ${data.recipientProfile.id}`
|
|
3452
|
+
);
|
|
3453
|
+
return Promise.resolve();
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
|
|
3457
|
+
// src/admin/aggregation/appointment/appointment.aggregation.service.ts
|
|
3458
|
+
var AppointmentAggregationService = class {
|
|
3459
|
+
/**
|
|
2593
3460
|
* Constructor for AppointmentAggregationService.
|
|
2594
3461
|
* @param mailgunClient - An initialized Mailgun client instance.
|
|
2595
3462
|
* @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
|
|
@@ -2944,6 +3811,16 @@ var AppointmentAggregationService = class {
|
|
|
2944
3811
|
// TODO: Properly import PatientProfileInfo and types
|
|
2945
3812
|
);
|
|
2946
3813
|
}
|
|
3814
|
+
if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
|
|
3815
|
+
Logger.info(
|
|
3816
|
+
`[AggService] Sending reschedule proposal push notification to patient ${after.patientId}`
|
|
3817
|
+
);
|
|
3818
|
+
await this.notificationsAdmin.sendAppointmentRescheduledProposalPush(
|
|
3819
|
+
after,
|
|
3820
|
+
after.patientId,
|
|
3821
|
+
patientProfile.expoTokens
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
2947
3824
|
Logger.info(
|
|
2948
3825
|
`[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`
|
|
2949
3826
|
);
|