diff --git a/index.html b/index.html new file mode 100644 index 0000000..6061787 --- /dev/null +++ b/index.html @@ -0,0 +1,206 @@ + + + + + +Demo Budget Request + + + + + + + + + + + + + + + + + + + + +

FY2026 Budget Request Form

+ +
+ +
+ + +
+ + + +
+
+ +
+ + +
+ +

Select an action item for each current position in your department.

+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employee NameJob NameFY25 SalaryFTEsFundAppropriationCost CenterAction
Jane DoeAssistant Director$100,0001.0100012345654321 +
+ + + +
+
John DoeTASS III$80,0001.0100012345654321 +
+ + + + +
Jane DoeAnalyst$60,0001.0100012345654321 +
+ + + + +
VacantAdministrative Assistant$50,0001.0100012345654321 +
+ + + + +
VacantAnalyst II$80,0001.0100012345654321 +
+ + + + +
+
+
+
+ + +
+
+
+ +
+
+ + + +
+
+
+ +
+ + + +
+
+ +
+ + + \ No newline at end of file diff --git a/static/js/excel-export.js b/static/js/excel-export.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..4bbb43e --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,199 @@ +import { saveTableData, loadTableData } from './storage-handlers.js'; + +document.addEventListener('DOMContentLoaded', function () { + + // running tallies of total spend + let supp = 0; + let current = 0; + let target = 240000; + + // Function to format number as currency + const formatCurrency = (amount) => { + var amount = parseFloat(amount); + return '$' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); + } ; + + // Add an event listener for the save button + document.getElementById('save').addEventListener('click', () => saveTableData(current, supp)); + + // Use button to show table + document.getElementById('start-over-btn').addEventListener('click', showTable); + document.getElementById('load-saved-data-btn').addEventListener('click', function(event){ + loadTableData(updateDisplay, formatCurrency); + showTable(); + }); + + // unhide table + function showTable() { + var positionPage = document.getElementById('position-page'); + positionPage.style.display = 'block'; + document.getElementById('welcome-page').style.display = "none"; + } + + // Function to update the display of the current and supp variables + function updateDisplay() { + document.getElementById('current').textContent = formatCurrency(current); + document.getElementById('supps').textContent = formatCurrency(supp); + if(current <= target){ + document.getElementById('current').style.color = "green"; + } + if(current > target){ + document.getElementById('current').style.color = "red"; + } + } + + // Function to turn the cell back into a textbox when clicked + function enableEditingForSalaryCell(cell) { + cell.onclick = function() { + var currentValue = cell.getAttribute('data-salary') || ''; + var textbox = document.createElement('input'); + textbox.type = 'text'; + textbox.value = currentValue; + + function commitAndRestoreText() { + var enteredValue = textbox.value; + cell.setAttribute('data-salary', enteredValue); + cell.textContent = formatCurrency(parseFloat(enteredValue)); + updateDisplay(); + + // Attach the click event to revert back to textbox upon future clicks + cell.onclick = function() { + enableEditingForSalaryCell(cell); + }; + } + + textbox.onblur = commitAndRestoreText; + textbox.onkeydown = function(event) { + if (event.key === 'Enter') { + commitAndRestoreText(); // Commit the value and restore the cell text + textbox.blur(); // Unfocus the textbox to trigger the blur event + } + }; + + cell.innerHTML = ''; // Clear cell content + cell.appendChild(textbox); // Embed textbox + cell.onclick = null; // Remove click listener so it doesn't interfere while editing + + textbox.focus(); + } + } + + // The modified addTextboxCell function + function addTextboxCell(cell, isSalaryCell = false) { + var textbox = document.createElement('input'); + textbox.type = 'text'; + textbox.placeholder = isSalaryCell ? 'Enter salary' : 'Type value'; + textbox.style.width = "100%"; // Modified the width to fit the cell + cell.appendChild(textbox); // Add the textbox to the cell + + if (isSalaryCell) { + cell.classList.add('salary'); // Ensure the cell has a 'salary' class + enableEditingForSalaryCell(cell); // Make the cell editable on click + textbox.onblur = function() { + var enteredValue = textbox.value; + cell.setAttribute('data-salary', enteredValue); + updateDisplay(); + } + } + } + + var addPositionButton = document.querySelector('.btn-add'); + function addRow() { + var table = document.getElementById("employee-table"); + var newRow = table.insertRow(-1); + newRow.classList.add("no-choice-yet"); + // var newNameCell = newRow.insertCell(0); + // count number of table columns using jQuery + let cols = $("#employee-table tr th").length; + for (let i = 0; i < cols-1; i++) { + var nextCell = newRow.insertCell(i); + if (i === 2) { // Check if it's the third cell + // Pass true so that the function knows this is a salary cell + addTextboxCell(nextCell, true); + } else { + addTextboxCell(nextCell); + }; + } + var lastCell = newRow.insertCell(cols-1); + lastCell.innerHTML = ` + + + + `; + } + addPositionButton.addEventListener('click', addRow) + + document.getElementById('employee-table').addEventListener('click', function(event) { + // Determine what was clicked on within the table + var clickedElement = event.target; + + // Check if a delete button was clicked + if (clickedElement.matches('.btn-delete')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("delete"); + // update variable counters + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "keep"){ + current -= salary + } else if (rowClass == "supp"){ + supp -= salary; + }; + updateDisplay(); + } + // Check if a supplemental button was clicked + else if (clickedElement.matches('.btn-supplemental')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("supp"); + // change counters + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "keep"){ + current -= salary + }; + if (rowClass != "supp"){ + supp += salary; + }; + updateDisplay(); + } + // Check if a carryover button was clicked + else if (clickedElement.matches('.btn-carryover')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("keep"); + // update counter + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "supp"){ + supp -= salary; + } ; + if (rowClass != "keep"){ + current += salary; + } ; + updateDisplay(); + } + }); + + // Download data button + function downloadTableAsExcel(tableId, filename) { + var table = document.getElementById(tableId); + var workbook = XLSX.utils.table_to_book(table); + XLSX.writeFile(workbook, filename + '.xlsx'); + } + // Add an event listener for the download button + document.getElementById('XLSX-download').addEventListener('click', function() { + downloadTableAsExcel('employee-table', 'table-export'); + }); + +}); \ No newline at end of file diff --git a/static/js/position-table.js b/static/js/position-table.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/position_buttons.js b/static/js/position_buttons.js new file mode 100644 index 0000000..4fda9f1 --- /dev/null +++ b/static/js/position_buttons.js @@ -0,0 +1,261 @@ +document.addEventListener('DOMContentLoaded', function () { + + // running tallies of total spend + let supp = 0; + let current = 0; + let target = 240000; + + // Function to format number as currency + const formatCurrency = (amount) => { + var amount = parseFloat(amount); + return '$' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); + } ; + + // function to save employee data in localStorage (client web cache) + function saveTableData() { + var table = document.getElementById("employee-table"); + var rows = table.rows; + var tableData = []; + + for (var i = 0; i < rows.length; i++) { + var cols = rows[i].cells; + var rowData = []; + for (var j = 0; j < cols.length; j++) { + // for the action buttons, save the chosen action, not the button text + if (cols[j].querySelector('button')) { + rowData.push(rows[i].className); + } else { + rowData.push(cols[j].innerText); + } + } + tableData.push(rowData); + } + + // Save JSON string to localStorage + localStorage.setItem("employeeTableData", JSON.stringify(tableData)); + // also save counters in sidebar + localStorage.setItem('current', current.toString()); + localStorage.setItem('supp', supp.toString()); + } + + // Add an event listener for the download button + document.getElementById('save').addEventListener('click',saveTableData); + + // Function to load saved data + function loadTableData() { + var data = localStorage.getItem("employeeTableData"); + if (data) { + var tableData = JSON.parse(data); + var table = document.getElementById("employee-table"); + + // It's good practice to empty the existing table first + while (table.rows.length > 1) { + table.deleteRow(1); + } + + // Now, add the loaded rows to the table + for (var i = 1; i < tableData.length; i++) { + var newRow = table.insertRow(-1); + var ncols = tableData[i].length; + for (var j = 0; j < ncols-1; j++) { + var newCell = newRow.insertCell(j); + newCell.innerText = tableData[i][j]; + } + // Add the action buttons and load saved class + var lastCell = newRow.insertCell(ncols-1); + lastCell.innerHTML = ` + + + + `; + newRow.classList.add(tableData[i][ncols-1]); + } + + //retrieve counter values + current = parseInt(localStorage.getItem('current'), 10); + console.log(current); + supp = parseInt(localStorage.getItem('supp'), 10); + updateDisplay(); + } + } + + // unhide table + function showTable() { + var positionPage = document.getElementById('position-page'); + positionPage.style.display = 'block'; + document.getElementById('welcome-page').style.display = "none"; + } + + // Use button to show table + document.getElementById('start-over-btn').addEventListener('click', showTable); + document.getElementById('load-saved-data-btn').addEventListener('click', function(event){ + loadTableData(); + showTable(); + }); + + // Function to update the display of the current and supp variables + function updateDisplay() { + document.getElementById('current').textContent = formatCurrency(current); + document.getElementById('supps').textContent = formatCurrency(supp); + if(current <= target){ + document.getElementById('current').style.color = "green"; + } + if(current > target){ + document.getElementById('current').style.color = "red"; + } + } + + // Function to turn the cell back into a textbox when clicked + function enableEditingForSalaryCell(cell) { + cell.onclick = function() { + var currentValue = cell.getAttribute('data-salary') || ''; + var textbox = document.createElement('input'); + textbox.type = 'text'; + textbox.value = currentValue; + + function commitAndRestoreText() { + var enteredValue = textbox.value; + cell.setAttribute('data-salary', enteredValue); + cell.textContent = formatCurrency(parseFloat(enteredValue)); + updateDisplay(); + + // Attach the click event to revert back to textbox upon future clicks + cell.onclick = function() { + enableEditingForSalaryCell(cell); + }; + } + + textbox.onblur = commitAndRestoreText; + textbox.onkeydown = function(event) { + if (event.key === 'Enter') { + commitAndRestoreText(); // Commit the value and restore the cell text + textbox.blur(); // Unfocus the textbox to trigger the blur event + } + }; + + cell.innerHTML = ''; // Clear cell content + cell.appendChild(textbox); // Embed textbox + cell.onclick = null; // Remove click listener so it doesn't interfere while editing + + textbox.focus(); + } + } + + // The modified addTextboxCell function + function addTextboxCell(cell, isSalaryCell = false) { + var textbox = document.createElement('input'); + textbox.type = 'text'; + textbox.placeholder = isSalaryCell ? 'Enter salary' : 'Type value'; + textbox.style.width = "100%"; // Modified the width to fit the cell + cell.appendChild(textbox); // Add the textbox to the cell + + if (isSalaryCell) { + cell.classList.add('salary'); // Ensure the cell has a 'salary' class + enableEditingForSalaryCell(cell); // Make the cell editable on click + textbox.onblur = function() { + var enteredValue = textbox.value; + cell.setAttribute('data-salary', enteredValue); + updateDisplay(); + } + } + } + + var addPositionButton = document.querySelector('.btn-add'); + function addRow() { + var table = document.getElementById("employee-table"); + var newRow = table.insertRow(-1); + // var newNameCell = newRow.insertCell(0); + // count number of table columns using jQuery + let cols = $("#employee-table tr th").length; + for (let i = 0; i < cols-1; i++) { + var nextCell = newRow.insertCell(i); + if (i === 2) { // Check if it's the third cell + // Pass true so that the function knows this is a salary cell + addTextboxCell(nextCell, true); + } else { + addTextboxCell(nextCell); + }; + } + var lastCell = newRow.insertCell(cols-1); + lastCell.innerHTML = ` + + + + `; + } + addPositionButton.addEventListener('click', addRow) + + document.getElementById('employee-table').addEventListener('click', function(event) { + // Determine what was clicked on within the table + var clickedElement = event.target; + + // Check if a delete button was clicked + if (clickedElement.matches('.btn-delete')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("delete"); + // update variable counters + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "keep"){ + current -= salary + } else if (rowClass == "supp"){ + supp -= salary; + }; + updateDisplay(); + } + // Check if a supplemental button was clicked + else if (clickedElement.matches('.btn-supplemental')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("supp"); + // change counters + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "keep"){ + current -= salary + }; + if (rowClass != "supp"){ + supp += salary; + }; + updateDisplay(); + } + // Check if a carryover button was clicked + else if (clickedElement.matches('.btn-carryover')) { + var currentRow = clickedElement.closest('tr'); + // get current class and update it + var rowClass = currentRow.className; + if (rowClass) { + currentRow.classList.remove(rowClass); + } + currentRow.classList.add("keep"); + // update counter + const salary = parseInt(event.target.closest('tr').querySelector('.salary').getAttribute('data-salary')); + if (rowClass == "supp"){ + supp -= salary; + } ; + if (rowClass != "keep"){ + current += salary; + } ; + updateDisplay(); + } + }); + + // Download data button + function downloadTableAsExcel(tableId, filename) { + var table = document.getElementById(tableId); + var workbook = XLSX.utils.table_to_book(table); + XLSX.writeFile(workbook, filename + '.xlsx'); + } + // Add an event listener for the download button + document.getElementById('XLSX-download').addEventListener('click', function() { + downloadTableAsExcel('employee-table', 'table-export'); + }); + +}); \ No newline at end of file diff --git a/static/js/storage-handlers.js b/static/js/storage-handlers.js new file mode 100644 index 0000000..b788c16 --- /dev/null +++ b/static/js/storage-handlers.js @@ -0,0 +1,65 @@ +// function to save employee data in localStorage (client web cache) +function saveTableData(current, supp) { + var table = document.getElementById("employee-table"); + var rows = table.rows; + var tableData = []; + + for (var i = 0; i < rows.length; i++) { + var cols = rows[i].cells; + var rowData = []; + for (var j = 0; j < cols.length; j++) { + // for the action buttons, save the chosen action, not the button text + if (cols[j].querySelector('button')) { + rowData.push(rows[i].className); + } else { + rowData.push(cols[j].innerText); + } + } + tableData.push(rowData); + } + + // Save JSON string to localStorage + localStorage.setItem("employeeTableData", JSON.stringify(tableData)); + // also save counters in sidebar + localStorage.setItem('current', current.toString()); + localStorage.setItem('supp', supp.toString()); +} + + +// Function to load saved data +function loadTableData(updateDisplay, formatCurrency) { + var data = localStorage.getItem("employeeTableData"); + if (data) { + var tableData = JSON.parse(data); + var table = document.getElementById("employee-table"); + + // It's good practice to empty the existing table first + while (table.rows.length > 1) { + table.deleteRow(1); + } + + // Now, add the loaded rows to the table + for (var i = 1; i < tableData.length; i++) { + var newRow = table.insertRow(-1); + var ncols = tableData[i].length; + for (var j = 0; j < ncols-1; j++) { + var newCell = newRow.insertCell(j); + newCell.innerText = tableData[i][j]; + } + // Add the action buttons and load saved class + var lastCell = newRow.insertCell(ncols-1); + lastCell.innerHTML = ` + + + + `; + newRow.classList.add(tableData[i][ncols-1]); + } + + //retrieve counter values + current = parseInt(localStorage.getItem('current'), 10); + console.log(current); + supp = parseInt(localStorage.getItem('supp'), 10); + updateDisplay(); + } +} \ No newline at end of file diff --git a/static/js/table-fns.js b/static/js/table-fns.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..e69de29 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..ec5cc42 --- /dev/null +++ b/static/style.css @@ -0,0 +1,108 @@ +:root { + --orange: #FB6523; + --yellow: #D99733; + --blue: #43748E; + --lightBlue: #b7c9e2; + --darkGray: #373A37; + --lightGray: #D7DAD7; + --white: #FBFDFB; + --black: #212121; + --green: #71BC78; + font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; +} + +h1 { + text-align: center; +} + +body { + margin: 20px; +} + +.sidebar { + min-height: 100vh; /* Full height of viewport */ +} + +.delete { + background-color: #ee9286; +} +.delete .btn-delete, .keep .btn-carryover, .supp .btn-supplemental { + background-color: #373A37; +} +.keep { + background-color: #90ee90; +} +.supp { + background-color: #e0b472; +} + +.btn { + cursor:pointer; + padding: 10px; + border-radius: 10px; + background-color: gray; + color: white; +} +.btn-delete { + background-color: var(--orange); + color: white; +} +.btn-supplemental { + background-color: var(--yellow); + color: white; +} +.btn-carryover { + background-color: var(--green); + color: white; +} +.btn-add { + background-color: var(--blue); + color: white; +} + +.new-row{ + margin: 20px; + text-align: center; +} +.next-button-row { + text-align: right; +} + +/* Some additional styling to the table for aesthetics */ +.table-container { + margin-top: 20px; +} + + +thead > tr > th { + text-align: left; +} + +th { + background-color: var(--lightGray); +} + +tr { + border-width: 2px; +} + +tr td { + border-bottom: 1px solid black; +} + +.col-employee { min-width: 8em; } +.col-job { min-width: 8em; } +.col-salary { min-width: 5em; } +.col-fund { min-width: 6em; } +.col-appropriation { min-width: 8em; } +.col-cost-center { min-width: 5em; } +.col-actions { min-width: 27em; } + +#sidebar { + background-color: lightgrey; +} + +#position-page { + display: none; +} \ No newline at end of file