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 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
-
-
-
-
-
-
-
-
-
\ 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.
-
-
-
-
-
-
-
-
- Vendor |
- Account String |
- Appropriation |
- Cost Center |
- FY25 Salary |
- Action |
-
-
-
-
-
-
- Jane Doe |
- Assistant Director |
- 1000 |
- 12345 |
- 654321 |
- $100,000 |
-
-
-
-
-
-
- |
-
-
-
- John Doe |
- TASS III |
- 1000 |
- 12345 |
- 654321 |
- $80,000 |
-
-
-
-
-
-
- |
-
-
-
- Jane Doe |
- Analyst |
- 1000 |
- 12345 |
- 654321 |
- $60,000 |
-
-
-
-
-
-
- |
-
-
-
-
- Vacant |
- Administrative Assistant |
- 1000 |
- 12345 |
- 654321 |
- $50,000 |
-
-
-
-
-
-
- |
-
-
-
- Vacant |
- Analyst II |
- 1000 |
- 12345 |
- 654321 |
- $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 Name |
- Acount String |
- Service |
- Current FTEs (FY25) |
- Baseline FTEs |
- Supplemental FTEs |
- Average FY25 Salary |
- Total Cost (Baseline)
- |
- Total Cost (Supplementary) |
-
-
-
-
-
-
-
- Deputy Counsel |
-
- 1000-29320-320010
-
- |
-
-
- |
- 1 |
- 0 |
- 0 |
- $150,000 |
-
- $ -
-
- |
-
- $ -
-
- |
-
-
-
-
-
-
- Legal Secretary |
-
- 1000-29320-320010
-
- |
-
-
- |
- 5 |
- 0 |
- 0 |
- $55,000 |
-
- $ -
-
- |
-
- $ -
-
- |
-
-
-
-
-
-
-
- Assistant Counsel |
-
- 1000-29320-320010
-
- |
-
-
- |
- 10 |
- 0 |
- 0 |
- $80,000 |
-
- $ -
-
- |
-
- $ -
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fund:
-
-
-
-
- Appropriation:
-
-
-
-
- Cost Center:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Total cost = FTEs × average base salary × (1 + cost of living adjustment rate) × (1 + step up/merit increase rate) × (1 + fringe benefit rate)
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
|