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 = '' + 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 = '' + 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); }); }); }); }