placementt-core 11.0.892 → 11.0.951

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.
@@ -1,6 +1,7 @@
1
1
  import {DocumentData, DocumentReference, Timestamp} from "firebase/firestore";
2
2
  import {Descendant} from "slate";
3
3
  import {emailTemplates} from "./constants";
4
+ import {FilterObject} from "./hooks";
4
5
 
5
6
  export type Products = "institutes"|"providers"|"students"|"admin";
6
7
 
@@ -21,6 +22,7 @@ export type StudentPlacementData = {
21
22
  providerEmailed: Timestamp,
22
23
  parentEmailed: Timestamp,
23
24
  providerPhone: string,
25
+ studentDescription?: string,
24
26
  website?: string,
25
27
  id:string,
26
28
  providerCompleted: string[],
@@ -142,17 +144,17 @@ export type PlacementQuestions = {
142
144
  payFrequency: "total"|"hourly"|"daily"|"weekly"|"monthly"|"annually",
143
145
  }
144
146
 
145
- // export type SavedPlacement = {
146
- // placementId: string,
147
- // savedByProduct: Products,
148
- // savedByUserType: "Organisation"|"Student",
149
- // savedById: string,
150
- // schoolId?: string,
151
- // status?: "Accepted" | "Blocked" | "providerReview";
152
- // listed: boolean,
153
- // concurrentPlacements?: number,
154
- // id: string
155
- // }
147
+ export type SavedPlacement = {
148
+ placementId: string,
149
+ savedByProduct: Products,
150
+ savedByUserType: "Organisation"|"Student",
151
+ savedById: string,
152
+ schoolId?: string,
153
+ status?: "Accepted" | "Blocked" | "providerReview";
154
+ listed: boolean,
155
+ concurrentPlacements?: number,
156
+ id: string
157
+ }
156
158
 
157
159
  // export type PlacementListingStages = "basic"|"address"|"students"|"responsibilities"|"applications"|"onboarding"|"review"|"complete";
158
160
 
@@ -368,9 +370,24 @@ export type UserData = {
368
370
  provider: number[],
369
371
  student: number[],
370
372
  }},
371
- alumniConversationUid?: string
373
+ alumniConversationUid?: string,
374
+ savedDataViewerFilters?: {
375
+ [cohortId: string]: {
376
+ [key in "cohortPlacements"|"cohortStudents"]: {
377
+ [viewKey: string]: DataViewerFilterView
378
+ }
379
+ }
380
+ }
372
381
  };
373
382
 
383
+ export type DataViewerFilterView = {
384
+ title: string,
385
+ search?: string,
386
+ sort?: string,
387
+ filters?: FilterObject,
388
+ permanent?: boolean,
389
+ }
390
+
374
391
  export type AnalyticsItem = {
375
392
  units?: "placements"|"weeks"|"days"|"hours",
376
393
  name?: string
@@ -593,7 +610,7 @@ export type ProviderContactData = {
593
610
  insuranceExpiry?: string,
594
611
  savedBy?: {[key:string]: {
595
612
  exists: true,
596
- activities?: string[], // IDs of consented activities.
613
+ activities?: null|string[], // IDs of consented activities.
597
614
  providerConsent?: boolean,
598
615
  providerConsentDate?: string,
599
616
  MATschools?: string[],
@@ -638,7 +655,8 @@ export type InstituteData = {
638
655
  shareAlumni?: boolean
639
656
  approveAlumniMessages?: boolean,
640
657
  anonymiseAlumniConvoStudents?: boolean,
641
- alumniConversations?: boolean
658
+ alumniConversations?: boolean,
659
+ activityInvitesSent?: string
642
660
  }&Address;
643
661
 
644
662
  export type ExternalActivity = {
@@ -651,7 +669,8 @@ export type ExternalActivity = {
651
669
  oId?: string,
652
670
  alumni?: boolean,
653
671
  provider?: boolean,
654
- permanent?: boolean
672
+ permanent?: boolean,
673
+ activityType: "workExperience"|"workplaceVisits"|"mentoring"|"mockInterviews"|"other"
655
674
  }
656
675
 
657
676
  export type NotificationObject = {
@@ -706,7 +725,7 @@ export type StaffRoles = {
706
725
  staff: string[]
707
726
  };
708
727
 
709
- export type QueryObjectConstraint = [string, "=="|"<"|"<="|">="|">"|"!="|"array-contains"|"array-contains-any"|"in"|"not-in", string|boolean][]
728
+ export type QueryObjectConstraint = [string, "=="|"<"|"<="|">="|">"|"!="|"array-contains"|"array-contains-any"|"in"|"not-in", string|boolean|string[]][]
710
729
 
711
730
  export type QueryObject = {
712
731
  path: string[],
@@ -762,7 +781,7 @@ export type CohortData = {
762
781
  endSubmission: string,
763
782
  startPlacements: string,
764
783
  endPlacements: string,
765
- stage: "info"|"name"|"placementType"|"students"|"workflow"|"review"|"created"|"archived",
784
+ stage: "info"|"name"|"placementType"|"students"|"workflow"|"review"|"created"|"archived"|"database",
766
785
  workflow: WorkflowStage[]
767
786
  customWorkflow?: boolean,
768
787
  oId: string,
@@ -771,6 +790,8 @@ export type CohortData = {
771
790
  designatedStaff?: string,
772
791
  autoArchiveDate?: string,
773
792
  logType?: "basic"|"custom",
793
+ listingReleaseDate?: string,
794
+ enableListings?: boolean,
774
795
  logs?: {
775
796
  students?: string,
776
797
  staff?: string,
@@ -789,7 +810,16 @@ export type CohortData = {
789
810
  providerFeedbackSent?: Timestamp,
790
811
  studentsFeedbackSent?: Timestamp,
791
812
  emailTemplates?: {[key in keyof typeof emailTemplates]: string},
792
- skillsTargets?: string[]
813
+ skillsTargets?: string[],
814
+ recurringEmails?: {
815
+ sendType?: "all"|"lastWeek",
816
+ frequency?: "oneTime"|"weeklyMonday"|"fortnightlyMonday"
817
+ },
818
+ savedDataViewerFilters?: {
819
+ [key in "cohortPlacements"|"cohortStudents"]: {
820
+ [key: string]: DataViewerFilterView
821
+ }
822
+ }
793
823
  }
794
824
 
795
825
  export type ArrowObject = {
@@ -863,7 +893,10 @@ export type PlacementReviewDetails = {
863
893
  feedback: CustomFormSchema,
864
894
  providerFeedback?: {[key: string]: unknown},
865
895
  primaryColor?: string,
866
- primaryImage?: string
896
+ primaryImage?: string,
897
+ savedBySchool?: boolean,
898
+ consentedActivities?: string[],
899
+ instituteActivities?: {[key: string]: ExternalActivity}
867
900
  }
868
901
 
869
902
  export type BlogCategories = "students"|"providers"|"institutes"|"placementt"
@@ -1016,7 +1049,7 @@ export type EmailTemplateConfig = {
1016
1049
  description: string,
1017
1050
  params: string[],
1018
1051
  button?: {text: string, link: string},
1019
- type: "workflow"|"feedback"
1052
+ type: "workflow"|"feedback"|"event"
1020
1053
  }
1021
1054
 
1022
1055
 
@@ -1059,10 +1092,16 @@ export type SchoolData = OrganisationAddress&{
1059
1092
  export type ExternalEvent = {
1060
1093
  name: string,
1061
1094
  description: string,
1095
+ staffInformation?: string,
1096
+ studentInformation?: string,
1097
+ employerInformation?: string
1098
+ designatedStaff: string,
1062
1099
  created: string,
1063
1100
  activityId?: string,
1101
+ gatsbyBenchmarks?: string, // String array
1064
1102
  oId: string,
1065
1103
  submissionClose: string,
1104
+ acceptingEmployers?: boolean,
1066
1105
  schoolId?: string,
1067
1106
  startDate?: string,
1068
1107
  endDate?: string,
@@ -1082,7 +1121,24 @@ export type ExternalEvent = {
1082
1121
  employers: {
1083
1122
  shareable?: boolean,
1084
1123
  filters: {[key: string]: unknown}
1124
+ },
1125
+ employerFeedback: {
1126
+ files: string[],
1127
+ forms: string[],
1128
+ requiredFiles: {
1129
+ fileName: string,
1130
+ fileType: string
1131
+ }[],
1132
+ },
1133
+ studentFeedback: {
1134
+ files: string[],
1135
+ forms: string[],
1136
+ requiredFiles: {
1137
+ fileName: string,
1138
+ fileType: string
1139
+ }[],
1085
1140
  }
1141
+ completedStages: ("basicDetails"|"employerInvites"|"studentInvites"|"employerSelection"|"feedback")[]
1086
1142
  }&Address
1087
1143
 
1088
1144
 
@@ -1160,4 +1216,24 @@ export type Update = {
1160
1216
  buttonLink?: string,
1161
1217
  date: string,
1162
1218
  imageData?: boolean
1219
+ }
1220
+
1221
+
1222
+
1223
+ export type ExternalEventAttendee = {
1224
+ oId: string,
1225
+ eventId: string,
1226
+ providerContactId: string,
1227
+ status: "invited"|"providerAvailable"|"finalConfirmationSent"|"providerConfirmed"|"feedbackSent"
1228
+ emails: {
1229
+ [k in "invited"|"finalConfirmationSent"|"feedbackSent"]: {
1230
+ sent: number, // If below 3, keep sending reminders.
1231
+ reconciled: string|boolean,
1232
+ log: {
1233
+ [k: string]: string // emailNumber_dateTimeString : eventType
1234
+ }
1235
+ }
1236
+ }
1237
+ attended?: boolean,
1238
+ feedback: {[key: string]: unknown}
1163
1239
  }
package/src/util.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import {where} from "firebase/firestore";
2
2
  import FirebaseQuery from "./firebase/firebaseQuery";
3
- import {AlumniConversation, FlagCodes, InstituteData, StudentPlacementData, UserData, WorkflowStage} from "./typeDefinitions";
3
+ import {AlumniConversation, EmailTemplateConfig, FlagCodes, InstituteData, StudentPlacementData, UserData, WorkflowStage} from "./typeDefinitions";
4
4
  import {convertDate} from "./firebase/util";
5
+ import {Descendant} from "slate";
6
+ import {PRIMARY_COLOUR} from "./constants";
5
7
 
6
8
  type PlacementFlagCodeParams = {
7
9
  placement : StudentPlacementData, // Placement we get flag codes for
@@ -33,8 +35,8 @@ export const getPlacementFlagCodes = async ({placement, studentData, workflow, i
33
35
  // If placement after provider review and not verified
34
36
  const placementIsPostProviderReview = placement.leadTimes.some((x) => x.split("_")[0] === "3");
35
37
  const placementNotEnded = !placement.leadTimes.some((x) => x.split("_")[0] === "8");
36
- const providerUnverified = placement.providerId && !institute?.verifiedProviders?.includes(placement.providerId);
37
- const awaitingProviderInsurance = placement.providerId && institute?.awaitingProviderInsurance?.includes(placement.providerId);
38
+ const providerUnverified = placement.providerContactId && !institute?.verifiedProviders?.includes(placement.providerContactId);
39
+ const awaitingProviderInsurance = placement.providerContactId && institute?.awaitingProviderInsurance?.includes(placement.providerContactId);
38
40
 
39
41
  const riskAssessmentNotVerified = !institute?.verifiedRiskAssessments?.includes(placement.placementId || placement.id);
40
42
  const awaitingRiskAssessment = !placement.riskAssessment || institute?.awaitingPlacementRiskAssessments?.includes(placement.placementId || placement.id);
@@ -174,4 +176,348 @@ export const getMostRecentAlumniMessage = (conversation: AlumniConversation) =>
174
176
  .sort((a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime())[0];
175
177
 
176
178
  return mostRecent || null; // Return most recent or null if none found
177
- };
179
+ };
180
+
181
+
182
+ export function buildEmailHTML({preheader, title, salutation, body, primaryColor=PRIMARY_COLOUR, secondaryBody, primaryButton, secondaryButton, primaryImage="http://cdn.mcauto-images-production.sendgrid.net/d17240a596025cf2/637d5af1-79d2-42c9-a663-e88abfea32b9/686x143.png", designatedStaffEmail, organisationName, params, data}:{preheader: string, title: string, salutation?: string, body: string|Descendant[], primaryColor?: string, secondaryBody?: string, primaryButton?: {title: string, url: string}, secondaryButton?: {title: string, url: string}, primaryImage?: string, designatedStaffEmail?: string, organisationName?: string, params?: EmailTemplateConfig, data?: {[key: string]: string|undefined}}) {
183
+ const serialiseSlate = (nodes: Descendant[]) => {
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ return nodes.map((n:any) => {
186
+ if (n.type === "paragraph") {
187
+ const text:string[]=[];
188
+ for (let i = 0; i < n.children.length; i++) {
189
+ const item = n.children[i];
190
+ let subtext = item.text as string || "<br/>";
191
+ console.log("serialise text", subtext);
192
+
193
+ subtext = subtext.replace("​", "");
194
+ if (item.underline) {
195
+ subtext = `<u>${subtext}</u>`;
196
+ }
197
+ if (item.bold) {
198
+ subtext = `<strong>${subtext}</strong>`;
199
+ }
200
+ if (item.italic) {
201
+ subtext = `<i>${subtext}</i>`;
202
+ }
203
+ text.push(subtext);
204
+ }
205
+ return `<div style="font-family: inherit; text-align: inherit">${text.join("")}</div>`;
206
+ }
207
+ return;
208
+ }).filter((a) => a).join("<div style=\"font-family: inherit; text-align: inherit\"><br></div>");
209
+ };
210
+
211
+ const getEmailContentFromTemplate = (template: string|Descendant[], params: EmailTemplateConfig, data: {[key: string]: string|undefined}) => {
212
+ let finalBody = typeof template === "string" ? template : serialiseSlate(template);
213
+ console.log("Body before processing", finalBody);
214
+
215
+ // Replace all instances of placeholder with actual data.
216
+ [...params.params, ...Object.keys(data)].forEach((param) => {
217
+ console.log(`Replacing {{${param}}} with "${data[param]}"`);
218
+ finalBody = finalBody.replace(`{{${param}}}`, data[param] as string);
219
+ });
220
+ console.log("Processed body", finalBody);
221
+ // Still have the {{button}} elements to replace. In this case, split the string up between the button elements.
222
+ const itemsInBody = finalBody.split(/(\{\{button\}\})/);
223
+
224
+ const itemsInBodyWithFormattedButtons = itemsInBody.map((item) => {
225
+ if (item !== "{{button}}" || !params.button) return item;
226
+
227
+ let title = params.button.text;
228
+ let url = "https://placementt.co.uk"+params.button.link;
229
+
230
+ // Replace any params in the url or title with the relevant data.
231
+ [...params.params, ...Object.keys(data)].forEach((param) => {
232
+ title = title.replace(`{{${param}}}`, data[param] as string);
233
+ });
234
+ [...params.params, ...Object.keys(data)].forEach((param) => {
235
+ url = url.replace(`{{${param}}}`, data[param] as string);
236
+ });
237
+ return {
238
+ title: title,
239
+ url: url,
240
+ };
241
+ });
242
+
243
+ return itemsInBodyWithFormattedButtons;
244
+ };
245
+
246
+ let formattedBody = (params && data) ? "" : body;
247
+ let formattedSecondaryBody = (params && data) ? "" : secondaryBody;
248
+ let formattedPrimaryButton = (params && data) ? undefined : primaryButton;
249
+ let formattedSecondaryButton = (params && data) ? undefined : secondaryButton;
250
+
251
+ if (params && data) {
252
+ const processedBody = getEmailContentFromTemplate(body, params, data);
253
+
254
+ processedBody.forEach((item) => {
255
+ if (typeof item === "string") {
256
+ if (!formattedBody) {
257
+ formattedBody = item;
258
+ return;
259
+ }
260
+ formattedSecondaryBody = item;
261
+ return;
262
+ }
263
+ // Must be a button
264
+ if (!formattedPrimaryButton) {
265
+ formattedPrimaryButton = item;
266
+ return;
267
+ }
268
+ formattedSecondaryButton = item;
269
+ });
270
+ formattedBody = processedBody[0] as string;
271
+ }
272
+
273
+ const emailHeadAndStylesHTML = `
274
+ <head>
275
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
276
+ <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
277
+ <!--[if !mso]><!-->
278
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
279
+ <!--<![endif]--><!--[if (gte mso 9)|(IE)]>
280
+ <xml>
281
+ <o:OfficeDocumentSettings>
282
+ <o:AllowPNG/>
283
+ <o:PixelsPerInch>96</o:PixelsPerInch>
284
+ </o:OfficeDocumentSettings>
285
+ </xml>
286
+ <![endif]--><!--[if (gte mso 9)|(IE)]>
287
+ <style type="text/css"> body {width: 600px;margin: 0 auto;} table {border-collapse: collapse;} table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;} img {-ms-interpolation-mode: bicubic;} </style>
288
+ <![endif]-->
289
+ <style type="text/css"> body, p, div {font-family: inherit;font-size: 14px;} body {color: #000000;} body a {color: #000000;text-decoration: none;} p { margin: 0; padding: 0; } table.wrapper {width:100% !important;table-layout: fixed;-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: 100%;-moz-text-size-adjust: 100%;-ms-text-size-adjust: 100%;} img.max-width {max-width: 100% !important;} .column.of-2 {width: 50%;} .column.of-3 {width: 33.333%;} .column.of-4 {width: 25%;} ul ul ul ul {list-style-type: disc !important;} ol ol {list-style-type: lower-roman !important;} ol ol ol {list-style-type: lower-latin !important;} ol ol ol ol {list-style-type: decimal !important;} @media screen and (max-width:480px) {.preheader .rightColumnContent, .footer .rightColumnContent {text-align: left !important;} .preheader .rightColumnContent div, .preheader .rightColumnContent span, .footer .rightColumnContent div, .footer .rightColumnContent span {text-align: left !important;} .preheader .rightColumnContent, .preheader .leftColumnContent {font-size: 80% !important;padding: 5px 0;} table.wrapper-mobile {width: 100% !important;table-layout: fixed;} img.max-width {height: auto !important;max-width: 100% !important;} a.bulletproof-button {display: block !important;width: auto !important;font-size: 80%;padding-left: 0 !important;padding-right: 0 !important;} .columns {width: 100% !important;} .column {display: block !important;width: 100% !important;padding-left: 0 !important;padding-right: 0 !important;margin-left: 0 !important;margin-right: 0 !important;} .social-icon-column {display: inline-block !important;} } </style>
290
+ <style> @media screen and (max-width:480px) {table\0 {width: 480px !important;} } </style>
291
+ <!--user entered Head Start-->
292
+ <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
293
+ <style> * {font-family: 'nunito', sans-serif;} </style>
294
+ <!--End Head user entered-->
295
+ </head>
296
+ `;
297
+
298
+ const primaryButtonTextLinkHTML = formattedPrimaryButton ? `
299
+ <tr>
300
+ <td style="padding:0px 15px 0px 15px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
301
+ <div>
302
+ <div style="font-family: inherit; text-align: inherit">Or go to <a href="${formattedPrimaryButton.url}">${formattedPrimaryButton.url}</a></div>
303
+ <div></div>
304
+ </div>
305
+ </td>
306
+ </tr>` : "";
307
+
308
+ const primaryButtonHTML = formattedPrimaryButton ? `
309
+ <tr>
310
+ <td align="center" bgcolor="${primaryColor}" class="inner-td" style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;"><a href="${formattedPrimaryButton.url}" style="background-color:${primaryColor}; border:1px solid #ffffff; border-color:#ffffff; border-radius:30px; border-width:1px; color:#ffffff; display:inline-block; font-size:16px; font-weight:normal; letter-spacing:0px; line-height:normal; padding:12px 40px 12px 40px; text-align:center; text-decoration:none; border-style:solid; font-family:nunito,sans-serif;" target="_blank">${formattedPrimaryButton.title}</a></td>
311
+ </tr>` : "";
312
+
313
+ const secondaryButtonTextHTML = formattedSecondaryButton ? `
314
+ <tr>
315
+ <td align="center" class="inner-td" style="text-align:center; background-color:inherit;"><a href="${formattedSecondaryButton.url}" style="display:inline-block; font-weight:normal; color:#aeaeae; letter-spacing:0px; line-height:normal; text-align:center; text-decoration:none; font-family:nunito,sans-serif;" target="_blank">${formattedSecondaryButton.title}</a></td>
316
+ </tr>` : "";
317
+
318
+ const buttonTableHTML = (primaryButtonHTML || secondaryButtonTextHTML) ? `
319
+ <table border="0" cellpadding="0" cellspacing="0" class="module" data-role="module-button" data-type="button" role="module" style="table-layout:fixed;" width="100%" data-muid="3757586a-ce69-48ba-bd9a-0c0b7937a616">
320
+ <tbody>
321
+ <tr>
322
+ <td align="center" bgcolor="" class="outer-td" style="padding:0px 0px 40px 0px;">
323
+ <table border="0" cellpadding="0" cellspacing="0" class="wrapper-mobile" style="text-align:center;">
324
+ <tbody>
325
+ ${primaryButtonHTML}
326
+ ${secondaryButtonTextHTML}
327
+ </tbody>
328
+ </table>
329
+ </td>
330
+ </tr>
331
+ ${primaryButtonTextLinkHTML}
332
+ </tbody>
333
+ </table>` : "";
334
+
335
+ const secondaryBodyHTML = formattedSecondaryBody ? `
336
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="c589a68d-fd31-4291-b822-cddb80753f13" data-mc-module-version="2019-10-22">
337
+ <tbody>
338
+ <tr>
339
+ <td style="padding:0px 15px 0px 15px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
340
+ <div>
341
+ <div style="font-family: inherit; text-align: inherit"><br></div>
342
+ <div style="font-family: inherit; text-align: inherit">${formattedSecondaryBody}</div>
343
+ <div></div>
344
+ </div>
345
+ </td>
346
+ </tr>
347
+ </tbody>
348
+ </table>` : "";
349
+
350
+ const organisationContactSentence = (designatedStaffEmail || organisationName) ?
351
+ `
352
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="50de54bb-94d0-4d21-a7f2-100719150961" data-mc-module-version="2019-10-22">
353
+ <tbody>
354
+ <tr>
355
+ <td style="padding:0px 15px 18px 15px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
356
+ <div>
357
+
358
+ <div style="font-family: inherit; text-align: inherit">
359
+
360
+ ${organisationName ? `<span style="color: #aeaeae">This email was sent by Placementt on behalf of ${organisationName}.</span>` :
361
+ designatedStaffEmail ? `Please do not reply directly to this email. For any queries, contact <a href="mailto:${designatedStaffEmail}">${designatedStaffEmail}</a>` : ""}
362
+ </div>
363
+ </div>
364
+ </td>
365
+ </tr>
366
+ </tbody>
367
+ </table>` :
368
+ "";
369
+
370
+ return `<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml">
371
+ ${emailHeadAndStylesHTML}
372
+ <body>
373
+ <center class="wrapper" data-link-color="#000000" data-body-style="font-size:14px; font-family:inherit; color:#000000; background-color:#F9F4FF;">
374
+ <div class="webkit">
375
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F9F4FF">
376
+ <tbody>
377
+ <tr>
378
+ <td valign="top" bgcolor="#F9F4FF" width="100%">
379
+ <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
380
+ <tbody>
381
+ <tr>
382
+ <td width="100%">
383
+ <table width="100%" cellpadding="0" cellspacing="0" border="0">
384
+ <tbody>
385
+ <tr>
386
+ <td>
387
+ <!--[if mso]>
388
+ <center>
389
+ <table>
390
+ <tr>
391
+ <td width="600">
392
+ <![endif]-->
393
+ <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%; max-width:600px;" align="center">
394
+ <tbody>
395
+ <tr>
396
+ <td role="modules-container" style="padding:0px 0px 0px 0px; color:#000000; text-align:left;" bgcolor="#ffffff" width="100%" align="left">
397
+ <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
398
+ <tbody>
399
+ <tr>
400
+ <td role="module-content">
401
+ <p>${preheader}</p>
402
+ </td>
403
+ </tr>
404
+ </tbody>
405
+ </table>
406
+ <table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" role="module" data-type="columns" style="padding:20px 0px 20px 0px;" bgcolor="#ffffff" data-distribution="1">
407
+ <tbody>
408
+ <tr role="module-content">
409
+ <td height="100%" valign="top">
410
+ <table width="600" style="width:600px; border-spacing:0; border-collapse:collapse; margin:0px 0px 0px 0px;" cellpadding="0" cellspacing="0" align="left" border="0" bgcolor="" class="column column-0">
411
+ <tbody>
412
+ <tr>
413
+ <td style="padding:0px;margin:0px;border-spacing:0;">
414
+ <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="9f29c991-6500-41ef-9f0e-d56cb5dc1238">
415
+ <tbody>
416
+ <tr>
417
+ <td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;" valign="top" align="center"><img class="max-width" border="0" style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:52% !important; width:52%; height:auto !important; max-height: 100px; object-fit: contain" width="312" alt="" data-proportionally-constrained="false" data-responsive="true" src="${primaryImage}"></td>
418
+ </tr>
419
+ </tbody>
420
+ </table>
421
+ </td>
422
+ </tr>
423
+ </tbody>
424
+ </table>
425
+ </td>
426
+ </tr>
427
+ </tbody>
428
+ </table>
429
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="b35b8ff4-8b3c-4b35-9ed3-f9f25170affc" data-mc-module-version="2019-10-22">
430
+ <tbody>
431
+ <tr>
432
+ <td style="padding:20px 20px 18px 20px; line-height:28px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
433
+ <div>
434
+ <div style="font-family: nunito, sans-serif; text-align: center"><span style="font-size: 24px; font-family: inherit">${title}</span></div>
435
+ <div></div>
436
+ </div>
437
+ </td>
438
+ </tr>
439
+ </tbody>
440
+ </table>
441
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="c589a68d-fd31-4291-b822-cddb80753f13" data-mc-module-version="2019-10-22">
442
+ <tbody>
443
+ <tr>
444
+ <td style="padding:18px 15px 18px 15px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
445
+ <div>
446
+ ${salutation ? `<div style="font-family: inherit; text-align: inherit"><span style="color: #000000; font-family: Colfax, Helvetica, Arial, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline">${salutation},</span></div><div style="font-family: inherit; text-align: inherit"><br></div>` : ""}
447
+ <div style="font-family: inherit; text-align: inherit">${formattedBody}</div>
448
+ <div></div>
449
+ </div>
450
+ </td>
451
+ </tr>
452
+ </tbody>
453
+ </table>
454
+ ${buttonTableHTML}
455
+ ${secondaryBodyHTML}
456
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="50de54bb-94d0-4d21-a7f2-100719150961" data-mc-module-version="2019-10-22">
457
+ <tbody>
458
+ <tr>
459
+ <td style="padding:18px 15px 9px 15px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content">
460
+ <div>
461
+ <div style="font-family: inherit; text-align: inherit"><span style="color: #aeaeae">If you do not recognise this email, you can safely ignore it.</span></div>
462
+ <div></div>
463
+ </div>
464
+ </td>
465
+ </tr>
466
+ </tbody>
467
+ </table>
468
+ ${organisationContactSentence}
469
+ <table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" role="module" data-type="columns" style="padding:0px 0px 0px 0px;" bgcolor="#FFFFFF" data-distribution="1">
470
+ <tbody>
471
+ <tr role="module-content">
472
+ <td height="100%" valign="top">
473
+ <table width="600" style="width:600px; border-spacing:0; border-collapse:collapse; margin:0px 0px 0px 0px;" cellpadding="0" cellspacing="0" align="left" border="0" bgcolor="" class="column column-0">
474
+ <tbody>
475
+ <tr>
476
+ <td style="padding:0px;margin:0px;border-spacing:0;">
477
+ <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="a49405df-a253-4a28-8d3d-be95449c7d30" data-mc-module-version="2019-10-22">
478
+ <tbody>
479
+ <tr>
480
+ <td style="padding:18px 0px 18px 0px; line-height:0px; text-align:inherit; background-color:${primaryColor};" height="100%" valign="top" bgcolor="${primaryColor}" role="module-content">
481
+ <div>
482
+ <div style="font-family: inherit; text-align: center"><a href="http://"><span style="font-size: 12px; color: #ffffff; font-family: inherit">www.placementt.co.uk</span></a></div>
483
+ <div></div>
484
+ </div>
485
+ </td>
486
+ </tr>
487
+ </tbody>
488
+ </table>
489
+ </td>
490
+ </tr>
491
+ </tbody>
492
+ </table>
493
+ </td>
494
+ </tr>
495
+ </tbody>
496
+ </table>
497
+ </td>
498
+ </tr>
499
+ </tbody>
500
+ </table>
501
+ <!--[if mso]>
502
+ </td>
503
+ </tr>
504
+ </table>
505
+ </center>
506
+ <![endif]-->
507
+ </td>
508
+ </tr>
509
+ </tbody>
510
+ </table>
511
+ </td>
512
+ </tr>
513
+ </tbody>
514
+ </table>
515
+ </td>
516
+ </tr>
517
+ </tbody>
518
+ </table>
519
+ </div>
520
+ </center>
521
+ </body>
522
+ </html>`;
523
+ }