583 lines
No EOL
17 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|
|
} |