@blackcode_sa/metaestetics-api 1.14.56 → 1.14.58

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.
@@ -2287,6 +2287,284 @@ var clinicAppointmentRequestedTemplate = `
2287
2287
  </body>
2288
2288
  </html>
2289
2289
  `;
2290
+ var appointmentRescheduledProposalTemplate = `
2291
+ <!DOCTYPE html>
2292
+ <html lang="en">
2293
+ <head>
2294
+ <meta charset="UTF-8">
2295
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2296
+ <title>Appointment Reschedule Proposal</title>
2297
+ <style>
2298
+ body {
2299
+ margin: 0;
2300
+ padding: 0;
2301
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
2302
+ background: linear-gradient(135deg, #a48a76 0%, #67574A 100%);
2303
+ min-height: 100vh;
2304
+ }
2305
+ .email-container {
2306
+ max-width: 600px;
2307
+ margin: 0 auto;
2308
+ background: #ffffff;
2309
+ border-radius: 20px;
2310
+ overflow: hidden;
2311
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
2312
+ margin-top: 40px;
2313
+ margin-bottom: 40px;
2314
+ }
2315
+ .header {
2316
+ background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
2317
+ padding: 40px 30px;
2318
+ text-align: center;
2319
+ color: white;
2320
+ }
2321
+ .header h1 {
2322
+ margin: 0;
2323
+ font-size: 28px;
2324
+ font-weight: 300;
2325
+ letter-spacing: 1px;
2326
+ }
2327
+ .header .subtitle {
2328
+ margin: 10px 0 0 0;
2329
+ font-size: 16px;
2330
+ opacity: 0.9;
2331
+ font-weight: 300;
2332
+ }
2333
+ .content {
2334
+ padding: 40px 30px;
2335
+ }
2336
+ .greeting {
2337
+ font-size: 18px;
2338
+ color: #333;
2339
+ margin-bottom: 25px;
2340
+ font-weight: 400;
2341
+ }
2342
+ .info-box {
2343
+ background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
2344
+ border-radius: 15px;
2345
+ padding: 25px;
2346
+ margin: 25px 0;
2347
+ border-left: 5px solid #ff9800;
2348
+ }
2349
+ .info-box p {
2350
+ margin: 0;
2351
+ color: #e65100;
2352
+ font-size: 15px;
2353
+ font-weight: 500;
2354
+ line-height: 1.6;
2355
+ }
2356
+ .time-comparison {
2357
+ display: grid;
2358
+ gap: 20px;
2359
+ margin: 25px 0;
2360
+ }
2361
+ .time-card {
2362
+ background: linear-gradient(135deg, #f8f6f5 0%, #f5f3f2 100%);
2363
+ border-radius: 15px;
2364
+ padding: 25px;
2365
+ border-left: 5px solid #a48a76;
2366
+ }
2367
+ .time-card.old-time {
2368
+ border-left-color: #9e9e9e;
2369
+ opacity: 0.8;
2370
+ }
2371
+ .time-card.new-time {
2372
+ border-left-color: #ff9800;
2373
+ background: linear-gradient(135deg, #fff8e1 0%, #ffe0b2 100%);
2374
+ }
2375
+ .time-label {
2376
+ font-size: 14px;
2377
+ font-weight: 600;
2378
+ color: #666;
2379
+ text-transform: uppercase;
2380
+ letter-spacing: 0.5px;
2381
+ margin-bottom: 10px;
2382
+ }
2383
+ .time-label.old {
2384
+ color: #757575;
2385
+ }
2386
+ .time-label.new {
2387
+ color: #f57c00;
2388
+ }
2389
+ .appointment-card {
2390
+ background: linear-gradient(135deg, #f8f6f5 0%, #f5f3f2 100%);
2391
+ border-radius: 15px;
2392
+ padding: 30px;
2393
+ margin: 25px 0;
2394
+ border-left: 5px solid #a48a76;
2395
+ }
2396
+ .appointment-title {
2397
+ font-size: 20px;
2398
+ color: #a48a76;
2399
+ margin-bottom: 20px;
2400
+ font-weight: 600;
2401
+ }
2402
+ .appointment-details {
2403
+ display: grid;
2404
+ gap: 15px;
2405
+ }
2406
+ .detail-row {
2407
+ display: flex;
2408
+ align-items: center;
2409
+ padding: 8px 0;
2410
+ }
2411
+ .detail-label {
2412
+ font-weight: 600;
2413
+ color: #555;
2414
+ min-width: 120px;
2415
+ font-size: 14px;
2416
+ }
2417
+ .detail-value {
2418
+ color: #333;
2419
+ font-size: 16px;
2420
+ font-weight: 500;
2421
+ }
2422
+ .procedure-name {
2423
+ color: #67574A;
2424
+ font-weight: 600;
2425
+ }
2426
+ .clinic-name {
2427
+ color: #a48a76;
2428
+ font-weight: 600;
2429
+ }
2430
+ .action-section {
2431
+ background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
2432
+ border-radius: 15px;
2433
+ padding: 30px;
2434
+ margin: 30px 0;
2435
+ text-align: center;
2436
+ border-left: 5px solid #4caf50;
2437
+ }
2438
+ .action-section h3 {
2439
+ margin: 0 0 15px 0;
2440
+ color: #2e7d32;
2441
+ font-weight: 600;
2442
+ font-size: 18px;
2443
+ }
2444
+ .action-section p {
2445
+ margin: 0 0 20px 0;
2446
+ color: #555;
2447
+ font-size: 15px;
2448
+ line-height: 1.6;
2449
+ }
2450
+ .footer {
2451
+ background: #f8f9fa;
2452
+ padding: 25px 30px;
2453
+ text-align: center;
2454
+ color: #666;
2455
+ font-size: 14px;
2456
+ border-top: 1px solid #eee;
2457
+ }
2458
+ .logo {
2459
+ font-size: 24px;
2460
+ font-weight: 700;
2461
+ color: white;
2462
+ margin-bottom: 5px;
2463
+ }
2464
+ .divider {
2465
+ height: 2px;
2466
+ background: linear-gradient(90deg, #a48a76, #67574A);
2467
+ margin: 25px 0;
2468
+ border-radius: 1px;
2469
+ }
2470
+ .icon {
2471
+ text-align: center;
2472
+ margin: 20px 0;
2473
+ font-size: 48px;
2474
+ }
2475
+ .arrow {
2476
+ text-align: center;
2477
+ font-size: 32px;
2478
+ color: #ff9800;
2479
+ margin: 10px 0;
2480
+ }
2481
+ </style>
2482
+ </head>
2483
+ <body>
2484
+ <div class="email-container">
2485
+ <div class="header">
2486
+ <div class="logo">MetaEstetics</div>
2487
+ <h1>Appointment Reschedule Proposal</h1>
2488
+ <div class="subtitle">Action Required</div>
2489
+ </div>
2490
+
2491
+ <div class="content">
2492
+ <div class="icon">\u{1F4C5}</div>
2493
+
2494
+ <div class="greeting">
2495
+ Dear <strong>{{patientName}}</strong>,
2496
+ </div>
2497
+
2498
+ <p style="color: #555; font-size: 16px; line-height: 1.6; margin-bottom: 25px;">
2499
+ 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.
2500
+ </p>
2501
+
2502
+ <div class="info-box">
2503
+ <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>
2504
+ </div>
2505
+
2506
+ <div class="appointment-card">
2507
+ <div class="appointment-title">\u{1F4CB} Appointment Details</div>
2508
+ <div class="appointment-details">
2509
+ <div class="detail-row">
2510
+ <div class="detail-label">Procedure:</div>
2511
+ <div class="detail-value procedure-name">{{procedureName}}</div>
2512
+ </div>
2513
+ <div class="detail-row">
2514
+ <div class="detail-label">Practitioner:</div>
2515
+ <div class="detail-value">{{practitionerName}}</div>
2516
+ </div>
2517
+ <div class="detail-row">
2518
+ <div class="detail-label">Location:</div>
2519
+ <div class="detail-value clinic-name">{{clinicName}}</div>
2520
+ </div>
2521
+ </div>
2522
+ </div>
2523
+
2524
+ <div class="time-comparison">
2525
+ <div class="time-card old-time">
2526
+ <div class="time-label old">Previous Time</div>
2527
+ <div style="font-size: 18px; font-weight: 600; color: #424242; margin-bottom: 8px;">{{previousDate}}</div>
2528
+ <div style="font-size: 16px; color: #616161;">{{previousTime}}</div>
2529
+ </div>
2530
+
2531
+ <div class="arrow">\u2193</div>
2532
+
2533
+ <div class="time-card new-time">
2534
+ <div class="time-label new">Proposed New Time</div>
2535
+ <div style="font-size: 18px; font-weight: 600; color: #e65100; margin-bottom: 8px;">{{newDate}}</div>
2536
+ <div style="font-size: 16px; color: #f57c00; font-weight: 500;">{{newTime}}</div>
2537
+ </div>
2538
+ </div>
2539
+
2540
+ <div class="divider"></div>
2541
+
2542
+ <div class="action-section">
2543
+ <h3>What's Next?</h3>
2544
+ <p>
2545
+ Please open the MetaEstetics app to accept or reject this reschedule proposal.
2546
+ If the new time works for you, simply tap "Accept Reschedule".
2547
+ If not, you can reject it and we'll work with you to find an alternative time.
2548
+ </p>
2549
+ </div>
2550
+
2551
+ <p style="color: #555; font-size: 14px; line-height: 1.6; margin-top: 25px;">
2552
+ <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
+ </p>
2554
+ </div>
2555
+
2556
+ <div class="footer">
2557
+ <p style="margin: 0 0 10px 0;">
2558
+ <strong>MetaEstetics</strong> - Premium Aesthetic Services
2559
+ </p>
2560
+ <p style="margin: 0; font-size: 12px; color: #999;">
2561
+ This is an automated message. Please do not reply to this email.
2562
+ </p>
2563
+ </div>
2564
+ </div>
2565
+ </body>
2566
+ </html>
2567
+ `;
2290
2568
  var AppointmentMailingService = class extends BaseMailingService {
2291
2569
  constructor(firestore19, mailgunClient) {
2292
2570
  super(firestore19, mailgunClient);
@@ -2491,11 +2769,97 @@ var AppointmentMailingService = class extends BaseMailingService {
2491
2769
  );
2492
2770
  return Promise.resolve();
2493
2771
  }
2772
+ /**
2773
+ * Sends a reschedule proposal email to the patient
2774
+ * @param data - Appointment reschedule proposal email data
2775
+ * @returns Promise with the sending result
2776
+ */
2494
2777
  async sendAppointmentRescheduledProposalEmail(data) {
2778
+ var _a, _b, _c, _d;
2495
2779
  Logger.info(
2496
- `[AppointmentMailingService] Placeholder for sendAppointmentRescheduledProposalEmail to patient: ${data.patientProfile.id}`
2780
+ `[AppointmentMailingService] Preparing to send reschedule proposal email to patient: ${data.patientProfile.id}`
2497
2781
  );
2498
- return Promise.resolve();
2782
+ const recipientEmail = data.patientProfile.email;
2783
+ if (!recipientEmail) {
2784
+ Logger.error("[AppointmentMailingService] Patient email not found for reschedule proposal.", {
2785
+ patientId: data.patientProfile.id
2786
+ });
2787
+ throw new Error("Patient email address is missing.");
2788
+ }
2789
+ const clinicTimezone = data.appointment.clinic_tz || "UTC";
2790
+ Logger.debug("[AppointmentMailingService] Formatting appointment times for reschedule", {
2791
+ clinicTimezone,
2792
+ previousTime: data.previousStartTime.toDate().toISOString(),
2793
+ newTime: data.appointment.appointmentStartTime.toDate().toISOString()
2794
+ });
2795
+ const previousFormattedTime = this.formatTimestampInClinicTimezone(
2796
+ data.previousStartTime,
2797
+ clinicTimezone,
2798
+ "time"
2799
+ );
2800
+ const previousFormattedDate = this.formatTimestampInClinicTimezone(
2801
+ data.previousStartTime,
2802
+ clinicTimezone,
2803
+ "date"
2804
+ );
2805
+ const previousTimezoneName = this.getTimezoneDisplayName(clinicTimezone);
2806
+ const newFormattedTime = this.formatTimestampInClinicTimezone(
2807
+ data.appointment.appointmentStartTime,
2808
+ clinicTimezone,
2809
+ "time"
2810
+ );
2811
+ const newFormattedDate = this.formatTimestampInClinicTimezone(
2812
+ data.appointment.appointmentStartTime,
2813
+ clinicTimezone,
2814
+ "date"
2815
+ );
2816
+ const newTimezoneName = this.getTimezoneDisplayName(clinicTimezone);
2817
+ const templateVariables = {
2818
+ patientName: data.appointment.patientInfo.fullName,
2819
+ procedureName: data.appointment.procedureInfo.name,
2820
+ practitionerName: data.appointment.practitionerInfo.name,
2821
+ clinicName: data.appointment.clinicInfo.name,
2822
+ previousDate: previousFormattedDate,
2823
+ previousTime: `${previousFormattedTime} (${previousTimezoneName})`,
2824
+ newDate: newFormattedDate,
2825
+ newTime: `${newFormattedTime} (${newTimezoneName})`
2826
+ };
2827
+ const html = this.renderTemplate(appointmentRescheduledProposalTemplate, templateVariables);
2828
+ const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Action Required: Reschedule Proposal for Your ${data.appointment.procedureInfo.name} Appointment`;
2829
+ 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}>`;
2830
+ const domainToSendFrom = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
2831
+ const mailgunSendData = {
2832
+ to: recipientEmail,
2833
+ from: fromAddress,
2834
+ subject,
2835
+ html
2836
+ };
2837
+ try {
2838
+ const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
2839
+ await this.logEmailAttempt(
2840
+ { to: recipientEmail, subject, templateName: "appointment_rescheduled_proposal" },
2841
+ true
2842
+ );
2843
+ Logger.info(
2844
+ `[AppointmentMailingService] Successfully sent reschedule proposal email to ${recipientEmail}`
2845
+ );
2846
+ return result;
2847
+ } catch (error) {
2848
+ await this.logEmailAttempt(
2849
+ {
2850
+ to: recipientEmail,
2851
+ subject,
2852
+ templateName: "appointment_rescheduled_proposal"
2853
+ },
2854
+ false,
2855
+ error
2856
+ );
2857
+ Logger.error(
2858
+ `[AppointmentMailingService] Error sending reschedule proposal email to ${recipientEmail}:`,
2859
+ error
2860
+ );
2861
+ throw error;
2862
+ }
2499
2863
  }
2500
2864
  async sendReviewRequestEmail(data) {
2501
2865
  Logger.info(
package/dist/index.d.mts CHANGED
@@ -5586,6 +5586,7 @@ interface LinkedFormInfo {
5586
5586
  path: string;
5587
5587
  submittedAt?: Timestamp;
5588
5588
  completedAt?: Timestamp;
5589
+ procedureId?: string;
5589
5590
  }
5590
5591
  /**
5591
5592
  * Interface for summarized patient review information linked to an appointment.
@@ -5650,8 +5651,8 @@ interface ZoneItemData {
5650
5651
  notes?: string;
5651
5652
  notesVisibleToPatient?: boolean;
5652
5653
  subtotal?: number;
5653
- ionNumber?: string;
5654
- expiryDate?: string;
5654
+ ionNumber?: string | null;
5655
+ expiryDate?: string | null;
5655
5656
  createdAt?: string;
5656
5657
  updatedAt?: string;
5657
5658
  }
@@ -5704,6 +5705,7 @@ interface ExtendedProcedureInfo {
5704
5705
  procedureId: string;
5705
5706
  procedureName: string;
5706
5707
  procedureDescription?: string;
5708
+ procedurePrice?: number;
5707
5709
  procedureFamily?: ProcedureFamily;
5708
5710
  procedureCategoryId: string;
5709
5711
  procedureCategoryName: string;
@@ -6897,10 +6899,29 @@ declare class ProcedureService extends BaseService {
6897
6899
  procedures: Procedure[];
6898
6900
  lastDoc: any;
6899
6901
  }>;
6902
+ /**
6903
+ * Creates a serializable cursor from a DocumentSnapshot or returns the cursor values.
6904
+ * This format can be passed through React Native state/Redux without losing data.
6905
+ *
6906
+ * @param doc - The Firestore DocumentSnapshot
6907
+ * @param orderByField - The field used in orderBy clause
6908
+ * @returns Serializable cursor object with values needed for startAfter
6909
+ */
6910
+ private createSerializableCursor;
6911
+ /**
6912
+ * Converts a serializable cursor back to values for startAfter.
6913
+ * Handles both native DocumentSnapshots and serialized cursor objects.
6914
+ *
6915
+ * @param lastDoc - Either a DocumentSnapshot or a serializable cursor object
6916
+ * @param orderByField - The field used in orderBy clause (for validation)
6917
+ * @returns Values to spread into startAfter, or null if invalid
6918
+ */
6919
+ private getCursorValuesForStartAfter;
6900
6920
  /**
6901
6921
  * Searches and filters procedures based on multiple criteria
6902
6922
  *
6903
- * @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
6923
+ * @note Frontend can now send either a DocumentSnapshot or a serializable cursor object.
6924
+ * The serializable cursor format is: { __cursor: true, values: [...], id: string, orderByField: string }
6904
6925
  *
6905
6926
  * @param filters - Various filters to apply
6906
6927
  * @param filters.nameSearch - Optional search text for procedure name
package/dist/index.d.ts CHANGED
@@ -5586,6 +5586,7 @@ interface LinkedFormInfo {
5586
5586
  path: string;
5587
5587
  submittedAt?: Timestamp;
5588
5588
  completedAt?: Timestamp;
5589
+ procedureId?: string;
5589
5590
  }
5590
5591
  /**
5591
5592
  * Interface for summarized patient review information linked to an appointment.
@@ -5650,8 +5651,8 @@ interface ZoneItemData {
5650
5651
  notes?: string;
5651
5652
  notesVisibleToPatient?: boolean;
5652
5653
  subtotal?: number;
5653
- ionNumber?: string;
5654
- expiryDate?: string;
5654
+ ionNumber?: string | null;
5655
+ expiryDate?: string | null;
5655
5656
  createdAt?: string;
5656
5657
  updatedAt?: string;
5657
5658
  }
@@ -5704,6 +5705,7 @@ interface ExtendedProcedureInfo {
5704
5705
  procedureId: string;
5705
5706
  procedureName: string;
5706
5707
  procedureDescription?: string;
5708
+ procedurePrice?: number;
5707
5709
  procedureFamily?: ProcedureFamily;
5708
5710
  procedureCategoryId: string;
5709
5711
  procedureCategoryName: string;
@@ -6897,10 +6899,29 @@ declare class ProcedureService extends BaseService {
6897
6899
  procedures: Procedure[];
6898
6900
  lastDoc: any;
6899
6901
  }>;
6902
+ /**
6903
+ * Creates a serializable cursor from a DocumentSnapshot or returns the cursor values.
6904
+ * This format can be passed through React Native state/Redux without losing data.
6905
+ *
6906
+ * @param doc - The Firestore DocumentSnapshot
6907
+ * @param orderByField - The field used in orderBy clause
6908
+ * @returns Serializable cursor object with values needed for startAfter
6909
+ */
6910
+ private createSerializableCursor;
6911
+ /**
6912
+ * Converts a serializable cursor back to values for startAfter.
6913
+ * Handles both native DocumentSnapshots and serialized cursor objects.
6914
+ *
6915
+ * @param lastDoc - Either a DocumentSnapshot or a serializable cursor object
6916
+ * @param orderByField - The field used in orderBy clause (for validation)
6917
+ * @returns Values to spread into startAfter, or null if invalid
6918
+ */
6919
+ private getCursorValuesForStartAfter;
6900
6920
  /**
6901
6921
  * Searches and filters procedures based on multiple criteria
6902
6922
  *
6903
- * @note Frontend MORA da šalje ceo snapshot (ili barem sva polja po kojima sortiraš, npr. nameLower) kao lastDoc za paginaciju, a ne samo id!
6923
+ * @note Frontend can now send either a DocumentSnapshot or a serializable cursor object.
6924
+ * The serializable cursor format is: { __cursor: true, values: [...], id: string, orderByField: string }
6904
6925
  *
6905
6926
  * @param filters - Various filters to apply
6906
6927
  * @param filters.nameSearch - Optional search text for procedure name