@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
325 lines (288 loc) • 10.5 kB
text/typescript
import Dict = NodeJS.Dict
import { components } from '../../types/api'
import { Cell, DataTable, FieldDefinition } from '../DataTableBuilder/types'
import type { SummaryTemplate, Template } from '../../types/Templates'
import DataTableBuilder from '../DataTableBuilder/DataTableBuilder'
import { distinct } from '../arrayUtils'
import SummaryDataTableBuilder from '../SummaryDataTableBuilder/SummaryDataTableBuilder'
import { SectionSortKey } from './types'
export default class SectionedDataTableBuilder extends DataTableBuilder {
sections: Array<string>
template: Template
constructor(specification: components['schemas']['Specification']) {
const { fields, sections, template } = specification
super(fields)
this.sections = sections
this.template = template
}
/**
* Creates the section heading strings
*
* @param {Dict<string>[]} data
* @param {FieldDefinition[]} sectionFields
* @return {*} {string[]} array of section headings
*/
private createSectionHeadings(data: Dict<string>[], sectionFields: FieldDefinition[]): string[] {
return data
.map(
(rowData): SectionSortKey => ({
description: this.mapSectionDescription(rowData),
sortKey: this.getSortKey(rowData, sectionFields),
}),
)
.sort(this.sortKeyComparison())
.map((s) => s.description)
.reduce(distinct, [])
}
/**
* Initialise section heading arrays
*
* @private
* @param {string[]} sectionDescriptions
* @return {*}
* @memberof SectionedDataTableBuilder
*/
initSectionData(sectionDescriptions: string[]) {
const sectionedData: Dict<Cell[][]> | Dict<Array<Dict<string>>> = {}
sectionDescriptions.forEach((sectionDescription) => {
sectionedData[sectionDescription] = []
})
return sectionedData
}
/**
* Maps the rows to the correct section
*
* @private
* @param {Array<Dict<string>>} data
* @param {Dict<Cell[][]>} sectionedData
* @return {*}
* @memberof SectionedDataTableBuilder
*/
private mapRowsToSection(data: Array<Dict<string>>, sectionedData: Dict<Cell[][]>) {
return data.reduce((previousValue, rowData) => {
const sectionDescription: string = this.mapSectionDescription(rowData)
const mappedData = this.mapRow(rowData)
return {
...previousValue,
[sectionDescription]: previousValue[sectionDescription].concat([mappedData]),
}
}, sectionedData)
}
/**
* Maps the rows to the correct section
*
* @private
* @param {Array<Dict<string>>} data
* @param {Dict<Cell[][]>} sectionedData
* @return {*}
* @memberof SectionedDataTableBuilder
*/
private mapDataToSection(data: Array<Dict<string>>, sectionedData: Dict<Array<Dict<string>>>) {
return data.reduce((previousValue, rowData) => {
const sectionDescription: string = this.mapSectionDescription(rowData)
const section = {
...previousValue,
[sectionDescription]: previousValue[sectionDescription].concat([rowData]),
}
return section
}, sectionedData)
}
/**
* Gets the counts for rows in section
*
* @param {Dict<Cell[][]>} sectionedData
* @param {string} sectionDescription
* @return {*}
* @memberof SectionedDataTableBuilder
*/
getSectionCount(sectionedData: Dict<Cell[][]> | Dict<Dict<string>[]>, sectionDescription: string) {
const count = sectionedData[sectionDescription].length
const countDescription = `${count} result${count === 1 ? '' : 's'}`
return {
count,
countDescription,
}
}
/**
* Creates the summaries and builds the table with summaries
*
* @private
* @param {string} sectionDescription
* @param {Cell[][]} mappedTableData
* @param {Cell[]} header
* @return {*}
* @memberof SectionedDataTableBuilder
*/
private mapSummariesAndCreateTable(sectionDescription: string, mappedTableData: Cell[][], header: Cell[]) {
let tableContent: Cell[][] = []
let mappedSectionHeaderSummary: Cell[][] = []
let mappedHeaderSummary: Cell[][] = []
let mappedFooterSummary: Cell[][] = []
let mappedSectionFooterSummary: Cell[][] = []
mappedSectionHeaderSummary = this.mapSectionSummaryTables(sectionDescription, 'section-header', this.columns.length)
mappedHeaderSummary = this.mapSectionSummaryRows('table-header', sectionDescription)
mappedFooterSummary = this.mapSectionSummaryRows('table-footer', sectionDescription)
mappedSectionFooterSummary = this.mapSectionSummaryTables(sectionDescription, 'section-footer', this.columns.length)
tableContent = mappedSectionHeaderSummary
.concat(mappedTableData.length > 0 ? [header] : [])
.concat(mappedHeaderSummary)
.concat(mappedTableData)
.concat(mappedFooterSummary)
.concat(mappedSectionFooterSummary)
return tableContent
}
/**
* Creates the table data
* - if summaries are present, includes the summaries data
*
* @private
* @param {string[]} sectionDescriptions
* @param {Dict<Cell[][]>} sectionedData
* @param {Cell[]} header
* @return {*}
* @memberof SectionedDataTableBuilder
*/
private createTableContent(sectionDescriptions: string[], sectionedData: Dict<Cell[][]>, header: Cell[]) {
return sectionDescriptions.flatMap((sectionDescription, index) => {
const { count, countDescription } = this.getSectionCount(sectionedData, sectionDescription)
const mappedTableData = sectionedData[sectionDescription]
let tableContent: Cell[][] = []
if (Object.keys(this.reportSummaries).length) {
tableContent = this.mapSummariesAndCreateTable(sectionDescription, mappedTableData, header)
} else {
tableContent = tableContent.concat(mappedTableData.length > 0 ? [header] : []).concat(mappedTableData)
}
const sectionHeader = this.createSectionHeader(sectionDescription, index, count, countDescription)
return [...sectionHeader, ...tableContent]
})
}
createSectionHeader(sectionDescription: string, index: number, count?: number, countDescription?: string) {
const header = []
if (index !== 0) {
header.push([
{
classes: 'dpr-section-header-spacer',
colspan: this.columns.length,
text: '',
},
])
}
header.push([
{
classes: 'dpr-section-header',
colspan: this.columns.length,
html: `<h2 class="govuk-heading-m">${sectionDescription}${
count > 0 ? ` <span class='govuk-caption-m'>${countDescription}</span>` : ''
}</h2>`,
},
])
header.push([
{
classes: 'dpr-section-header-spacer-bottom',
colspan: this.columns.length,
text: '',
},
])
return header
}
private mapSectionSummaryRows(template: SummaryTemplate, sectionDescription: string): Cell[][] {
if (this.reportSummaries[template]) {
return this.reportSummaries[template].flatMap((reportSummary) =>
reportSummary.data
.filter((rowData) => this.mapSectionDescription(rowData) === sectionDescription)
.map((rowData) =>
this.mapRow(rowData, `dpr-report-summary-cell dpr-report-summary-cell-${template}`, reportSummary.fields),
),
)
}
return []
}
private mapSectionSummaryTables(
sectionDescription: string,
summaryTemplate: SummaryTemplate,
columnsLength: number,
): Cell[][] {
const summaries = this.reportSummaries[summaryTemplate]
if (summaries) {
const htmlTables = summaries.map((summary) => {
const data = summary.data.filter((row) => this.mapSectionDescription(row) === sectionDescription)
if (data.length > 0) {
const dataTable = new SummaryDataTableBuilder(summary, this.sections).buildTable(data)
const htmlTable = this.convertDataTableToHtml(dataTable)
return `<div class='dpr-summary-container'>${htmlTable}</div>`
}
return ''
})
const summaryContent = htmlTables.join('')
if (summaryContent.length > 0) {
return [
[
{
classes: 'dpr-summary-cell',
colspan: columnsLength,
html: `<div class='dpr-summary-container-group dpr-summary-container-group-${summaryTemplate}'>${summaryContent}</div>`,
},
],
]
}
}
return []
}
mapSectionDescription(rowData: NodeJS.Dict<string>): string {
const { sections } = this
return this.mapNamesToFields(sections)
.map((s) => `${s.display}: ${this.mapCellValue(s, rowData[s.name])}`)
.join(', ')
}
mapSections(data: Array<Dict<string>>) {
const sectionHeadings = this.initSectionedHeadings(data)
let { sectionedData } = sectionHeadings
// Maps data to sections
if (this.template !== 'summary-section') {
if (this.template === 'parent-child-section') {
sectionedData = this.mapDataToSection(data, sectionedData as Dict<Dict<string>[]>)
} else {
sectionedData = this.mapRowsToSection(data, sectionedData as Dict<Cell[][]>)
}
}
return {
sectionDescriptions: sectionHeadings.sectionDescriptions,
sectionedData,
}
}
initSectionedHeadings(data: Array<Dict<string>>) {
// Get the section definition data
const sectionFields = this.mapNamesToFields(this.sections)
// create the sectionHeadings
const sectionDescriptions = this.createSectionHeadings(data, sectionFields)
// init empty sections
const sectionedData = this.initSectionData(sectionDescriptions)
return {
sectionDescriptions,
sectionedData,
}
}
/**
* Creates the table rows.
*
* @private
* @param {Array<Dict<string>>} data
* @param {Cell[]} header
* @return {*} {Cell[][]}
* @memberof SectionedDataTableBuilder
*/
private mapSectionedData(data: Array<Dict<string>>, header: Cell[]): Cell[][] {
const { sectionDescriptions, sectionedData } = this.mapSections(data)
// Create the table
const tableContent = this.createTableContent(sectionDescriptions, sectionedData as Dict<Cell[][]>, header)
return tableContent
}
buildTable(data: Array<Dict<string>>): DataTable {
return {
head: null,
rows: this.mapSectionedData(data, this.mapHeader(true, 'govuk-table__header')),
rowCount: data.length,
colCount: this.columns.length,
}
}
}