@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
238 lines (234 loc) • 8.61 kB
JavaScript
'use strict';
var definitionUtils = require('../../definitionUtils.js');
var DateMapper = require('../../DateMapper/DateMapper.js');
class SectionedDataHelper {
sections = [];
data = [];
fields = [];
summariesData = [];
sectionedDataArray = [];
sectionedData;
reportQuery;
createSectionKey(row) {
// Build structured entries with column names + values
const keyObj = this.sections.map(col => ({
name: col,
value: row[col] ?? '',
}));
const keyValues = keyObj.map(v => v.value).join('');
const key = keyValues !== '' ? JSON.stringify(keyObj) : 'mainSection';
return {
keyObj: key === 'mainSection' ? [] : keyObj,
key,
};
}
createSectionTitle(keyObj, fields) {
return keyObj
.map(column => {
const fieldName = definitionUtils.getFieldDisplayName(fields, column.name);
return `${fieldName}: ${column.value}`;
})
.join(', ');
}
/**
* Groups the data into sections
*
* @return {*} {SectionedData}
* @memberof SectionedDataBuilder
*/
groupBySections(data, fields, summary) {
const sectionMap = data.reduce((acc, row) => {
const { key, keyObj } = this.createSectionKey(row);
const title = this.createSectionTitle(keyObj, fields);
// Create the section if needed
if (!acc[key]) {
acc[key] = {
title,
count: 0,
key,
keyObj,
summaries: [],
data: [],
};
}
const section = acc[key];
if (summary) {
const { id, template } = summary;
const existingSummary = section.summaries.find(s => s.template === template);
if (existingSummary) {
existingSummary.data.push(row);
}
else {
section.summaries.push({
id,
template,
fields,
data: [row],
});
}
}
else {
section.data.push(row);
section.count += 1;
}
return acc;
}, {});
this.sectionedDataArray.push({
sections: Object.values(sectionMap),
});
}
createSummarySections() {
this.summariesData.forEach(summaryData => {
const { fields, id, template, data } = summaryData;
this.groupBySections(data, fields, { id, template });
});
}
mergeSections() {
return {
sections: this.sectionedDataArray
.flatMap(input => input.sections)
.reduce((acc, section) => {
const { key, keyObj } = section;
const existing = acc.find(s => s.key === key);
// If section doesn't exist yet → add fresh copy
if (!existing) {
return acc.concat({
key: section.key || '',
keyObj: keyObj || [],
title: section.title || '',
count: section.count,
summaries: section.summaries ? [...section.summaries] : [],
data: section.data ? [...section.data] : [],
});
}
// If exists → update it by concatenating data & summaries
const updated = acc.map(s => s.key === key
? {
...s,
count: s.count + section.count,
summaries: [...(s.summaries ?? []), ...(section.summaries ?? [])],
data: (s.data ?? []).concat(section.data ?? []),
}
: s);
return updated;
}, []),
};
}
getSortField() {
const { sortColumn } = this.reportQuery;
return sortColumn && this.sections.includes(sortColumn) ? [sortColumn] : [];
}
getSortDirection() {
const { sortedAsc } = this.reportQuery;
return sortedAsc ? 1 : -1;
}
enforceSummaryRowOrder(sectionedData) {
return {
sections: sectionedData.sections.map(section => ({
...section,
summaries: (section.summaries ?? []).map(summary => ({
...summary,
data: [...summary.data].sort((a, b) => (Number(a['index']) ?? 0) - (Number(b['index']) ?? 0)),
})),
})),
};
}
/**
* Sorts sections based on keyObj values using an optional nameOrder override.
* - nameOrder can be partial; unspecified names fall back to default order
* - Default order is derived from the first section's keyObj sequence
*/
sortSections(sectionedData) {
const dataMapper = new DateMapper.DateMapper();
const nameOrder = this.getSortField();
const direction = this.getSortDirection();
const { sections } = sectionedData;
if (sections.length === 0)
return sectionedData;
// Default order is the order of the keyObj.name fields in the first section
const defaultOrder = sections[0].keyObj.map(k => k.name);
// Merge partial nameOrder with default order
const finalOrder = nameOrder
// 1. start with user-specified names
.filter(name => defaultOrder.includes(name))
// 2. append missing names in default order
.concat(defaultOrder.filter(name => !nameOrder.includes(name)));
const buildMap = (keyObj) => keyObj.reduce((acc, ko) => ({ ...acc, [ko.name]: ko.value }), {});
const sortedSections = [...sections].sort((a, b) => {
const aMap = buildMap(a.keyObj);
const bMap = buildMap(b.keyObj);
// Compare using final order
const comparisons = finalOrder.map(name => {
const aVal = aMap[name] ?? '';
const bVal = bMap[name] ?? '';
const aDate = dataMapper.parseIfDate(aVal);
const bDate = dataMapper.parseIfDate(bVal);
// date sorting
if (aDate && bDate) {
const diff = aDate.valueOf() - bDate.valueOf();
if (diff !== 0)
return diff * direction;
return 0;
}
// fallback to string sorting
return aVal.localeCompare(bVal, undefined, { numeric: true }) * direction;
});
// Return first non-zero comparison
return comparisons.find(x => x !== 0) ?? 0;
});
return {
sections: sortedSections,
};
}
withSections(sections) {
this.sections = sections || [];
return this;
}
withFields(fields) {
this.fields = definitionUtils.getFieldsByName(this.sections, fields);
return this;
}
withData(data) {
this.data = data;
return this;
}
withSummaries(summaryData) {
this.summariesData = (summaryData || []).map(summary => ({
...summary,
data: summary.data.map((row, index) => ({
...row,
index: String(index),
})),
}));
return this;
}
withReportQuery(reportQuery) {
this.reportQuery = reportQuery;
return this;
}
build() {
let sections;
if (this.sections.length) {
// Group main data into sections
this.groupBySections(this.data, this.fields);
// Create the summary and group into sections
this.createSummarySections();
sections = this.mergeSections();
sections = this.enforceSummaryRowOrder(sections);
sections = this.sortSections(sections);
}
else {
const singleSection = {
key: '',
keyObj: [],
data: this.data,
count: this.data.length,
summaries: this.summariesData,
};
sections = { sections: [singleSection] };
}
return sections;
}
}
exports.SectionedDataHelper = SectionedDataHelper;
//# sourceMappingURL=SectionedDataHelper.js.map