@blackcode_sa/metaestetics-api 1.14.57 → 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.
- package/dist/admin/index.d.mts +5 -0
- package/dist/admin/index.d.ts +5 -0
- package/dist/admin/index.js +366 -2
- package/dist/admin/index.mjs +366 -2
- package/dist/index.d.mts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +94 -27
- package/dist/index.mjs +95 -27
- package/package.json +1 -1
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +383 -2
- package/src/services/procedure/procedure.service.ts +137 -40
package/dist/admin/index.mjs
CHANGED
|
@@ -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]
|
|
2780
|
+
`[AppointmentMailingService] Preparing to send reschedule proposal email to patient: ${data.patientProfile.id}`
|
|
2497
2781
|
);
|
|
2498
|
-
|
|
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
|
@@ -6899,10 +6899,29 @@ declare class ProcedureService extends BaseService {
|
|
|
6899
6899
|
procedures: Procedure[];
|
|
6900
6900
|
lastDoc: any;
|
|
6901
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;
|
|
6902
6920
|
/**
|
|
6903
6921
|
* Searches and filters procedures based on multiple criteria
|
|
6904
6922
|
*
|
|
6905
|
-
* @note Frontend
|
|
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 }
|
|
6906
6925
|
*
|
|
6907
6926
|
* @param filters - Various filters to apply
|
|
6908
6927
|
* @param filters.nameSearch - Optional search text for procedure name
|
package/dist/index.d.ts
CHANGED
|
@@ -6899,10 +6899,29 @@ declare class ProcedureService extends BaseService {
|
|
|
6899
6899
|
procedures: Procedure[];
|
|
6900
6900
|
lastDoc: any;
|
|
6901
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;
|
|
6902
6920
|
/**
|
|
6903
6921
|
* Searches and filters procedures based on multiple criteria
|
|
6904
6922
|
*
|
|
6905
|
-
* @note Frontend
|
|
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 }
|
|
6906
6925
|
*
|
|
6907
6926
|
* @param filters - Various filters to apply
|
|
6908
6927
|
* @param filters.nameSearch - Optional search text for procedure name
|
package/dist/index.js
CHANGED
|
@@ -22601,10 +22601,72 @@ var ProcedureService = class extends BaseService {
|
|
|
22601
22601
|
throw error;
|
|
22602
22602
|
}
|
|
22603
22603
|
}
|
|
22604
|
+
/**
|
|
22605
|
+
* Creates a serializable cursor from a DocumentSnapshot or returns the cursor values.
|
|
22606
|
+
* This format can be passed through React Native state/Redux without losing data.
|
|
22607
|
+
*
|
|
22608
|
+
* @param doc - The Firestore DocumentSnapshot
|
|
22609
|
+
* @param orderByField - The field used in orderBy clause
|
|
22610
|
+
* @returns Serializable cursor object with values needed for startAfter
|
|
22611
|
+
*/
|
|
22612
|
+
createSerializableCursor(doc47, orderByField = "createdAt") {
|
|
22613
|
+
if (!doc47) return null;
|
|
22614
|
+
const data = typeof doc47.data === "function" ? doc47.data() : doc47;
|
|
22615
|
+
const docId = doc47.id || (data == null ? void 0 : data.id);
|
|
22616
|
+
if (!docId) return null;
|
|
22617
|
+
let orderByValue = data == null ? void 0 : data[orderByField];
|
|
22618
|
+
if (orderByValue && typeof orderByValue.toDate === "function") {
|
|
22619
|
+
orderByValue = orderByValue.toMillis();
|
|
22620
|
+
} else if (orderByValue && orderByValue.seconds) {
|
|
22621
|
+
orderByValue = orderByValue.seconds * 1e3 + (orderByValue.nanoseconds || 0) / 1e6;
|
|
22622
|
+
}
|
|
22623
|
+
return {
|
|
22624
|
+
__cursor: true,
|
|
22625
|
+
values: [orderByValue],
|
|
22626
|
+
id: docId,
|
|
22627
|
+
orderByField
|
|
22628
|
+
};
|
|
22629
|
+
}
|
|
22630
|
+
/**
|
|
22631
|
+
* Converts a serializable cursor back to values for startAfter.
|
|
22632
|
+
* Handles both native DocumentSnapshots and serialized cursor objects.
|
|
22633
|
+
*
|
|
22634
|
+
* @param lastDoc - Either a DocumentSnapshot or a serializable cursor object
|
|
22635
|
+
* @param orderByField - The field used in orderBy clause (for validation)
|
|
22636
|
+
* @returns Values to spread into startAfter, or null if invalid
|
|
22637
|
+
*/
|
|
22638
|
+
getCursorValuesForStartAfter(lastDoc, orderByField = "createdAt") {
|
|
22639
|
+
if (!lastDoc) return null;
|
|
22640
|
+
if (typeof lastDoc.data === "function") {
|
|
22641
|
+
return [lastDoc];
|
|
22642
|
+
}
|
|
22643
|
+
if (lastDoc.__cursor && Array.isArray(lastDoc.values)) {
|
|
22644
|
+
if (orderByField === "createdAt" && typeof lastDoc.values[0] === "number") {
|
|
22645
|
+
const timestamp = import_firestore58.Timestamp.fromMillis(lastDoc.values[0]);
|
|
22646
|
+
return [timestamp];
|
|
22647
|
+
}
|
|
22648
|
+
return lastDoc.values;
|
|
22649
|
+
}
|
|
22650
|
+
if (Array.isArray(lastDoc)) {
|
|
22651
|
+
return lastDoc;
|
|
22652
|
+
}
|
|
22653
|
+
if (lastDoc[orderByField]) {
|
|
22654
|
+
let value = lastDoc[orderByField];
|
|
22655
|
+
if (typeof value === "number" && orderByField === "createdAt") {
|
|
22656
|
+
value = import_firestore58.Timestamp.fromMillis(value);
|
|
22657
|
+
} else if (value.seconds && orderByField === "createdAt") {
|
|
22658
|
+
value = new import_firestore58.Timestamp(value.seconds, value.nanoseconds || 0);
|
|
22659
|
+
}
|
|
22660
|
+
return [value];
|
|
22661
|
+
}
|
|
22662
|
+
console.warn("[PROCEDURE_SERVICE] Could not parse lastDoc cursor:", typeof lastDoc);
|
|
22663
|
+
return null;
|
|
22664
|
+
}
|
|
22604
22665
|
/**
|
|
22605
22666
|
* Searches and filters procedures based on multiple criteria
|
|
22606
22667
|
*
|
|
22607
|
-
* @note Frontend
|
|
22668
|
+
* @note Frontend can now send either a DocumentSnapshot or a serializable cursor object.
|
|
22669
|
+
* The serializable cursor format is: { __cursor: true, values: [...], id: string, orderByField: string }
|
|
22608
22670
|
*
|
|
22609
22671
|
* @param filters - Various filters to apply
|
|
22610
22672
|
* @param filters.nameSearch - Optional search text for procedure name
|
|
@@ -22700,12 +22762,10 @@ var ProcedureService = class extends BaseService {
|
|
|
22700
22762
|
constraints.push((0, import_firestore58.where)("nameLower", "<=", searchTerm + "\uF8FF"));
|
|
22701
22763
|
constraints.push((0, import_firestore58.orderBy)("nameLower"));
|
|
22702
22764
|
if (filters.lastDoc) {
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
} else {
|
|
22708
|
-
constraints.push((0, import_firestore58.startAfter)(filters.lastDoc));
|
|
22765
|
+
const cursorValues = this.getCursorValuesForStartAfter(filters.lastDoc, "nameLower");
|
|
22766
|
+
if (cursorValues) {
|
|
22767
|
+
constraints.push((0, import_firestore58.startAfter)(...cursorValues));
|
|
22768
|
+
console.log("[PROCEDURE_SERVICE] Strategy 1: Using cursor for pagination");
|
|
22709
22769
|
}
|
|
22710
22770
|
}
|
|
22711
22771
|
constraints.push((0, import_firestore58.limit)(filters.pagination || 10));
|
|
@@ -22724,8 +22784,9 @@ var ProcedureService = class extends BaseService {
|
|
|
22724
22784
|
if (querySnapshot.docs.length < (filters.pagination || 10)) {
|
|
22725
22785
|
return { procedures, lastDoc: null };
|
|
22726
22786
|
}
|
|
22727
|
-
const
|
|
22728
|
-
|
|
22787
|
+
const lastDocSnapshot = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
22788
|
+
const serializableCursor = this.createSerializableCursor(lastDocSnapshot, "nameLower");
|
|
22789
|
+
return { procedures, lastDoc: serializableCursor };
|
|
22729
22790
|
} catch (error) {
|
|
22730
22791
|
console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
|
|
22731
22792
|
}
|
|
@@ -22743,12 +22804,10 @@ var ProcedureService = class extends BaseService {
|
|
|
22743
22804
|
constraints.push((0, import_firestore58.where)("name", "<=", searchTerm + "\uF8FF"));
|
|
22744
22805
|
constraints.push((0, import_firestore58.orderBy)("name"));
|
|
22745
22806
|
if (filters.lastDoc) {
|
|
22746
|
-
|
|
22747
|
-
|
|
22748
|
-
|
|
22749
|
-
|
|
22750
|
-
} else {
|
|
22751
|
-
constraints.push((0, import_firestore58.startAfter)(filters.lastDoc));
|
|
22807
|
+
const cursorValues = this.getCursorValuesForStartAfter(filters.lastDoc, "name");
|
|
22808
|
+
if (cursorValues) {
|
|
22809
|
+
constraints.push((0, import_firestore58.startAfter)(...cursorValues));
|
|
22810
|
+
console.log("[PROCEDURE_SERVICE] Strategy 2: Using cursor for pagination");
|
|
22752
22811
|
}
|
|
22753
22812
|
}
|
|
22754
22813
|
constraints.push((0, import_firestore58.limit)(filters.pagination || 10));
|
|
@@ -22767,8 +22826,9 @@ var ProcedureService = class extends BaseService {
|
|
|
22767
22826
|
if (querySnapshot.docs.length < (filters.pagination || 10)) {
|
|
22768
22827
|
return { procedures, lastDoc: null };
|
|
22769
22828
|
}
|
|
22770
|
-
const
|
|
22771
|
-
|
|
22829
|
+
const lastDocSnapshot = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
22830
|
+
const serializableCursor = this.createSerializableCursor(lastDocSnapshot, "name");
|
|
22831
|
+
return { procedures, lastDoc: serializableCursor };
|
|
22772
22832
|
} catch (error) {
|
|
22773
22833
|
console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
|
|
22774
22834
|
}
|
|
@@ -22818,12 +22878,10 @@ var ProcedureService = class extends BaseService {
|
|
|
22818
22878
|
);
|
|
22819
22879
|
constraints.push((0, import_firestore58.orderBy)("createdAt", "desc"));
|
|
22820
22880
|
if (filters.lastDoc) {
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
} else {
|
|
22826
|
-
constraints.push((0, import_firestore58.startAfter)(filters.lastDoc));
|
|
22881
|
+
const cursorValues = this.getCursorValuesForStartAfter(filters.lastDoc, "createdAt");
|
|
22882
|
+
if (cursorValues) {
|
|
22883
|
+
constraints.push((0, import_firestore58.startAfter)(...cursorValues));
|
|
22884
|
+
console.log("[PROCEDURE_SERVICE] Strategy 3: Using cursor for pagination");
|
|
22827
22885
|
}
|
|
22828
22886
|
}
|
|
22829
22887
|
constraints.push((0, import_firestore58.limit)(filters.pagination || 10));
|
|
@@ -22854,8 +22912,9 @@ var ProcedureService = class extends BaseService {
|
|
|
22854
22912
|
if (querySnapshot.docs.length < (filters.pagination || 10)) {
|
|
22855
22913
|
return { procedures, lastDoc: null };
|
|
22856
22914
|
}
|
|
22857
|
-
const
|
|
22858
|
-
|
|
22915
|
+
const lastDocSnapshot = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
22916
|
+
const serializableCursor = this.createSerializableCursor(lastDocSnapshot, "createdAt");
|
|
22917
|
+
return { procedures, lastDoc: serializableCursor };
|
|
22859
22918
|
} catch (error) {
|
|
22860
22919
|
console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
|
|
22861
22920
|
}
|
|
@@ -22871,6 +22930,13 @@ var ProcedureService = class extends BaseService {
|
|
|
22871
22930
|
if (filters.clinicId) {
|
|
22872
22931
|
constraints.push((0, import_firestore58.where)("clinicBranchId", "==", filters.clinicId));
|
|
22873
22932
|
}
|
|
22933
|
+
if (filters.lastDoc) {
|
|
22934
|
+
const cursorValues = this.getCursorValuesForStartAfter(filters.lastDoc, "createdAt");
|
|
22935
|
+
if (cursorValues) {
|
|
22936
|
+
constraints.push((0, import_firestore58.startAfter)(...cursorValues));
|
|
22937
|
+
console.log("[PROCEDURE_SERVICE] Strategy 4: Using cursor for pagination");
|
|
22938
|
+
}
|
|
22939
|
+
}
|
|
22874
22940
|
constraints.push((0, import_firestore58.limit)(filters.pagination || 10));
|
|
22875
22941
|
const q = (0, import_firestore58.query)((0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION), ...constraints);
|
|
22876
22942
|
const querySnapshot = await (0, import_firestore58.getDocs)(q);
|
|
@@ -22885,8 +22951,9 @@ var ProcedureService = class extends BaseService {
|
|
|
22885
22951
|
if (querySnapshot.docs.length < (filters.pagination || 10)) {
|
|
22886
22952
|
return { procedures, lastDoc: null };
|
|
22887
22953
|
}
|
|
22888
|
-
const
|
|
22889
|
-
|
|
22954
|
+
const lastDocSnapshot = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
22955
|
+
const serializableCursor = this.createSerializableCursor(lastDocSnapshot, "createdAt");
|
|
22956
|
+
return { procedures, lastDoc: serializableCursor };
|
|
22890
22957
|
} catch (error) {
|
|
22891
22958
|
console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
|
|
22892
22959
|
}
|