UNPKG

@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
'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