generic table structure
MODIFY
file.txt
file.txt
<mjml><mj-head>
- <mj-title>[missing "en.email.end-of-session.title" translation]</mj-title>
+ <mj-title>Session Finished</mj-title>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
</mj-section>
<mj-section>
<mj-column>
- <mj-text font-weight="500" font-size="20px">[missing "en.email.end-of-session.title" translation]</mj-text>
+ <mj-text font-weight="500" font-size="20px">Session Finished</mj-text>
</mj-column>
</mj-section>
,<mj-section padding-top="20px">
<mj-column>
<mj-text>
<p>Hello ! User,</p>
- <p>[missing "en.email.end-of-session.text" translation]</p>
+ <p>Your session on the charging station SAP Moujins 19 A, connector A has just finished.</p>
</mj-text>
</mj-column>
</mj-section>
+,<mj-section>
+ <mj-column width="70%">
+ <mj-table>
+ <tr><th style="font-size:18px;font-weight:300">Consumption</th><td style="font-size:18px;font-weight:400;width:50%;text-align:center">52,3 kW.h</td></tr><tr><th style="font-size:18px;font-weight:300">Total Duration</th><td style="font-size:18px;font-weight:400;width:50%;text-align:center">5h41</td></tr><tr><th style="font-size:18px;font-weight:300">Total Inactivity</th><td style="font-size:18px;font-weight:400;width:50%;text-align:center">0h13</td></tr>
+ </mj-table>
+ </mj-column>
+</mj-section>
,<mj-section>
<mj-column>
- <mj-button font-size="16px" href="undefined">
- [missing "en.email.end-of-session.buttonText" translation]
+ <mj-button background-color="#8fd365" font-size="16px" href="charging station url">
+ View Session
</mj-button>
</mj-column>
</mj-section>

MODIFY
src/assets/i18n/en.json
src/assets/i18n/en.json
"title":"Reset Password",
"buttonText":"Reset Password",
"text":["You have just requested to reset your password." , "Click on the link below to generate and receive a new one."]
+ },
+ "optimal-charge-reached": {
+ "title":"Optimal Charge Reached",
+ "buttonText":"View Session",
+ "text":["Your electric vehicle, which is connected to {{chargeBoxID}}, connector {{connectorId}}, reached its optimal charge."],
+ "tableColumns":["Battery Level"]
+ },
+ "end-of-session": {
+ "title":"Session Finished",
+ "buttonText":"View Session",
+ "text":["Your session on the charging station {{chargeBoxID}}, connector {{connectorId}} has just finished."],
+ "tableColumns":["Consumption","Total Duration","Total Inactivity","Battery Level"]
+ },
+ "end-of-charge": {
+ "title":"Charge Finished",
+ "buttonText":"View Session",
+ "text":["Your electric vehicle, which is connected to the charging station {{chargeBoxID}}, connector {{connectorId}}, has just finished charging."],
+ "tableColumns":["Consumption","Total Duration","Battery Level"]
}
}
}

MODIFY
src/notification/email/EMailNotificationTask.ts
src/notification/email/EMailNotificationTask.ts
@@ -65,17 +65,23 @@ export default class EMailNotificationTask implements NotificationTask {
public async sendOptimalChargeReached(data: OptimalChargeReachedNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
data.buttonUrl = data.evseDashboardChargingStationURL;
- return this.prepareAndSendEmail('optimal-charge-reached', data, user, tenant, severity);
+ data.tableValues = [data.stateOfCharge + '%'];
+ const optionalComponents = await EmailComponentManager.getComponent(EmailComponent.TABLE);
+ return this.prepareAndSendEmail('optimal-charge-reached', data, user, tenant, severity,false,optionalComponents);
}
public async sendEndOfCharge(data: EndOfChargeNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
data.buttonUrl = data.evseDashboardChargingStationURL;
- return this.prepareAndSendEmail('end-of-charge', data, user, tenant, severity);
+ data.tableValues = [data.totalConsumption + ' kW.h',data.totalDuration];
+ const optionalComponents = await EmailComponentManager.getComponent(EmailComponent.TABLE);
+ return this.prepareAndSendEmail('end-of-charge', data, user, tenant, severity,false,optionalComponents);
}
public async sendEndOfSession(data: EndOfSessionNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
data.buttonUrl = data.evseDashboardChargingStationURL;
- return this.prepareAndSendEmail('end-of-session', data, user, tenant, severity);
+ data.tableValues = [data.totalConsumption + ' kW.h',data.totalDuration,data.totalInactivity];
+ const optionalComponents = await EmailComponentManager.getComponent(EmailComponent.TABLE);
+ return this.prepareAndSendEmail('end-of-session', data, user, tenant, severity,false,optionalComponents);
}
public async sendEndOfSignedSession(data: EndOfSignedSessionNotification, user: User, tenant: Tenant, severity: NotificationSeverity): Promise<void> {
@@ -304,7 +310,7 @@ export default class EMailNotificationTask implements NotificationTask {
}
}
- private async sendSmartEmail(prefix: string, context: any, user: User, tenant: Tenant, severity: NotificationSeverity, useSmtpClientBackup = false): Promise<void> {
+ private async sendSmartEmail(prefix: string, context: any, user: User, tenant: Tenant, severity: NotificationSeverity, useSmtpClientBackup = false, optionalComponents?: string): Promise<void> {
let startTime: number;
let emailContent = {} as EmailNotificationMessage;
try {
@@ -323,6 +329,7 @@ export default class EMailNotificationTask implements NotificationTask {
const template = (await mjmlBuilder.initialize())
.addToBody(await EmailComponentManager.getComponent(EmailComponent.TITLE))
.addToBody(await EmailComponentManager.getComponent(EmailComponent.TEXT1))
+ .addToBody(optionalComponents)
.addToBody(await EmailComponentManager.getComponent(EmailComponent.BUTTON))
.buildTemplate();
template.resolve(i18nInstance, context,prefix);
@@ -355,9 +362,9 @@ export default class EMailNotificationTask implements NotificationTask {
}
}
- private async prepareAndSendEmail(templateName: string, data: any, user: User, tenant: Tenant, severity: NotificationSeverity, useSmtpClientBackup = false): Promise<void> {
+ private async prepareAndSendEmail(templateName: string, data: any, user: User, tenant: Tenant, severity: NotificationSeverity, useSmtpClientBackup = false,optionalComponents?:string): Promise<void> {
if (FeatureToggles.isFeatureActive(Feature.NEW_EMAIL_TEMPLATES)) {
- return await this.sendSmartEmail(templateName, data, user, tenant, severity);
+ return await this.sendSmartEmail(templateName, data, user, tenant, severity,false,optionalComponents);
}
let startTime: number;

MODIFY
src/notification/email/email-component-manager/EmailComponentManager.ts
src/notification/email/email-component-manager/EmailComponentManager.ts
@@ -10,6 +10,8 @@ export enum EmailComponent {
TITLE = 'title.mjml',
TEXT1 = 'text1.mjml',
TEXT2 = 'text2.mjml',
+
+ TABLE='table.mjml',
}
export default class EmailComponentManager {

MODIFY
src/notification/email/mjml-builder/MjmlBuilder.ts
src/notification/email/mjml-builder/MjmlBuilder.ts
@@ -17,9 +17,10 @@ export default class MjmlBuilder {
public static async initialize(): Promise<MjmlBuilder> {
const instance = new MjmlBuilder();
- instance.addConfig(await EmailComponentManager.getComponent(EmailComponent.CONFIG));
- instance.addHeader(await EmailComponentManager.getComponent(EmailComponent.HEADER));
- instance.addFooter(await EmailComponentManager.getComponent(EmailComponent.FOOTER));
+ instance.addConfig(await EmailComponentManager.getComponent(EmailComponent.CONFIG))
+ .addHeader(await EmailComponentManager.getComponent(EmailComponent.HEADER))
+ .addFooter(await EmailComponentManager.getComponent(EmailComponent.FOOTER));
+
return instance;
}
@@ -29,7 +30,9 @@ export default class MjmlBuilder {
}
public addToBody(component: string): this {
+ if (component) {
this.body.push(component);
+ }
return this;
}

MODIFY
src/notification/email/mjml-components/button.mjml
src/notification/email/mjml-components/button.mjml
<mj-section>
<mj-column>
- <mj-button font-size="16px" href="{{buttonUrl}}">
+ <mj-button background-color="#8fd365" font-size="16px" href="{{buttonUrl}}">
{{i18n:buttonText}}
</mj-button>
</mj-column>

ADD
src/notification/email/mjml-components/table.mjml
+<mj-section>
+ <mj-column width="70%">
+ <mj-table>
+ {{_TABLEBUILD}}
+ </mj-table>
+ </mj-column>
+</mj-section>

MODIFY
src/notification/email/mjml-template/MjmlTemplate.ts
src/notification/email/mjml-template/MjmlTemplate.ts
@@ -18,6 +18,7 @@ export default class MjmlTemplate {
}
public resolve(i18nManager: I18nManager, context: any, prefix: string): void {
+ this.buildComponents(i18nManager,context,prefix);
this.preparei18nSelectors(prefix);
const i18nSelectors = this.geti18nSelectors();
for (const selector of i18nSelectors) {
@@ -36,6 +37,24 @@ export default class MjmlTemplate {
}
}
+ private buildTable(i18nManager: I18nManager,context:any,prefix:string): void {
+ const regex = new RegExp(/_TABLEBUILD/, 'g');
+ const match = this.template.match(regex);
+ if (!match) {
+ return;
+ }
+ let table = '';
+ const tableColumns = i18nManager.translate('email.' + prefix + '.tableColumns');
+ for (const [index,tableValue] of context.tableValues.entries()) {
+ table = table + `<tr><th style="font-size:18px;font-weight:300">${tableColumns[index]}</th><td style="font-size:18px;font-weight:400;width:50%;text-align:center">${tableValue as string}</td></tr>`;
+ }
+ this.replace(match[0],table);
+ }
+
+ private buildComponents(i18nManager: I18nManager,context:any,prefix:string) {
+ this.buildTable(i18nManager,context,prefix);
+ }
+
private replace(selector: string, value: string): void {
this.template = this.template.replace('{{' + selector + '}}', value);
}

MODIFY
src/types/UserNotifications.ts
src/types/UserNotifications.ts
@@ -101,6 +101,7 @@ export interface BaseNotification {
buttonUrl?:string;
appUrl?:string;
userFirstName?:string;
+ tableValues?:string[];
}
export interface EndOfChargeNotification extends BaseNotification {

MODIFY
test/api/EmailTest.ts
test/api/EmailTest.ts
@@ -67,10 +67,10 @@ describe('Initialization', () => {
siteID: 'site_id',
siteAreaID: 'site area id',
companyID: 'company id',
- chargeBoxID: 'charge box id',
- connectorId: 'connector id',
+ chargeBoxID: 'Charging Station 19 SAP',
+ connectorId: 'A',
totalConsumption: 'total consumption',
- stateOfCharge: 1,
+ stateOfCharge: 89,
evseDashboardChargingStationURL: 'charging station url',
user,
evseDashboardURL: 'some_url',
@@ -86,10 +86,10 @@ describe('Initialization', () => {
companyID: 'company id',
chargeBoxID: 'charing box id',
connectorId: 'connector id',
- totalConsumption: 'total consumption',
+ totalConsumption: '48.3',
stateOfCharge: 1,
- totalDuration: 'total duration',
- evseDashboardChargingStationURL: 'charing station url',
+ totalDuration: '5h14',
+ evseDashboardChargingStationURL: 'some_url',
user,
evseDashboardURL: 'some_url',
} as EndOfChargeNotification;
@@ -102,12 +102,12 @@ describe('Initialization', () => {
siteID: 'site id',
siteAreaID: 'site area id',
companyID: 'company id',
- chargeBoxID: 'charge box id',
- connectorId: 'connector id',
- totalConsumption: 'total consumption',
- totalInactivity: 'total inactivity',
+ chargeBoxID: 'SAP Moujins 19 A',
+ connectorId: 'A',
+ totalConsumption: '52,3',
+ totalInactivity: '0h13',
stateOfCharge: 1,
- totalDuration: 'total duration',
+ totalDuration: '5h41',
tenant: tenant.subdomain,
user,
alternateUser: user,
@@ -117,27 +117,27 @@ describe('Initialization', () => {
await emailNotificationTask.sendEndOfSession(data,user,tenant,severity);
});
- it('End Of Signed Session', async () => {
- data = {
- user,
- alternateUser: user,
- transactionId: 1,
- chargeBoxID: 'charging box id',
- connectorId: 'connector id',
- tagId: 'tag id',
- startDate: 'start date',
- endDate: 'end date',
- meterStart: 'meter start',
- meterStop: 'meter stop',
- totalConsumption: 'total consumption',
- price: 1,
- relativeCost: 1,
- startSignedData: 'start signed data',
- endSignedData: 'end signed data',
- evseDashboardURL: 'some_url',
- } as EndOfSignedSessionNotification;
- await emailNotificationTask.sendEndOfSession(data,user,tenant,severity);
- });
+ // it('End Of Signed Session', async () => {
+ // data = {
+ // user,
+ // alternateUser: user,
+ // transactionId: 1,
+ // chargeBoxID: 'charging box id',
+ // connectorId: 'connector id',
+ // tagId: 'tag id',
+ // startDate: 'start date',
+ // endDate: 'end date',
+ // meterStart: 'meter start',
+ // meterStop: 'meter stop',
+ // totalConsumption: 'total consumption',
+ // price: 1,
+ // relativeCost: 1,
+ // startSignedData: 'start signed data',
+ // endSignedData: 'end signed data',
+ // evseDashboardURL: 'some_url',
+ // } as EndOfSignedSessionNotification;
+ // await emailNotificationTask.sendEndOfSession(data,user,tenant,severity);
+ // });
});
});

generic table structure