Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat.user trigger changes #2051

Open
wants to merge 9 commits into
base: fix.update-forecastTrigger-references
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
@if (area.mainExposureValue) {
<ion-note size="small">
<span
data-testid="main-exposure-indicator"
[innerHTML]="
'chat-component.' + disasterTypeName + '.active-event.exposed'
| translate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,25 @@
}}</span>
}
</p>
@if (forecastSource) {
@if (forecastSource && !event.userTrigger) {
<p>
<strong
>{{ 'chat-component.common.alertLevel.source' | translate }} </strong
><a href="{{ forecastSource.url }}">{{ forecastSource.label }}</a>
</p>
}
@if (event.userTrigger) {
<p
[innerHTML]="
'chat-component.common.alertLevel.set-by'
| translate
: {
userTriggerName: event.userTriggerName,
userTriggerDate: event.userTriggerDate,
}
"
></p>
}
<p>
<strong
>{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@
></app-date-button>
</ion-button>
}
<app-tooltip
data-testid="tooltip-button"
class="event-header--tooltip"
[value]="'timeline-component.tooltip' | translate"
color="ibf-no-alert-primary"
></app-tooltip>
</ion-item>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { TimelineComponent } from 'src/app/components/timeline/timeline.component';
import { TimelineService } from 'src/app/services/timeline.service';

Expand All @@ -16,7 +17,11 @@ describe('TimelineComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [TimelineComponent],
imports: [IonicModule, RouterModule.forRoot([])],
imports: [
IonicModule,
RouterModule.forRoot([]),
TranslateModule.forRoot(),
],
providers: [
{ provide: TimelineService },
provideHttpClient(withInterceptorsFromDi()),
Expand Down
3 changes: 3 additions & 0 deletions interfaces/IBF-dashboard/src/app/mocks/event-state.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const MOCK_EVENT: EventSummary = {
alertLevel: AlertLevel.TRIGGER,
eventName: 'National',
disasterSpecificProperties: {},
userTrigger: false,
userTriggerDate: null,
userTriggerName: null,
};
export const MOCK_EVENT_STATE: EventState = {
events: [MOCK_EVENT, MOCK_EVENT],
Expand Down
6 changes: 6 additions & 0 deletions interfaces/IBF-dashboard/src/app/services/event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export class EventSummary {
nrAlertAreas?: number;
mainExposureValueSum?: number;
alertLevel: AlertLevel;
userTrigger: boolean;
userTriggerDate: string;
userTriggerName: string;
}

export enum AlertLevel {
Expand Down Expand Up @@ -218,6 +221,9 @@ export class EventService {
event.firstIssuedDate = DateTime.fromISO(
event.firstIssuedDate,
).toFormat('cccc, dd LLLL');
event.userTriggerDate = DateTime.fromISO(
event.userTriggerDate,
).toFormat('cccc, dd LLLL');
event.firstLeadTimeLabel = LeadTimeTriggerKey[event.firstLeadTime];
event.timeUnit = event.firstLeadTime?.split('-')[1];

Expand Down
6 changes: 5 additions & 1 deletion interfaces/IBF-dashboard/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
"error": "Something went wrong in setting the trigger. Please try again."
}
},
"timeline-component": {
"tooltip": "The timeline displays dates of the predicted events with an outlined alert sign for warning and a filled-in alert sign for trigger. When selecting an event, the corresponding period / start date will be highlighted with a purple outline."
},
"chat-component": {
"common": {
"no-info": "No information available",
Expand All @@ -126,7 +129,8 @@
"expected": "expected on {{ date }}. ",
"source": "Forecast source: ",
"exposed-areas": "Exposed {{adminAreaLabelPlural}}:",
"no-data": "No exposure data available"
"no-data": "No exposure data available",
"set-by": "<strong>Set by:</strong> {{ userTriggerName }} on {{ userTriggerDate }}"
},
"set-trigger": {
"btn-text": "Set trigger"
Expand Down
17 changes: 11 additions & 6 deletions services/API-service/src/api/event/event.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,14 @@ export class EventService {
event.eventName,
false,
);
event.firstTriggerLeadTime = await this.getFirstLeadTime(
countryCodeISO3,
disasterType,
event.eventName,
true,
);
event.firstTriggerLeadTime = event.userTrigger
? event.firstLeadTime
: await this.getFirstLeadTime(
countryCodeISO3,
disasterType,
event.eventName,
true,
);
event.alertLevel = this.getAlertLevel(event);
if (disasterType === DisasterType.Typhoon) {
event.disasterSpecificProperties =
Expand Down Expand Up @@ -189,6 +191,7 @@ export class EventService {
.createQueryBuilder('event')
.select(['area."countryCodeISO3"', 'event."eventName"'])
.leftJoin('event.adminArea', 'area')
.leftJoin('event.user', 'user')
.groupBy('area."countryCodeISO3"')
.addGroupBy('event."eventName"')
.addSelect([
Expand All @@ -198,6 +201,8 @@ export class EventService {
'MAX(event."forecastSeverity")::float AS "forecastSeverity"',
'MAX(event."forecastTrigger"::int)::boolean AS "forecastTrigger"',
'MAX(event."userTrigger"::int)::boolean AS "userTrigger"',
'MAX(event."userTriggerDate") AS "userTriggerDate"',
'MAX("user"."firstName" || \' \' || "user"."lastName") AS "userTriggerName"',
'sum(event."mainExposureValue")::int AS "mainExposureValueSum"', // FIX: this goes wrong in case of percentage indicator (% houses affected typhoon)
])
.andWhere('area."countryCodeISO3" = :countryCodeISO3', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
AlertArea,
DisasterSpecificProperties,
EapAlertClass,
EventSummaryCountry,
} from '../../../shared/data.model';
import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum';

export class NotificationDataPerEventDto {
event: EventSummaryCountry;
triggerStatusLabel: AlertStatusLabelEnum;
eventName: string;
disasterSpecificProperties: DisasterSpecificProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ export class EmailService {
lastUploadDate: LastUploadDateDto,
): Promise<void | string> {
const emailContent =
await this.notificationContentService.getContentTriggerNotification(
await this.notificationContentService.getContentActiveEvents(
country,
disasterType,
activeEvents,
);
let emailHtml = '';

emailHtml += this.mjmlService.getTriggerEmailHtmlOutput({
emailHtml += this.mjmlService.getActiveEventEmailHtmlOutput({
emailContent,
date: lastUploadDate.timestamp,
});
Expand Down Expand Up @@ -92,7 +92,7 @@ export class EmailService {
await this.notificationContentService.getDisasterTypeLabel(disasterType);

const emailContent =
await this.notificationContentService.getContentTriggerNotification(
await this.notificationContentService.getContentActiveEvents(
country,
disasterType,
finishedEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import mjml2html from 'mjml';

import { HelperService } from '../../../shared/helper.service';
import { ContentEventEmail } from '../dto/content-trigger-email.dto';
import { ContentEventEmail } from '../dto/content-event-email.dto';
import {
BODY_WIDTH,
EMAIL_HEAD,
Expand Down Expand Up @@ -70,7 +70,7 @@ export class MjmlService {
socialMediaType,
});

public getTriggerEmailHtmlOutput({
public getActiveEventEmailHtmlOutput({
emailContent,
date,
}: {
Expand Down
45 changes: 42 additions & 3 deletions services/API-service/src/api/notification/email/mjml/body-event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LeadTime } from '../../../admin-area-dynamic-data/enum/lead-time.enum';
import { DisasterType } from '../../../disaster-type/disaster-type.enum';
import { ContentEventEmail } from '../../dto/content-trigger-email.dto';
import { ContentEventEmail } from '../../dto/content-event-email.dto';
import {
AlertStatusLabelEnum,
NotificationDataPerEventDto,
Expand Down Expand Up @@ -34,6 +34,8 @@ const getMjmlBodyEvent = ({
eapLink,
triggerStatusLabel,
disasterSpecificCopy,
forecastSource,
userTriggerData,
}: {
color: string;
defaultAdminAreaLabel: string;
Expand All @@ -51,6 +53,8 @@ const getMjmlBodyEvent = ({
eapLink: string;
triggerStatusLabel: string;
disasterSpecificCopy: string;
forecastSource: { label: string; url: string };
userTriggerData: { name: string; date: Date };
}): object => {
const icon = getInlineImage({ src: triangleIcon, size: 16 });

Expand Down Expand Up @@ -90,9 +94,28 @@ const getMjmlBodyEvent = ({
`<strong>Expected exposed ${defaultAdminAreaLabel}:</strong> ${nrOfAlertAreas} (see list below)`,
);

if (forecastSource) {
contentContent.push(
forecastSource.url
? `<strong>Forecast source:</strong> <a href="${forecastSource.url}">${forecastSource.label}</a>`
: `<strong>Forecast source:</strong> ${forecastSource.label}`,
);
}

if (userTriggerData) {
contentContent.push(
`<strong>Set by:</strong> ${userTriggerData.name} on ${dateObjectToDateTimeString(
userTriggerData.date,
'UTC',
)}`,
);
}

contentContent.push(
triggerStatusLabel === AlertStatusLabelEnum.Trigger
? `<strong>Advisory:</strong> Activate <a href="${eapLink}">Early Action Protocol</a>`
? eapLink
? `<strong>Advisory:</strong> Activate <a href="${eapLink}">Protocol</a>` // Not all implemtations have an EAP, so for now defaulting to more generic copy
: `<strong>Advisory:</strong> Activate Protocol`
: `<strong>Advisory:</strong> Inform all potentialy exposed ${defaultAdminAreaLabel}`,
);

Expand Down Expand Up @@ -156,6 +179,11 @@ export const getMjmlEventListBody = (emailContent: ContentEventEmail) => {
disasterSpecificCopy = getTyphoonSpecificCopy(event);
}

const countryDisasterSettings =
emailContent.country.countryDisasterSettings.find(
(setting) => setting.disasterType === emailContent.disasterType,
);

eventList.push(
getMjmlBodyEvent({
eventName: event.eventName,
Expand Down Expand Up @@ -194,7 +222,18 @@ export const getMjmlEventListBody = (emailContent: ContentEventEmail) => {
event.eapAlertClass?.color,
event.triggerStatusLabel,
),

forecastSource: event.event.userTrigger // Hide forecast source for "set" triggers
? null
: (countryDisasterSettings.forecastSource as unknown as {
label: string;
url: string;
}),
userTriggerData: event.event.userTrigger // Hide forecast source for "set" triggers
? {
name: event.event.userTriggerName,
date: event.event.userTriggerDate,
}
: null,
// Disaster-specific copy
disasterSpecificCopy,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NumberFormat } from '../../../../shared/enums/number-format.enum';
import { IndicatorMetadataEntity } from '../../../metadata/indicator-metadata.entity';
import { AdminAreaLabel } from '../../dto/admin-area-notification-info.dto';
import { ContentEventEmail } from '../../dto/content-trigger-email.dto';
import { ContentEventEmail } from '../../dto/content-event-email.dto';
import { NotificationDataPerEventDto } from '../../dto/notification-date-per-event.dto';
import {
COLOR_WHITE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AlertLevel } from '../../event/enum/alert-level.enum';
import { EventService } from '../../event/event.service';
import { IndicatorMetadataEntity } from '../../metadata/indicator-metadata.entity';
import { AdminAreaLabel } from '../dto/admin-area-notification-info.dto';
import { ContentEventEmail } from '../dto/content-trigger-email.dto';
import { ContentEventEmail } from '../dto/content-event-email.dto';
import {
AlertStatusLabelEnum,
NotificationDataPerEventDto,
Expand All @@ -35,7 +35,7 @@ export class NotificationContentService {
private readonly helperService: HelperService,
) {}

public async getContentTriggerNotification(
public async getContentActiveEvents(
country: CountryEntity,
disasterType: DisasterType,
activeEvents: EventSummaryCountry[],
Expand Down Expand Up @@ -163,6 +163,7 @@ export class NotificationContentService {
disasterType: DisasterType,
): Promise<NotificationDataPerEventDto> {
const data = new NotificationDataPerEventDto();
data.event = event;
data.triggerStatusLabel =
event.alertLevel === AlertLevel.TRIGGER
? AlertStatusLabelEnum.Trigger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ export class ProcessPipelineController {
);
}

// NOTE: remove after all pipelines migrated to /event/process
@Roles(UserRole.PipelineUser)
@ApiOperation({
summary:
'[DEV/TEST/DEMO only] Send test notification (e-mail and/or whatsapp) about events to recipients for given country and disaster-type.',
'[EXTERNALLY USED - PIPELINE] Old endpoint to send notification instructions. Runs full /events/process in practice.',
})
@ApiResponse({
status: 201,
Expand All @@ -109,7 +110,7 @@ export class ProcessPipelineController {
description:
'If true, only returns the notification content without sending it',
})
@Post('notification/send') // NOTE: Change to /event/notify after all pipelines have migrated
@Post('notification/send')
@ApiConsumes()
@UseInterceptors()
public async send(
Expand All @@ -128,4 +129,42 @@ export class ProcessPipelineController {
noNotifications,
);
}

@Roles(UserRole.PipelineUser)
@ApiOperation({
summary:
'[DEV/TEST/DEMO only] Send test notification (e-mail and/or whatsapp) about events to recipients for given country and disaster-type.',
})
@ApiResponse({
status: 201,
description:
'Notification request sent (actual e-mails/whatsapps sent only if there is an active event)',
})
@ApiQuery({
name: 'noNotifications',
required: false,
schema: { default: false, type: 'boolean' },
type: 'boolean',
description:
'If true, only returns the notification content without sending it',
})
@Post('events/notify')
@ApiConsumes()
@UseInterceptors()
public async notify(
@Body() sendNotification: ProcessEventsDto,
@Query(
'noNotifications',
new ParseBoolPipe({
optional: true,
}),
)
noNotifications: boolean,
): Promise<void | NotificationApiTestResponseDto> {
return await this.processPipelineService.notify(
sendNotification.countryCodeISO3,
sendNotification.disasterType,
noNotifications,
);
}
}
Loading
Loading