@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
389 lines (353 loc) • 11.4 kB
text/typescript
import { Response, Request } from 'express'
import dayjs from 'dayjs'
import RecentlyViewedStoreService from '../../services/recentlyViewedService'
import RequestedReportService from '../../services/requestedReportService'
import { RenderTableListResponse } from './types'
import Dict = NodeJS.Dict
import {
FormattedUserReportData,
UserReportData,
RequestStatus,
ReportType,
RequestedReport,
StoredReportData,
} from '../../types/UserReports'
import { AsyncReportUtilsParams } from '../../types/AsyncReportUtils'
import { getExpiredStatus } from '../../utils/requestStatusHelper'
import SelectedFiltersUtils from '../_filters/filters-selected/utils'
import { itemActionsHtml, createListItemProduct } from '../../utils/reportListsHelper'
import { Services } from '../../types/Services'
import RequestedReportUtils from './requested/utils'
import RecentlyViewedCardGroupUtils from './viewed/utils'
import BookmarklistUtils from './bookmarks/utils'
import LocalsHelper from '../../utils/localsHelper'
import DateMapper from '../../utils/DateMapper/DateMapper'
import UserStoreItemBuilder from '../../utils/UserStoreItemBuilder'
import { FilterValue } from '../_filters/types'
const formatData = (reportData: UserReportData): FormattedUserReportData => {
const reportDataCopy: UserReportData = JSON.parse(JSON.stringify(reportData))
const {
executionId,
tableId,
variantName,
name,
reportId,
variantId,
id,
description,
query,
interactiveQuery,
status,
timestamp,
reportName,
dataProductDefinitionsPath,
type: reportType,
} = reportDataCopy
let summary: { name: string; value: string }[] = []
if (query) {
summary = query.summary as { name: string; value: string }[]
}
let interactiveSummary: { name: string; value: string }[] = []
if (interactiveQuery) {
interactiveSummary = interactiveQuery.summary as { name: string; value: string }[]
}
const type = reportType || ReportType.REPORT
return {
id: executionId,
text: name || variantName,
reportName,
description,
tag: 'MIS',
summary,
interactiveSummary,
timestamp,
status,
type,
...setDataFromStatus(status, reportDataCopy),
meta: {
reportId,
id: variantId || id,
executionId,
tableId,
status,
type,
dataProductDefinitionsPath,
},
}
}
const formatTable = (data: FormattedUserReportData[], type: 'requested' | 'viewed') => {
const rows = data.map((card: FormattedUserReportData) => {
return formatTableRow(card, type)
})
return {
rows,
head: [
{ text: 'Product', classes: 'dpr-req-product-head' },
{ text: 'Filters', classes: 'dpr-req-filters-head' },
{ text: 'Status', classes: 'dpr-req-status-head' },
{ text: 'Actions', classes: 'dpr-req-actions-head' },
],
}
}
const formatTableRow = (data: FormattedUserReportData, type: 'requested' | 'viewed') => {
let statusClass
let itemActions = ''
const { href, id, reportName, text, timestamp, type: reportType, status } = data
switch (status) {
case RequestStatus.FAILED:
statusClass = 'govuk-tag--red'
itemActions = itemActionsHtml(href, id, type, status)
break
case RequestStatus.EXPIRED:
statusClass = 'govuk-tag--grey'
itemActions = itemActionsHtml(href, id, type, status)
break
case RequestStatus.ABORTED:
statusClass = 'govuk-tag--orange'
itemActions = itemActionsHtml(href, id, type, status)
break
case RequestStatus.READY:
case RequestStatus.FINISHED:
itemActions = `<a class='govuk-link govuk-link--no-visited-state' href="${href}">Go to ${reportType}</a>`
statusClass = 'govuk-tag--green'
break
case RequestStatus.PICKED:
case RequestStatus.SUBMITTED:
case RequestStatus.STARTED:
itemActions = `<a class='govuk-link govuk-link--no-visited-state' href="${href}">Go to status</a>`
break
default:
break
}
let filtersSummary = ''
if (data.summary || data.interactiveSummary) {
filtersSummary = createSummaryHtml(data)
}
return [
{
html: createListItemProduct(reportName, text, reportType, timestamp),
},
{ html: filtersSummary },
{
html: `<strong class="govuk-tag dpr-request-status-tag ${statusClass}">${status}</strong>`,
classes: 'dpr-req-cell dpr-req-cell__status',
},
{
html: `${itemActions}`,
classes: 'dpr-req-cell dpr-req-cell__status',
},
]
}
const getTotals = (formattedCount: number, maxRows: number) => {
return {
amount: formattedCount,
shown: formattedCount > maxRows ? maxRows : formattedCount,
max: maxRows,
}
}
const createSummaryHtml = (data: FormattedUserReportData) => {
const summaryHtml = data.summary
.map((item) => `<li class="govuk-body-s dpr-query-summary"><strong>${item.name}</strong>: ${item.value}</li>`)
.join('')
const interactiveSummaryHtml = data.interactiveSummary
.map(
(item) =>
`<li class="govuk-body-s dpr-interactive-query-summary"><strong>${item.name}</strong>: ${item.value}</li>`,
)
.join('')
return `<ul class="dpr-card-group__item__filters-list govuk-!-margin-top-0 govuk-!-margin-bottom-0">${summaryHtml}${interactiveSummaryHtml}</ul>`
}
const getMeta = (formattedData: FormattedUserReportData[]) => {
return formattedData.map((d) => {
return {
reportId: d.meta.reportId,
id: d.meta.id,
executionId: d.meta.executionId,
tableId: d.meta.tableId,
status: d.meta.status,
requestedAt: d.meta.requestedAt,
type: d.meta.type,
dataProductDefinitionsPath: d.meta.dataProductDefinitionsPath,
}
})
}
export const setDataFromStatus = (status: RequestStatus, reportsData: UserReportData) => {
let timestamp
let href
let formattedDate
const { url, timestamp: time } = reportsData
const dateMapper = new DateMapper()
switch (status) {
case RequestStatus.FAILED: {
formattedDate = time.failed
? dateMapper.toDateString(<string>(<unknown>time.failed), 'local-date')
: dayjs().format('DD/MM/YYYY')
href = `${url.polling.fullUrl}`
timestamp = `Failed at: ${formattedDate}`
break
}
case RequestStatus.ABORTED: {
href = `${url.request.fullUrl}`
formattedDate = dateMapper.toDateString(<string>(<unknown>time.aborted), 'local-date')
timestamp = `Aborted at: ${formattedDate}`
break
}
case RequestStatus.FINISHED:
href = url.report.fullUrl
formattedDate = dateMapper.toDateString(<string>(<unknown>time.completed), 'local-date')
timestamp = `Ready at: ${formattedDate}`
break
case RequestStatus.EXPIRED: {
href = `${url.request.fullUrl}`
formattedDate = dateMapper.toDateString(<string>(<unknown>time.expired), 'local-date')
timestamp = `Expired at: ${formattedDate}`
break
}
case RequestStatus.READY: {
href = `${url.report.fullUrl}`
formattedDate = dateMapper.toDateString(<string>(<unknown>time.lastViewed), 'local-date')
timestamp = `Last viewed: ${formattedDate}`
break
}
case RequestStatus.SUBMITTED:
case RequestStatus.STARTED:
case RequestStatus.PICKED:
href = url.polling.fullUrl
formattedDate = dateMapper.toDateString(<string>(<unknown>time.requested), 'local-date')
timestamp = `Requested at: ${formattedDate}`
break
default:
formattedDate = dateMapper.toDateString(<string>(<unknown>time.lastViewed), 'local-date')
timestamp = `Last viewed: ${formattedDate}`
break
}
return {
timestamp,
href,
}
}
const renderList = async ({
res,
reportsData,
maxRows,
filterFunction,
type,
}: {
res: Response
reportsData: StoredReportData[]
maxRows?: number
filterFunction: (report: UserReportData) => boolean
type: 'requested' | 'viewed'
}): Promise<RenderTableListResponse> => {
const { csrfToken } = LocalsHelper.getValues(res)
let formatted = reportsData.filter(filterFunction).map(formatData)
const formattedCount = formatted.length
if (maxRows) formatted = formatted.slice(0, maxRows)
const tableData = formatTable(formatted, type)
const head = {
...(formatted.length && { href: `./async-reports/${type}` }),
...(!formatted.length && { emptyMessage: `You have 0 ${type} reports` }),
}
const result = {
head,
tableData,
total: getTotals(formattedCount, maxRows),
meta: getMeta(formatted),
csrfToken,
maxRows,
}
return result
}
export default {
renderList,
getExpiredStatus: async ({
req,
res,
services,
storeService,
}: AsyncReportUtilsParams & { storeService: RequestedReportService | RecentlyViewedStoreService }) => {
const { userId } = LocalsHelper.getValues(res)
const report = await getExpiredStatus({ req, res, services })
if (report && report.isExpired) {
await storeService.setToExpired(report.executionId, userId)
}
return report ? report.isExpired : false
},
init: async ({
services,
res,
req,
maxRows = 6,
}: {
services: Services
res: Response
req: Request
maxRows?: number
}) => {
const { requestedReports, recentlyViewedReports, bookmarkingEnabled } = LocalsHelper.getValues(res)
const requestedReportsList = await renderList({
res,
reportsData: requestedReports,
filterFunction: RequestedReportUtils.filterReports,
maxRows,
type: 'requested',
})
const viewedReportsList = await renderList({
res,
reportsData: recentlyViewedReports,
filterFunction: RecentlyViewedCardGroupUtils.filterReports,
maxRows,
type: 'viewed',
})
let bookmarks
if (bookmarkingEnabled) {
bookmarks = await BookmarklistUtils.renderBookmarkList({
res,
req,
services,
maxRows,
})
}
return {
requestedReports: requestedReportsList,
viewedReports: viewedReportsList,
bookmarks,
}
},
updateLastViewed: async ({
req,
services,
reportStateData,
userId,
filters,
}: {
req: Request
services: Services
reportStateData: RequestedReport
userId: string
search: string
href: string
filters: FilterValue[]
}) => {
const { type, reportId, reportName, description, id, name, executionId, tableId, query, url } = reportStateData
const reportData = { type, reportId, reportName, description, id, name }
const executionData = { executionId, tableId }
const queryData = query ? { query: query.data, querySummary: query.summary } : { query: {}, querySummary: [] }
const interactiveQueryData: { query: Dict<string>; querySummary: Array<Dict<string>> } = {
query: <Dict<string>>req.query,
querySummary: SelectedFiltersUtils.getQuerySummary(req, filters),
}
const recentlyViewedData = new UserStoreItemBuilder()
.addReportData(reportData)
.addExecutionData(executionData)
.addQuery(queryData)
.addInteractiveQuery(interactiveQueryData)
.addStatus(RequestStatus.READY)
.addTimestamp()
.addAsyncUrls(url)
.addReportUrls(req)
.build()
await services.requestedReportService.updateLastViewed(reportStateData.executionId, userId)
await services.recentlyViewedService.setRecentlyViewed(recentlyViewedData, userId)
},
}