Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional revival of Excel download at any time #99

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ <h3 id="supp-title" class="accordion-title">
<div id="sidebar-panel">
<div class="sidebar-content">
<br>
<h4 id="sidebar-title">Summary</h4>
<h4 id="sidebar-title">
<span id = "summary-title">Summary</span>
<button class="btn" id="sidebar-download-btn">
Download <br> Excel
</button>
</h4>
<br>
<h5 class="section-header"><strong>Baseline</strong></h5>
<div id="baseline-stats"></div>
Expand Down
4 changes: 0 additions & 4 deletions src/js/components/accordion/accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ function redirectForEdit(){
}
}

const AppropriationTable = {

}

const ExpenseTable = {
table_id : (account_string) => { return `table-${account_string}` },
init(account_string) {
Expand Down
11 changes: 11 additions & 0 deletions src/js/components/sidebar/sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,15 @@ h6 {

.sidebar-stat-line.fund-total .stat {
margin-right: 05px; /* 25px lines up with lines above (with edit symbol) */
}

#sidebar-download-btn {
/* background-color: var(--darkGray); */
margin-left: 60px;
margin-bottom: 10px;
}

#summary-title {
/* TODO: fix */
margin-bottom: -100px;
}
4 changes: 4 additions & 0 deletions src/js/components/sidebar/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './sidebar.css'
import { BaselineSection } from './subcomponents/baseline_section';

import SuppSection from './subcomponents/supp_section'
import { downloadXLSX } from '../../utils/XLSX_handlers';

// fetch CSS variables saved in :root
const root = document.documentElement;
Expand Down Expand Up @@ -29,6 +30,9 @@ function showSidebar() {

// add event listener to resize content if window is adjusted
window.addEventListener('resize', showSidebar);

// initialize download excel button
document.querySelector('#sidebar-download-btn').addEventListener('click', downloadXLSX);
}

function updateSidebarTitle(new_title){
Expand Down
91 changes: 91 additions & 0 deletions src/js/utils/archive/archived_fns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Transforms a specified cell into an editable element by attaching an input field.
* Once the editing is committed, the new value is saved in the specified attribute
* of the element and passed through an optional formatting function before being
* displayed in the cell. An optional callback can be triggered after the update
* to perform additional actions.
*/
function createEditableCell(cell, attribute = 'value', formatValueCallback, updateCallback, validate) {
// Add a click event to the cell to make it editable
cell.onclick = function() {
// Fetch the current attribute value of the cell or fall back to an empty string
var currentValue = cell.getAttribute(attribute) || '';
// Create an input element to edit the value
var textbox = document.createElement('input');
textbox.type = 'text';
textbox.value = currentValue;
var feedback = document.createElement('p');
feedback.style.color = "red";

// Function to commit the textbox value and restore static text
function commitAndRestoreText() {
// Retrieve the entered value
var enteredValue = textbox.value;
// Set the attribute to the entered value
cell.setAttribute(attribute, enteredValue);

// validate text against validation criteria
let feedback_text = '';
if (validate){
feedback_text = validate(enteredValue);
}

// if there's an error, show it
if (feedback_text){
feedback.textContent = feedback_text;
// otherwise, proceed
} else {
// Format and set the cell's text content
cell.textContent = formatValueCallback ? formatValueCallback(enteredValue) : enteredValue;
// If there is an update callback provided, call it
if (updateCallback) {
updateCallback();
}
};

// Reattach the onclick event to allow editing again in the future
cell.onclick = function() {
createEditableCell(cell, attribute, formatValueCallback, updateCallback, validate);
};
}

// When the textbox loses focus, commit its value
textbox.onblur = commitAndRestoreText;
// When the user presses the 'Enter' key, commit the value and blur the textbox
textbox.onkeydown = function(event) {
if (event.key === 'Enter') {
commitAndRestoreText();
textbox.blur();
}
};

// Clear the current content and append the textbox to the cell
cell.innerHTML = '';
cell.appendChild(textbox);
cell.appendChild(feedback);
// Temporarily remove the onclick event handler to prevent re-triggering during edit
cell.onclick = null;

// Focus on the textbox to start editing
textbox.focus();
}
}

// Function to apply createEditableCell to all cells matching a given selector
function applyEditableCells(selector, attribute = 'value', formatValueCallback, updateCallback, validate) {
// Select all elements that match the provided selector
var cells = document.querySelectorAll(selector);
// Iterate over each cell and make it editable
cells.forEach(function(cell) {
createEditableCell(cell, attribute, formatValueCallback, updateCallback, validate);
});
}

function validateNumber(input){
var number = parseFloat(input);
if (isNaN(number)){
return "Field only accepts numbers";
};
return "";
}

42 changes: 42 additions & 0 deletions src/js/utils/data_utils/JSON_data_handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export async function fetchJSON(jsonFilePath) {
return fetch(jsonFilePath)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
}

export function convertToJSON(table, colsToRemove = []){
const rows = table.rows;
// Extract headers from the first row
const headerRow = rows[0].cells;
const headers = [];
for (let j = 0; j < headerRow.length; j++) {
headers.push(headerRow[j].innerText);
}

// initialize data
var tableData = [];

for (var i = 1; i < rows.length; i++) {
const cols = rows[i].cells;
const rowData = {};
headers.forEach((header, index) => {
if (colsToRemove.includes(header)){
return;
}
else if (cols[index].classList.contains('cost')) {
rowData[header] = cols[index].getAttribute('value');
} else {
rowData[header] = cols[index].innerText;
}
});
tableData.push(rowData);
}
return JSON.stringify(tableData);
}



156 changes: 156 additions & 0 deletions src/js/utils/data_utils/XLSX_handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@


import { SHEETS } from '../../init.js';
import { FundLookupTable, Services } from './budget_data_handlers.js';
import { removeNewLines } from '../common_utils.js';
import { Baseline } from './local_storage_handlers.js';

function deleteTopRowsUntilFullData(data) {
// function to try to find the top of the usable data
let fullDataRowFound = false;

while (!fullDataRowFound && data.length > 0) {
const row = data[0]; // Get the top row
let hasAllData = true;

for (const cell of row) {
if (cell == null || cell === '') {
hasAllData = false;
break;
}
}

if (hasAllData && row.length > 1) {
fullDataRowFound = true;
} else {
// delete the top row if it's not the header row
data.shift();
}
}

return data;
}

export function processWorkbook(arrayBuffer) {
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
workbook.SheetNames.forEach(sheetName => {
// only convert sheets we need
if (Object.keys(SHEETS).includes(sheetName)) {
// read in sheets
const sheet = workbook.Sheets[sheetName];
const rawData = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });

// Clean the data by removing top rows with incomplete data
const dataRows = deleteTopRowsUntilFullData(rawData);

// get new headers
const headers = dataRows[0];

// isolate Fund column to split data
const fundIndex = headers.indexOf('Fund');
if (fundIndex === -1) {
console.error(`No 'Fund' column found in sheet ${sheetName}`);
return;
}

// Save a dictionary of data for each fund for each sheet
const fundData = {};

dataRows.forEach(row => {
const fund = row[fundIndex];
if(fund && fund != "Fund"){
if (!fundData[fund]) {
fundData[fund] = [];
}
const rowData = {};
headers.forEach((header, index) => {
rowData[removeNewLines(header)] = row[index];
});
fundData[fund].push(rowData);
}
});

// save fund number and name as we go along
FundLookupTable.update(fundData);

Object.keys(fundData).forEach(fund => {
const key = `${SHEETS[sheetName]}_${fund}`;
localStorage.setItem(key, JSON.stringify(fundData[fund]));
});
}

// But also save the possible services
else if (sheetName == 'Drop-Downs'){
const sheet = workbook.Sheets[sheetName];
// Convert the sheet to JSON to easily manipulate data
const sheetData = XLSX.utils.sheet_to_json(sheet, { header: 1 });

// Locate the "services" column header in row 0
const headerRow = sheetData[0];
const servicesIndex = headerRow.indexOf('Services');

if (servicesIndex === -1) {
console.error('Header "Services" not found');
} else {
// Extract data from the "services" column (excluding the header row)
const servicesColumn = sheetData.slice(1).map(row => row[servicesIndex]);
const cleanedServicesColumn = servicesColumn.filter(value => value != null);
// save the data
Services.save(cleanedServicesColumn);
}
}
});

console.log('all excel data saved');
}

// Utility function to append a sheet to the workbook if data is present
function appendSheetToWorkbook(workbook, data, sheetName) {
if (data.length > 0) {
const sheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
}
}

export function downloadXLSX() {
const baseline = new Baseline();
const workbook = XLSX.utils.book_new(); // Create a new workbook

const dataMap = {
Personnel: 'personnel',
Overtime: 'overtime',
NonPersonnel: 'nonpersonnel',
Revenue: 'revenue'
};

const sheetData = {
Personnel: [],
Overtime: [],
NonPersonnel: [],
Revenue: []
};

baseline.funds.forEach(fund => {
Object.keys(dataMap).forEach(sheetName => {
if (fund[dataMap[sheetName]] && fund[dataMap[sheetName]].table) {
sheetData[sheetName].push(...fund[dataMap[sheetName]].table);
}
});
});

Object.keys(sheetData).forEach(sheetName => {
appendSheetToWorkbook(workbook, sheetData[sheetName], sheetName);
});

// Generate a downloadable file
const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });

// Create a link and trigger the download
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "Filled_Detail_Sheet.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
Loading