web-app-demos/apps/reporter/static/js/script_reports.js
2025-03-17 16:11:01 -06:00

583 lines
No EOL
17 KiB
JavaScript

let table = null;
let period = null;
let columns = null;
let subcolumns = null;
let tblcolumns = null;
let rows = null;
let subrows = null;
let options = {};
// let mouse = "drag";
const form = document.getElementById('report_options');
const title_el = document.getElementById('report_title');
const title_contain = document.getElementById('title_container');
const progress = document.getElementById('report_progress');
const report_status = document.getElementById('report_status');
const dt = document.getElementById('datatable');
const searchbar = document.getElementById('filter');
//const tool_select = document.getElementById('tool_select');
const export_excel = document.getElementById('export_excel');
const btn_print = document.getElementById('btn_print');
const btn_link = document.getElementById('create_link');
const btn_expand = document.getElementById('btn_expand');
const btn_collapse = document.getElementById('btn_collapse');
let report_title = title_el.textContent;
let full_title = report_title;
let expandedNested = new Set();
let now = new Date();
let tblWidth = 0;
const sendOptions = async () => {
const formData = new FormData(form);
try {
const response = await fetch(window.location.href, {
method: "POST",
body: formData
});
result = await response.json();
if (result.status == 'ok') {
//console.log(result);
now = new Date();
renderReport(result);
progress.style.width = "100%";
}
else {
progress.style.width = "0%";
report_status.textContent = "Invalid options selected.";
for (error in result.errors) {
document.getElementById(`${error}:error`).textContent = result.errors[error];
}
}
} catch (e) {
progress.style.width = "0%";
report_status.textContent = "Could not contact server.";
console.error(e);
}
}
const processColumns = (_columns) => {
let result = [];
_columns.forEach((item, index) => {
let column = {title: item.name, field: index.toString()};
if ('footer' in item) {
column.minWidth = 130;
if (item.footer === "sum") {
column.bottomCalc = "sum";
}
else if (item.footer === "margin") {
column.bottomCalc = (values, data, calcParams) => {
const sales = data.reduce((acc, row) => acc + row[item.margin[0]], 0);
if (sales == 0) {
return '';
}
const costs = data.reduce((acc, row) => acc + row[item.margin[1]], 0);
return 1 - costs/sales;
};
}
else if (item.footer == "count") {
column.bottomCalc = (values, data, calcParams) => {
//console.log(values);
const counts = {};
for (const value of values) {
counts[value] = counts[value] ? counts[value] + 1 : 1;
}
return `${counts['Order'] ?? 0} order${counts['Order'] == 1 ? '' : 's'}, ${counts['Quote'] ?? 0} quote${counts['Quote'] == 1 ? '' : 's'}`
};
}
}
if ('format' in item) {
if (item.format === "int") {
column.formatter = intFormatter;
column.bottomCalcFormatter = intFormatter;
column.hozAlign = "right";
}
else if (item.format === "$") {
column.formatter = "money";
column.bottomCalcFormatter = "money";
column.hozAlign = "right";
}
else if (item.format === "%") {
column.formatter = percentFormatter;
column.bottomCalcFormatter = percentFormatter;
column.hozAlign = "right";
}
else if (item.format === "textarea") {
column.formatter = "textarea";
}
}
if ('frozen' in item) {
column.frozen = true;
}
result.push(column);
});
return result;
}
const renderReport = (result) => {
report_title = result.title;
period = result.period;
columns = result.columns;
subcolumns = result.subcolumns;
tblcolumns = processColumns(result.columns);
rows = result.rows;
subrows = result.subrows;
options = result.options;
let subrowcount = 0;
if (period) {
full_title = `${report_title} for ${period}`;
title_el.textContent = full_title;
}
progress.style.width = "60%";
if (table !== null) { table.destroy(); }
tblWidth = title_contain.offsetWidth - 50;
let tableOptions = {
columns: tblcolumns,
data: rows,
importFormat: "array",
layout: "fitDataFill",
selectableRows: false,
movableColumns: true,
//height: 300,
printRowRange: "all",
columnDefaults: {resizable: "header"}
}
if (options.group_by != null) {
if (typeof(options.group_by) == 'number') {
tableOptions.groupBy = options.group_by.toString();
}
else if (Array.isArray(options.group_by)) {
tableOptions.groupBy = Array.from(options.group_by.map(x => x.toString()));
}
// else {
// tableOptions.groupBy = '0';
// }
tableOptions.groupToggleElement = "header";
tableOptions.columnCalcs = "both";
tableOptions.groupClosedShowCalcs = true;
document.getElementById("group-controls").classList.remove('d-none');
}
else {
document.getElementById("group-controls").classList.add('d-none');
}
if (result.subcolumns) {
for (let subrow in result.subrows) {
subrowcount += result.subrows[subrow].length;
}
tableOptions.rowFormatter = (row) => {
let key = row.getCell("0").getValue();
if (key in result.subrows) {
let holderEl = document.createElement("div");
holderEl.classList.add('pb-2', 'ps-4');
holderEl.style.maxWidth = tblWidth + 'px';
let linkEl = document.createElement("a");
linkEl.href = '#';
let count = result.subrows[key].length;
let text = count == 1 ? "1 entry" : `${count} entries`;
holderEl.appendChild(linkEl);
if (expandedNested.has(key)) {
linkEl.innerHTML = '<i class="bi bi-caret-down me-1"></i>' + text;
linkEl.addEventListener('click', (event) => {
expandedNested.delete(key);
row.reformat();
});
let tableEl = document.createElement("div");
holderEl.appendChild(tableEl);
row.getElement().appendChild(holderEl);
let subTableOptions = {
columns: processColumns(result.subcolumns),
data: result.subrows[key],
importFormat: "array",
layout:"fitDataFill",
responsiveLayout:"collapse",
columnDefaults: {headerSort: false, resizable: false, responsive: 0},
selectableRows: false,
renderVertical:"basic"
}
subTableOptions.columns[4].responsive = 2;
let subTable = new Tabulator(tableEl, subTableOptions);
}
else {
linkEl.innerHTML = '<i class="bi bi-caret-right me-1"></i>' + text;
linkEl.addEventListener('click', (event) => {
expandedNested.add(key);
row.reformat();
});
row.getElement().appendChild(holderEl);
}
}
}
tableOptions.rowFormatterPrint = false;
// The virtual DOM renderer is buggy when the row height is large.
tableOptions.renderVertical = 'basic';
}
report_status.textContent = `Showing ${rows.length}${subrowcount == 0 ? '' : '+'+subrowcount} record${rows.length == 1 ? '' : 's'}.`;
table = new Tabulator("#datatable", tableOptions);
table.on("dataLoaded", (data) => {
//pointerScroll(document.getElementsByClassName("tabulator-tableholder")[0]);
document.getElementById('table-controls').classList.remove('invisible');
});
searchbar.addEventListener("keyup", () => {
let filters = [];
table.getColumns().forEach((column) => {
filters.push({
field: column.getField(),
type: "like",
value: searchbar.value,
});
});
table.setFilter([filters]);
});
}
// const pointerScroll = (elem) => {
// const dragStart = (ev) => {
// if(mouse === "drag" && ev.button == 0 && ev.target.tagName != 'A' && !Array.from(document.getElementsByClassName('tabulator-group')).some((node) => {
// return node.contains(ev.target);
// })) {
// ev.preventDefault();
// elem.setPointerCapture(ev.pointerId);
// }
// }
// const dragEnd = (ev) => {
// if(mouse === "drag" && ev.button == 0) {
// elem.releasePointerCapture(ev.pointerId);
// }
// }
// const drag = (ev) => {
// ev.preventDefault();
// if(ev.buttons != 0 && elem.hasPointerCapture(ev.pointerId)) {
// elem.scrollLeft -= ev.movementX;
// elem.scrollTop -= ev.movementY;
// }
// }
// elem.addEventListener("pointerdown", dragStart);
// elem.addEventListener("pointerup", dragEnd);
// elem.addEventListener("pointermove", drag);
// };
const excelWidth = (width) => {
return Math.floor((width * 7 + 5) / 7 * 256) / 256;
}
const excelFixColumns = (sheet, icolumns, irows, mainsheet=true) => {
let rowskip = mainsheet ? 2 : 0;
let colskip = mainsheet ? 0 : 2;
for (let i = 0; i < icolumns.length+colskip; i++) {
const column = sheet.getColumn(i+1);
let maxLength = 10;
for (let j = 1+rowskip; j <= irows.length+1+rowskip; j++) {
if (column.values[j]) {
maxLength = Math.max(maxLength, column.values[j].toString().length)
}
}
column.width = excelWidth(maxLength + 2);
if (i >= colskip && 'format' in icolumns[i-colskip]) {
if (icolumns[i-colskip].format == "int") {
column.numFmt = "0";
}
else if (icolumns[i-colskip].format == "$") {
column.numFmt = "#,##0.00";
}
else if (icolumns[i-colskip].format == "%") {
column.numFmt = "0.00%";
}
}
}
}
const excelFormatter = async (list, _options, setFileContents) => {
const workbook = new ExcelJS.Workbook();
const sheet_name = report_title.replaceAll(/[\*\?\:\\\/\[\]]/g, ' ');
if (options.formatting == "parker") {
const todayDate = new Date();
const lastMonth = new Date(todayDate.getFullYear(), todayDate.getMonth() - 1, 1);
const reportMonth = String(lastMonth.getMonth()+1).padStart(2, '0');
const reportYear = String(lastMonth.getFullYear()).slice(2);
const sheet = workbook.addWorksheet("CEA");
sheet.columns = [
{ header: '*DFH', key: '0', width: excelWidth(4) },
{ header: '292512', key: '1', width: excelWidth(10) },
{ header: 'Custom Energized Air Ltd., Edmonton, AB', key: '2', width: excelWidth(3) },
{ key: '3', width: excelWidth(25) },
{ key: '4', width: excelWidth(18) },
{ key: '5', width: excelWidth(2) },
{ key: '6', width: excelWidth(5) },
{ key: '7', width: excelWidth(9) },
{ key: '8', width: excelWidth(4) },
{ header: `${reportMonth}${reportYear}`, key: '9', width: excelWidth(20) },
{ key: '10', width: excelWidth(6) },
{ key: '11', width: excelWidth(4) },
{ key: '12', width: excelWidth(10) },
]
for (let row of rows.toSorted( (a,b) => a[6].localeCompare(b[6]) )) {
sheet.addRow(['*DDD', row[0], null, row[1], row[2], row[3], null, row[8], null, row[6], null, row[9]]);
}
sheet.addRow(['*DFT', '292512', '0000000000']);
}
else if (options.formatting) {
let export_columns = [];
columns.forEach(column => {
let col_options = { name: column.name, filterButton: true }
if ('footer' in column) {
if (column.footer === "sum") {
col_options.totalsRowFunction = 'sum';
}
else if (column.footer === 'margin') {
col_options.totalsRowFunction = 'custom';
let sales = `SUBTOTAL(109,[${columns[column.margin[0]].name}])`;
let costs = `SUBTOTAL(109,[${columns[column.margin[1]].name}])`;
col_options.totalsRowFormula = `IFERROR(1 - ${costs} / ${sales}, "")`;
}
}
export_columns.push(col_options);
});
if (!('footer' in columns[0])) {
export_columns[0].totalsRowLabel = 'Totals:';
}
const link = create_link(true);
const sheet = workbook.addWorksheet(sheet_name, {
views: [ {state: 'frozen', xSplit: 0, ySplit:3} ]
});
sheet.getCell('A1').value = `${full_title} - Ran ${now.toLocaleString('sv')}`;
sheet.getCell('A2').value = { text: link, hyperlink: link };
sheet.addTable({
name: 'data',
ref: 'A3',
headerRow: true,
totalsRow: true,
style: { showRowStripes: true },
columns: export_columns,
rows: rows
});
excelFixColumns(sheet, columns, rows);
if (subcolumns !== null) {
let export_subcolumns = Array.from(subcolumns.map(column => {return {name: column.name, filterButton: true};}));
export_subcolumns.unshift({name: columns[1].name, filterButton: true});
export_subcolumns.unshift({name: columns[0].name, filterButton: true});
let descriptions = rows.reduce((result, row) => {
result[row[0]] = row[1];
return result;
}, {});
const subsheet = workbook.addWorksheet('Detail', {
views: [ {state: 'frozen', xSplit: 0, ySplit:1} ]
});
let flatrows = [];
for (let key in subrows) {
for (let subrow of subrows[key]) {
flatrows.push([key, descriptions[key], ...subrow])
}
}
if (flatrows.length == 0) {
flatrows.push(['', '', '', '', '', '', '', '', '', '', ''])
}
subsheet.addTable({
name: 'detail',
ref: 'A1',
headerRow: true,
style: { showRowStripes: true },
columns: export_subcolumns,
rows: flatrows
});
if (options.formatting != "performance") {
excelFixColumns(subsheet, subcolumns, flatrows, false);
}
}
}
else {
const sheet = workbook.addWorksheet(sheet_name);
sheet.columns = tblcolumns.map(col => { return {header: col.title, key: col.field} });
for (let row of rows) {
sheet.addRow(row);
}
}
const buffer = await workbook.xlsx.writeBuffer();
setFileContents(buffer, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
}
const intFormatter = (cell, formatterParams, onRendered) => {
if(typeof cell.getValue() === "undefined") {
return '';
}
let num = parseInt(cell.getValue().toString(), 10);
return isNaN(num) ? '' : num;
}
const percentFormatter = (cell, formatterParams, onRendered) => {
if(typeof cell.getValue() === "undefined") {
return '';
}
let num = parseFloat(cell.getValue().toString());
return isNaN(num) ? '' : (num*100).toFixed(2)+'%';
}
const create_link = (everything) => {
let base_url = window.location.href.split(/[?#]/)[0];
let params = [];
document.querySelectorAll("input[type=checkbox]").forEach( (input) => {
if (input.checked) {
params.push(input.id);
}
});
document.querySelectorAll("input[type=radio]").forEach( (input) => {
if (input.checked) {
params.push(`${input.name}=${input.value}`);
}
});
document.querySelectorAll("input[type=number]").forEach( (input) => {
params.push(`${input.name}=${input.value}`);
});
if (everything) {
document.querySelectorAll("input").forEach( (input) => {
if (['date', 'month'].includes(input.type)) {
params.push(`${input.name}=${input.value}`);
}
else if (input.type == 'text' && input.id != 'filter') {
params.push(`${input.name}=${encodeURIComponent(input.value)}`);
}
});
}
params.push('run');
return `${base_url}?${params.join('&')}`;
}
const checkall = (name) => {
const group_checkboxes = [...document.querySelectorAll("input[type=checkbox]")]
.filter( (input) => input.name.startsWith(name) );
const state = group_checkboxes.every( (input) => input.checked );
group_checkboxes.forEach( (input) => input.checked = !state );
}
const getSuggestions = async (query) => {
try {
const method = document.querySelector(`input[name=${suggest.getAttribute('suggest')}]:checked`).value;
const formData = new FormData();
formData.append("options", method);
const response = await fetch('/suggest', {
method: "POST",
body: formData
});
return await response.json();
} catch (error) {
console.error(error);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
progress.style.width = "20%";
report_status.textContent = "Retrieving data...";
for (message_field of document.getElementsByClassName('error')) {
message_field.textContent = ""
}
sendOptions();
});
if (document.getElementById('autorun').value == 'true') {
sendOptions();
}
btn_link.addEventListener("click", (event) => {
window.location = create_link(true);
});
// tool_select.addEventListener("click", (event) => {
// if (mouse == "drag") {
// mouse = "select";
// tool_select.textContent = "Mouse: Select";
// tool_select.classList.add("active");
// dt.childNodes[1].classList.add("selectable");
// }
// else if (mouse == "select") {
// mouse = "drag";
// tool_select.textContent = "Mouse: Drag";
// tool_select.classList.remove("active");
// dt.childNodes[1].classList.remove("selectable");
// }
// });
export_excel.addEventListener("click", (event) => {
table.download(excelFormatter, `${full_title} - Ran ${now.toLocaleString('sv').replace(':', '-')}.xlsx`);
});
btn_print.addEventListener("click", (event) => {
table.print();
});
btn_expand.addEventListener("click", (event) => {
table.setGroupStartOpen(true);
let original = table.options.groupBy;
table.setGroupBy(false);
table.setGroupBy(original);
});
btn_collapse.addEventListener("click", (event) => {
table.setGroupStartOpen(false);
let original = table.options.groupBy;
table.setGroupBy(false);
table.setGroupBy(original);
});
let suggest = document.querySelector('[suggest]');
if (suggest) {
suggest.addEventListener("selection", (event) => {
suggest.value = event.detail.selection.value.split(' - ')[0];
});
let config = {
selector: () => { return suggest },
data: {
src: getSuggestions,
cache: true
},
resultItem: { highlight: true },
resultsList: {
destination: '#searchresults',
position: 'afterbegin',
maxResults: 100,
//noResults: true,
tabSelect: true
},
wrapper: false
};
let autoCompleteJS = new autoComplete(config);
document.querySelectorAll(`input[name=${suggest.getAttribute('suggest')}]`).forEach( (input) => {
input.addEventListener("change", (radio) => {
getSuggestions('').then((data) => {
autoCompleteJS.data.store = data;
autoCompleteJS.start(suggest.value);
});
});
});
}