diff --git a/pages/archive/index.html b/archive/index.html similarity index 100% rename from pages/archive/index.html rename to archive/index.html diff --git a/pages/archive/personnel.html b/archive/personnel.html similarity index 100% rename from pages/archive/personnel.html rename to archive/personnel.html diff --git a/archive/start_from_save_main.js b/archive/start_from_save_main.js new file mode 100644 index 0000000..362a86c --- /dev/null +++ b/archive/start_from_save_main.js @@ -0,0 +1,13 @@ +document.addEventListener('DOMContentLoaded', function () { + + // If starting over, reset storage + document.getElementById('start-over-btn').addEventListener('click', function(event){ + localStorage.setItem("employeeTableData", ""); + }); + + // Show start from saved data button only if there is saved data + if (localStorage.getItem("employeeTableData")) { + document.getElementById('load-saved-data-btn').style.display = "block" + } + +}); \ No newline at end of file diff --git a/css/02_new_initiatives.css b/css/02_new_initiatives.css deleted file mode 100644 index 32a3752..0000000 --- a/css/02_new_initiatives.css +++ /dev/null @@ -1,24 +0,0 @@ -/* New initiative page */ - -.btn.btn-yes { background-color: var(--green);} -.btn.btn-no {background-color: var(--orange);} -.btn.btn-yes, .btn.btn-no{ font-size: 1.5em;} -.btn.btn-yes:hover, .btn.btn-no:hover {color: white;} - -#col-init-explanation{ width: 70%; } - -#init-explanation {height: 100px; width: 100%;} -#submit-btn-container {text-align: center;} -.btn.btn-submit {background-color: var(--spiritgreen); width: 50%; color:white;} -.btn.btn-submit:hover { - color: black; -} - -#initiative-table-div { - display: none; - /* text-align: center; */ -} - -#initial-questions { - text-align: center; -} diff --git a/css/03_revenue.css b/css/03_revenue.css deleted file mode 100644 index 126ea53..0000000 --- a/css/03_revenue.css +++ /dev/null @@ -1,3 +0,0 @@ - -.btn.btn-yes { background-color: var(--green);} -.btn.btn-no {background-color: var(--orange);} \ No newline at end of file diff --git a/css/04_personnel.css b/css/04_personnel.css deleted file mode 100644 index 6981829..0000000 --- a/css/04_personnel.css +++ /dev/null @@ -1,28 +0,0 @@ -.btn-info { - border-radius: 0%; - background-color: var(--lightBlue); - margin-left: 10px; - color: black; -} - -.btn-edit { - background-color: var(--blue); - margin-left: 10px; -} - -.btn-see-calcs { - display: none; -} - -.icon-button { - background: none; - border: none; - padding: 0; - cursor: pointer; - font-size: 1.5em; - color: var(--blue); -} - -.icon-button:hover { - color: var(--orange); -} \ No newline at end of file diff --git a/css/common.css b/css/common.css index b446866..2bb9994 100644 --- a/css/common.css +++ b/css/common.css @@ -19,23 +19,18 @@ /* Every page */ -h1 { - text-align: center; -} - -h2 { - color: var(--citygreen); - text-align: center; -} +/* start by hiding everything */ +#welcome-page{display: none;} +#prompt-div{display: none;} body { - margin-top: 20px; + margin: 10px; } /* Button styling */ .btn { - cursor:pointer; + cursor: pointer; padding: 10px; margin-top: 5px; border-radius: 10px; @@ -43,39 +38,6 @@ body { color: white; } -/* Sidebar */ - -#sidebar { - background-color: lightgrey; - /* min-height: 100vh; Full height of viewport */ - } - -#supp-total { - color: var(--yellow); -} - -/* Table generics */ - -.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; -} - /* Action buttons */ .action-btns { @@ -98,28 +60,7 @@ tr td { .btn-delete {background-color: var(--orange);} .btn-supplemental { background-color: var(--yellow);} .btn-carryover {background-color: var(--green);} -.btn-add { background-color: var(--spiritgreen);} - -/* Go to next page */ -.new-row{ - margin: 20px; - text-align: center; -} -.next-button-row { - text-align: right; -} -.btn-next, .btn-last { - background-color: var(--blue); -} -.btn-next:hover, .btn-last:hover { - background-color: var(--yellow); -} - -/* textbox width in table */ -input { - width: 100%; -} .error-message { color: red; diff --git a/data/law_dept_sample/personnel_data.json b/data/law_dept_sample/personnel_data.json new file mode 100644 index 0000000..becc1ce --- /dev/null +++ b/data/law_dept_sample/personnel_data.json @@ -0,0 +1,26 @@ +[ + { + "Job Name": "Deputy Counsel", + "Account String": "1000-29320-320010", + "Current FTEs (FY25)": 1, + "Baseline FTEs": 0, + "Supplemental FTEs": 0, + "Current Average Salary": "150000" + }, + { + "Job Name": "Legal Secretary", + "Account String": "1000-29320-320010", + "Current FTEs (FY25)": 5, + "Baseline FTEs": 0, + "Supplemental FTEs": 0, + "Current Average Salary": "55000" + }, + { + "Job Name": "Assistant Counsel", + "Account String": "1000-29320-320010", + "Current FTEs (FY25)": 10, + "Baseline FTEs": 0, + "Supplemental FTEs": 0, + "Current Average Salary": "80000" + } +] \ No newline at end of file diff --git a/data/law_dept_sample/services.json b/data/law_dept_sample/services.json new file mode 100644 index 0000000..2a4dd15 --- /dev/null +++ b/data/law_dept_sample/services.json @@ -0,0 +1,10 @@ +[ + { "id" : "", + "name" : "Select"}, + { "id" : "Appeals", + "name" : "Appeals"}, + { "id" : "FOIA", + "name" : "FOIA" }, + { "id" : "Lobbying", + "name" : "Lobbying"} +] \ No newline at end of file diff --git a/data/law_dept_sample/strings.json b/data/law_dept_sample/strings.json new file mode 100644 index 0000000..955a422 --- /dev/null +++ b/data/law_dept_sample/strings.json @@ -0,0 +1,39 @@ +{ + "1000" : { + "label" : "General Fund", + "appropriations" : { + "29320" : { + "label" : "Efficient and Innovative Operations Support", + "cost centers" : { + "320010" : { "label" : "Law Administration" }, + "321111" : { "label" : "Law Department Grants" } + } + } + } + }, + + "2119" : { + "label" : "FY2020 MIDC Grant", + "appropriations" : { + "21206" : { + "label" : "2023 Michigan Indigent Defense Commission", + "cost centers" : { + "320010" : { "label" : "Law Administration" }, + "321111" : { "label" : "Law Department Grants" } + } + } + } + }, + + "2490" : { + "label" : "Construction Code Fund", + "appropriations" : { + "25130" : { + "label" : "BSEED Safe Buildings", + "cost centers" : { + "320010" : { "label" : "Law Administration" } + } + } + } + } +} \ No newline at end of file diff --git a/index.html b/index.html index 03fb7f3..a1e2c85 100644 --- a/index.html +++ b/index.html @@ -7,62 +7,196 @@ - + - + + + + + + + + + + + +

FY2026 Budget Form

+




-
- - - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + +
+ + + + + +
+

+
+ + +
+ + +
+
+
+ + + + +
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ +
+ + + +
+
+ + + diff --git a/js/components/form/form.css b/js/components/form/form.css new file mode 100644 index 0000000..b432df0 --- /dev/null +++ b/js/components/form/form.css @@ -0,0 +1,12 @@ +textarea {height: 100px; width: 100%;} + +textarea, input { + margin-bottom: 20px; +} + +.btn-submit { + margin-top: 20px; + width: 60%; + margin-left: 20%; + background-color: var(--spiritgreen); +} \ No newline at end of file diff --git a/js/components/form/form.js b/js/components/form/form.js new file mode 100644 index 0000000..9081a0f --- /dev/null +++ b/js/components/form/form.js @@ -0,0 +1,136 @@ +// function to add questions to forms +// type is 'input' or 'textarea' +// inputType is for validation ('number' or 'text', etc) +function appendFormElement(type, label, inputId, required, inputType, form_id = 'new-form', cost = false) { + + // change if we want forms elsewhere + const form = document.getElementById(form_id); + + // create outer wrapper for element + const wrapper = document.createElement('div'); + + // label question + const labelEl = document.createElement('label'); + labelEl.textContent = label; + + // set type (input or textarea) + let inputEl; + if (type === 'input') { + inputEl = document.createElement('input'); + inputEl.type = inputType; + } else if (type === 'textarea') { + inputEl = document.createElement('textarea'); + } else { + throw new Error('Unsupported element type'); + } + + // mark as required if applicable + inputEl.required = required; + + // If an ID is provided, set it on the element + if (inputId) { + inputEl.id = inputId; + } + + // add elements + wrapper.appendChild(labelEl); + wrapper.appendChild(inputEl); + form.appendChild(wrapper); +} + + + +// Individual functions for each type of input. +export function addTextInput(label, inputId, required = false, form_id = 'new-form', cost = false) { + appendFormElement('input', label, inputId, required, 'text', form_id); +} + +export function addNumericInput(label, inputId, required = false, form_id = 'new-form', cost = true) { + appendFormElement('input', label, inputId, required, 'number', form_id); +} + +export function addTextarea(label, inputId, required = false, form_id = 'new-form', cost = false) { + appendFormElement('textarea', label, inputId, required, form_id); +} + +export function addSubmitButtonToForm(form_id = 'new-form') { + // Find the form by its ID + const form = document.getElementById(form_id); + + // Create the container `div` for the button + const buttonContainer = document.createElement('div'); + buttonContainer.id = 'submit-btn-container'; + + // Create the submit input + const submitInput = document.createElement('input'); + submitInput.className = 'btn btn-submit'; // Use appropriate class for your design + submitInput.type = 'submit'; + submitInput.value = 'Submit'; + + // Append the submit input to the container + buttonContainer.appendChild(submitInput); + + // Append the container to the form + form.appendChild(buttonContainer); +} + +export function fetchAllResponses(event) { + event.preventDefault(); // Prevent the default form submission + + // Assuming `event.target` is the form itself + const form = event.target; + + // Initialize an empty array to hold the input values + let formData = {}; + + // Loop through each form element + for (let i = 0; i < form.elements.length; i++) { + const element = form.elements[i]; + + // Exclude elements that aren't inputs, textareas, or select + if (element.tagName === 'INPUT' || + element.tagName === 'TEXTAREA' || + element.tagName === 'SELECT') { + // Exclude input types that are not considered for submission (such as `submit`) + if (element.type !== 'submit' && element.type !== 'button') { + formData[element.id] = element.value; + } + } + } + + form.reset(); + return formData; +} + +export function addForm(element_id = 'modal-body', form_id = 'new-form') { + + const target_elem = document.getElementById(element_id); + + // create form + const form = document.createElement('form'); + form.setAttribute('id', form_id); + + // Append the form to the modal body + target_elem.appendChild(form); + +} + +export async function createDropdownFromJSON(json_path) { + // Fetch JSON data from a file asynchronously + const response = await fetch(json_path); + const dataArray = await response.json(); + + // Creating a select element + const selectElement = document.createElement('select'); + + // Looping through the array and creating an option for each element + dataArray.forEach(item => { + const optionElement = document.createElement('option'); + optionElement.value = item.id; // Setting the option value to the item id + optionElement.textContent = item.name; // Setting the display text to the item name + selectElement.appendChild(optionElement); // Appending the option to the select + }); + + // Return the select element so it can be appended to the document + return selectElement.outerHTML; +} \ No newline at end of file diff --git a/js/components/header/header.css b/js/components/header/header.css new file mode 100644 index 0000000..ccd9d76 --- /dev/null +++ b/js/components/header/header.css @@ -0,0 +1,9 @@ + +h1 { + text-align: center; +} + +h2 { + color: var(--citygreen); + text-align: center; +} \ No newline at end of file diff --git a/js/components/header/header.js b/js/components/header/header.js new file mode 100644 index 0000000..d8e0e6f --- /dev/null +++ b/js/components/header/header.js @@ -0,0 +1,3 @@ +export function updateSubtitle(subtitle){ + document.getElementById("subtitle").textContent = subtitle; +} \ No newline at end of file diff --git a/js/components/modal/modal.css b/js/components/modal/modal.css new file mode 100644 index 0000000..e69de29 diff --git a/js/components/modal/modal.js b/js/components/modal/modal.js new file mode 100644 index 0000000..7f0275f --- /dev/null +++ b/js/components/modal/modal.js @@ -0,0 +1,22 @@ +export function hideModal(modal_id) { + $('#' + modal_id).modal('hide'); +} + +export function showModal(modal_id) { + $('#' + modal_id).modal('show'); +} + +export function addModalLink(button_id, modal_id){ + document.getElementById(button_id).addEventListener('click', function() { + showModal(modal_id); + }); +} + +export function updateModalTitle(title){ + document.getElementById('modal-title').textContent = title; +} + +export function clearModal(){ + updateModalTitle(''); + document.getElementById('modal-body').innerHTML = ''; +} \ No newline at end of file diff --git a/js/components/nav_buttons/nav_buttons.css b/js/components/nav_buttons/nav_buttons.css new file mode 100644 index 0000000..8560007 --- /dev/null +++ b/js/components/nav_buttons/nav_buttons.css @@ -0,0 +1,12 @@ +#nav-btns { + margin: 20px; + text-align: center; +} + +#btn-next, #btn-last { + background-color: var(--blue); +} + +#btn-next:hover, #btn-last:hover { + background-color: var(--yellow); +} \ No newline at end of file diff --git a/js/components/nav_buttons/nav_buttons.js b/js/components/nav_buttons/nav_buttons.js new file mode 100644 index 0000000..ddce93f --- /dev/null +++ b/js/components/nav_buttons/nav_buttons.js @@ -0,0 +1,64 @@ + +import { initializeWelcomePage } from '../../pages/00_welcome/main.js'; +import { loadNewInitiatives } from '../../pages/02_new_initiatives/main.js' +import { loadRevenuePage } from '../../pages/03_revenue/main.js' +import { loadPersonnelPage } from '../../pages/04_personnel/main.js'; +import { loadPageState } from '../../utils/storage-handlers.js' + + +let pages = {'welcome' : initializeWelcomePage, + 'new-inits' : loadNewInitiatives, + 'revenue' : loadRevenuePage, + 'personnel' : loadPersonnelPage } + +export function hideNavButtons() { + document.getElementById('nav-btns').style.display = 'none'; +} + +export function showNavButtons() { + document.getElementById('nav-btns').style.display = 'block'; +} + +// imputs next and last should be functions to render the appropriate pages +export function initializeNavButtons(){ + // initialize last button + const last_btn = document.getElementById('btn-last'); + last_btn.addEventListener('click', lastPage); + // initialize next button + const next_btn = document.getElementById('btn-next'); + next_btn.addEventListener('click', nextPage); +} + +function nextPage(page_state){ + + var page_state = loadPageState(); + const keys = Object.keys(pages); + + // Find the index of the current key + const currentIndex = keys.indexOf(page_state); + + // Check if there is a next key + if (currentIndex >= 0 && currentIndex < keys.length - 1) { + // Get the next key + const nextKey = keys[currentIndex + 1]; + const nextFn = pages[nextKey]; + nextFn(); + } +} + +function lastPage(){ + + var page_state = loadPageState(); + const keys = Object.keys(pages); + + // Find the index of the current key + const currentIndex = keys.indexOf(page_state); + + // Check if there is a next key + if (currentIndex >= 1) { + // Get the next key + const lastKey = keys[currentIndex - 1]; + const lastFn = pages[lastKey]; + lastFn(); + } +} \ No newline at end of file diff --git a/js/components/prompt/prompt.css b/js/components/prompt/prompt.css new file mode 100644 index 0000000..968f9eb --- /dev/null +++ b/js/components/prompt/prompt.css @@ -0,0 +1,14 @@ +#prompt-div { + display: none; + text-align: center; + width: 60%; + margin-left: 20%; +} + +#prompt { + text-align: center; +} + +#option1 { background-color: var(--green);} +#option2 {background-color: var(--orange);} +#option1, #option2 { font-size: 1.5em; } \ No newline at end of file diff --git a/js/components/prompt/prompt.js b/js/components/prompt/prompt.js new file mode 100644 index 0000000..0c38dbc --- /dev/null +++ b/js/components/prompt/prompt.js @@ -0,0 +1,29 @@ +export function showPrompt(){ + document.getElementById("prompt-div").style.display = "block"; +} + +export function hidePrompt(){ + document.getElementById('prompt-div').style.display = 'none'; +} + + +export function updatePrompt(prompt){ + document.getElementById('prompt').textContent = prompt; +} + +export function updatePromptButtons(option1, option2){ + document.getElementById('option1').textContent = option1; + document.getElementById('option2').textContent = option2; + // make buttons visible + document.getElementById('option1').style.display = 'inline'; + document.getElementById('option2').style.display = 'inline'; +} + +export function addPromptButtonAction(button_id, action_fn){ + document.getElementById(button_id).addEventListener('click', action_fn); +} + +export function hidePromptButtons(){ + document.getElementById('option1').style.display = 'none'; + document.getElementById('option2').style.display = 'none'; +} \ No newline at end of file diff --git a/js/components/sidebar/sidebar.css b/js/components/sidebar/sidebar.css new file mode 100644 index 0000000..6f795d2 --- /dev/null +++ b/js/components/sidebar/sidebar.css @@ -0,0 +1,12 @@ +#sidebar-panel { + background-color: lightgrey; + /* min-height: 100vh; Full height of viewport */ + } + +#supp-total .stat { + color: var(--yellow); +} + +.stat { + font-weight: bold; +} \ No newline at end of file diff --git a/js/components/sidebar/sidebar.js b/js/components/sidebar/sidebar.js new file mode 100644 index 0000000..a48a90e --- /dev/null +++ b/js/components/sidebar/sidebar.js @@ -0,0 +1,59 @@ +import { formatCurrency } from "../../utils/utils.js"; + +export function hideSideBar(){ + document.getElementById('sidebar-panel').style.display = 'none'; + document.getElementById('main-panel').className = 'col-md-12'; +} + +export function showSideBar(){ + document.getElementById('sidebar-panel').className = 'col-md-2'; + document.getElementById('sidebar-panel').style.display = 'block'; + document.getElementById('main-panel').className = 'col-md-10'; +} + +function updateSidebarStat(stat_id, new_figure){ + replaceSidebarStat(stat_id, new_figure); + // TODO: save in memory + updateTotals(); +} + +function replaceSidebarStat(stat_id, new_figure){ + const span = document.querySelector(`#${stat_id} .stat`); + span.setAttribute('value', new_figure); + span.textContent = formatCurrency(new_figure); +} + +export function incrementSidebarStat(stat_id, new_figure){ + updateSidebarStat(stat_id, fetchStat(stat_id) + new_figure) +} + +export function fetchStat(stat_id){ + const stat = document.querySelector(`#${stat_id} .stat`); + return parseFloat(stat.getAttribute('value')) || 0; +} + +// Function to update the display of the current and supp variables +export function updateTotals() { + // update bottom lines + let supp_total = -fetchStat('supp-revenue') + + fetchStat('supp-personnel') + + fetchStat('supp-nonpersonnel'); + let baseline_total = -fetchStat('baseline-revenue') + + fetchStat('baseline-personnel') + + fetchStat('baseline-nonpersonnel'); + replaceSidebarStat('supp-total', supp_total); + replaceSidebarStat('baseline-total', baseline_total); + + // color code based on target + var target = fetchStat('target'); + if(baseline_total <= target){ + document.querySelector('#baseline-total .stat').style.color = "green"; + } + if(baseline_total > target){ + document.getElementById('#baseline-total .stat').style.color = "red"; + } +} + +export function addTarget(target){ + replaceSidebarStat('target', target); +} \ No newline at end of file diff --git a/js/components/table/table.css b/js/components/table/table.css new file mode 100644 index 0000000..df26837 --- /dev/null +++ b/js/components/table/table.css @@ -0,0 +1,56 @@ +thead > tr > th { + text-align: left; +} + +th { + background-color: var(--lightGray); +} + +tr { + border-width: 2px; +} + +tr td { + border-bottom: 1px solid black; +} + +/* textbox width in table */ +input { + width: 100%; +} + +.table-container { + display: table; + margin: auto; +} + +#main-table { + width: auto; + margin: auto; +} + +/* Add new row button */ +.btn-add { + background-color: var(--spiritgreen); + margin-top: 20px; + display: none; +} + +#add-btn-div { + display: flex; + justify-content: center; /* Aligns horizontally */ + align-items: center; /* Aligns vertically */ + width: 100%; +} + +.btn-edit { + background-color: var(--spiritgreen); +} + +.active-editing { + background-color: var(--palegreen); +} + +.btn-confirm { + display: none; +} \ No newline at end of file diff --git a/js/components/table/table.js b/js/components/table/table.js new file mode 100644 index 0000000..190a372 --- /dev/null +++ b/js/components/table/table.js @@ -0,0 +1,211 @@ +import { formatCurrency } from "../../utils/utils.js"; + +export function addTableHeaders(table_id, header_array){ + + // Get the table element by its ID + const table = document.getElementById(table_id); + + // Create a table header row element + const headerRow = document.createElement('tr'); + + for (const headerText of header_array) { + + // Create a header cell element + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + + // Append the header cell to the header row + headerRow.appendChild(headerCell); + } + + // Append the header row to the table header + let thead = table.querySelector('thead'); + thead.appendChild(headerRow); +} + +export function addNewRow(table_id, data_dictionary){ + // Get the table element by its ID + const table = document.getElementById(table_id); + + // check if header has already been added + let header_row = table.querySelector('thead tr'); + if (!header_row) { + addTableHeaders(table_id, Object.keys(data_dictionary)); + } + + // add row of data + const new_row = document.createElement('tr'); + const cell_data_array = Object.values(data_dictionary); + + for (const cell_data of cell_data_array) { + // Create new cell and add it to the row + const newCell = document.createElement('td'); + newCell.textContent = cell_data; + new_row.appendChild(newCell); + } + + // Append the new row to the table body + let tbody = table.querySelector('tbody'); + tbody.appendChild(new_row); + +} + +export function adjustTableWidth(table_id, width_pct){ + const table = document.getElementById(table_id); + table.style.width = width_pct; +} + +export function clearTable(table_id){ + const table = document.getElementById(table_id); + table.querySelector('thead').innerHTML = ''; + table.querySelector('tbody'). + innerHTML = ''; +} + +// Add button functions +export function hideAddButton(){ + document.getElementById('add-btn').style.display = 'none'; +} + +export function showAddButton(){ + document.getElementById('add-btn').style.display = 'block'; +} + +export function updateAddButtonText(text){ + document.getElementById('add-btn').textContent = text; +} + +// Show and hide table + +export function hideTable(table_id){ + const table = document.getElementById(table_id); + table.style.display = 'none'; + hideAddButton(); +} + +export function showTable(table_id){ + const table = document.getElementById(table_id); + table.style.display = 'table'; +} + +// position is index at which new column will be inserted +export function addCol(tableId, position, htmlContent = '', headerTitle = '') { + // Get the table element by its ID + let table = document.getElementById(tableId); + + if (!table) { + console.error(`Table with ID ${tableId} not found.`); + return; + } + + // Validate position + let maxPosition = table.rows[0].cells.length; + if (position < 0 || position > maxPosition) { + console.error(`Position ${position} is out of bounds.`); + return; + } + + // Insert the header if provided + let thead = table.tHead; + if (headerTitle && thead) { + let th = document.createElement('th'); + th.innerHTML = headerTitle; // Use innerHTML to insert HTML content + thead.rows[0].insertBefore(th, thead.rows[0].cells[position]); + } + + // Insert new cells into each row of the table body + let tbody = table.tBodies[0]; + if (tbody) { + for (let i = 0; i < tbody.rows.length; i++) { + let row = tbody.rows[i]; + let td = document.createElement('td'); + td.innerHTML = htmlContent; // Use innerHTML to insert HTML content + row.insertBefore(td, row.cells[position]); + } + } +} + +function ncols(tableId){ + const table = document.getElementById(tableId); + // Ensure that the row exists before counting the columns + return table.rows[0].cells.length; +} + +export function addColToEnd(tableId, htmlContents = [], headerTitle = ''){ + // count columns and add new column to the end + const position = ncols(tableId); + addCol(tableId, position, htmlContents, headerTitle); +} + +// functions for editing rows +function editButton() { + var edit_btn = ''; + var confirm_btn = ''; + return edit_btn + confirm_btn; +}; + +export function addEditCol(tableId){ + addColToEnd(tableId, editButton(), ' '); +} + +export function assignClassToColumn(tableId, headerName, className) { + // Get the table element by its ID + let table = document.getElementById(tableId); + + // Find the index of the column by its header name + const thead = table.tHead; + if (!thead || thead.rows.length === 0) { + console.error('The table header is not found or has no rows.'); + return; + } + + let headerCellIndex = -1; + const headerCells = thead.rows[0].cells; // Assuming the first row contains header cells () + for (let i = 0; i < headerCells.length; i++) { + if (headerCells[i].textContent.trim() === headerName) { + headerCellIndex = i; + break; + } + } + + if (headerCellIndex === -1) { + console.error(`No header found with name "${headerName}"`); + return; + } + + // Assign the class to each cell in the specified column index within the tbody + let tbody = table.tBodies[0]; + if (tbody) { + let bodyRows = tbody.rows; + for (let row of bodyRows) { + if (row.cells[headerCellIndex]) { + row.cells[headerCellIndex].classList.add(className); + } + } + } + } + +export function AddCostClass(tableId, headerName){ + assignClassToColumn(tableId, headerName, 'cost'); + + // Get all the cells with the specified class name + const cells = document.querySelectorAll(`.cost`); + + cells.forEach(cell => { + // Get the current text content of the cell and assign it to 'value' attribute + if (!cell.getAttribute('value')){ + const cellValue = cell.textContent.trim(); + cell.setAttribute('value', cellValue); + + // Now format the text content like currency and replace it in the cell + const formattedCurrency = formatCurrency(parseFloat(cellValue)); + cell.textContent = formattedCurrency; + } + + }); + +} + +export function updateCellValue(cell, newValue){ + pass; +} \ No newline at end of file diff --git a/css/00_welcome.css b/js/components/welcome/welcome.css similarity index 92% rename from css/00_welcome.css rename to js/components/welcome/welcome.css index 87fa6b5..23b5108 100644 --- a/css/00_welcome.css +++ b/js/components/welcome/welcome.css @@ -24,6 +24,5 @@ display: flex; justify-content: center; /* Center horizontally */ align-items: center; /* Center vertically */ - /* height: 100vh; Take up full viewport height */ flex-wrap: wrap; } \ No newline at end of file diff --git a/js/components/welcome/welcome.js b/js/components/welcome/welcome.js new file mode 100644 index 0000000..eebfb33 --- /dev/null +++ b/js/components/welcome/welcome.js @@ -0,0 +1,7 @@ +// Hide and unhide welcome buttons +export function unhideWelcomeButtons(){ + document.getElementById("welcome-page").style.display = "flex"; +} +export function hideWelcomeButtons(){ + document.getElementById("welcome-page").style.display = "none"; +} diff --git a/js/init.js b/js/init.js index d928e40..0183b99 100644 --- a/js/init.js +++ b/js/init.js @@ -1,10 +1,35 @@ -// running tallies of total spend -let personnel_supp = 0; -let personnel_baseline = 0; -let nonpersonnel_supp = 0; -let nonpersonnel_baseline = 0; -let target = 2000000; -let baseline_revenue = 0; -let supp_revenue = 0; -let supp_total = personnel_supp - supp_revenue; -let baseline_total = personnel_baseline - baseline_revenue; \ No newline at end of file +// import functions +import { initializeWelcomePage } from './pages/00_welcome/main.js'; +import { loadNewInitiatives } from './pages/02_new_initiatives/main.js' +import { loadRevenuePage } from './pages/03_revenue/main.js' +import { loadPageState } from './utils/storage-handlers.js' +import { initializeNavButtons } from './components/nav_buttons/nav_buttons.js'; +import { loadPersonnelPage } from './pages/04_personnel/main.js'; +import { addTarget } from './components/sidebar/sidebar.js'; + +export let DATA_ROOT = '../../../data/law_dept_sample/' +export let REVENUE = 0; + +document.addEventListener('DOMContentLoaded', function () { + + var page_state = loadPageState(); + initializeNavButtons(); + addTarget(2000000); + + switch (page_state){ + case 'welcome': + initializeWelcomePage(); + break; + case 'new-inits': + loadNewInitiatives(); + break; + case 'revenue': + loadRevenuePage(); + break; + case 'personnel': + loadPersonnelPage(); + break; + }; + + +}); \ No newline at end of file diff --git a/js/pages/00_welcome/helpers.js b/js/pages/00_welcome/helpers.js new file mode 100644 index 0000000..1313f4c --- /dev/null +++ b/js/pages/00_welcome/helpers.js @@ -0,0 +1,28 @@ + +import { hidePrompt } from '../../components/prompt/prompt.js' +import { hideNavButtons } from '../../components/nav_buttons/nav_buttons.js' +import { hideSideBar } from '../../components/sidebar/sidebar.js' +import { hideTable } from '../../components/table/table.js' +import { updateSubtitle } from '../../components/header/header.js' +import { unhideWelcomeButtons } from '../../components/welcome/welcome.js' +import { loadNewInitiatives } from '../02_new_initiatives/main.js' +import { loadRevenuePage } from '../03_revenue/main.js' +import { loadPersonnelPage } from '../04_personnel/main.js' + +export function initializePageView(){ + // page set up + hideTable('main-table'); + hideSideBar(); + updateSubtitle("Welcome"); + unhideWelcomeButtons(); + hidePrompt(); + hideNavButtons(); +} + +export function addLinks(){ + // initialize links in buttons + document.getElementById('step-initiatives').addEventListener('click', loadNewInitiatives) + document.getElementById('step-revenue').addEventListener('click', loadRevenuePage) + document.getElementById('step-personnel').addEventListener('click', loadPersonnelPage) + +} diff --git a/js/pages/00_welcome/main.js b/js/pages/00_welcome/main.js index 362a86c..401addc 100644 --- a/js/pages/00_welcome/main.js +++ b/js/pages/00_welcome/main.js @@ -1,13 +1,11 @@ -document.addEventListener('DOMContentLoaded', function () { - // If starting over, reset storage - document.getElementById('start-over-btn').addEventListener('click', function(event){ - localStorage.setItem("employeeTableData", ""); - }); +import { updatePageState } from '../../utils/storage-handlers.js' +import { initializePageView, addLinks } from './helpers.js' - // Show start from saved data button only if there is saved data - if (localStorage.getItem("employeeTableData")) { - document.getElementById('load-saved-data-btn').style.display = "block" - } +export function initializeWelcomePage(){ -}); \ No newline at end of file + updatePageState('welcome'); + initializePageView(); + addLinks(); + +} \ No newline at end of file diff --git a/js/pages/02_new_initiatives/helpers.js b/js/pages/02_new_initiatives/helpers.js new file mode 100644 index 0000000..91b8267 --- /dev/null +++ b/js/pages/02_new_initiatives/helpers.js @@ -0,0 +1,78 @@ + +import { hideWelcomeButtons } from '../../components/welcome/welcome.js' +import { updateSubtitle } from '../../components/header/header.js' +import { hidePrompt, showPrompt, updatePrompt, updatePromptButtons, addPromptButtonAction } from '../../components/prompt/prompt.js' +import { showNavButtons } from '../../components/nav_buttons/nav_buttons.js' +import { loadRevenuePage } from '../03_revenue/main.js' +import { addModalLink, updateModalTitle, clearModal, hideModal } from '../../components/modal/modal.js' +import { fetchAllResponses, addTextarea, addTextInput, addNumericInput, addSubmitButtonToForm, addForm } from '../../components/form/form.js' +import { adjustTableWidth, hideTable, clearTable, updateAddButtonText, addNewRow, showTable, showAddButton } from '../../components/table/table.js' +import { hideSideBar } from '../../components/sidebar/sidebar.js' + +export function initializePageView() { + // Load text + updateSubtitle('New Initiatives'); + updatePrompt('Do you have any new initiatives for FY26?'); + updatePromptButtons('Yes', 'No'); + + // Prepare page view + hideWelcomeButtons(); + showNavButtons(); + hideSideBar(); + showPrompt(); + hideTable('main-table'); +} + +export function setUpModal() { + // Initialize modal + clearModal(); + addModalLink('option1', 'main-modal'); + updateModalTitle('New initiative'); + addModalLink('add-btn', 'main-modal'); +} + +export function setUpForm() { + // Set up form + addForm(); + addTextInput('Initiative Name:', 'Initiative Name', true); // Add required field + addTextarea('Explain why this initiative is necessary and describe its potential impact.', 'Explanation', true); + addNumericInput('Roughly how additional money would this initiative require?', 'Cost', true); + addSubmitButtonToForm(); + // Initialize form submission to table data + handleFormSubmissions(); +} + +export function setUpTable() { + // Set up table + clearTable('main-table'); + adjustTableWidth('main-table', '70%'); + updateAddButtonText('Add another new initiative'); +} + +export function handleNavigation() { + // clicking 'No' (no new initiatives) will also take us to the next page + addPromptButtonAction('option2', loadRevenuePage); +} + +export function handleFormSubmissions(event){ + // initialize form submission + const modal = document.getElementById('main-modal'); + modal.addEventListener('submit', function(event) { + // get answers from form, hide form, show answers in table + const responses = fetchAllResponses(event); + // make sure it's not an empty response + if (Object.values(responses)[0] != ''){ + // change page view + hideModal('main-modal'); + hidePrompt(); + + // add data to table + addNewRow('main-table', responses); + showTable('main-table'); + showAddButton(); + // TODO: save table data + // TODO: edit cost to show currency correctly + } + + }) +} diff --git a/js/pages/02_new_initiatives/main.js b/js/pages/02_new_initiatives/main.js index 4292a14..63d636b 100644 --- a/js/pages/02_new_initiatives/main.js +++ b/js/pages/02_new_initiatives/main.js @@ -1,44 +1,14 @@ -document.addEventListener('DOMContentLoaded', function () { - - document.getElementById('new-init-form').addEventListener('submit', function(event) { - handleFormSubmissions(event); - }); - -}); - -// process new initiative submission -function handleFormSubmissions(event){ - event.preventDefault(); // Prevent the default form submission - - // get values from form - var name = document.getElementById('init-name').value; - var explanation = document.getElementById('init-explanation').value; - var request = document.getElementById('init-request').value; - - var table = document.getElementById('initiative-table'); - - // Insert a row at the end of the table - var newRow = table.insertRow(table.rows.length); - - // Insert cells in the row - var cell1 = newRow.insertCell(0); - var cell2 = newRow.insertCell(1); - var cell3 = newRow.insertCell(2); - - // Add some text to the new cells - cell1.innerHTML = name; - cell2.innerHTML = explanation; - cell3.innerHTML = formatCurrency(request); - cell3.classList.add('cost'); - - // Clear the form for the next entries - document.getElementById('new-init-form').reset(); - - //show table - document.getElementById('initiative-table-div').style.display = "block"; - - // hide modal and Y/N questions - $('#new-init-modal').modal('hide'); - document.getElementById('initial-questions').style.display = 'none'; -} \ No newline at end of file +import { initializePageView, setUpModal, setUpForm, setUpTable, handleNavigation } from './helpers.js' +import { updatePageState } from '../../utils/storage-handlers.js' + + +// set up page and initialize all buttons +export function loadNewInitiatives() { + updatePageState('new-inits'); + initializePageView(); + setUpModal(); + setUpForm(); + setUpTable(); + handleNavigation(); +} diff --git a/js/pages/03_revenue/main.js b/js/pages/03_revenue/main.js new file mode 100644 index 0000000..7d1c494 --- /dev/null +++ b/js/pages/03_revenue/main.js @@ -0,0 +1,34 @@ +import { updatePageState } from '../../utils/storage-handlers.js' +import { hideWelcomeButtons } from '../../components/welcome/welcome.js' +import { updateSubtitle } from '../../components/header/header.js' +import { showPrompt, updatePrompt, updatePromptButtons, addPromptButtonAction } from '../../components/prompt/prompt.js' +import { showNavButtons } from '../../components/nav_buttons/nav_buttons.js' +import { loadNewInitiatives } from '../02_new_initiatives/main.js' +import { hideTable } from '../../components/table/table.js' +import { hideSideBar } from '../../components/sidebar/sidebar.js' +import { formatCurrency } from '../../utils/utils.js' + +import { REVENUE } from '../../init.js' + +export function loadRevenuePage() { + + //update page state + updatePageState('revenue'); + + // prepare page view + hideWelcomeButtons(); + showPrompt(); + showNavButtons(); + hideTable('main-table'); + hideSideBar(); + + // update page text + updateSubtitle('Revenue Projections'); + // TODO: update to make dynamic + updatePrompt(`Your revenue projection for FY26 is ${formatCurrency(REVENUE, true)}`); + updatePromptButtons('Confirm and continue.', "This doesn't look right"); + + // clicking 'confirm and continue' will also take us to the next page + addPromptButtonAction('option1', loadNewInitiatives); + +} \ No newline at end of file diff --git a/js/pages/04_personnel/helpers.js b/js/pages/04_personnel/helpers.js new file mode 100644 index 0000000..206c537 --- /dev/null +++ b/js/pages/04_personnel/helpers.js @@ -0,0 +1,174 @@ +import { hideWelcomeButtons } from "../../components/welcome/welcome.js"; +import { hidePromptButtons, showPrompt, updatePrompt } from "../../components/prompt/prompt.js"; +import { showNavButtons } from "../../components/nav_buttons/nav_buttons.js"; +import { updateSubtitle } from "../../components/header/header.js"; +import { loadJSONIntoTable } from "../../utils/data-handlers.js"; +import { AddCostClass, addCol, addColToEnd, addEditCol, adjustTableWidth, assignClassToColumn, showTable } from "../../components/table/table.js"; +import { incrementSidebarStat, showSideBar } from "../../components/sidebar/sidebar.js"; +import { formatCurrency } from "../../utils/utils.js"; +import { DATA_ROOT } from "../../init.js" +import { createDropdownFromJSON } from "../../components/form/form.js"; + +// variables on the salary +var fringe = 0.36 +var cola = 0.02 +var merit = 0.02 + +export function preparePageView(){ + // prepare page view + hideWelcomeButtons(); + showPrompt(); + showNavButtons(); + showSideBar(); + hidePromptButtons(); + adjustTableWidth('main-table', '90%'); + + // update page text + updateSubtitle('Personnel'); + updatePrompt('For each job in your department, select the service and request the number of baseline and supplemental FTEs.'); +} + +export async function initializePersonnelTable(){ + // load table data from json + await loadJSONIntoTable(DATA_ROOT + 'personnel_data.json', 'main-table'); + //after table is loaded, fill it + showTable('main-table'); + addCol('main-table', 3, '', 'Service'); + addColToEnd('main-table', '0', 'Total Cost (Baseline)'); + addColToEnd('main-table', '0', 'Total Cost (Supplementary)'); + addEditCol('main-table'); + // assign cost classes + assignClassToColumn('main-table', 'Current Average Salary', 'avg-salary'); + AddCostClass('main-table', 'Current Average Salary'); + assignClassToColumn('main-table', 'Total Cost (Baseline)', 'total-baseline'); + AddCostClass('main-table', 'Total Cost (Baseline)'); + assignClassToColumn('main-table', 'Total Cost (Supplementary)', 'total-supp'); + AddCostClass('main-table', 'Total Cost (Supplementary)'); + // assign other classes + assignClassToColumn('main-table', 'Job Name', 'job-name'); + assignClassToColumn('main-table', 'Baseline FTEs', 'baseline-ftes'); + assignClassToColumn('main-table', 'Supplemental FTEs', 'supp-ftes'); + assignClassToColumn('main-table', 'Service', 'service'); + // manage edit buttons + handleRowEdit(); +} + +export function handleRowEdit(){ + // attach an event listener to each edit button in every row + var editButtons = document.getElementsByClassName('btn-edit'); + for (var i = 0; i < editButtons.length; i++) { + editButtons[i].addEventListener('click', async function(event) { + // Determine what was clicked on within the table + var rowToEdit = event.target.closest('tr'); + // mark row as being edited + rowToEdit.classList.add('active-editing'); + + // turn relevant entries into textboxes + createEditableCell('baseline-ftes'); + createEditableCell('supp-ftes'); + // add service dropdown + const serviceDropdown = await createDropdownFromJSON(DATA_ROOT + 'services.json'); + rowToEdit.querySelector('.service').innerHTML = serviceDropdown; + + // hide edit buttons + var editButtons = document.getElementsByClassName('btn-edit'); + for (var i = 0; i < editButtons.length; i++) { + editButtons[i].style.display = 'none'; + } + + initializeConfirmButton(rowToEdit); + }); + }; +} + + +function createEditableCell(cellClass, attribute = 'value'){ + // get cell + const cell = document.querySelector(`.active-editing td.${cellClass}`); + // Create an input element to edit the value + var textbox = document.createElement('input'); + textbox.type = 'text'; + textbox.value = cell.textContent; + // Clear the current content and append the textbox to the cell + cell.innerHTML = ''; + cell.appendChild(textbox); + //cell.appendChild(feedback); +} + + +function initializeConfirmButton(rowToEdit){ + // get element and add listener for click + const confirm_btn = rowToEdit.querySelector(".btn-confirm"); + // show confirm button + confirm_btn.style.display = 'block'; + confirm_btn.addEventListener('click', function(event){ + // get current row + const rowToEdit = event.target.closest('tr'); + var textboxes = rowToEdit.querySelectorAll('input'); + // save all text in textboxes + textboxes.forEach( textbox => { + var enteredValue = textbox.value; + var cell = textbox.parentElement; + cell.textContent = enteredValue; + cell.setAttribute('value', enteredValue); + }) + // set service selection + const serviceSelector = rowToEdit.querySelector('select'); + var cell = serviceSelector.parentElement; + cell.textContent = serviceSelector.value; + //set service value + + + // update values in sidebar + updateDisplayandTotals(); + + // make row no longer green + rowToEdit.classList.remove('active-editing'); + + // show edit buttons + var editButtons = document.getElementsByClassName('btn-edit'); + for (var i = 0; i < editButtons.length; i++) { + editButtons[i].style.display = 'block'; + } + + // hide confirm button + confirm_btn.style.display = 'none'; + }); +} + +function getCellValue(row, className){ + var cellValue = row.querySelector(`.${className}`).getAttribute('value'); + return parseFloat(cellValue); +} + +function calculateTotalCost(ftes, avg_salary, fringe, cola, merit){ + return ftes * avg_salary * (1 + fringe) * (1 + cola) * (1 + merit); +} + +export function updateTableCell(row, col_class, new_value){ + const cell = row.querySelector(`.${col_class}`); + cell.setAttribute('value', new_value); + cell.textContent = formatCurrency(new_value); +} + +// update sidebar and also cost totals when the FTEs are edited +function updateDisplayandTotals(){ + // get row + const row = document.querySelector('.active-editing'); + // fetch values for calculations + let avg_salary = getCellValue(row, 'avg-salary'); + let baseline_ftes = getCellValue(row, 'baseline-ftes'); + let supp_ftes = getCellValue(row, 'supp-ftes'); + + // calcuate #FTEs x average salary + COLA adjustments + merit adjustments + fringe + let total_baseline_cost = calculateTotalCost(baseline_ftes, avg_salary, fringe, cola, merit); + let total_supp_cost = calculateTotalCost(supp_ftes, avg_salary, fringe, cola, merit); + + // update counters + incrementSidebarStat('baseline-personnel', total_baseline_cost); + incrementSidebarStat('supp-personnel', total_supp_cost); + + // update totals in table + updateTableCell(row, 'total-baseline', total_baseline_cost); + updateTableCell(row, 'total-supp', total_supp_cost); +} diff --git a/js/pages/04_personnel/main.js b/js/pages/04_personnel/main.js index 54e9d6e..c067081 100644 --- a/js/pages/04_personnel/main.js +++ b/js/pages/04_personnel/main.js @@ -1,47 +1,12 @@ -document.addEventListener('DOMContentLoaded', function () { - // // Load from last local storage - // loadTableData("employeeTableData"); - - // // Add an event listener for the save button - // document.getElementById('save').addEventListener('click', function() { - // saveTableData("employeeTableData"); - // }); - - // // Add an event listener for the download button - // document.getElementById('XLSX-download').addEventListener('click', function() { - // saveTableData("employee-table"); - // downloadTableAsExcel('employeeTableData', 'Personnel', 'table-export'); - // }); - - // Mark row to be edited on edit button click - var editButtons = document.getElementsByClassName('btn-edit'); - for (var i = 0; i < editButtons.length; i++) { - editButtons[i].addEventListener('click', handleAccountEdit); - }; - // Remove edit marker when finished - document.getElementById('modal-close-x').addEventListener('click', exitAccountEditModal); - document.getElementById('modal-done-btn').addEventListener('click', exitAccountEditModal); - - // Update account string based on info in modal dropdowns - document.getElementById('dropdown-fund').addEventListener("change", function(event){ - updateAccountString('dropdown-fund', 'fund-string'); - }); - document.getElementById('dropdown-approp').addEventListener("change", function(event){ - updateAccountString('dropdown-approp', 'approp-string'); - }); - document.getElementById('dropdown-cc').addEventListener("change", function(event){ - updateAccountString('dropdown-cc', 'cc-string'); - }); - - // Make FTEs editable - applyEditableCells('.ftes', 'value', null, updateDisplayandTotals, validateNumber) - - // Initialize continue button - document.getElementById('continue-btn').addEventListener('click', continueToNonPersonnel); - -}); +import { updatePageState } from "../../utils/storage-handlers.js"; +import { preparePageView, initializePersonnelTable } from "./helpers.js"; +export function loadPersonnelPage(){ + updatePageState('personnel'); + preparePageView(); + initializePersonnelTable(); +} diff --git a/js/pages/04_personnel/main_archived.js b/js/pages/04_personnel/main_archived.js new file mode 100644 index 0000000..54e9d6e --- /dev/null +++ b/js/pages/04_personnel/main_archived.js @@ -0,0 +1,47 @@ +document.addEventListener('DOMContentLoaded', function () { + + // // Load from last local storage + // loadTableData("employeeTableData"); + + // // Add an event listener for the save button + // document.getElementById('save').addEventListener('click', function() { + // saveTableData("employeeTableData"); + // }); + + // // Add an event listener for the download button + // document.getElementById('XLSX-download').addEventListener('click', function() { + // saveTableData("employee-table"); + // downloadTableAsExcel('employeeTableData', 'Personnel', 'table-export'); + // }); + + // Mark row to be edited on edit button click + var editButtons = document.getElementsByClassName('btn-edit'); + for (var i = 0; i < editButtons.length; i++) { + editButtons[i].addEventListener('click', handleAccountEdit); + }; + // Remove edit marker when finished + document.getElementById('modal-close-x').addEventListener('click', exitAccountEditModal); + document.getElementById('modal-done-btn').addEventListener('click', exitAccountEditModal); + + // Update account string based on info in modal dropdowns + document.getElementById('dropdown-fund').addEventListener("change", function(event){ + updateAccountString('dropdown-fund', 'fund-string'); + }); + document.getElementById('dropdown-approp').addEventListener("change", function(event){ + updateAccountString('dropdown-approp', 'approp-string'); + }); + document.getElementById('dropdown-cc').addEventListener("change", function(event){ + updateAccountString('dropdown-cc', 'cc-string'); + }); + + // Make FTEs editable + applyEditableCells('.ftes', 'value', null, updateDisplayandTotals, validateNumber) + + // Initialize continue button + document.getElementById('continue-btn').addEventListener('click', continueToNonPersonnel); + +}); + + + + diff --git a/js/pages/04_personnel/rollup-helpers.js b/js/pages/04_personnel/rollup-helpers.js index 9bd63f3..931e08f 100644 --- a/js/pages/04_personnel/rollup-helpers.js +++ b/js/pages/04_personnel/rollup-helpers.js @@ -27,50 +27,6 @@ function exitAccountEditModal() { document.getElementById('editing').removeAttribute('id'); } -// update sidebar and also cost totals when the FTEs are edited -function updateDisplayandTotals(){ - // reset to sum all - personnel_baseline = 0; - personnel_supp = 0; - // calculate for each row - let rows = document.getElementsByTagName('tr'); - for (let i = 1; i < rows.length; i++){ - // get all the right values - let avg_salary = rows[i].querySelector('.salary').getAttribute('value'); - let baseline_ftes_cell = rows[i].querySelector('.ftes-baseline'); - let baseline_ftes = baseline_ftes_cell.getAttribute('value'); - let supp_ftes_cell = rows[i].querySelector('.ftes-supp'); - let supp_ftes = supp_ftes_cell.getAttribute('value'); - // calcuate #FTEs x average salary + COLA adjustments + merit adjustments + fringe - total_baseline_cost = baseline_ftes * avg_salary * (1 + fringe) * (1 + cola) * (1 + merit); - total_supp_cost = supp_ftes * avg_salary * (1 + fringe) * (1 + cola) * (1 + merit); - - // update counters - personnel_baseline += total_baseline_cost; - personnel_supp += total_supp_cost; - - // update totals in table - rows[i].querySelector('.calculated-total-supp').textContent = formatCurrency(total_supp_cost); - rows[i].querySelector('.calculated-total-baseline').textContent = formatCurrency(total_baseline_cost); - - // actions if there is an update - if ((baseline_ftes + supp_ftes) > 0){ - // make ? icon visible - let buttons = rows[i].querySelectorAll('.icon-button.btn-see-calcs'); - buttons.forEach(function(button) { - button.style.display = "block"; - }); - // update colors if relevant - if (baseline_ftes > 0){ - baseline_ftes_cell.classList.add("keep"); - } - if (supp_ftes > 0){ - supp_ftes_cell.classList.add("supp"); - } - } - } - updateDisplay(); -} // check if all service boxes are filled function validateServiceSelections(){ diff --git a/js/utils/data-handlers.js b/js/utils/data-handlers.js new file mode 100644 index 0000000..4d00f1f --- /dev/null +++ b/js/utils/data-handlers.js @@ -0,0 +1,47 @@ +export function loadJSONIntoTable(jsonFilePath, tableId) { + return fetch(jsonFilePath) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if(Array.isArray(data)) { + const table = document.getElementById(tableId); + const thead = table.querySelector('thead'); + const tbody = table.querySelector('tbody'); + + // Clear any existing content + thead.innerHTML = ''; + tbody.innerHTML = ''; + + // Create table header row + const headerRow = document.createElement('tr'); + Object.keys(data[0]).forEach(key => { + const header = document.createElement('th'); + header.textContent = key; + headerRow.appendChild(header); + }); + thead.appendChild(headerRow); + + // Create table body rows + data.forEach(item => { + const row = document.createElement('tr'); + Object.values(item).forEach(val => { + const cell = document.createElement('td'); + cell.textContent = val; + row.appendChild(cell); + }); + tbody.appendChild(row); + }); + + } else { + console.error('The provided JSON file does not contain an array of objects.'); + } + }) + .catch(error => { + console.error('Failed to load and parse the JSON file:', error); + }); + } + \ No newline at end of file diff --git a/js/utils/storage-handlers.js b/js/utils/storage-handlers.js index d427424..c5587e8 100644 --- a/js/utils/storage-handlers.js +++ b/js/utils/storage-handlers.js @@ -82,4 +82,15 @@ function loadCounters(){ personnel_baseline = parseInt(localStorage.getItem('personnel_baseline'), 10); personnel_supp = parseInt(localStorage.getItem('personnel_supp'), 10); updateDisplay(); +} + +// save page state +export function updatePageState(page){ + localStorage.setItem('page_state', page); +} + +// load page state +export function loadPageState(page){ + const pageState = localStorage.getItem('page_state'); + return pageState !== null ? pageState : 'welcome'; } \ No newline at end of file diff --git a/js/utils/utils.js b/js/utils/utils.js index 2bde4c7..6c05a7e 100644 --- a/js/utils/utils.js +++ b/js/utils/utils.js @@ -1,5 +1,5 @@ // Function to format number as currency -const formatCurrency = (amount) => { +export const formatCurrency = (amount, return_zero = false) => { var amount = parseFloat(amount); if (amount == NaN){ return "$ -" @@ -7,6 +7,9 @@ const formatCurrency = (amount) => { if (amount < 0){ return '($' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,') + ')'; } else if (amount == 0) { + if (return_zero){ + return '$0'; + } return "$ -" } return '$' + amount.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); @@ -19,45 +22,12 @@ const unformatCurrency = (formattedAmount) => { return parseFloat(numericalPart); }; - -// Function to update the display of the current and supp variables -function updateDisplay() { - // document.getElementById('target').textContent = formatCurrency(target); - // update and format sidebar values from variables - document.getElementById('personnel-baseline').textContent = formatCurrency(personnel_baseline); - document.getElementById('personnel-supp').textContent = formatCurrency(personnel_supp); - document.getElementById('nonpersonnel-baseline').textContent = formatCurrency(nonpersonnel_baseline); - document.getElementById('nonpersonnel-supp').textContent = formatCurrency(nonpersonnel_supp); - // update bottom lines - supp_total = -supp_revenue + personnel_supp + nonpersonnel_supp; - baseline_total = -baseline_revenue + personnel_baseline + nonpersonnel_baseline; - document.getElementById('baseline-total').textContent = formatCurrency(baseline_total); - document.getElementById('supp-total').textContent = formatCurrency(supp_total); - if(baseline_total <= target){ - document.getElementById('baseline-total').style.color = "green"; - } - if(baseline_total > target){ - document.getElementById('baseline-total').style.color = "red"; - } -} - /** * 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. - * - * @param {HTMLElement} cell - The DOM element representing the cell to be made editable. - * @param {string} attribute - The attribute name of the cell where the value will be stored. - * @param {function} [formatValueCallback] - Optional. A function to format the value - * before displaying it in the cell. The function must accept a string and return - * a formatted string. - * @param {function} [updateCallback] - Optional. A function to be called after the cell - * value has been updated. Use this to trigger any additional side effects or updates - * to related data or UI elements. - * @param {function} [validate] - Optional. A function to validate input and return an error - * message if relevant. */ function createEditableCell(cell, attribute = 'value', formatValueCallback, updateCallback, validate) { // Add a click event to the cell to make it editable @@ -141,4 +111,5 @@ function validateNumber(input){ return "Field only accepts numbers"; }; return ""; -} \ No newline at end of file +} + diff --git a/pages/02_new_initiatives.html b/pages/02_new_initiatives.html deleted file mode 100644 index b0fdcb8..0000000 --- a/pages/02_new_initiatives.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - -Demo Budget Form - - - - - - - - - - - - - - - - - - - - -

FY2026 Budget Form

-

New Initiatives

- -
-
-
- -
- - -
-

Do you have any new initiatives for FY26?

-
- - - - -
- - -
-
- - - - - - - - -
- Initiative name - - Explanation - - Request estimate ($) -
-
- - - - -
-
- - - - - - \ No newline at end of file diff --git a/pages/03_revenue.html b/pages/03_revenue.html deleted file mode 100644 index 4637b4f..0000000 --- a/pages/03_revenue.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - -Demo Budget Form - - - - - - - - - - - - - - - - - - - -

FY2026 Budget Form

-

Revenue

- -
-
-
- -
- - -
-

Your revenue projection for FY26 is - $0 -

-
- - - - -
- -
- - - \ No newline at end of file diff --git a/pages/04.5_overtime.html b/pages/04.5_overtime.html deleted file mode 100644 index 34a49b7..0000000 --- a/pages/04.5_overtime.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - -Demo Budget Form - - - - - - - - - - - - - - - - - - - - - - - - - -

FY2026 Budget Form

- -
-
- -
- - -
- -

Select an action item for each non-personnel line item in your department.

- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
VendorAccount StringAppropriationCost CenterFY25 SalaryAction
Jane DoeAssistant Director100012345654321$100,000 -
- - - -
-
John DoeTASS III100012345654321$80,000 -
- - - -
-
Jane DoeAnalyst100012345654321$60,000 -
- - - -
-
VacantAdministrative Assistant100012345654321$50,000 -
- - - -
-
VacantAnalyst II100012345654321$80,000 -
- - - -
-
-
-
-
- - -
-
-
- -
-
- - - -
-
-
- -
- - - -
-
- -
- - - \ No newline at end of file diff --git a/pages/04_personnel.html b/pages/04_personnel.html deleted file mode 100644 index 6e60e2b..0000000 --- a/pages/04_personnel.html +++ /dev/null @@ -1,335 +0,0 @@ - - - - - -Demo Budget Form - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

FY2026 Budget Form

- -
-
- -
- - -
- -

For each job in your department, select the service and request the number - of baseline and supplemental FTEs.

- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Job NameServiceCurrent FTEs (FY25)Baseline FTEsSupplemental FTEsAverage FY25 SalaryTotal Cost (Baseline) - Total Cost (Supplementary)
Deputy Counsel - 1000-29320-320010 - - - - 100$150,000 - $ - - - - $ - - -
Legal Secretary - 1000-29320-320010 - - - - 500$55,000 - $ - - - - $ - - -
Assistant Counsel - 1000-29320-320010 - - - - 1000$80,000 - $ - - - - $ - - -
-
-
-
- - -
-
- -
- - -
-
-
-
- - - -
-
- -
- - - - - - - - - - - \ No newline at end of file