@blackcode_sa/metaestetics-api 1.12.57 → 1.12.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 +14 -0
- package/dist/admin/index.d.ts +14 -0
- package/dist/admin/index.js +105 -16
- package/dist/admin/index.mjs +105 -16
- package/package.json +1 -1
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +117 -4
package/dist/admin/index.d.mts
CHANGED
|
@@ -3558,6 +3558,20 @@ interface ReviewAddedEmailData extends AppointmentEmailDataBase {
|
|
|
3558
3558
|
declare class AppointmentMailingService extends BaseMailingService {
|
|
3559
3559
|
private readonly DEFAULT_MAILGUN_DOMAIN;
|
|
3560
3560
|
constructor(firestore: admin.firestore.Firestore, mailgunClient: NewMailgunClient$2);
|
|
3561
|
+
/**
|
|
3562
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
3563
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
3564
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3565
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
3566
|
+
* @returns Formatted string in clinic's local timezone
|
|
3567
|
+
*/
|
|
3568
|
+
private formatTimestampInClinicTimezone;
|
|
3569
|
+
/**
|
|
3570
|
+
* Gets a user-friendly display name for a timezone
|
|
3571
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3572
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
3573
|
+
*/
|
|
3574
|
+
private getTimezoneDisplayName;
|
|
3561
3575
|
sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any>;
|
|
3562
3576
|
sendAppointmentRequestedEmailToClinic(data: AppointmentRequestedEmailData): Promise<any>;
|
|
3563
3577
|
sendAppointmentCancelledEmail(data: AppointmentCancellationEmailData): Promise<any>;
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -3558,6 +3558,20 @@ interface ReviewAddedEmailData extends AppointmentEmailDataBase {
|
|
|
3558
3558
|
declare class AppointmentMailingService extends BaseMailingService {
|
|
3559
3559
|
private readonly DEFAULT_MAILGUN_DOMAIN;
|
|
3560
3560
|
constructor(firestore: admin.firestore.Firestore, mailgunClient: NewMailgunClient$2);
|
|
3561
|
+
/**
|
|
3562
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
3563
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
3564
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3565
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
3566
|
+
* @returns Formatted string in clinic's local timezone
|
|
3567
|
+
*/
|
|
3568
|
+
private formatTimestampInClinicTimezone;
|
|
3569
|
+
/**
|
|
3570
|
+
* Gets a user-friendly display name for a timezone
|
|
3571
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
3572
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
3573
|
+
*/
|
|
3574
|
+
private getTimezoneDisplayName;
|
|
3561
3575
|
sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any>;
|
|
3562
3576
|
sendAppointmentRequestedEmailToClinic(data: AppointmentRequestedEmailData): Promise<any>;
|
|
3563
3577
|
sendAppointmentCancelledEmail(data: AppointmentCancellationEmailData): Promise<any>;
|
package/dist/admin/index.js
CHANGED
|
@@ -1765,6 +1765,9 @@ var CalendarAdminService = class {
|
|
|
1765
1765
|
}
|
|
1766
1766
|
};
|
|
1767
1767
|
|
|
1768
|
+
// src/admin/mailing/appointment/appointment.mailing.service.ts
|
|
1769
|
+
var import_luxon = require("luxon");
|
|
1770
|
+
|
|
1768
1771
|
// src/admin/mailing/base.mailing.service.ts
|
|
1769
1772
|
var admin5 = __toESM(require("firebase-admin"));
|
|
1770
1773
|
var BaseMailingService = class {
|
|
@@ -2323,6 +2326,62 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2323
2326
|
this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
2324
2327
|
Logger.info("[AppointmentMailingService] Initialized.");
|
|
2325
2328
|
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
2331
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
2332
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
2333
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
2334
|
+
* @returns Formatted string in clinic's local timezone
|
|
2335
|
+
*/
|
|
2336
|
+
formatTimestampInClinicTimezone(timestamp, clinicTimezone, format = "datetime") {
|
|
2337
|
+
try {
|
|
2338
|
+
const dateTimeInClinicTz = import_luxon.DateTime.fromMillis(timestamp.toMillis(), {
|
|
2339
|
+
zone: clinicTimezone
|
|
2340
|
+
});
|
|
2341
|
+
switch (format) {
|
|
2342
|
+
case "date":
|
|
2343
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATE_FULL);
|
|
2344
|
+
case "time":
|
|
2345
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.TIME_SIMPLE);
|
|
2346
|
+
case "datetime":
|
|
2347
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
|
|
2348
|
+
default:
|
|
2349
|
+
return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
|
|
2350
|
+
}
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
Logger.error("[AppointmentMailingService] Error formatting timestamp in clinic timezone:", {
|
|
2353
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2354
|
+
clinicTimezone
|
|
2355
|
+
});
|
|
2356
|
+
return timestamp.toDate().toLocaleString();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Gets a user-friendly display name for a timezone
|
|
2361
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
2362
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
2363
|
+
*/
|
|
2364
|
+
getTimezoneDisplayName(timezone) {
|
|
2365
|
+
try {
|
|
2366
|
+
const timezoneMap = {
|
|
2367
|
+
"Europe/Zurich": "Clinic Time - Central European Time",
|
|
2368
|
+
"Europe/London": "Clinic Time - GMT/BST",
|
|
2369
|
+
"America/New_York": "Clinic Time - Eastern Time",
|
|
2370
|
+
"America/Los_Angeles": "Clinic Time - Pacific Time",
|
|
2371
|
+
"Asia/Dubai": "Clinic Time - Gulf Standard Time",
|
|
2372
|
+
"Asia/Karachi": "Clinic Time - Pakistan Standard Time",
|
|
2373
|
+
"Asia/Kolkata": "Clinic Time - India Standard Time",
|
|
2374
|
+
"Australia/Sydney": "Clinic Time - Australian Eastern Time"
|
|
2375
|
+
};
|
|
2376
|
+
return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
Logger.error("[AppointmentMailingService] Error getting timezone display name:", {
|
|
2379
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2380
|
+
timezone
|
|
2381
|
+
});
|
|
2382
|
+
return `Clinic Time - ${timezone}`;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2326
2385
|
async sendAppointmentConfirmedEmail(data) {
|
|
2327
2386
|
var _a, _b, _c, _d, _e;
|
|
2328
2387
|
Logger.info(
|
|
@@ -2336,11 +2395,26 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2336
2395
|
});
|
|
2337
2396
|
throw new Error("Recipient email address is missing.");
|
|
2338
2397
|
}
|
|
2398
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
2399
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time", {
|
|
2400
|
+
clinicTimezone,
|
|
2401
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
2402
|
+
});
|
|
2403
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
2404
|
+
data.appointment.appointmentStartTime,
|
|
2405
|
+
clinicTimezone,
|
|
2406
|
+
"time"
|
|
2407
|
+
);
|
|
2408
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
2339
2409
|
const templateVariables = {
|
|
2340
2410
|
patientName: data.appointment.patientInfo.fullName,
|
|
2341
2411
|
procedureName: data.appointment.procedureInfo.name,
|
|
2342
|
-
appointmentDate:
|
|
2343
|
-
|
|
2412
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
2413
|
+
data.appointment.appointmentStartTime,
|
|
2414
|
+
clinicTimezone,
|
|
2415
|
+
"date"
|
|
2416
|
+
),
|
|
2417
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
2344
2418
|
practitionerName: data.appointment.practitionerInfo.name,
|
|
2345
2419
|
clinicName: data.appointment.clinicInfo.name
|
|
2346
2420
|
};
|
|
@@ -2387,12 +2461,27 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2387
2461
|
);
|
|
2388
2462
|
throw new Error("Clinic contact email address is missing.");
|
|
2389
2463
|
}
|
|
2464
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
2465
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for clinic", {
|
|
2466
|
+
clinicTimezone,
|
|
2467
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
2468
|
+
});
|
|
2469
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
2470
|
+
data.appointment.appointmentStartTime,
|
|
2471
|
+
clinicTimezone,
|
|
2472
|
+
"time"
|
|
2473
|
+
);
|
|
2474
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
2390
2475
|
const templateVariables = {
|
|
2391
2476
|
clinicName: data.clinicProfile.name,
|
|
2392
2477
|
patientName: data.appointment.patientInfo.fullName,
|
|
2393
2478
|
procedureName: data.appointment.procedureInfo.name,
|
|
2394
|
-
appointmentDate:
|
|
2395
|
-
|
|
2479
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
2480
|
+
data.appointment.appointmentStartTime,
|
|
2481
|
+
clinicTimezone,
|
|
2482
|
+
"date"
|
|
2483
|
+
),
|
|
2484
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
2396
2485
|
practitionerName: data.appointment.practitionerInfo.name
|
|
2397
2486
|
};
|
|
2398
2487
|
const html = this.renderTemplate(clinicAppointmentRequestedTemplate, templateVariables);
|
|
@@ -6576,7 +6665,7 @@ var ReviewsAggregationService = class {
|
|
|
6576
6665
|
|
|
6577
6666
|
// src/admin/booking/booking.calculator.ts
|
|
6578
6667
|
var import_firestore2 = require("firebase/firestore");
|
|
6579
|
-
var
|
|
6668
|
+
var import_luxon2 = require("luxon");
|
|
6580
6669
|
var BookingAvailabilityCalculator = class {
|
|
6581
6670
|
/**
|
|
6582
6671
|
* Calculate available booking slots based on the provided data
|
|
@@ -6667,8 +6756,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6667
6756
|
*/
|
|
6668
6757
|
static createWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
|
|
6669
6758
|
const workingIntervals = [];
|
|
6670
|
-
let start =
|
|
6671
|
-
const end =
|
|
6759
|
+
let start = import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz });
|
|
6760
|
+
const end = import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz });
|
|
6672
6761
|
while (start <= end) {
|
|
6673
6762
|
const dayOfWeek = start.weekday;
|
|
6674
6763
|
const dayName = [
|
|
@@ -6698,8 +6787,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6698
6787
|
millisecond: 0
|
|
6699
6788
|
});
|
|
6700
6789
|
if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
|
|
6701
|
-
const intervalStart = workStart <
|
|
6702
|
-
const intervalEnd = workEnd >
|
|
6790
|
+
const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
|
|
6791
|
+
const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
|
|
6703
6792
|
workingIntervals.push({
|
|
6704
6793
|
start: import_firestore2.Timestamp.fromMillis(intervalStart.toMillis()),
|
|
6705
6794
|
end: import_firestore2.Timestamp.fromMillis(intervalEnd.toMillis())
|
|
@@ -6806,8 +6895,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6806
6895
|
*/
|
|
6807
6896
|
static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
|
|
6808
6897
|
const workingIntervals = [];
|
|
6809
|
-
let start =
|
|
6810
|
-
const end =
|
|
6898
|
+
let start = import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz });
|
|
6899
|
+
const end = import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz });
|
|
6811
6900
|
while (start <= end) {
|
|
6812
6901
|
const dayOfWeek = start.weekday;
|
|
6813
6902
|
const dayName = [
|
|
@@ -6830,8 +6919,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6830
6919
|
});
|
|
6831
6920
|
const workEnd = start.set({ hour: endHours, minute: endMinutes });
|
|
6832
6921
|
if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
|
|
6833
|
-
const intervalStart = workStart <
|
|
6834
|
-
const intervalEnd = workEnd >
|
|
6922
|
+
const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
|
|
6923
|
+
const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
|
|
6835
6924
|
workingIntervals.push({
|
|
6836
6925
|
start: import_firestore2.Timestamp.fromMillis(intervalStart.toMillis()),
|
|
6837
6926
|
end: import_firestore2.Timestamp.fromMillis(intervalEnd.toMillis())
|
|
@@ -6895,7 +6984,7 @@ var BookingAvailabilityCalculator = class {
|
|
|
6895
6984
|
for (const interval of intervals) {
|
|
6896
6985
|
const intervalStart = interval.start.toDate();
|
|
6897
6986
|
const intervalEnd = interval.end.toDate();
|
|
6898
|
-
let slotStart =
|
|
6987
|
+
let slotStart = import_luxon2.DateTime.fromMillis(intervalStart.getTime(), { zone: tz });
|
|
6899
6988
|
const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
|
|
6900
6989
|
const minutesRemainder = minutesIntoDay % intervalMinutes;
|
|
6901
6990
|
if (minutesRemainder > 0) {
|
|
@@ -6927,8 +7016,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6927
7016
|
*/
|
|
6928
7017
|
static isSlotFullyAvailable(slotStart, slotEnd, intervals, tz) {
|
|
6929
7018
|
return intervals.some((interval) => {
|
|
6930
|
-
const intervalStart =
|
|
6931
|
-
const intervalEnd =
|
|
7019
|
+
const intervalStart = import_luxon2.DateTime.fromMillis(interval.start.toMillis(), { zone: tz });
|
|
7020
|
+
const intervalEnd = import_luxon2.DateTime.fromMillis(interval.end.toMillis(), { zone: tz });
|
|
6932
7021
|
return slotStart >= intervalStart && slotEnd <= intervalEnd;
|
|
6933
7022
|
});
|
|
6934
7023
|
}
|
package/dist/admin/index.mjs
CHANGED
|
@@ -1703,6 +1703,9 @@ var CalendarAdminService = class {
|
|
|
1703
1703
|
}
|
|
1704
1704
|
};
|
|
1705
1705
|
|
|
1706
|
+
// src/admin/mailing/appointment/appointment.mailing.service.ts
|
|
1707
|
+
import { DateTime } from "luxon";
|
|
1708
|
+
|
|
1706
1709
|
// src/admin/mailing/base.mailing.service.ts
|
|
1707
1710
|
import * as admin5 from "firebase-admin";
|
|
1708
1711
|
var BaseMailingService = class {
|
|
@@ -2261,6 +2264,62 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2261
2264
|
this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
2262
2265
|
Logger.info("[AppointmentMailingService] Initialized.");
|
|
2263
2266
|
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
2269
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
2270
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
2271
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
2272
|
+
* @returns Formatted string in clinic's local timezone
|
|
2273
|
+
*/
|
|
2274
|
+
formatTimestampInClinicTimezone(timestamp, clinicTimezone, format = "datetime") {
|
|
2275
|
+
try {
|
|
2276
|
+
const dateTimeInClinicTz = DateTime.fromMillis(timestamp.toMillis(), {
|
|
2277
|
+
zone: clinicTimezone
|
|
2278
|
+
});
|
|
2279
|
+
switch (format) {
|
|
2280
|
+
case "date":
|
|
2281
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATE_FULL);
|
|
2282
|
+
case "time":
|
|
2283
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.TIME_SIMPLE);
|
|
2284
|
+
case "datetime":
|
|
2285
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
|
|
2286
|
+
default:
|
|
2287
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
|
|
2288
|
+
}
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
Logger.error("[AppointmentMailingService] Error formatting timestamp in clinic timezone:", {
|
|
2291
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2292
|
+
clinicTimezone
|
|
2293
|
+
});
|
|
2294
|
+
return timestamp.toDate().toLocaleString();
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Gets a user-friendly display name for a timezone
|
|
2299
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
2300
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
2301
|
+
*/
|
|
2302
|
+
getTimezoneDisplayName(timezone) {
|
|
2303
|
+
try {
|
|
2304
|
+
const timezoneMap = {
|
|
2305
|
+
"Europe/Zurich": "Clinic Time - Central European Time",
|
|
2306
|
+
"Europe/London": "Clinic Time - GMT/BST",
|
|
2307
|
+
"America/New_York": "Clinic Time - Eastern Time",
|
|
2308
|
+
"America/Los_Angeles": "Clinic Time - Pacific Time",
|
|
2309
|
+
"Asia/Dubai": "Clinic Time - Gulf Standard Time",
|
|
2310
|
+
"Asia/Karachi": "Clinic Time - Pakistan Standard Time",
|
|
2311
|
+
"Asia/Kolkata": "Clinic Time - India Standard Time",
|
|
2312
|
+
"Australia/Sydney": "Clinic Time - Australian Eastern Time"
|
|
2313
|
+
};
|
|
2314
|
+
return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
Logger.error("[AppointmentMailingService] Error getting timezone display name:", {
|
|
2317
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2318
|
+
timezone
|
|
2319
|
+
});
|
|
2320
|
+
return `Clinic Time - ${timezone}`;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2264
2323
|
async sendAppointmentConfirmedEmail(data) {
|
|
2265
2324
|
var _a, _b, _c, _d, _e;
|
|
2266
2325
|
Logger.info(
|
|
@@ -2274,11 +2333,26 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2274
2333
|
});
|
|
2275
2334
|
throw new Error("Recipient email address is missing.");
|
|
2276
2335
|
}
|
|
2336
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
2337
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time", {
|
|
2338
|
+
clinicTimezone,
|
|
2339
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
2340
|
+
});
|
|
2341
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
2342
|
+
data.appointment.appointmentStartTime,
|
|
2343
|
+
clinicTimezone,
|
|
2344
|
+
"time"
|
|
2345
|
+
);
|
|
2346
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
2277
2347
|
const templateVariables = {
|
|
2278
2348
|
patientName: data.appointment.patientInfo.fullName,
|
|
2279
2349
|
procedureName: data.appointment.procedureInfo.name,
|
|
2280
|
-
appointmentDate:
|
|
2281
|
-
|
|
2350
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
2351
|
+
data.appointment.appointmentStartTime,
|
|
2352
|
+
clinicTimezone,
|
|
2353
|
+
"date"
|
|
2354
|
+
),
|
|
2355
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
2282
2356
|
practitionerName: data.appointment.practitionerInfo.name,
|
|
2283
2357
|
clinicName: data.appointment.clinicInfo.name
|
|
2284
2358
|
};
|
|
@@ -2325,12 +2399,27 @@ var AppointmentMailingService = class extends BaseMailingService {
|
|
|
2325
2399
|
);
|
|
2326
2400
|
throw new Error("Clinic contact email address is missing.");
|
|
2327
2401
|
}
|
|
2402
|
+
const clinicTimezone = data.appointment.clinic_tz || "UTC";
|
|
2403
|
+
Logger.debug("[AppointmentMailingService] Formatting appointment time for clinic", {
|
|
2404
|
+
clinicTimezone,
|
|
2405
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
|
|
2406
|
+
});
|
|
2407
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
2408
|
+
data.appointment.appointmentStartTime,
|
|
2409
|
+
clinicTimezone,
|
|
2410
|
+
"time"
|
|
2411
|
+
);
|
|
2412
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
2328
2413
|
const templateVariables = {
|
|
2329
2414
|
clinicName: data.clinicProfile.name,
|
|
2330
2415
|
patientName: data.appointment.patientInfo.fullName,
|
|
2331
2416
|
procedureName: data.appointment.procedureInfo.name,
|
|
2332
|
-
appointmentDate:
|
|
2333
|
-
|
|
2417
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
2418
|
+
data.appointment.appointmentStartTime,
|
|
2419
|
+
clinicTimezone,
|
|
2420
|
+
"date"
|
|
2421
|
+
),
|
|
2422
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
2334
2423
|
practitionerName: data.appointment.practitionerInfo.name
|
|
2335
2424
|
};
|
|
2336
2425
|
const html = this.renderTemplate(clinicAppointmentRequestedTemplate, templateVariables);
|
|
@@ -6514,7 +6603,7 @@ var ReviewsAggregationService = class {
|
|
|
6514
6603
|
|
|
6515
6604
|
// src/admin/booking/booking.calculator.ts
|
|
6516
6605
|
import { Timestamp } from "firebase/firestore";
|
|
6517
|
-
import { DateTime } from "luxon";
|
|
6606
|
+
import { DateTime as DateTime2 } from "luxon";
|
|
6518
6607
|
var BookingAvailabilityCalculator = class {
|
|
6519
6608
|
/**
|
|
6520
6609
|
* Calculate available booking slots based on the provided data
|
|
@@ -6605,8 +6694,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6605
6694
|
*/
|
|
6606
6695
|
static createWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
|
|
6607
6696
|
const workingIntervals = [];
|
|
6608
|
-
let start =
|
|
6609
|
-
const end =
|
|
6697
|
+
let start = DateTime2.fromMillis(startDate.getTime(), { zone: tz });
|
|
6698
|
+
const end = DateTime2.fromMillis(endDate.getTime(), { zone: tz });
|
|
6610
6699
|
while (start <= end) {
|
|
6611
6700
|
const dayOfWeek = start.weekday;
|
|
6612
6701
|
const dayName = [
|
|
@@ -6636,8 +6725,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6636
6725
|
millisecond: 0
|
|
6637
6726
|
});
|
|
6638
6727
|
if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
|
|
6639
|
-
const intervalStart = workStart <
|
|
6640
|
-
const intervalEnd = workEnd >
|
|
6728
|
+
const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
|
|
6729
|
+
const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
|
|
6641
6730
|
workingIntervals.push({
|
|
6642
6731
|
start: Timestamp.fromMillis(intervalStart.toMillis()),
|
|
6643
6732
|
end: Timestamp.fromMillis(intervalEnd.toMillis())
|
|
@@ -6744,8 +6833,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6744
6833
|
*/
|
|
6745
6834
|
static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
|
|
6746
6835
|
const workingIntervals = [];
|
|
6747
|
-
let start =
|
|
6748
|
-
const end =
|
|
6836
|
+
let start = DateTime2.fromMillis(startDate.getTime(), { zone: tz });
|
|
6837
|
+
const end = DateTime2.fromMillis(endDate.getTime(), { zone: tz });
|
|
6749
6838
|
while (start <= end) {
|
|
6750
6839
|
const dayOfWeek = start.weekday;
|
|
6751
6840
|
const dayName = [
|
|
@@ -6768,8 +6857,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6768
6857
|
});
|
|
6769
6858
|
const workEnd = start.set({ hour: endHours, minute: endMinutes });
|
|
6770
6859
|
if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
|
|
6771
|
-
const intervalStart = workStart <
|
|
6772
|
-
const intervalEnd = workEnd >
|
|
6860
|
+
const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
|
|
6861
|
+
const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
|
|
6773
6862
|
workingIntervals.push({
|
|
6774
6863
|
start: Timestamp.fromMillis(intervalStart.toMillis()),
|
|
6775
6864
|
end: Timestamp.fromMillis(intervalEnd.toMillis())
|
|
@@ -6833,7 +6922,7 @@ var BookingAvailabilityCalculator = class {
|
|
|
6833
6922
|
for (const interval of intervals) {
|
|
6834
6923
|
const intervalStart = interval.start.toDate();
|
|
6835
6924
|
const intervalEnd = interval.end.toDate();
|
|
6836
|
-
let slotStart =
|
|
6925
|
+
let slotStart = DateTime2.fromMillis(intervalStart.getTime(), { zone: tz });
|
|
6837
6926
|
const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
|
|
6838
6927
|
const minutesRemainder = minutesIntoDay % intervalMinutes;
|
|
6839
6928
|
if (minutesRemainder > 0) {
|
|
@@ -6865,8 +6954,8 @@ var BookingAvailabilityCalculator = class {
|
|
|
6865
6954
|
*/
|
|
6866
6955
|
static isSlotFullyAvailable(slotStart, slotEnd, intervals, tz) {
|
|
6867
6956
|
return intervals.some((interval) => {
|
|
6868
|
-
const intervalStart =
|
|
6869
|
-
const intervalEnd =
|
|
6957
|
+
const intervalStart = DateTime2.fromMillis(interval.start.toMillis(), { zone: tz });
|
|
6958
|
+
const intervalEnd = DateTime2.fromMillis(interval.end.toMillis(), { zone: tz });
|
|
6870
6959
|
return slotStart >= intervalStart && slotEnd <= intervalEnd;
|
|
6871
6960
|
});
|
|
6872
6961
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as admin from 'firebase-admin';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
2
3
|
import { BaseMailingService, NewMailgunClient } from '../base.mailing.service';
|
|
3
4
|
import { Logger } from '../../logger';
|
|
4
5
|
import { Appointment } from '../../../types/appointment';
|
|
@@ -466,6 +467,78 @@ export class AppointmentMailingService extends BaseMailingService {
|
|
|
466
467
|
Logger.info('[AppointmentMailingService] Initialized.');
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Formats a Firestore Timestamp in the clinic's timezone
|
|
472
|
+
* @param timestamp - Firestore Timestamp (UTC)
|
|
473
|
+
* @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
474
|
+
* @param format - Format type: 'date', 'time', or 'datetime'
|
|
475
|
+
* @returns Formatted string in clinic's local timezone
|
|
476
|
+
*/
|
|
477
|
+
private formatTimestampInClinicTimezone(
|
|
478
|
+
timestamp: admin.firestore.Timestamp,
|
|
479
|
+
clinicTimezone: string,
|
|
480
|
+
format: 'date' | 'time' | 'datetime' = 'datetime',
|
|
481
|
+
): string {
|
|
482
|
+
try {
|
|
483
|
+
// Convert UTC timestamp to clinic's timezone
|
|
484
|
+
const dateTimeInClinicTz = DateTime.fromMillis(timestamp.toMillis(), {
|
|
485
|
+
zone: clinicTimezone,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Format based on requested type
|
|
489
|
+
switch (format) {
|
|
490
|
+
case 'date':
|
|
491
|
+
// Format: "Monday, November 5, 2025"
|
|
492
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATE_FULL);
|
|
493
|
+
case 'time':
|
|
494
|
+
// Format: "2:30 PM"
|
|
495
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.TIME_SIMPLE);
|
|
496
|
+
case 'datetime':
|
|
497
|
+
// Format: "Monday, November 5, 2025 at 2:30 PM"
|
|
498
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
|
|
499
|
+
default:
|
|
500
|
+
return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
Logger.error('[AppointmentMailingService] Error formatting timestamp in clinic timezone:', {
|
|
504
|
+
error: error instanceof Error ? error.message : String(error),
|
|
505
|
+
clinicTimezone,
|
|
506
|
+
});
|
|
507
|
+
// Fallback to UTC formatting if timezone conversion fails
|
|
508
|
+
return timestamp.toDate().toLocaleString();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Gets a user-friendly display name for a timezone
|
|
514
|
+
* @param timezone - IANA timezone string (e.g., "Europe/Zurich")
|
|
515
|
+
* @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
|
|
516
|
+
*/
|
|
517
|
+
private getTimezoneDisplayName(timezone: string): string {
|
|
518
|
+
try {
|
|
519
|
+
// For common timezones, provide a more readable format
|
|
520
|
+
const timezoneMap: Record<string, string> = {
|
|
521
|
+
'Europe/Zurich': 'Clinic Time - Central European Time',
|
|
522
|
+
'Europe/London': 'Clinic Time - GMT/BST',
|
|
523
|
+
'America/New_York': 'Clinic Time - Eastern Time',
|
|
524
|
+
'America/Los_Angeles': 'Clinic Time - Pacific Time',
|
|
525
|
+
'Asia/Dubai': 'Clinic Time - Gulf Standard Time',
|
|
526
|
+
'Asia/Karachi': 'Clinic Time - Pakistan Standard Time',
|
|
527
|
+
'Asia/Kolkata': 'Clinic Time - India Standard Time',
|
|
528
|
+
'Australia/Sydney': 'Clinic Time - Australian Eastern Time',
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Return mapped name if available, otherwise use the IANA timezone
|
|
532
|
+
return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
Logger.error('[AppointmentMailingService] Error getting timezone display name:', {
|
|
535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
536
|
+
timezone,
|
|
537
|
+
});
|
|
538
|
+
return `Clinic Time - ${timezone}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
469
542
|
async sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any> {
|
|
470
543
|
Logger.info(
|
|
471
544
|
`[AppointmentMailingService] Preparing to send appointment confirmation email to ${data.recipientRole}: ${data.recipientProfile.id}`,
|
|
@@ -481,11 +554,31 @@ export class AppointmentMailingService extends BaseMailingService {
|
|
|
481
554
|
throw new Error('Recipient email address is missing.');
|
|
482
555
|
}
|
|
483
556
|
|
|
557
|
+
// Get clinic timezone from appointment data, default to UTC if not available
|
|
558
|
+
const clinicTimezone = data.appointment.clinic_tz || 'UTC';
|
|
559
|
+
|
|
560
|
+
Logger.debug('[AppointmentMailingService] Formatting appointment time', {
|
|
561
|
+
clinicTimezone,
|
|
562
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString(),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Format time with timezone label for clarity
|
|
566
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
567
|
+
data.appointment.appointmentStartTime,
|
|
568
|
+
clinicTimezone,
|
|
569
|
+
'time',
|
|
570
|
+
);
|
|
571
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
572
|
+
|
|
484
573
|
const templateVariables = {
|
|
485
574
|
patientName: data.appointment.patientInfo.fullName,
|
|
486
575
|
procedureName: data.appointment.procedureInfo.name,
|
|
487
|
-
appointmentDate:
|
|
488
|
-
|
|
576
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
577
|
+
data.appointment.appointmentStartTime,
|
|
578
|
+
clinicTimezone,
|
|
579
|
+
'date',
|
|
580
|
+
),
|
|
581
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
489
582
|
practitionerName: data.appointment.practitionerInfo.name,
|
|
490
583
|
clinicName: data.appointment.clinicInfo.name,
|
|
491
584
|
};
|
|
@@ -539,12 +632,32 @@ export class AppointmentMailingService extends BaseMailingService {
|
|
|
539
632
|
throw new Error('Clinic contact email address is missing.');
|
|
540
633
|
}
|
|
541
634
|
|
|
635
|
+
// Get clinic timezone from appointment data, default to UTC if not available
|
|
636
|
+
const clinicTimezone = data.appointment.clinic_tz || 'UTC';
|
|
637
|
+
|
|
638
|
+
Logger.debug('[AppointmentMailingService] Formatting appointment time for clinic', {
|
|
639
|
+
clinicTimezone,
|
|
640
|
+
utcTime: data.appointment.appointmentStartTime.toDate().toISOString(),
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Format time with timezone label for clarity
|
|
644
|
+
const formattedTime = this.formatTimestampInClinicTimezone(
|
|
645
|
+
data.appointment.appointmentStartTime,
|
|
646
|
+
clinicTimezone,
|
|
647
|
+
'time',
|
|
648
|
+
);
|
|
649
|
+
const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
|
|
650
|
+
|
|
542
651
|
const templateVariables = {
|
|
543
652
|
clinicName: data.clinicProfile.name,
|
|
544
653
|
patientName: data.appointment.patientInfo.fullName,
|
|
545
654
|
procedureName: data.appointment.procedureInfo.name,
|
|
546
|
-
appointmentDate:
|
|
547
|
-
|
|
655
|
+
appointmentDate: this.formatTimestampInClinicTimezone(
|
|
656
|
+
data.appointment.appointmentStartTime,
|
|
657
|
+
clinicTimezone,
|
|
658
|
+
'date',
|
|
659
|
+
),
|
|
660
|
+
appointmentTime: `${formattedTime} (${timezoneName})`,
|
|
548
661
|
practitionerName: data.appointment.practitionerInfo.name,
|
|
549
662
|
};
|
|
550
663
|
|