@blackcode_sa/metaestetics-api 1.7.33 → 1.7.34
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 +238 -57
- package/dist/admin/index.d.ts +238 -57
- package/dist/admin/index.js +1167 -7
- package/dist/admin/index.mjs +1166 -7
- package/package.json +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +247 -12
- package/src/admin/index.ts +12 -1
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -0
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -0
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -0
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -0
package/dist/admin/index.js
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
CalendarAdminService: () => CalendarAdminService,
|
|
41
41
|
ClinicAggregationService: () => ClinicAggregationService,
|
|
42
42
|
DocumentManagerAdminService: () => DocumentManagerAdminService,
|
|
43
|
+
ExistingPractitionerInviteMailingService: () => ExistingPractitionerInviteMailingService,
|
|
43
44
|
FilledFormsAggregationService: () => FilledFormsAggregationService,
|
|
44
45
|
Logger: () => Logger,
|
|
45
46
|
MediaType: () => MediaType,
|
|
@@ -1789,22 +1790,44 @@ var PractitionerInviteAggregationService = class {
|
|
|
1789
1790
|
/**
|
|
1790
1791
|
* Constructor for PractitionerInviteAggregationService.
|
|
1791
1792
|
* @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
|
|
1793
|
+
* @param mailingService Optional mailing service for sending emails
|
|
1792
1794
|
*/
|
|
1793
|
-
constructor(firestore17) {
|
|
1795
|
+
constructor(firestore17, mailingService) {
|
|
1794
1796
|
this.db = firestore17 || admin5.firestore();
|
|
1797
|
+
this.mailingService = mailingService;
|
|
1795
1798
|
Logger.info("[PractitionerInviteAggregationService] Initialized.");
|
|
1796
1799
|
}
|
|
1797
1800
|
/**
|
|
1798
1801
|
* Handles side effects when a practitioner invite is first created.
|
|
1799
1802
|
* This function would typically be called by a Firestore onCreate trigger.
|
|
1800
1803
|
* @param {PractitionerInvite} invite - The newly created PractitionerInvite object.
|
|
1804
|
+
* @param {object} emailConfig - Optional email configuration for sending invite emails
|
|
1801
1805
|
* @returns {Promise<void>}
|
|
1802
1806
|
*/
|
|
1803
|
-
async handleInviteCreate(invite) {
|
|
1807
|
+
async handleInviteCreate(invite, emailConfig) {
|
|
1804
1808
|
Logger.info(
|
|
1805
1809
|
`[PractitionerInviteAggService] Handling CREATE for invite: ${invite.id}, practitioner: ${invite.practitionerId}, clinic: ${invite.clinicId}, status: ${invite.status}`
|
|
1806
1810
|
);
|
|
1807
1811
|
try {
|
|
1812
|
+
if (this.mailingService && emailConfig && invite.status === "pending" /* PENDING */) {
|
|
1813
|
+
Logger.info(
|
|
1814
|
+
`[PractitionerInviteAggService] Sending invitation email for invite: ${invite.id}`
|
|
1815
|
+
);
|
|
1816
|
+
try {
|
|
1817
|
+
await this.mailingService.handleInviteCreationEvent(
|
|
1818
|
+
invite,
|
|
1819
|
+
emailConfig
|
|
1820
|
+
);
|
|
1821
|
+
Logger.info(
|
|
1822
|
+
`[PractitionerInviteAggService] Successfully sent invitation email for invite: ${invite.id}`
|
|
1823
|
+
);
|
|
1824
|
+
} catch (emailError) {
|
|
1825
|
+
Logger.error(
|
|
1826
|
+
`[PractitionerInviteAggService] Error sending invitation email for invite ${invite.id}:`,
|
|
1827
|
+
emailError
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1808
1831
|
Logger.info(
|
|
1809
1832
|
`[PractitionerInviteAggService] Successfully processed CREATE for invite: ${invite.id}`
|
|
1810
1833
|
);
|
|
@@ -1821,9 +1844,10 @@ var PractitionerInviteAggregationService = class {
|
|
|
1821
1844
|
* This function would typically be called by a Firestore onUpdate trigger.
|
|
1822
1845
|
* @param {PractitionerInvite} before - The PractitionerInvite object before the update.
|
|
1823
1846
|
* @param {PractitionerInvite} after - The PractitionerInvite object after the update.
|
|
1847
|
+
* @param {object} emailConfig - Optional email configuration for sending notification emails
|
|
1824
1848
|
* @returns {Promise<void>}
|
|
1825
1849
|
*/
|
|
1826
|
-
async handleInviteUpdate(before, after) {
|
|
1850
|
+
async handleInviteUpdate(before, after, emailConfig) {
|
|
1827
1851
|
Logger.info(
|
|
1828
1852
|
`[PractitionerInviteAggService] Handling UPDATE for invite: ${after.id}. Status ${before.status} -> ${after.status}`
|
|
1829
1853
|
);
|
|
@@ -1837,12 +1861,12 @@ var PractitionerInviteAggregationService = class {
|
|
|
1837
1861
|
Logger.info(
|
|
1838
1862
|
`[PractitionerInviteAggService] Invite ${after.id} PENDING -> ACCEPTED. Adding practitioner to clinic.`
|
|
1839
1863
|
);
|
|
1840
|
-
await this.handleInviteAccepted(after);
|
|
1864
|
+
await this.handleInviteAccepted(after, emailConfig);
|
|
1841
1865
|
} else if (before.status === "pending" /* PENDING */ && after.status === "rejected" /* REJECTED */) {
|
|
1842
1866
|
Logger.info(
|
|
1843
1867
|
`[PractitionerInviteAggService] Invite ${after.id} PENDING -> REJECTED.`
|
|
1844
1868
|
);
|
|
1845
|
-
await this.handleInviteRejected(after);
|
|
1869
|
+
await this.handleInviteRejected(after, emailConfig);
|
|
1846
1870
|
} else if (before.status === "pending" /* PENDING */ && after.status === "cancelled" /* CANCELLED */) {
|
|
1847
1871
|
Logger.info(
|
|
1848
1872
|
`[PractitionerInviteAggService] Invite ${after.id} PENDING -> CANCELLED.`
|
|
@@ -1887,9 +1911,10 @@ var PractitionerInviteAggregationService = class {
|
|
|
1887
1911
|
* Handles the business logic when a practitioner accepts an invite.
|
|
1888
1912
|
* This includes adding the practitioner to the clinic and the clinic to the practitioner.
|
|
1889
1913
|
* @param {PractitionerInvite} invite - The accepted invite
|
|
1914
|
+
* @param {object} emailConfig - Optional email configuration for sending notification emails
|
|
1890
1915
|
* @returns {Promise<void>}
|
|
1891
1916
|
*/
|
|
1892
|
-
async handleInviteAccepted(invite) {
|
|
1917
|
+
async handleInviteAccepted(invite, emailConfig) {
|
|
1893
1918
|
var _a, _b, _c, _d;
|
|
1894
1919
|
Logger.info(
|
|
1895
1920
|
`[PractitionerInviteAggService] Processing accepted invite ${invite.id} for practitioner ${invite.practitionerId} and clinic ${invite.clinicId}`
|
|
@@ -1950,6 +1975,27 @@ var PractitionerInviteAggregationService = class {
|
|
|
1950
1975
|
);
|
|
1951
1976
|
await this.updatePractitionerWorkingHours(practitioner.id, invite);
|
|
1952
1977
|
}
|
|
1978
|
+
if (this.mailingService && emailConfig) {
|
|
1979
|
+
Logger.info(
|
|
1980
|
+
`[PractitionerInviteAggService] Sending acceptance notification email for invite: ${invite.id}`
|
|
1981
|
+
);
|
|
1982
|
+
try {
|
|
1983
|
+
await this.sendAcceptanceNotificationEmail(
|
|
1984
|
+
invite,
|
|
1985
|
+
practitioner,
|
|
1986
|
+
clinic,
|
|
1987
|
+
emailConfig
|
|
1988
|
+
);
|
|
1989
|
+
Logger.info(
|
|
1990
|
+
`[PractitionerInviteAggService] Successfully sent acceptance notification email for invite: ${invite.id}`
|
|
1991
|
+
);
|
|
1992
|
+
} catch (emailError) {
|
|
1993
|
+
Logger.error(
|
|
1994
|
+
`[PractitionerInviteAggService] Error sending acceptance notification email for invite ${invite.id}:`,
|
|
1995
|
+
emailError
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1953
1999
|
Logger.info(
|
|
1954
2000
|
`[PractitionerInviteAggService] Successfully processed accepted invite ${invite.id}`
|
|
1955
2001
|
);
|
|
@@ -1964,13 +2010,41 @@ var PractitionerInviteAggregationService = class {
|
|
|
1964
2010
|
/**
|
|
1965
2011
|
* Handles the business logic when a practitioner rejects an invite.
|
|
1966
2012
|
* @param {PractitionerInvite} invite - The rejected invite
|
|
2013
|
+
* @param {object} emailConfig - Optional email configuration for sending notification emails
|
|
1967
2014
|
* @returns {Promise<void>}
|
|
1968
2015
|
*/
|
|
1969
|
-
async handleInviteRejected(invite) {
|
|
2016
|
+
async handleInviteRejected(invite, emailConfig) {
|
|
1970
2017
|
Logger.info(
|
|
1971
2018
|
`[PractitionerInviteAggService] Processing rejected invite ${invite.id}`
|
|
1972
2019
|
);
|
|
1973
2020
|
try {
|
|
2021
|
+
if (this.mailingService && emailConfig) {
|
|
2022
|
+
Logger.info(
|
|
2023
|
+
`[PractitionerInviteAggService] Sending rejection notification email for invite: ${invite.id}`
|
|
2024
|
+
);
|
|
2025
|
+
try {
|
|
2026
|
+
const [practitioner, clinic] = await Promise.all([
|
|
2027
|
+
this.fetchPractitionerById(invite.practitionerId),
|
|
2028
|
+
this.fetchClinicById(invite.clinicId)
|
|
2029
|
+
]);
|
|
2030
|
+
if (practitioner && clinic) {
|
|
2031
|
+
await this.sendRejectionNotificationEmail(
|
|
2032
|
+
invite,
|
|
2033
|
+
practitioner,
|
|
2034
|
+
clinic,
|
|
2035
|
+
emailConfig
|
|
2036
|
+
);
|
|
2037
|
+
Logger.info(
|
|
2038
|
+
`[PractitionerInviteAggService] Successfully sent rejection notification email for invite: ${invite.id}`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
} catch (emailError) {
|
|
2042
|
+
Logger.error(
|
|
2043
|
+
`[PractitionerInviteAggService] Error sending rejection notification email for invite ${invite.id}:`,
|
|
2044
|
+
emailError
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
1974
2048
|
Logger.info(
|
|
1975
2049
|
`[PractitionerInviteAggService] Successfully processed rejected invite ${invite.id}`
|
|
1976
2050
|
);
|
|
@@ -2196,6 +2270,92 @@ var PractitionerInviteAggregationService = class {
|
|
|
2196
2270
|
return null;
|
|
2197
2271
|
}
|
|
2198
2272
|
}
|
|
2273
|
+
// --- Email Helper Methods ---
|
|
2274
|
+
/**
|
|
2275
|
+
* Sends acceptance notification email to clinic admin
|
|
2276
|
+
* @param invite The accepted invite
|
|
2277
|
+
* @param practitioner The practitioner who accepted
|
|
2278
|
+
* @param clinic The clinic that sent the invite
|
|
2279
|
+
* @param emailConfig Email configuration
|
|
2280
|
+
*/
|
|
2281
|
+
async sendAcceptanceNotificationEmail(invite, practitioner, clinic, emailConfig) {
|
|
2282
|
+
var _a, _b, _c;
|
|
2283
|
+
if (!this.mailingService) return;
|
|
2284
|
+
const notificationData = {
|
|
2285
|
+
invite,
|
|
2286
|
+
practitioner: {
|
|
2287
|
+
firstName: practitioner.basicInfo.firstName || "",
|
|
2288
|
+
lastName: practitioner.basicInfo.lastName || "",
|
|
2289
|
+
specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
|
|
2290
|
+
(s) => s.name || s
|
|
2291
|
+
)) || [],
|
|
2292
|
+
profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null,
|
|
2293
|
+
experienceYears: void 0
|
|
2294
|
+
// This would need to be calculated or stored in practitioner data
|
|
2295
|
+
},
|
|
2296
|
+
clinic: {
|
|
2297
|
+
name: clinic.name,
|
|
2298
|
+
adminName: void 0,
|
|
2299
|
+
// This would need to be fetched from clinic admin data
|
|
2300
|
+
adminEmail: clinic.contactInfo.email
|
|
2301
|
+
},
|
|
2302
|
+
context: {
|
|
2303
|
+
invitationDate: invite.createdAt.toDate().toLocaleDateString(),
|
|
2304
|
+
responseDate: ((_c = invite.acceptedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
2305
|
+
},
|
|
2306
|
+
urls: {
|
|
2307
|
+
clinicDashboardUrl: emailConfig.clinicDashboardUrl,
|
|
2308
|
+
practitionerProfileUrl: emailConfig.practitionerProfileUrl
|
|
2309
|
+
},
|
|
2310
|
+
options: {
|
|
2311
|
+
fromAddress: emailConfig.fromAddress,
|
|
2312
|
+
mailgunDomain: emailConfig.domain
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
await this.mailingService.sendAcceptedNotificationEmail(notificationData);
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Sends rejection notification email to clinic admin
|
|
2319
|
+
* @param invite The rejected invite
|
|
2320
|
+
* @param practitioner The practitioner who rejected
|
|
2321
|
+
* @param clinic The clinic that sent the invite
|
|
2322
|
+
* @param emailConfig Email configuration
|
|
2323
|
+
*/
|
|
2324
|
+
async sendRejectionNotificationEmail(invite, practitioner, clinic, emailConfig) {
|
|
2325
|
+
var _a, _b, _c;
|
|
2326
|
+
if (!this.mailingService) return;
|
|
2327
|
+
const notificationData = {
|
|
2328
|
+
invite,
|
|
2329
|
+
practitioner: {
|
|
2330
|
+
firstName: practitioner.basicInfo.firstName || "",
|
|
2331
|
+
lastName: practitioner.basicInfo.lastName || "",
|
|
2332
|
+
specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
|
|
2333
|
+
(s) => s.name || s
|
|
2334
|
+
)) || [],
|
|
2335
|
+
profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
|
|
2336
|
+
},
|
|
2337
|
+
clinic: {
|
|
2338
|
+
name: clinic.name,
|
|
2339
|
+
adminName: void 0,
|
|
2340
|
+
// This would need to be fetched from clinic admin data
|
|
2341
|
+
adminEmail: clinic.contactInfo.email
|
|
2342
|
+
},
|
|
2343
|
+
context: {
|
|
2344
|
+
invitationDate: invite.createdAt.toDate().toLocaleDateString(),
|
|
2345
|
+
responseDate: ((_c = invite.rejectedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString(),
|
|
2346
|
+
rejectionReason: invite.rejectionReason || void 0
|
|
2347
|
+
},
|
|
2348
|
+
urls: {
|
|
2349
|
+
clinicDashboardUrl: emailConfig.clinicDashboardUrl,
|
|
2350
|
+
findPractitionersUrl: emailConfig.findPractitionersUrl
|
|
2351
|
+
},
|
|
2352
|
+
options: {
|
|
2353
|
+
fromAddress: emailConfig.fromAddress,
|
|
2354
|
+
mailgunDomain: emailConfig.domain
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
await this.mailingService.sendRejectedNotificationEmail(notificationData);
|
|
2358
|
+
}
|
|
2199
2359
|
};
|
|
2200
2360
|
|
|
2201
2361
|
// src/admin/aggregation/procedure/procedure.aggregation.service.ts
|
|
@@ -5996,6 +6156,1005 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
|
|
|
5996
6156
|
}
|
|
5997
6157
|
};
|
|
5998
6158
|
|
|
6159
|
+
// src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts
|
|
6160
|
+
var existingPractitionerInvitationTemplate = `
|
|
6161
|
+
<!DOCTYPE html>
|
|
6162
|
+
<html lang="en">
|
|
6163
|
+
<head>
|
|
6164
|
+
<meta charset="UTF-8">
|
|
6165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6166
|
+
<title>Invitation to Join {{clinicName}}</title>
|
|
6167
|
+
<style>
|
|
6168
|
+
body {
|
|
6169
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
6170
|
+
line-height: 1.6;
|
|
6171
|
+
color: #333;
|
|
6172
|
+
max-width: 600px;
|
|
6173
|
+
margin: 0 auto;
|
|
6174
|
+
padding: 20px;
|
|
6175
|
+
background-color: #f4f4f4;
|
|
6176
|
+
}
|
|
6177
|
+
.container {
|
|
6178
|
+
background-color: #ffffff;
|
|
6179
|
+
padding: 30px;
|
|
6180
|
+
border-radius: 10px;
|
|
6181
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
6182
|
+
}
|
|
6183
|
+
.header {
|
|
6184
|
+
text-align: center;
|
|
6185
|
+
margin-bottom: 30px;
|
|
6186
|
+
border-bottom: 2px solid #007bff;
|
|
6187
|
+
padding-bottom: 20px;
|
|
6188
|
+
}
|
|
6189
|
+
.logo {
|
|
6190
|
+
font-size: 28px;
|
|
6191
|
+
font-weight: bold;
|
|
6192
|
+
color: #007bff;
|
|
6193
|
+
margin-bottom: 10px;
|
|
6194
|
+
}
|
|
6195
|
+
h1 {
|
|
6196
|
+
color: #007bff;
|
|
6197
|
+
text-align: center;
|
|
6198
|
+
margin-bottom: 20px;
|
|
6199
|
+
}
|
|
6200
|
+
.highlight {
|
|
6201
|
+
background-color: #e7f3ff;
|
|
6202
|
+
padding: 15px;
|
|
6203
|
+
border-left: 4px solid #007bff;
|
|
6204
|
+
margin: 20px 0;
|
|
6205
|
+
}
|
|
6206
|
+
.button {
|
|
6207
|
+
display: inline-block;
|
|
6208
|
+
background-color: #28a745;
|
|
6209
|
+
color: white;
|
|
6210
|
+
padding: 12px 25px;
|
|
6211
|
+
text-decoration: none;
|
|
6212
|
+
border-radius: 5px;
|
|
6213
|
+
margin: 10px 5px;
|
|
6214
|
+
font-weight: bold;
|
|
6215
|
+
text-align: center;
|
|
6216
|
+
}
|
|
6217
|
+
.button.reject {
|
|
6218
|
+
background-color: #dc3545;
|
|
6219
|
+
}
|
|
6220
|
+
.button:hover {
|
|
6221
|
+
opacity: 0.9;
|
|
6222
|
+
}
|
|
6223
|
+
.action-section {
|
|
6224
|
+
text-align: center;
|
|
6225
|
+
margin: 30px 0;
|
|
6226
|
+
padding: 20px;
|
|
6227
|
+
background-color: #f8f9fa;
|
|
6228
|
+
border-radius: 5px;
|
|
6229
|
+
}
|
|
6230
|
+
.details {
|
|
6231
|
+
background-color: #f8f9fa;
|
|
6232
|
+
padding: 15px;
|
|
6233
|
+
border-radius: 5px;
|
|
6234
|
+
margin: 15px 0;
|
|
6235
|
+
}
|
|
6236
|
+
.footer {
|
|
6237
|
+
margin-top: 30px;
|
|
6238
|
+
padding-top: 20px;
|
|
6239
|
+
border-top: 1px solid #eee;
|
|
6240
|
+
font-size: 14px;
|
|
6241
|
+
color: #666;
|
|
6242
|
+
text-align: center;
|
|
6243
|
+
}
|
|
6244
|
+
.contact-info {
|
|
6245
|
+
margin-top: 15px;
|
|
6246
|
+
padding: 10px;
|
|
6247
|
+
background-color: #e9ecef;
|
|
6248
|
+
border-radius: 5px;
|
|
6249
|
+
}
|
|
6250
|
+
</style>
|
|
6251
|
+
</head>
|
|
6252
|
+
<body>
|
|
6253
|
+
<div class="container">
|
|
6254
|
+
<div class="header">
|
|
6255
|
+
<div class="logo">MetaEstetics</div>
|
|
6256
|
+
<p>Professional Medical Network</p>
|
|
6257
|
+
</div>
|
|
6258
|
+
|
|
6259
|
+
<h1>You're Invited to Join {{clinicName}}!</h1>
|
|
6260
|
+
|
|
6261
|
+
<p>Dear Dr. {{practitionerName}},</p>
|
|
6262
|
+
|
|
6263
|
+
<p>We hope this message finds you well. You have been invited to join <strong>{{clinicName}}</strong> as a practicing medical professional.</p>
|
|
6264
|
+
|
|
6265
|
+
<div class="highlight">
|
|
6266
|
+
<strong>Invitation Details:</strong>
|
|
6267
|
+
<div class="details">
|
|
6268
|
+
<p><strong>Clinic:</strong> {{clinicName}}</p>
|
|
6269
|
+
<p><strong>Location:</strong> {{clinicAddress}}</p>
|
|
6270
|
+
<p><strong>Proposed Working Hours:</strong> {{workingHours}}</p>
|
|
6271
|
+
<p><strong>Invitation Expires:</strong> {{expirationDate}}</p>
|
|
6272
|
+
</div>
|
|
6273
|
+
</div>
|
|
6274
|
+
|
|
6275
|
+
<p>By accepting this invitation, you will:</p>
|
|
6276
|
+
<ul>
|
|
6277
|
+
<li>Join the {{clinicName}} team as a featured practitioner</li>
|
|
6278
|
+
<li>Have your profile displayed on their clinic page</li>
|
|
6279
|
+
<li>Be available for appointments at the specified working hours</li>
|
|
6280
|
+
<li>Access their patient booking system and clinic resources</li>
|
|
6281
|
+
</ul>
|
|
6282
|
+
|
|
6283
|
+
<div class="action-section">
|
|
6284
|
+
<p><strong>Please respond to this invitation:</strong></p>
|
|
6285
|
+
<a href="{{acceptUrl}}" class="button">Accept Invitation</a>
|
|
6286
|
+
<a href="{{rejectUrl}}" class="button reject">Decline Invitation</a>
|
|
6287
|
+
</div>
|
|
6288
|
+
|
|
6289
|
+
<div class="contact-info">
|
|
6290
|
+
<p><strong>Questions?</strong> Contact the clinic administrator:</p>
|
|
6291
|
+
<p>\u{1F4E7} {{contactEmail}}<br>
|
|
6292
|
+
\u{1F4DE} {{contactPhone}}</p>
|
|
6293
|
+
</div>
|
|
6294
|
+
|
|
6295
|
+
<p>We look forward to welcoming you to the {{clinicName}} team!</p>
|
|
6296
|
+
|
|
6297
|
+
<div class="footer">
|
|
6298
|
+
<p>Best regards,<br>
|
|
6299
|
+
<strong>The MetaEstetics Team</strong></p>
|
|
6300
|
+
<p>This invitation will expire on {{expirationDate}}. Please respond before this date.</p>
|
|
6301
|
+
<hr>
|
|
6302
|
+
<p style="font-size: 12px; color: #999;">
|
|
6303
|
+
This is an automated message from MetaEstetics. If you received this email in error, please ignore it.
|
|
6304
|
+
<br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
|
|
6305
|
+
</p>
|
|
6306
|
+
</div>
|
|
6307
|
+
</div>
|
|
6308
|
+
</body>
|
|
6309
|
+
</html>
|
|
6310
|
+
`;
|
|
6311
|
+
|
|
6312
|
+
// src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts
|
|
6313
|
+
var inviteAcceptedNotificationTemplate = `
|
|
6314
|
+
<!DOCTYPE html>
|
|
6315
|
+
<html lang="en">
|
|
6316
|
+
<head>
|
|
6317
|
+
<meta charset="UTF-8">
|
|
6318
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6319
|
+
<title>Practitioner Invitation Accepted - {{practitionerName}}</title>
|
|
6320
|
+
<style>
|
|
6321
|
+
body {
|
|
6322
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
6323
|
+
line-height: 1.6;
|
|
6324
|
+
color: #333;
|
|
6325
|
+
max-width: 600px;
|
|
6326
|
+
margin: 0 auto;
|
|
6327
|
+
padding: 20px;
|
|
6328
|
+
background-color: #f4f4f4;
|
|
6329
|
+
}
|
|
6330
|
+
.container {
|
|
6331
|
+
background-color: #ffffff;
|
|
6332
|
+
padding: 30px;
|
|
6333
|
+
border-radius: 10px;
|
|
6334
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
6335
|
+
}
|
|
6336
|
+
.header {
|
|
6337
|
+
text-align: center;
|
|
6338
|
+
margin-bottom: 30px;
|
|
6339
|
+
border-bottom: 2px solid #28a745;
|
|
6340
|
+
padding-bottom: 20px;
|
|
6341
|
+
}
|
|
6342
|
+
.logo {
|
|
6343
|
+
font-size: 28px;
|
|
6344
|
+
font-weight: bold;
|
|
6345
|
+
color: #28a745;
|
|
6346
|
+
margin-bottom: 10px;
|
|
6347
|
+
}
|
|
6348
|
+
h1 {
|
|
6349
|
+
color: #28a745;
|
|
6350
|
+
text-align: center;
|
|
6351
|
+
margin-bottom: 20px;
|
|
6352
|
+
}
|
|
6353
|
+
.success-badge {
|
|
6354
|
+
background-color: #d4edda;
|
|
6355
|
+
color: #155724;
|
|
6356
|
+
padding: 15px;
|
|
6357
|
+
border-left: 4px solid #28a745;
|
|
6358
|
+
margin: 20px 0;
|
|
6359
|
+
border-radius: 0 5px 5px 0;
|
|
6360
|
+
}
|
|
6361
|
+
.practitioner-card {
|
|
6362
|
+
background-color: #f8f9fa;
|
|
6363
|
+
padding: 20px;
|
|
6364
|
+
border-radius: 8px;
|
|
6365
|
+
margin: 20px 0;
|
|
6366
|
+
border: 1px solid #dee2e6;
|
|
6367
|
+
}
|
|
6368
|
+
.practitioner-photo {
|
|
6369
|
+
width: 80px;
|
|
6370
|
+
height: 80px;
|
|
6371
|
+
border-radius: 50%;
|
|
6372
|
+
object-fit: cover;
|
|
6373
|
+
float: left;
|
|
6374
|
+
margin-right: 15px;
|
|
6375
|
+
border: 3px solid #28a745;
|
|
6376
|
+
}
|
|
6377
|
+
.practitioner-info {
|
|
6378
|
+
overflow: hidden;
|
|
6379
|
+
}
|
|
6380
|
+
.practitioner-name {
|
|
6381
|
+
font-size: 18px;
|
|
6382
|
+
font-weight: bold;
|
|
6383
|
+
color: #333;
|
|
6384
|
+
margin-bottom: 5px;
|
|
6385
|
+
}
|
|
6386
|
+
.practitioner-details {
|
|
6387
|
+
color: #666;
|
|
6388
|
+
font-size: 14px;
|
|
6389
|
+
line-height: 1.4;
|
|
6390
|
+
}
|
|
6391
|
+
.details-grid {
|
|
6392
|
+
display: grid;
|
|
6393
|
+
grid-template-columns: 1fr 1fr;
|
|
6394
|
+
gap: 15px;
|
|
6395
|
+
margin: 20px 0;
|
|
6396
|
+
}
|
|
6397
|
+
.detail-item {
|
|
6398
|
+
background-color: #f8f9fa;
|
|
6399
|
+
padding: 10px;
|
|
6400
|
+
border-radius: 5px;
|
|
6401
|
+
border-left: 3px solid #28a745;
|
|
6402
|
+
}
|
|
6403
|
+
.detail-label {
|
|
6404
|
+
font-weight: bold;
|
|
6405
|
+
color: #495057;
|
|
6406
|
+
font-size: 12px;
|
|
6407
|
+
text-transform: uppercase;
|
|
6408
|
+
margin-bottom: 5px;
|
|
6409
|
+
}
|
|
6410
|
+
.detail-value {
|
|
6411
|
+
color: #333;
|
|
6412
|
+
font-size: 14px;
|
|
6413
|
+
}
|
|
6414
|
+
.button {
|
|
6415
|
+
display: inline-block;
|
|
6416
|
+
background-color: #007bff;
|
|
6417
|
+
color: white;
|
|
6418
|
+
padding: 12px 25px;
|
|
6419
|
+
text-decoration: none;
|
|
6420
|
+
border-radius: 5px;
|
|
6421
|
+
margin: 10px 5px;
|
|
6422
|
+
font-weight: bold;
|
|
6423
|
+
text-align: center;
|
|
6424
|
+
}
|
|
6425
|
+
.button:hover {
|
|
6426
|
+
opacity: 0.9;
|
|
6427
|
+
}
|
|
6428
|
+
.action-section {
|
|
6429
|
+
text-align: center;
|
|
6430
|
+
margin: 30px 0;
|
|
6431
|
+
padding: 20px;
|
|
6432
|
+
background-color: #e9ecef;
|
|
6433
|
+
border-radius: 5px;
|
|
6434
|
+
}
|
|
6435
|
+
.footer {
|
|
6436
|
+
margin-top: 30px;
|
|
6437
|
+
padding-top: 20px;
|
|
6438
|
+
border-top: 1px solid #eee;
|
|
6439
|
+
font-size: 14px;
|
|
6440
|
+
color: #666;
|
|
6441
|
+
text-align: center;
|
|
6442
|
+
}
|
|
6443
|
+
.working-hours {
|
|
6444
|
+
background-color: #fff3cd;
|
|
6445
|
+
border: 1px solid #ffeaa7;
|
|
6446
|
+
border-radius: 5px;
|
|
6447
|
+
padding: 15px;
|
|
6448
|
+
margin: 15px 0;
|
|
6449
|
+
}
|
|
6450
|
+
.working-hours h4 {
|
|
6451
|
+
color: #856404;
|
|
6452
|
+
margin-top: 0;
|
|
6453
|
+
}
|
|
6454
|
+
</style>
|
|
6455
|
+
</head>
|
|
6456
|
+
<body>
|
|
6457
|
+
<div class="container">
|
|
6458
|
+
<div class="header">
|
|
6459
|
+
<div class="logo">MetaEstetics</div>
|
|
6460
|
+
<p>Clinic Management System</p>
|
|
6461
|
+
</div>
|
|
6462
|
+
|
|
6463
|
+
<h1>\u{1F389} Great News! Invitation Accepted</h1>
|
|
6464
|
+
|
|
6465
|
+
<div class="success-badge">
|
|
6466
|
+
<strong>\u2705 Dr. {{practitionerName}} has accepted your invitation!</strong>
|
|
6467
|
+
<p style="margin: 5px 0 0 0;">They are now part of the {{clinicName}} team.</p>
|
|
6468
|
+
</div>
|
|
6469
|
+
|
|
6470
|
+
<p>Dear {{clinicAdminName}},</p>
|
|
6471
|
+
|
|
6472
|
+
<p>We're excited to inform you that <strong>Dr. {{practitionerName}}</strong> has accepted your invitation to join <strong>{{clinicName}}</strong>.</p>
|
|
6473
|
+
|
|
6474
|
+
<div class="practitioner-card">
|
|
6475
|
+
{{#practitionerPhoto}}
|
|
6476
|
+
<img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
|
|
6477
|
+
{{/practitionerPhoto}}
|
|
6478
|
+
<div class="practitioner-info">
|
|
6479
|
+
<div class="practitioner-name">Dr. {{practitionerName}}</div>
|
|
6480
|
+
<div class="practitioner-details">
|
|
6481
|
+
{{#practitionerSpecialties}}
|
|
6482
|
+
<p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
|
|
6483
|
+
{{/practitionerSpecialties}}
|
|
6484
|
+
{{#practitionerExperience}}
|
|
6485
|
+
<p><strong>Experience:</strong> {{practitionerExperience}} years</p>
|
|
6486
|
+
{{/practitionerExperience}}
|
|
6487
|
+
<p><strong>Joined:</strong> {{acceptedDate}}</p>
|
|
6488
|
+
</div>
|
|
6489
|
+
</div>
|
|
6490
|
+
</div>
|
|
6491
|
+
|
|
6492
|
+
<div class="details-grid">
|
|
6493
|
+
<div class="detail-item">
|
|
6494
|
+
<div class="detail-label">Status</div>
|
|
6495
|
+
<div class="detail-value">\u2705 Active Team Member</div>
|
|
6496
|
+
</div>
|
|
6497
|
+
<div class="detail-item">
|
|
6498
|
+
<div class="detail-label">Invitation Date</div>
|
|
6499
|
+
<div class="detail-value">{{invitationDate}}</div>
|
|
6500
|
+
</div>
|
|
6501
|
+
</div>
|
|
6502
|
+
|
|
6503
|
+
<div class="working-hours">
|
|
6504
|
+
<h4>\u{1F4C5} Scheduled Working Hours</h4>
|
|
6505
|
+
<p>{{workingHours}}</p>
|
|
6506
|
+
</div>
|
|
6507
|
+
|
|
6508
|
+
<div class="action-section">
|
|
6509
|
+
<p><strong>Next Steps:</strong></p>
|
|
6510
|
+
<a href="{{clinicDashboardUrl}}" class="button">View Clinic Dashboard</a>
|
|
6511
|
+
<a href="{{practitionerProfileUrl}}" class="button">View Practitioner Profile</a>
|
|
6512
|
+
</div>
|
|
6513
|
+
|
|
6514
|
+
<p><strong>What happens next?</strong></p>
|
|
6515
|
+
<ul>
|
|
6516
|
+
<li>Dr. {{practitionerName}} is now visible on your clinic profile</li>
|
|
6517
|
+
<li>Patients can book appointments with them during their scheduled hours</li>
|
|
6518
|
+
<li>They have access to your clinic's patient management system</li>
|
|
6519
|
+
<li>You can manage their schedule and availability from your dashboard</li>
|
|
6520
|
+
</ul>
|
|
6521
|
+
|
|
6522
|
+
<p>Welcome Dr. {{practitionerName}} to the team! \u{1F38A}</p>
|
|
6523
|
+
|
|
6524
|
+
<div class="footer">
|
|
6525
|
+
<p>Best regards,<br>
|
|
6526
|
+
<strong>The MetaEstetics Team</strong></p>
|
|
6527
|
+
<hr>
|
|
6528
|
+
<p style="font-size: 12px; color: #999;">
|
|
6529
|
+
This is an automated notification from MetaEstetics.
|
|
6530
|
+
<br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
|
|
6531
|
+
</p>
|
|
6532
|
+
</div>
|
|
6533
|
+
</div>
|
|
6534
|
+
</body>
|
|
6535
|
+
</html>
|
|
6536
|
+
`;
|
|
6537
|
+
|
|
6538
|
+
// src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts
|
|
6539
|
+
var inviteRejectedNotificationTemplate = `
|
|
6540
|
+
<!DOCTYPE html>
|
|
6541
|
+
<html lang="en">
|
|
6542
|
+
<head>
|
|
6543
|
+
<meta charset="UTF-8">
|
|
6544
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6545
|
+
<title>Practitioner Invitation Declined - {{practitionerName}}</title>
|
|
6546
|
+
<style>
|
|
6547
|
+
body {
|
|
6548
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
6549
|
+
line-height: 1.6;
|
|
6550
|
+
color: #333;
|
|
6551
|
+
max-width: 600px;
|
|
6552
|
+
margin: 0 auto;
|
|
6553
|
+
padding: 20px;
|
|
6554
|
+
background-color: #f4f4f4;
|
|
6555
|
+
}
|
|
6556
|
+
.container {
|
|
6557
|
+
background-color: #ffffff;
|
|
6558
|
+
padding: 30px;
|
|
6559
|
+
border-radius: 10px;
|
|
6560
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
6561
|
+
}
|
|
6562
|
+
.header {
|
|
6563
|
+
text-align: center;
|
|
6564
|
+
margin-bottom: 30px;
|
|
6565
|
+
border-bottom: 2px solid #dc3545;
|
|
6566
|
+
padding-bottom: 20px;
|
|
6567
|
+
}
|
|
6568
|
+
.logo {
|
|
6569
|
+
font-size: 28px;
|
|
6570
|
+
font-weight: bold;
|
|
6571
|
+
color: #dc3545;
|
|
6572
|
+
margin-bottom: 10px;
|
|
6573
|
+
}
|
|
6574
|
+
h1 {
|
|
6575
|
+
color: #dc3545;
|
|
6576
|
+
text-align: center;
|
|
6577
|
+
margin-bottom: 20px;
|
|
6578
|
+
}
|
|
6579
|
+
.notice-badge {
|
|
6580
|
+
background-color: #f8d7da;
|
|
6581
|
+
color: #721c24;
|
|
6582
|
+
padding: 15px;
|
|
6583
|
+
border-left: 4px solid #dc3545;
|
|
6584
|
+
margin: 20px 0;
|
|
6585
|
+
border-radius: 0 5px 5px 0;
|
|
6586
|
+
}
|
|
6587
|
+
.practitioner-card {
|
|
6588
|
+
background-color: #f8f9fa;
|
|
6589
|
+
padding: 20px;
|
|
6590
|
+
border-radius: 8px;
|
|
6591
|
+
margin: 20px 0;
|
|
6592
|
+
border: 1px solid #dee2e6;
|
|
6593
|
+
}
|
|
6594
|
+
.practitioner-photo {
|
|
6595
|
+
width: 80px;
|
|
6596
|
+
height: 80px;
|
|
6597
|
+
border-radius: 50%;
|
|
6598
|
+
object-fit: cover;
|
|
6599
|
+
float: left;
|
|
6600
|
+
margin-right: 15px;
|
|
6601
|
+
border: 3px solid #dc3545;
|
|
6602
|
+
}
|
|
6603
|
+
.practitioner-info {
|
|
6604
|
+
overflow: hidden;
|
|
6605
|
+
}
|
|
6606
|
+
.practitioner-name {
|
|
6607
|
+
font-size: 18px;
|
|
6608
|
+
font-weight: bold;
|
|
6609
|
+
color: #333;
|
|
6610
|
+
margin-bottom: 5px;
|
|
6611
|
+
}
|
|
6612
|
+
.practitioner-details {
|
|
6613
|
+
color: #666;
|
|
6614
|
+
font-size: 14px;
|
|
6615
|
+
line-height: 1.4;
|
|
6616
|
+
}
|
|
6617
|
+
.details-grid {
|
|
6618
|
+
display: grid;
|
|
6619
|
+
grid-template-columns: 1fr 1fr;
|
|
6620
|
+
gap: 15px;
|
|
6621
|
+
margin: 20px 0;
|
|
6622
|
+
}
|
|
6623
|
+
.detail-item {
|
|
6624
|
+
background-color: #f8f9fa;
|
|
6625
|
+
padding: 10px;
|
|
6626
|
+
border-radius: 5px;
|
|
6627
|
+
border-left: 3px solid #dc3545;
|
|
6628
|
+
}
|
|
6629
|
+
.detail-label {
|
|
6630
|
+
font-weight: bold;
|
|
6631
|
+
color: #495057;
|
|
6632
|
+
font-size: 12px;
|
|
6633
|
+
text-transform: uppercase;
|
|
6634
|
+
margin-bottom: 5px;
|
|
6635
|
+
}
|
|
6636
|
+
.detail-value {
|
|
6637
|
+
color: #333;
|
|
6638
|
+
font-size: 14px;
|
|
6639
|
+
}
|
|
6640
|
+
.button {
|
|
6641
|
+
display: inline-block;
|
|
6642
|
+
background-color: #007bff;
|
|
6643
|
+
color: white;
|
|
6644
|
+
padding: 12px 25px;
|
|
6645
|
+
text-decoration: none;
|
|
6646
|
+
border-radius: 5px;
|
|
6647
|
+
margin: 10px 5px;
|
|
6648
|
+
font-weight: bold;
|
|
6649
|
+
text-align: center;
|
|
6650
|
+
}
|
|
6651
|
+
.button.secondary {
|
|
6652
|
+
background-color: #6c757d;
|
|
6653
|
+
}
|
|
6654
|
+
.button:hover {
|
|
6655
|
+
opacity: 0.9;
|
|
6656
|
+
}
|
|
6657
|
+
.action-section {
|
|
6658
|
+
text-align: center;
|
|
6659
|
+
margin: 30px 0;
|
|
6660
|
+
padding: 20px;
|
|
6661
|
+
background-color: #e9ecef;
|
|
6662
|
+
border-radius: 5px;
|
|
6663
|
+
}
|
|
6664
|
+
.footer {
|
|
6665
|
+
margin-top: 30px;
|
|
6666
|
+
padding-top: 20px;
|
|
6667
|
+
border-top: 1px solid #eee;
|
|
6668
|
+
font-size: 14px;
|
|
6669
|
+
color: #666;
|
|
6670
|
+
text-align: center;
|
|
6671
|
+
}
|
|
6672
|
+
.suggestions {
|
|
6673
|
+
background-color: #d1ecf1;
|
|
6674
|
+
border: 1px solid #bee5eb;
|
|
6675
|
+
border-radius: 5px;
|
|
6676
|
+
padding: 15px;
|
|
6677
|
+
margin: 15px 0;
|
|
6678
|
+
}
|
|
6679
|
+
.suggestions h4 {
|
|
6680
|
+
color: #0c5460;
|
|
6681
|
+
margin-top: 0;
|
|
6682
|
+
}
|
|
6683
|
+
.reason-box {
|
|
6684
|
+
background-color: #fff3cd;
|
|
6685
|
+
border: 1px solid #ffeaa7;
|
|
6686
|
+
border-radius: 5px;
|
|
6687
|
+
padding: 15px;
|
|
6688
|
+
margin: 15px 0;
|
|
6689
|
+
}
|
|
6690
|
+
</style>
|
|
6691
|
+
</head>
|
|
6692
|
+
<body>
|
|
6693
|
+
<div class="container">
|
|
6694
|
+
<div class="header">
|
|
6695
|
+
<div class="logo">MetaEstetics</div>
|
|
6696
|
+
<p>Clinic Management System</p>
|
|
6697
|
+
</div>
|
|
6698
|
+
|
|
6699
|
+
<h1>Invitation Update</h1>
|
|
6700
|
+
|
|
6701
|
+
<div class="notice-badge">
|
|
6702
|
+
<strong>\u274C Dr. {{practitionerName}} has declined your invitation</strong>
|
|
6703
|
+
<p style="margin: 5px 0 0 0;">They will not be joining {{clinicName}} at this time.</p>
|
|
6704
|
+
</div>
|
|
6705
|
+
|
|
6706
|
+
<p>Dear {{clinicAdminName}},</p>
|
|
6707
|
+
|
|
6708
|
+
<p>We wanted to let you know that <strong>Dr. {{practitionerName}}</strong> has declined your invitation to join <strong>{{clinicName}}</strong>.</p>
|
|
6709
|
+
|
|
6710
|
+
<div class="practitioner-card">
|
|
6711
|
+
{{#practitionerPhoto}}
|
|
6712
|
+
<img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
|
|
6713
|
+
{{/practitionerPhoto}}
|
|
6714
|
+
<div class="practitioner-info">
|
|
6715
|
+
<div class="practitioner-name">Dr. {{practitionerName}}</div>
|
|
6716
|
+
<div class="practitioner-details">
|
|
6717
|
+
{{#practitionerSpecialties}}
|
|
6718
|
+
<p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
|
|
6719
|
+
{{/practitionerSpecialties}}
|
|
6720
|
+
<p><strong>Decision Date:</strong> {{rejectedDate}}</p>
|
|
6721
|
+
</div>
|
|
6722
|
+
</div>
|
|
6723
|
+
</div>
|
|
6724
|
+
|
|
6725
|
+
<div class="details-grid">
|
|
6726
|
+
<div class="detail-item">
|
|
6727
|
+
<div class="detail-label">Status</div>
|
|
6728
|
+
<div class="detail-value">\u274C Invitation Declined</div>
|
|
6729
|
+
</div>
|
|
6730
|
+
<div class="detail-item">
|
|
6731
|
+
<div class="detail-label">Original Invitation</div>
|
|
6732
|
+
<div class="detail-value">{{invitationDate}}</div>
|
|
6733
|
+
</div>
|
|
6734
|
+
</div>
|
|
6735
|
+
|
|
6736
|
+
{{#rejectionReason}}
|
|
6737
|
+
<div class="reason-box">
|
|
6738
|
+
<h4>\u{1F4DD} Reason Provided:</h4>
|
|
6739
|
+
<p>"{{rejectionReason}}"</p>
|
|
6740
|
+
</div>
|
|
6741
|
+
{{/rejectionReason}}
|
|
6742
|
+
|
|
6743
|
+
<div class="suggestions">
|
|
6744
|
+
<h4>\u{1F4A1} What you can do next:</h4>
|
|
6745
|
+
<ul>
|
|
6746
|
+
<li><strong>Reach out directly:</strong> Consider contacting Dr. {{practitionerName}} to discuss their concerns</li>
|
|
6747
|
+
<li><strong>Adjust your offer:</strong> Review your working hours, compensation, or clinic benefits</li>
|
|
6748
|
+
<li><strong>Invite other practitioners:</strong> Look for other qualified professionals in your area</li>
|
|
6749
|
+
<li><strong>Try again later:</strong> Their circumstances may change in the future</li>
|
|
6750
|
+
</ul>
|
|
6751
|
+
</div>
|
|
6752
|
+
|
|
6753
|
+
<div class="action-section">
|
|
6754
|
+
<p><strong>Explore your options:</strong></p>
|
|
6755
|
+
<a href="{{findPractitionersUrl}}" class="button">Find More Practitioners</a>
|
|
6756
|
+
<a href="{{clinicDashboardUrl}}" class="button secondary">View Dashboard</a>
|
|
6757
|
+
</div>
|
|
6758
|
+
|
|
6759
|
+
<p>Don't let this discourage you! Finding the right team members takes time, and there are many talented practitioners who would be excited to join your clinic.</p>
|
|
6760
|
+
|
|
6761
|
+
<div class="footer">
|
|
6762
|
+
<p>Best regards,<br>
|
|
6763
|
+
<strong>The MetaEstetics Team</strong></p>
|
|
6764
|
+
<hr>
|
|
6765
|
+
<p style="font-size: 12px; color: #999;">
|
|
6766
|
+
This is an automated notification from MetaEstetics.
|
|
6767
|
+
<br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
|
|
6768
|
+
</p>
|
|
6769
|
+
<p style="font-size: 12px; color: #999;">
|
|
6770
|
+
<strong>Note:</strong> Practitioner contact information is not shared without their consent.
|
|
6771
|
+
</p>
|
|
6772
|
+
</div>
|
|
6773
|
+
</div>
|
|
6774
|
+
</body>
|
|
6775
|
+
</html>
|
|
6776
|
+
`;
|
|
6777
|
+
|
|
6778
|
+
// src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts
|
|
6779
|
+
var ExistingPractitionerInviteMailingService = class extends BaseMailingService {
|
|
6780
|
+
/**
|
|
6781
|
+
* Constructor for ExistingPractitionerInviteMailingService
|
|
6782
|
+
* @param firestore Firestore instance provided by the caller
|
|
6783
|
+
* @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
|
|
6784
|
+
*/
|
|
6785
|
+
constructor(firestore17, mailgunClient) {
|
|
6786
|
+
super(firestore17, mailgunClient);
|
|
6787
|
+
this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
6788
|
+
this.DEFAULT_FROM_ADDRESS = "MetaEstetics <no-reply@mg.metaesthetics.net>";
|
|
6789
|
+
}
|
|
6790
|
+
/**
|
|
6791
|
+
* Sends an invitation email to an existing practitioner
|
|
6792
|
+
* @param data The invitation email data
|
|
6793
|
+
* @returns Promise resolved when email is sent
|
|
6794
|
+
*/
|
|
6795
|
+
async sendPractitionerInvitationEmail(data) {
|
|
6796
|
+
var _a, _b, _c, _d;
|
|
6797
|
+
try {
|
|
6798
|
+
Logger.info(
|
|
6799
|
+
"[ExistingPractitionerInviteMailingService] Sending invitation email to practitioner",
|
|
6800
|
+
data.practitioner.email
|
|
6801
|
+
);
|
|
6802
|
+
const workingHours = this.formatWorkingHours(
|
|
6803
|
+
data.invite.proposedWorkingHours
|
|
6804
|
+
);
|
|
6805
|
+
const expiryDate = new Date(data.invite.createdAt.toDate());
|
|
6806
|
+
expiryDate.setDate(expiryDate.getDate() + 30);
|
|
6807
|
+
const expirationDate = expiryDate.toLocaleDateString("en-US", {
|
|
6808
|
+
weekday: "long",
|
|
6809
|
+
year: "numeric",
|
|
6810
|
+
month: "long",
|
|
6811
|
+
day: "numeric"
|
|
6812
|
+
});
|
|
6813
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
6814
|
+
const templateVariables = {
|
|
6815
|
+
clinicName: data.clinic.name,
|
|
6816
|
+
practitionerName,
|
|
6817
|
+
clinicAddress: data.clinic.address,
|
|
6818
|
+
workingHours,
|
|
6819
|
+
expirationDate,
|
|
6820
|
+
acceptUrl: data.urls.acceptUrl,
|
|
6821
|
+
rejectUrl: data.urls.rejectUrl,
|
|
6822
|
+
contactEmail: data.clinic.contactEmail,
|
|
6823
|
+
contactPhone: data.clinic.contactPhone || "Contact clinic directly",
|
|
6824
|
+
currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
6825
|
+
};
|
|
6826
|
+
const html = this.renderTemplate(
|
|
6827
|
+
existingPractitionerInvitationTemplate,
|
|
6828
|
+
templateVariables
|
|
6829
|
+
);
|
|
6830
|
+
const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Invitation to Join ${data.clinic.name}`;
|
|
6831
|
+
const from = ((_b = data.options) == null ? void 0 : _b.fromAddress) || this.DEFAULT_FROM_ADDRESS;
|
|
6832
|
+
const domain = ((_c = data.options) == null ? void 0 : _c.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
6833
|
+
const mailgunData = {
|
|
6834
|
+
to: data.practitioner.email,
|
|
6835
|
+
from,
|
|
6836
|
+
subject,
|
|
6837
|
+
html
|
|
6838
|
+
};
|
|
6839
|
+
Logger.info(
|
|
6840
|
+
"[ExistingPractitionerInviteMailingService] Sending email with data:",
|
|
6841
|
+
{
|
|
6842
|
+
domain,
|
|
6843
|
+
to: mailgunData.to,
|
|
6844
|
+
from: mailgunData.from,
|
|
6845
|
+
subject: mailgunData.subject,
|
|
6846
|
+
hasHtml: !!mailgunData.html
|
|
6847
|
+
}
|
|
6848
|
+
);
|
|
6849
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
6850
|
+
await this.logEmailAttempt(
|
|
6851
|
+
{
|
|
6852
|
+
to: data.practitioner.email,
|
|
6853
|
+
subject,
|
|
6854
|
+
templateName: "existing_practitioner_invitation"
|
|
6855
|
+
},
|
|
6856
|
+
true
|
|
6857
|
+
);
|
|
6858
|
+
return result;
|
|
6859
|
+
} catch (error) {
|
|
6860
|
+
Logger.error(
|
|
6861
|
+
"[ExistingPractitionerInviteMailingService] Error sending practitioner invitation:",
|
|
6862
|
+
{
|
|
6863
|
+
errorMessage: error.message,
|
|
6864
|
+
errorDetails: error.details,
|
|
6865
|
+
errorStatus: error.status,
|
|
6866
|
+
stack: error.stack
|
|
6867
|
+
}
|
|
6868
|
+
);
|
|
6869
|
+
await this.logEmailAttempt(
|
|
6870
|
+
{
|
|
6871
|
+
to: data.practitioner.email,
|
|
6872
|
+
subject: ((_d = data.options) == null ? void 0 : _d.customSubject) || `Invitation to Join ${data.clinic.name}`,
|
|
6873
|
+
templateName: "existing_practitioner_invitation"
|
|
6874
|
+
},
|
|
6875
|
+
false,
|
|
6876
|
+
error
|
|
6877
|
+
);
|
|
6878
|
+
throw error;
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Sends a notification email to clinic admin when practitioner accepts invitation
|
|
6883
|
+
* @param data The notification email data
|
|
6884
|
+
* @returns Promise resolved when email is sent
|
|
6885
|
+
*/
|
|
6886
|
+
async sendAcceptedNotificationEmail(data) {
|
|
6887
|
+
var _a, _b, _c, _d, _e, _f;
|
|
6888
|
+
try {
|
|
6889
|
+
Logger.info(
|
|
6890
|
+
"[ExistingPractitionerInviteMailingService] Sending acceptance notification to clinic admin",
|
|
6891
|
+
data.clinic.adminEmail
|
|
6892
|
+
);
|
|
6893
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
6894
|
+
const workingHours = this.formatWorkingHours(
|
|
6895
|
+
data.invite.proposedWorkingHours
|
|
6896
|
+
);
|
|
6897
|
+
const templateVariables = {
|
|
6898
|
+
clinicName: data.clinic.name,
|
|
6899
|
+
clinicAdminName: data.clinic.adminName || "Admin",
|
|
6900
|
+
practitionerName,
|
|
6901
|
+
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
6902
|
+
practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
|
|
6903
|
+
practitionerExperience: ((_b = data.practitioner.experienceYears) == null ? void 0 : _b.toString()) || "",
|
|
6904
|
+
invitationDate: data.context.invitationDate,
|
|
6905
|
+
acceptedDate: data.context.responseDate,
|
|
6906
|
+
workingHours,
|
|
6907
|
+
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
6908
|
+
practitionerProfileUrl: data.urls.practitionerProfileUrl || "#",
|
|
6909
|
+
currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
6910
|
+
};
|
|
6911
|
+
const html = this.renderTemplate(
|
|
6912
|
+
inviteAcceptedNotificationTemplate,
|
|
6913
|
+
templateVariables
|
|
6914
|
+
);
|
|
6915
|
+
const subject = ((_c = data.options) == null ? void 0 : _c.customSubject) || `Great News! Dr. ${practitionerName} Accepted Your Invitation`;
|
|
6916
|
+
const from = ((_d = data.options) == null ? void 0 : _d.fromAddress) || this.DEFAULT_FROM_ADDRESS;
|
|
6917
|
+
const domain = ((_e = data.options) == null ? void 0 : _e.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
6918
|
+
const mailgunData = {
|
|
6919
|
+
to: data.clinic.adminEmail,
|
|
6920
|
+
from,
|
|
6921
|
+
subject,
|
|
6922
|
+
html
|
|
6923
|
+
};
|
|
6924
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
6925
|
+
await this.logEmailAttempt(
|
|
6926
|
+
{
|
|
6927
|
+
to: data.clinic.adminEmail,
|
|
6928
|
+
subject,
|
|
6929
|
+
templateName: "invite_accepted_notification"
|
|
6930
|
+
},
|
|
6931
|
+
true
|
|
6932
|
+
);
|
|
6933
|
+
return result;
|
|
6934
|
+
} catch (error) {
|
|
6935
|
+
Logger.error(
|
|
6936
|
+
"[ExistingPractitionerInviteMailingService] Error sending acceptance notification:",
|
|
6937
|
+
error
|
|
6938
|
+
);
|
|
6939
|
+
await this.logEmailAttempt(
|
|
6940
|
+
{
|
|
6941
|
+
to: data.clinic.adminEmail,
|
|
6942
|
+
subject: ((_f = data.options) == null ? void 0 : _f.customSubject) || "Invitation Accepted",
|
|
6943
|
+
templateName: "invite_accepted_notification"
|
|
6944
|
+
},
|
|
6945
|
+
false,
|
|
6946
|
+
error
|
|
6947
|
+
);
|
|
6948
|
+
throw error;
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
/**
|
|
6952
|
+
* Sends a notification email to clinic admin when practitioner rejects invitation
|
|
6953
|
+
* @param data The notification email data
|
|
6954
|
+
* @returns Promise resolved when email is sent
|
|
6955
|
+
*/
|
|
6956
|
+
async sendRejectedNotificationEmail(data) {
|
|
6957
|
+
var _a, _b, _c, _d, _e;
|
|
6958
|
+
try {
|
|
6959
|
+
Logger.info(
|
|
6960
|
+
"[ExistingPractitionerInviteMailingService] Sending rejection notification to clinic admin",
|
|
6961
|
+
data.clinic.adminEmail
|
|
6962
|
+
);
|
|
6963
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
6964
|
+
const templateVariables = {
|
|
6965
|
+
clinicName: data.clinic.name,
|
|
6966
|
+
clinicAdminName: data.clinic.adminName || "Admin",
|
|
6967
|
+
practitionerName,
|
|
6968
|
+
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
6969
|
+
practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
|
|
6970
|
+
invitationDate: data.context.invitationDate,
|
|
6971
|
+
rejectedDate: data.context.responseDate,
|
|
6972
|
+
rejectionReason: data.context.rejectionReason || "",
|
|
6973
|
+
findPractitionersUrl: data.urls.findPractitionersUrl || "#",
|
|
6974
|
+
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
6975
|
+
currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
6976
|
+
};
|
|
6977
|
+
const html = this.renderTemplate(
|
|
6978
|
+
inviteRejectedNotificationTemplate,
|
|
6979
|
+
templateVariables
|
|
6980
|
+
);
|
|
6981
|
+
const subject = ((_b = data.options) == null ? void 0 : _b.customSubject) || `Invitation Update: Dr. ${practitionerName} Declined`;
|
|
6982
|
+
const from = ((_c = data.options) == null ? void 0 : _c.fromAddress) || this.DEFAULT_FROM_ADDRESS;
|
|
6983
|
+
const domain = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
|
|
6984
|
+
const mailgunData = {
|
|
6985
|
+
to: data.clinic.adminEmail,
|
|
6986
|
+
from,
|
|
6987
|
+
subject,
|
|
6988
|
+
html
|
|
6989
|
+
};
|
|
6990
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
6991
|
+
await this.logEmailAttempt(
|
|
6992
|
+
{
|
|
6993
|
+
to: data.clinic.adminEmail,
|
|
6994
|
+
subject,
|
|
6995
|
+
templateName: "invite_rejected_notification"
|
|
6996
|
+
},
|
|
6997
|
+
true
|
|
6998
|
+
);
|
|
6999
|
+
return result;
|
|
7000
|
+
} catch (error) {
|
|
7001
|
+
Logger.error(
|
|
7002
|
+
"[ExistingPractitionerInviteMailingService] Error sending rejection notification:",
|
|
7003
|
+
error
|
|
7004
|
+
);
|
|
7005
|
+
await this.logEmailAttempt(
|
|
7006
|
+
{
|
|
7007
|
+
to: data.clinic.adminEmail,
|
|
7008
|
+
subject: ((_e = data.options) == null ? void 0 : _e.customSubject) || "Invitation Declined",
|
|
7009
|
+
templateName: "invite_rejected_notification"
|
|
7010
|
+
},
|
|
7011
|
+
false,
|
|
7012
|
+
error
|
|
7013
|
+
);
|
|
7014
|
+
throw error;
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
7017
|
+
/**
|
|
7018
|
+
* Handles the practitioner invite creation event
|
|
7019
|
+
* Fetches necessary data and sends the invitation email to the practitioner
|
|
7020
|
+
* @param invite The practitioner invite object
|
|
7021
|
+
* @param mailgunConfig Mailgun configuration
|
|
7022
|
+
* @returns Promise resolved when the email is sent
|
|
7023
|
+
*/
|
|
7024
|
+
async handleInviteCreationEvent(invite, mailgunConfig) {
|
|
7025
|
+
var _a, _b;
|
|
7026
|
+
try {
|
|
7027
|
+
Logger.info(
|
|
7028
|
+
"[ExistingPractitionerInviteMailingService] Handling invite creation event for invite:",
|
|
7029
|
+
invite.id
|
|
7030
|
+
);
|
|
7031
|
+
if (!invite || !invite.id || !invite.practitionerId || !invite.clinicId) {
|
|
7032
|
+
throw new Error(
|
|
7033
|
+
`Invalid invite data: Missing required properties. Invite ID: ${invite == null ? void 0 : invite.id}`
|
|
7034
|
+
);
|
|
7035
|
+
}
|
|
7036
|
+
if (invite.status !== "pending" /* PENDING */) {
|
|
7037
|
+
Logger.info(
|
|
7038
|
+
"[ExistingPractitionerInviteMailingService] Invite is not pending, skipping email",
|
|
7039
|
+
{ inviteId: invite.id, status: invite.status }
|
|
7040
|
+
);
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
const [practitioner, clinic] = await Promise.all([
|
|
7044
|
+
this.fetchPractitionerById(invite.practitionerId),
|
|
7045
|
+
this.fetchClinicById(invite.clinicId)
|
|
7046
|
+
]);
|
|
7047
|
+
if (!practitioner) {
|
|
7048
|
+
throw new Error(`Practitioner ${invite.practitionerId} not found`);
|
|
7049
|
+
}
|
|
7050
|
+
if (!clinic) {
|
|
7051
|
+
throw new Error(`Clinic ${invite.clinicId} not found`);
|
|
7052
|
+
}
|
|
7053
|
+
const emailData = {
|
|
7054
|
+
invite,
|
|
7055
|
+
practitioner: {
|
|
7056
|
+
firstName: practitioner.basicInfo.firstName || "",
|
|
7057
|
+
lastName: practitioner.basicInfo.lastName || "",
|
|
7058
|
+
email: practitioner.basicInfo.email || "",
|
|
7059
|
+
specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
|
|
7060
|
+
(s) => s.name || s
|
|
7061
|
+
)) || [],
|
|
7062
|
+
profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
|
|
7063
|
+
},
|
|
7064
|
+
clinic: {
|
|
7065
|
+
name: clinic.name || "Medical Clinic",
|
|
7066
|
+
address: this.formatClinicAddress(clinic.location),
|
|
7067
|
+
contactEmail: clinic.contactInfo.email || "contact@clinic.com",
|
|
7068
|
+
contactPhone: clinic.contactInfo.phoneNumber
|
|
7069
|
+
},
|
|
7070
|
+
urls: {
|
|
7071
|
+
acceptUrl: mailgunConfig.acceptUrl,
|
|
7072
|
+
rejectUrl: mailgunConfig.rejectUrl
|
|
7073
|
+
},
|
|
7074
|
+
options: {
|
|
7075
|
+
fromAddress: mailgunConfig.fromAddress,
|
|
7076
|
+
mailgunDomain: mailgunConfig.domain
|
|
7077
|
+
}
|
|
7078
|
+
};
|
|
7079
|
+
await this.sendPractitionerInvitationEmail(emailData);
|
|
7080
|
+
Logger.info(
|
|
7081
|
+
"[ExistingPractitionerInviteMailingService] Invitation email sent successfully"
|
|
7082
|
+
);
|
|
7083
|
+
} catch (error) {
|
|
7084
|
+
Logger.error(
|
|
7085
|
+
"[ExistingPractitionerInviteMailingService] Error handling invite creation event:",
|
|
7086
|
+
{
|
|
7087
|
+
errorMessage: error.message,
|
|
7088
|
+
errorDetails: error.details,
|
|
7089
|
+
errorStatus: error.status,
|
|
7090
|
+
stack: error.stack,
|
|
7091
|
+
inviteId: invite == null ? void 0 : invite.id
|
|
7092
|
+
}
|
|
7093
|
+
);
|
|
7094
|
+
throw error;
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
// --- Private Helper Methods ---
|
|
7098
|
+
/**
|
|
7099
|
+
* Formats working hours for display in emails
|
|
7100
|
+
* @param workingHours The working hours object
|
|
7101
|
+
* @returns Formatted string representation
|
|
7102
|
+
*/
|
|
7103
|
+
formatWorkingHours(workingHours) {
|
|
7104
|
+
if (!workingHours) return "To be determined";
|
|
7105
|
+
return Object.entries(workingHours).map(([day, hours]) => `${day}: ${hours}`).join(", ");
|
|
7106
|
+
}
|
|
7107
|
+
/**
|
|
7108
|
+
* Formats clinic address for display
|
|
7109
|
+
* @param location The clinic location object
|
|
7110
|
+
* @returns Formatted address string
|
|
7111
|
+
*/
|
|
7112
|
+
formatClinicAddress(location) {
|
|
7113
|
+
if (!location) return "Address not specified";
|
|
7114
|
+
const parts = [
|
|
7115
|
+
location.street,
|
|
7116
|
+
location.city,
|
|
7117
|
+
location.state,
|
|
7118
|
+
location.country
|
|
7119
|
+
].filter(Boolean);
|
|
7120
|
+
return parts.join(", ");
|
|
7121
|
+
}
|
|
7122
|
+
/**
|
|
7123
|
+
* Fetches a practitioner by ID
|
|
7124
|
+
* @param practitionerId The practitioner ID
|
|
7125
|
+
* @returns The practitioner or null if not found
|
|
7126
|
+
*/
|
|
7127
|
+
async fetchPractitionerById(practitionerId) {
|
|
7128
|
+
try {
|
|
7129
|
+
const doc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
|
|
7130
|
+
return doc.exists ? doc.data() : null;
|
|
7131
|
+
} catch (error) {
|
|
7132
|
+
Logger.error(
|
|
7133
|
+
"[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
|
|
7134
|
+
error
|
|
7135
|
+
);
|
|
7136
|
+
return null;
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
/**
|
|
7140
|
+
* Fetches a clinic by ID
|
|
7141
|
+
* @param clinicId The clinic ID
|
|
7142
|
+
* @returns The clinic or null if not found
|
|
7143
|
+
*/
|
|
7144
|
+
async fetchClinicById(clinicId) {
|
|
7145
|
+
try {
|
|
7146
|
+
const doc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
|
|
7147
|
+
return doc.exists ? doc.data() : null;
|
|
7148
|
+
} catch (error) {
|
|
7149
|
+
Logger.error(
|
|
7150
|
+
"[ExistingPractitionerInviteMailingService] Error fetching clinic:",
|
|
7151
|
+
error
|
|
7152
|
+
);
|
|
7153
|
+
return null;
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
};
|
|
7157
|
+
|
|
5999
7158
|
// src/admin/booking/booking.admin.ts
|
|
6000
7159
|
var admin15 = __toESM(require("firebase-admin"));
|
|
6001
7160
|
|
|
@@ -7331,6 +8490,7 @@ TimestampUtils.enableServerMode();
|
|
|
7331
8490
|
CalendarAdminService,
|
|
7332
8491
|
ClinicAggregationService,
|
|
7333
8492
|
DocumentManagerAdminService,
|
|
8493
|
+
ExistingPractitionerInviteMailingService,
|
|
7334
8494
|
FilledFormsAggregationService,
|
|
7335
8495
|
Logger,
|
|
7336
8496
|
MediaType,
|