Skip to content

Commit

Permalink
FSR-1237 | Add Latest Levels on TA Warnings Page (#782)
Browse files Browse the repository at this point in the history
* FSR-1237: Add logic to show trigger levels on target area page

* FSR-1237: Add CSS for latest levels box

* FSR-1237: Update the logic to not show the latest levels box for TAs with more than 4 thresholds

* FSR-1237: Fix failing tests, update hyperlink text in target-area.html, and refactor code

* FSR-1237: Add unit tests for latest levels box

* FSR-1237: Fix SonarCloud issues and refactor code for improved quality

* FSR-1237: Add logic to show 'levels unavailable' message for offline and suspended stations and fix tests

* FSR-1237: Add unit tests for stations with different status

* FSR-1237: Exclude Closed and Welsh stations with no data from latest levels display and add tests

* FSR-1237: Refactor code to move HTML logic to the model and change the method name to en-UK spelling

* FSR-1237: Fix css

* FSR-1237: Fix console errors

* FSR-1237: Update elapsed time formatting and tests

* FSR-1237: Refactor code

---------

Co-authored-by: nikiwycherley <[email protected]>
  • Loading branch information
Keyurx11 and nikiwycherley authored Sep 4, 2024
1 parent 71794f1 commit cc50515
Show file tree
Hide file tree
Showing 18 changed files with 1,398 additions and 193 deletions.
33 changes: 33 additions & 0 deletions server/models/views/lib/latest-levels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { formatElapsedTime } = require('../../../util')

const WARNING_THRESHOLD_TYPES = ['FW RES FW', 'FW ACT FW', 'FW ACTCON FW']

function getThresholdsForTargetArea (thresholds) {
const filteredThresholds = thresholds.filter(threshold =>
threshold.status !== 'Closed' &&
!(threshold.iswales && threshold.latest_level === null)
)

const warningThresholds = findPrioritisedThresholds(filteredThresholds, WARNING_THRESHOLD_TYPES)
return warningThresholds.map(threshold => {
threshold.formatted_time = formatElapsedTime(threshold.value_timestamp)
threshold.isSuspendedOrOffline = threshold.status === 'Suspended' || (threshold.status === 'Active' && threshold.latest_level === null)
return threshold
})
}

function findPrioritisedThresholds (thresholds, types) {
const thresholdMap = {}

for (const type of types) {
for (const threshold of thresholds) {
if (threshold.threshold_type === type && !thresholdMap[threshold.rloi_id]) {
thresholdMap[threshold.rloi_id] = threshold
}
}
}

return Object.values(thresholdMap)
}

module.exports = getThresholdsForTargetArea
65 changes: 37 additions & 28 deletions server/models/views/target-area.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
const severity = require('../severity')
const moment = require('moment-timezone')
const { bingKeyMaps, floodRiskUrl } = require('../../config')
const getThresholdsForTargetArea = require('./lib/latest-levels')

class ViewModel {
constructor (options) {
const { area, flood, parentFlood } = options
const severityLevel = flood && severity.filter(item => {
return item.id === flood.severity_value
})[0]
const parentSeverityLevel = parentFlood && severity.filter(item => {
return item.id === parentFlood.severity_value
})[0]

const type = area.code.charAt(4).toLowerCase() === 'w'
? 'warning'
: 'alert'

let fallbackText
if (type === 'alert') {
fallbackText = '<p>We\'ll update this page when there\'s a flood alert in the area, which means flooding to low lying land is possible.</p>'
} else {
fallbackText = '<p>We\'ll update this page when there\'s a flood warning in the area.</p><p>A flood warning means flooding to some property is expected. A severe flood warning means there\'s a danger to life.</p>'
}
const { area, flood, parentFlood, thresholds } = options

const severityLevel = flood && severity.filter(item => item.id === flood.severity_value)[0]
const parentSeverityLevel = parentFlood && severity.filter(item => item.id === parentFlood.severity_value)[0]

const type = area.code.charAt(4).toLowerCase() === 'w' ? 'warning' : 'alert'

const fallbackText = getFallbackText(type)
let situation = fallbackText

if (flood?.situation) {
flood.situation = flood.situation.trim()
const message = flood.situation

situation = messageValidator(message)
situation = messageValidator(flood.situation)
}

const dateSituationChanged = flood
Expand All @@ -41,26 +30,28 @@ class ViewModel {

area.description = area.description.trim()
const description = area.description.endsWith('.') ? area.description.slice(0, -1) : area.description

const areaDescription = `Flood ${type} area: ${description}.`
const metaDescription = `Flooding information and advice for the area: ${description}.`

const parentAreaAlert = (!!(((flood && severityLevel.id === 4) && (type === 'warning')) || !flood) && (parentSeverityLevel?.isActive))
let situationChanged = flood
? `Updated ${timeSituationChanged} on ${dateSituationChanged}`
: `Up to date as of ${timeSituationChanged} on ${dateSituationChanged}`

if (severityLevel?.id === 4) {
situationChanged = `Removed ${timeSituationChanged} on ${dateSituationChanged}`
}
const situationChanged = getSituationChangedText(flood, severityLevel, timeSituationChanged, dateSituationChanged)

const pageTitle = severityLevel?.isActive
? `${severityLevel.title} for ${area.name}`
: `${area.name} flood ${type} area`

const pageTitle = (severityLevel?.isActive ? `${severityLevel.title} for ${area.name}` : `${area.name} flood ${type} area`)
const metaCanonical = `/target-area/${area.code}`

const latestLevels = thresholds ? getThresholdsForTargetArea(thresholds) : null

Object.assign(this, {
pageTitle,
metaDescription,
metaCanonical,
mapButtonText: `View map of the flood ${type} area`,
latestLevels,
placeName: area.name,
placeCentre: JSON.parse(area.centroid).coordinates,
featureId: area.id,
Expand All @@ -79,8 +70,26 @@ class ViewModel {
}
}

function getFallbackText (type) {
if (type === 'alert') {
return '<p>We\'ll update this page when there\'s a flood alert in the area, which means flooding to low lying land is possible.</p>'
} else {
return '<p>We\'ll update this page when there\'s a flood warning in the area.</p><p>A flood warning means flooding to some property is expected. A severe flood warning means there\'s a danger to life.</p>'
}
}

function getSituationChangedText (flood, severityLevel, timeSituationChanged, dateSituationChanged) {
if (severityLevel?.id === 4) {
return `Removed ${timeSituationChanged} on ${dateSituationChanged}`
}
return flood
? `Updated ${timeSituationChanged} on ${dateSituationChanged}`
: `Up to date as of ${timeSituationChanged} on ${dateSituationChanged}`
}

function messageValidator (message) {
const strippedMessage = message.replace(/(\r?\n)+/g, '\n')
return strippedMessage.split('\n').map(p => `<p>${p}</p>`).join(' ')
}

module.exports = ViewModel
3 changes: 2 additions & 1 deletion server/routes/target-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ module.exports = {
const area = await request.server.methods.flood.getFloodArea(code)
const flood = floods.find(n => n.ta_code === code)
const parentFlood = floods.find(n => n.ta_code === area.parent)
const model = new ViewModel({ area, flood, parentFlood })
const thresholds = flood ? await request.server.methods.flood.getTargetAreaThresholds(code) : []
const model = new ViewModel({ area, flood, parentFlood, thresholds })
return h.view('target-area', { model })
},
options: {
Expand Down
4 changes: 4 additions & 0 deletions server/services/flood.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ module.exports = {
return util.getJson(`${serviceUrl}/station/${id}/${direction}/imtd-thresholds`)
},

getTargetAreaThresholds (fwisCode) {
return util.getJson(`${serviceUrl}/target-area/${fwisCode}/imtd-thresholds`)
},

getStationForecastData (id) {
return util.getJson(`${serviceUrl}/station/${id}/forecast/data`)
},
Expand Down
8 changes: 8 additions & 0 deletions server/services/server-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ module.exports = server => {
}
})

server.method('flood.getTargetAreaThresholds', floodServices.getTargetAreaThresholds, {
cache: {
cache: cacheType,
expiresIn: minutes(1),
generateTimeout: seconds(10)
}
})

server.method('flood.getStationForecastData', floodServices.getStationForecastData, {
cache: {
cache: cacheType,
Expand Down
1 change: 1 addition & 0 deletions server/src/sass/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ $govuk-breakpoints: (
@import "components/map/map-overlays";
@import "components/map/map-days";
@import "components/bar-chart";
@import "components/latest-levels-box";
@import "components/line-chart";
@import "components/banners";
@import "components/flood-details";
Expand Down
22 changes: 22 additions & 0 deletions server/src/sass/components/_latest-levels-box.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.defra-live {
position: relative;
border: 2px solid govuk-colour('blue');
margin-top: 30px;
margin-bottom: 15px;
padding: 15px;
&__title {
@extend .govuk-tag;
@include govuk-font($size: 16);
margin-top: 0px;
margin-bottom: 15px;
font-weight: bold;
text-transform: uppercase;
background-color: #1C70B8;
color: white;
}
&__supplementary {
@include govuk-font($size: 16);
color: $govuk-secondary-text-colour;
margin: 0;
}
}
15 changes: 14 additions & 1 deletion server/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const wreck = require('@hapi/wreck').defaults({
})
const LocationSearchError = require('./location-search-error')
const ALLOWED_SEARCH_CHARS = 'a-zA-Z0-9\',-.& ()!'
const timezone = 'Europe/London'

async function request (method, url, options) {
let res, payload
Expand Down Expand Up @@ -50,7 +51,18 @@ function getJson (url) {
}

function formatDate (value, format = 'D/M/YY h:mma') {
return moment(value).tz('Europe/London').format(format)
return moment(value).tz(timezone).format(format)
}

function formatElapsedTime (datetime) {
const now = moment.tz(timezone)
const diffMinutes = now.diff(moment.tz(datetime, timezone), 'minutes')

if (diffMinutes < 60) {
return `${diffMinutes} minutes ago`
} else {
return 'More than 1 hour ago'
}
}

function toFixed (value, dp) {
Expand Down Expand Up @@ -182,6 +194,7 @@ module.exports = {
postJson,
request,
formatDate,
formatElapsedTime,
toFixed,
groupBy,
cleanseLocation,
Expand Down
23 changes: 23 additions & 0 deletions server/views/partials/latest-levels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="defra-live">
<h2 class="defra-live__title">Latest level{% if model.latestLevels.length > 1 %}s{% endif %}</h2>
{% for warnings in model.latestLevels %}
<div class="defra-live__item">
{% if warnings.isSuspendedOrOffline %}
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>Latest Level</strong></p>
<p>The {{ warnings.river_name }} level at {{ warnings.agency_name }} is currently unavailable.</p>
{% else %}
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>{{ warnings.formatted_time }}</strong></p>
<p>The {{ warnings.river_name }} level at {{ warnings.agency_name }} was {{ warnings.latest_level | toFixed(2) }} metres. Property flooding is possible when it goes above {{ warnings.threshold_value | toFixed(2) }} metres.
{% if model.latestLevels.length > 1 %}
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}">Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}</a>
{% endif %}
</p>
{% if model.latestLevels.length == 1 %}
<p>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}">Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}</a>
</p>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
Loading

0 comments on commit cc50515

Please sign in to comment.