diff --git a/src/js/components/form/form.css b/src/js/components/form/form.css
index 6dd8620..eeecba2 100644
--- a/src/js/components/form/form.css
+++ b/src/js/components/form/form.css
@@ -13,10 +13,10 @@ textarea, input {
#new-form label {
display: block; /* Ensure label is on its own line */
- margin-bottom: 0.5em;
+ margin-bottom: 0.25em;
}
#new-form select {
- margin: auto;
- width: 300px;
+ min-width: 300px;
+ margin-bottom: 1.25em;
}
\ No newline at end of file
diff --git a/src/js/components/form/subcomponents/dropdown.js b/src/js/components/form/subcomponents/dropdown.js
index 90ddf14..411f70f 100644
--- a/src/js/components/form/subcomponents/dropdown.js
+++ b/src/js/components/form/subcomponents/dropdown.js
@@ -1,11 +1,3 @@
-// async function createDropdownFromJSON(json_path) {
-// // Fetch JSON data from a file asynchronously
-// const response = await fetch(json_path);
-// const dataArray = await response.json();
-// // create and return element
-// return createDropdown(dataArray);
-// }
-
function createDropdown(dataArray) {
// Creating a select element
@@ -25,7 +17,6 @@ function createDropdown(dataArray) {
export const Dropdown = {
- // createFromJSON : function(json_path){ return createDropdownFromJSON(json_path) },
create : function(dataArray) { return createDropdown(dataArray) },
}
diff --git a/src/js/components/table/subcomponents/columns.js b/src/js/components/table/subcomponents/columns.js
index d6caaab..f641642 100644
--- a/src/js/components/table/subcomponents/columns.js
+++ b/src/js/components/table/subcomponents/columns.js
@@ -50,20 +50,18 @@ function assignClassToColumn(headerName, className) {
// 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) {
+ // assign the class to the header cell
+ headerCells[i].classList.add(className);
headerCellIndex = i;
break;
}
}
+ // error check
if (headerCellIndex === -1) {
console.error(`No header found with name "${headerName}"`);
return;
@@ -85,7 +83,7 @@ function addCostClass(headerName){
assignClassToColumn( headerName, 'cost');
// Get all the cells with the specified class name
- const cells = document.querySelectorAll(`.cost`);
+ const cells = document.querySelectorAll(`td.cost`);
cells.forEach(cell => {
// Get the current text content of the cell and assign it to 'value' attribute
diff --git a/src/js/components/table/subcomponents/data.js b/src/js/components/table/subcomponents/data.js
index 809ac0c..67c4b23 100644
--- a/src/js/components/table/subcomponents/data.js
+++ b/src/js/components/table/subcomponents/data.js
@@ -24,9 +24,9 @@ function fillTable(data) {
data.forEach(item => {
const row = document.createElement('tr');
Object.values(item).forEach(val => {
- const cell = document.createElement('td');
- cell.innerHTML = val;
- row.appendChild(cell);
+ const cell = document.createElement('td');
+ cell.innerHTML = val;
+ row.appendChild(cell);
});
tbody.appendChild(row);
});
diff --git a/src/js/components/table/subcomponents/headers.js b/src/js/components/table/subcomponents/headers.js
index 1304c8f..233f0af 100644
--- a/src/js/components/table/subcomponents/headers.js
+++ b/src/js/components/table/subcomponents/headers.js
@@ -1,21 +1,21 @@
-function addTableHeaders(header_array){
+function addTableHeaders(cols){
// Get the table element by its ID
const table = document.getElementById('main-table');
// Create a table header row element
const headerRow = document.createElement('tr');
-
- for (const headerText of header_array) {
+ cols.forEach(col => {
// Create a header cell element
const headerCell = document.createElement('th');
- headerCell.textContent = headerText;
+ headerCell.textContent = col['title'];
+ headerCell.classList.add(col['className']);
// 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);
diff --git a/src/js/components/table/subcomponents/rows.js b/src/js/components/table/subcomponents/rows.js
index b94861b..512bdb6 100644
--- a/src/js/components/table/subcomponents/rows.js
+++ b/src/js/components/table/subcomponents/rows.js
@@ -1,26 +1,36 @@
import Header from "./headers.js";
import { formatCurrency } from "../../../utils/common_utils.js";
-async function addNewRow(data_dictionary){
+async function addNewRow(data_dictionary, columns = []){
+
// Get the table element by its ID
const table = document.getElementById('main-table');
// check if header has already been added
let header_row = table.querySelector('thead tr');
if (!header_row) {
- Header.add(Object.keys(data_dictionary));
+ Header.add(columns);
+ header_row = table.querySelector('thead tr');
}
- // add row of data
+ // initialize new 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) {
+ // go through each header and add the right cell value depending on its class
+ let thElements = header_row.querySelectorAll('th');
+ thElements.forEach( (header_cell) => {
// Create new cell and add it to the row
const newCell = document.createElement('td');
- newCell.textContent = cell_data;
new_row.appendChild(newCell);
- }
+ // if the data has an appropriate class, add the info to the cell.
+ // Otherwise, keep empty cell
+ Object.keys(data_dictionary).forEach( (className) => {
+ if (header_cell.classList.contains(className) ){
+ newCell.textContent = data_dictionary[className];
+ newCell.classList.add(className);
+ }
+ });
+ });
// Append the new row to the table body
let tbody = table.querySelector('tbody');
@@ -53,8 +63,8 @@ function saveRowEdits(row){
}
const Rows = {
- add : function(data_dictionary){
- addNewRow(data_dictionary)
+ add : function(data_dictionary, cols){
+ addNewRow(data_dictionary, cols)
},
saveEdits : function(row){
saveRowEdits(row)
diff --git a/src/js/components/table/table.css b/src/js/components/table/table.css
index 5f2f784..27fdae9 100644
--- a/src/js/components/table/table.css
+++ b/src/js/components/table/table.css
@@ -1,5 +1,5 @@
#main-table {
- font-size: calc(0.6vw + 0.5em);
+ font-size: calc(0.5vw + 0.5em);
margin: auto;
/* width: 100%; */
}
@@ -8,6 +8,9 @@
text-align: left;
background-color: var(--darkGray);
color: white;
+ position: -webkit-sticky; /* For Safari */
+ position: sticky;
+ top: 0;
}
th {
diff --git a/src/js/components/table/table.js b/src/js/components/table/table.js
index e9f42f8..94f29dd 100644
--- a/src/js/components/table/table.js
+++ b/src/js/components/table/table.js
@@ -7,6 +7,7 @@ import Header from './subcomponents/headers.js'
import Rows from './subcomponents/rows.js'
import Data from './subcomponents/data.js'
import { saveTableData } from '../../utils/data_utils/local_storage_handlers.js'
+import Tooltip from '../tooltip/tooltip.js';
function adjustTableWidth(width_pct){
const table = document.getElementById('main-table');
@@ -44,7 +45,13 @@ const Table = {
clear : clearTable,
hide : hideTable,
show : showTable,
- save : saveTableData
+ save : async function() {
+ // remove the detail text
+ Tooltip.unlink();
+ saveTableData();
+ // relink, depending on page
+ Tooltip.linkAll();
+ }
}
export default Table;
\ No newline at end of file
diff --git a/src/js/components/tooltip/tooltip.js b/src/js/components/tooltip/tooltip.js
index 2406cb3..c86dd88 100644
--- a/src/js/components/tooltip/tooltip.js
+++ b/src/js/components/tooltip/tooltip.js
@@ -2,6 +2,7 @@ import { FISCAL_YEAR } from '../../init';
import Cell from '../table/subcomponents/cells';
import { formatCurrency } from '../../utils/common_utils';
import './tooltip.css'
+import { CurrentFund, CurrentPage } from '../../utils/data_utils/local_storage_handlers';
function hideTooltip() {
document.getElementById('tooltip').style.visibility = 'hidden';
@@ -21,8 +22,11 @@ function showAccountString(row){
const approp = Cell.getText(row, 'approp-name');
const cc = Cell.getText(row, 'cc-name');
const obj = Cell.getText(row, 'object-name');
- var message = `Appropriation: ${approp}
- Cost Center: ${cc}`;
+ const fund = Cell.getText(row, 'fund-name');
+ var message =
+ `Fund: ${fund}
+ Appropriation: ${approp}
+ Cost Center: ${cc}`;
if (obj) { message += ` Object: ${obj}`}
editTooltipText(message);
}
@@ -34,16 +38,15 @@ function showSalaryProjection(row){
const proj_salary = Cell.getValue(row, 'avg-salary');
if (current_salary){
var message = `The average salary/wage for this position was
- ${formatCurrency(current_salary)} as of September 20${FISCAL_YEAR-2}. With two general
- increases of ${general_increase*100}% and a merit increase of ${merit_increase*100}%, the
- Budget Office projects that the average annual
- salary/wage for this position will be ${formatCurrency(proj_salary)} in FY${FISCAL_YEAR}.`;
+ ${formatCurrency(current_salary)} as of September 20${FISCAL_YEAR-2}.
+ Given a ${general_increase*100}% general increase rate and a ${merit_increase*100}%
+ merit increase, the FY${FISCAL_YEAR} projection for this position's average
+ annual salary/wage is ${formatCurrency(proj_salary)}.`;
} else {
var message = `The average salary/wage for this position was
unknown as of September 20${FISCAL_YEAR-2}, or the position
- did not exist. The Budget Office projects that
- the average annual salary/wage for this position
- will be ${formatCurrency(proj_salary)} in FY2026.`
+ did not exist. The FY${FISCAL_YEAR} projection for this position's
+ average annual salary/wage is ${formatCurrency(proj_salary)}.`
}
editTooltipText(message);
@@ -64,7 +67,7 @@ function showFinalPersonnelCost(row){
function showFICA(row){
const fica = parseFloat(Cell.getText(row, 'fica'));
const ficaPercentage = (fica * 100).toFixed(2);
- const message = `This total is overtime wages plus overtime salary plus FICA,
+ const message = `This total is overtime wages plus overtime salary plus FICA (payroll tax),
which is ${ficaPercentage}% for this cost center.`
editTooltipText(message);
}
@@ -88,98 +91,106 @@ function showCPA(row){
editTooltipText(message);
}
-export const Tooltip = {
-
- hide : hideTooltip,
- show : showTooltip,
+function link(element, displayFn) {
+
+ // add class to show cell with an underline, etc
+ element.classList.add('tooltip-cell');
+
+ // Create and append (detail)
+ const detail = document.createElement('span');
+ detail.classList.add('detail');
+ detail.textContent = '(detail)';
+ element.appendChild(detail);
+
+ // add event listener to show tooltip on mouseover
+ element.addEventListener('click', function (event) {
+ const row = event.target.closest('tr');
+ displayFn(row);
+ showTooltip();
+ });
+ // and hide when mouse moves off
+ element.addEventListener('mouseout', function () {
+ hideTooltip();
+ });
+ // Update tooltip position on mouse move
+ element.addEventListener('mousemove', function (event) {
+ const tooltip = document.getElementById('tooltip');
+ tooltip.style.top = (event.clientY + 10) + 'px';
+ tooltip.style.left = (event.clientX + 10) + 'px';
+ });
+}
- link : function(element, displayFn) {
-
- // add class to show cell with an underline, etc
- element.classList.add('tooltip-cell');
-
- // Create and append the Font Awesome info icon
- // const infoIcon = document.createElement('i');
- // infoIcon.classList.add('fas', 'fa-info-circle', 'info-icon');
- // element.appendChild(infoIcon);
-
- // Create and append (detail)
- const detail = document.createElement('span');
- detail.classList.add('detail');
- detail.textContent = '(detail)';
- element.appendChild(detail);
-
- // add event listener to show tooltip on mouseover
- element.addEventListener('click', function (event) {
- const row = event.target.closest('tr');
- displayFn(row);
- showTooltip();
- });
- // and hide when mouse moves off
- element.addEventListener('mouseout', function () {
- hideTooltip();
- });
- // Update tooltip position on mouse move
- element.addEventListener('mousemove', function (event) {
- const tooltip = document.getElementById('tooltip');
- tooltip.style.top = (event.clientY + 10) + 'px';
- tooltip.style.left = (event.clientX + 10) + 'px';
- });
- },
-
- linkAccountStringCol : function() {
- // get all relevant cells
- document.querySelectorAll('.account-string').forEach( (cell) => {
- this.link(cell, showAccountString);
- })
- },
+function linkAccountStringCol() {
+ // get all relevant cells
+ document.querySelectorAll('td.account-string').forEach( (cell) => {
+ link(cell, showAccountString);
+ })
+}
- linkSalaryCol : function() {
- // get all relevant cells
- document.querySelectorAll('.avg-salary').forEach( (cell) => {
- this.link(cell, showSalaryProjection);
- })
- },
+function linkSalaryCol() {
+ // get all relevant cells
+ document.querySelectorAll('td.avg-salary').forEach( (cell) => {
+ link(cell, showSalaryProjection);
+ })
+}
- linkTotalPersonnelCostCol : function() {
- // get all relevant cells
- document.querySelectorAll('.total-baseline').forEach( (cell) => {
- this.link(cell, showFinalPersonnelCost);
- })
- },
+function linkTotalPersonnelCostCol() {
+ // get all relevant cells
+ document.querySelectorAll('td.total-baseline').forEach( (cell) => {
+ link(cell, showFinalPersonnelCost);
+ })
+}
- linkTotalOTCol : function() {
- // get all relevant cells
- document.querySelectorAll('.total').forEach( (cell) => {
- this.link(cell, showFICA);
- })
- },
+function linkTotalOTCol() {
+ // get all relevant cells
+ document.querySelectorAll('td.total').forEach( (cell) => {
+ link(cell, showFICA);
+ })
+}
- linkCPACol : function() {
- // get all relevant cells
- document.querySelectorAll('.cpa').forEach( (cell) => {
- this.link(cell, showCPA);
- })
- },
+function linkCPACol() {
+ // get all relevant cells
+ document.querySelectorAll('td.cpa').forEach( (cell) => {
+ link(cell, showCPA);
+ })
+}
- linkAllPersonnel : function() {
- this.linkAccountStringCol();
- this.linkSalaryCol();
- this.linkTotalPersonnelCostCol();
- },
+export const Tooltip = {
- linkAllOvertime : function() {
- // this.linkAccountStringCol();
- this.linkTotalOTCol();
- },
+ hide : hideTooltip,
+ show : showTooltip,
- linkAllNP : function() {
- this.linkAccountStringCol();
- this.linkCPACol();
+ linkAll : () => {
+ switch(CurrentPage.load()){
+ case 'personnel' :
+ linkAccountStringCol();
+ linkSalaryCol();
+ linkTotalPersonnelCostCol();
+ break;
+ case 'overtime':
+ linkTotalOTCol();
+ break;
+ case 'nonpersonnel':
+ linkAccountStringCol();
+ linkCPACol();
+ break;
+ case 'revenue':
+ linkAccountStringCol();
+ break;
+ case 'new-inits':
+ linkAccountStringCol();
+ break;
+ default:
+ break;
+
+ }
},
- linkAllRevenue : function() {
- this.linkAccountStringCol();
+ unlink : function() {
+ let details = document.querySelectorAll('.detail');
+ details.forEach( (span) => {
+ span.remove();
+ })
}
}
diff --git a/src/js/utils/archive/archived_fns.js b/src/js/utils/archive/archived_fns.js
deleted file mode 100644
index 2450097..0000000
--- a/src/js/utils/archive/archived_fns.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * Transforms a specified cell into an editable element by attaching an input field.
- * Once the editing is committed, the new value is saved in the specified attribute
- * of the element and passed through an optional formatting function before being
- * displayed in the cell. An optional callback can be triggered after the update
- * to perform additional actions.
- */
-function createEditableCell(cell, attribute = 'value', formatValueCallback, updateCallback, validate) {
- // Add a click event to the cell to make it editable
- cell.onclick = function() {
- // Fetch the current attribute value of the cell or fall back to an empty string
- var currentValue = cell.getAttribute(attribute) || '';
- // Create an input element to edit the value
- var textbox = document.createElement('input');
- textbox.type = 'text';
- textbox.value = currentValue;
- var feedback = document.createElement('p');
- feedback.style.color = "red";
-
- // Function to commit the textbox value and restore static text
- function commitAndRestoreText() {
- // Retrieve the entered value
- var enteredValue = textbox.value;
- // Set the attribute to the entered value
- cell.setAttribute(attribute, enteredValue);
-
- // validate text against validation criteria
- let feedback_text = '';
- if (validate){
- feedback_text = validate(enteredValue);
- }
-
- // if there's an error, show it
- if (feedback_text){
- feedback.textContent = feedback_text;
- // otherwise, proceed
- } else {
- // Format and set the cell's text content
- cell.textContent = formatValueCallback ? formatValueCallback(enteredValue) : enteredValue;
- // If there is an update callback provided, call it
- if (updateCallback) {
- updateCallback();
- }
- };
-
- // Reattach the onclick event to allow editing again in the future
- cell.onclick = function() {
- createEditableCell(cell, attribute, formatValueCallback, updateCallback, validate);
- };
- }
-
- // When the textbox loses focus, commit its value
- textbox.onblur = commitAndRestoreText;
- // When the user presses the 'Enter' key, commit the value and blur the textbox
- textbox.onkeydown = function(event) {
- if (event.key === 'Enter') {
- commitAndRestoreText();
- textbox.blur();
- }
- };
-
- // Clear the current content and append the textbox to the cell
- cell.innerHTML = '';
- cell.appendChild(textbox);
- cell.appendChild(feedback);
- // Temporarily remove the onclick event handler to prevent re-triggering during edit
- cell.onclick = null;
-
- // Focus on the textbox to start editing
- textbox.focus();
- }
-}
-
-// Function to apply createEditableCell to all cells matching a given selector
-function applyEditableCells(selector, attribute = 'value', formatValueCallback, updateCallback, validate) {
- // Select all elements that match the provided selector
- var cells = document.querySelectorAll(selector);
- // Iterate over each cell and make it editable
- cells.forEach(function(cell) {
- createEditableCell(cell, attribute, formatValueCallback, updateCallback, validate);
- });
-}
-
-function validateNumber(input){
- var number = parseFloat(input);
- if (isNaN(number)){
- return "Field only accepts numbers";
- };
- return "";
-}
-
diff --git a/src/js/utils/common_utils.js b/src/js/utils/common_utils.js
index eb154cf..814fadf 100644
--- a/src/js/utils/common_utils.js
+++ b/src/js/utils/common_utils.js
@@ -17,6 +17,7 @@ export const formatCurrency = (amount, return_zero = false) => {
// function to convert formatted number to a float
export const unformatCurrency = (formattedAmount) => {
+ if (!formattedAmount) { return 0 };
// Remove any currency symbols and commas
let numericalPart = formattedAmount.replace(/[^0-9.-]+/g, "");
if (numericalPart == '-'){
diff --git a/src/js/utils/data_utils/XLSX_handlers.js b/src/js/utils/data_utils/XLSX_handlers.js
index 1bb8dae..1b16ec3 100644
--- a/src/js/utils/data_utils/XLSX_handlers.js
+++ b/src/js/utils/data_utils/XLSX_handlers.js
@@ -149,7 +149,7 @@ export function downloadXLSX() {
// Create a link and trigger the download
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
- link.download = "baseline_data.xlsx";
+ link.download = "Filled_Detail_Sheet.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
diff --git a/src/js/utils/data_utils/budget_data_handlers.js b/src/js/utils/data_utils/budget_data_handlers.js
index 0a80041..187a046 100644
--- a/src/js/utils/data_utils/budget_data_handlers.js
+++ b/src/js/utils/data_utils/budget_data_handlers.js
@@ -1,3 +1,10 @@
+import { CurrentFund } from "./local_storage_handlers";
+
+function getUniqueValues(data, key) {
+ const values = data.map(obj => obj[key]);
+ return Array.from(new Set(values));
+}
+
export const FundLookupTable = {
retrieve : function() {
return JSON.parse(localStorage.getItem('fund-lookup-table')) || {};
@@ -5,9 +12,12 @@ export const FundLookupTable = {
save : function(fundDict){
localStorage.setItem('fund-lookup-table', JSON.stringify(fundDict));
},
+
update : function(fundData){
const table = this.retrieve();
+
for (let fund of Object.keys(fundData)){
+
// add to lookup table if not in there already
if (!table[fund]){
// get fund name
@@ -16,21 +26,69 @@ export const FundLookupTable = {
table[fund] = {};
table[fund]['name'] = fundName;
table[fund]['viewed'] = false;
+ // build lists of unique cost centers and appropriations
+ table[fund]['approp'] = getUniqueValues(fundData[fund], 'Appropriation Name');
+ table[fund]['cc'] = getUniqueValues(fundData[fund], 'Cost Center Name');
}
}
// save any updates
this.save(table);
},
+
+ getAll: function(key) {
+ // function to aggregate all approps or CCs for every fund in one array
+ const funds = this.retrieve();
+ const ret = [];
+ for (const fund in funds) {
+ if (funds.hasOwnProperty(fund)) {
+ for (let i in funds[fund][key]){
+ ret.push(funds[fund][key][i]);
+ }
+ }
+ }
+ return ret;
+ },
+
+ getCostCenters : function() {
+ // get current fund
+ const fund = CurrentFund.number()
+ if (this.retrieve()[fund]){
+ return this.retrieve()[fund]['cc'];
+ }
+ // if no fund (ie. we're on the new initiative page), return all options
+ return this.getAll('cc');
+ },
+
+ getApprops : function() {
+ // get current fund
+ const fund = CurrentFund.number()
+ if (this.retrieve()[fund]){
+ return this.retrieve()[fund]['approp'];
+ }
+ // if no fund (ie. we're on the new initiative page), return all options
+ return this.getAll('approp');
+ },
+
reset : function() {
this.save({});
},
getName : function(number){
- if(number == '') { return '' };
+ if(!number || !this.retrieve()) { return '' };
return this.retrieve()[number]['name'];
},
listFunds : function(){
return Object.keys(this.retrieve());
},
+ listFundNames : function(){
+ const funds = this.retrieve();
+ // initialize array
+ var ret = [];
+ Object.keys(funds).forEach( (fund_number) => {
+ var fund_name = funds[fund_number]['name'];
+ ret.push(fund_name);
+ });
+ return ret;
+ },
editFund : function(fund){
const table = this.retrieve();
if (table[fund]){
@@ -64,4 +122,58 @@ export const Services = {
list : function(){
return JSON.parse(localStorage.getItem('services-list')) || {};
}
+}
+
+export const ObjectCategories = {
+ list : [
+ // 'Salaries & Wages',
+ // 'Employee Benefits',
+ 'Professional & Contractual Services',
+ 'Operating Supplies',
+ 'Operating Services',
+ 'Equipment Acquisition',
+ 'Capital Outlays',
+ 'Fixed Charges',
+ 'Other Expenses'
+ ]
+}
+
+export const AccountString = {
+ getNumber: function(input) {
+ // isolate the numerical part of a appropriation/cost center/object
+ const match = input.match(/^\d+/);
+ return match ? match[0] : null;
+ },
+
+ build : function(approp, cc, obj = null, fund = null) {
+ // put together account string fund-approp-costcenter[-obj] (w optional object)
+ if (!fund) { fund = CurrentFund.number() };
+ // hits error here
+ approp = this.getNumber(approp);
+ cc = this.getNumber(cc);
+ var string = `${fund}-${approp}-${cc}`;
+ string = obj ? `${string}-${this.getNumber(obj)}` : string;
+ return string;
+ },
+
+ getAccountStringSection : function(account_string, section) {
+ const sections = account_string.split("-");
+ return sections.length > section ? sections[section] : null;
+ },
+
+ fund : function(account_string) {
+ return this.getAccountStringSection(account_string, 0)
+ },
+
+ approp : function(account_string) {
+ return this.getAccountStringSection(account_string, 1)
+ },
+
+ costCenter : function(account_string) {
+ return this.getAccountStringSection(account_string, 2)
+ },
+
+ object : function(account_string) {
+ return this.getAccountStringSection(account_string, 3)
+ },
}
\ No newline at end of file
diff --git a/src/js/utils/data_utils/local_storage_handlers.js b/src/js/utils/data_utils/local_storage_handlers.js
index 4c887ad..dd5e9b1 100644
--- a/src/js/utils/data_utils/local_storage_handlers.js
+++ b/src/js/utils/data_utils/local_storage_handlers.js
@@ -42,6 +42,7 @@ export function saveTableData() {
var save_as = CurrentPage.load();
}
localStorage.setItem(save_as, convertToJSON(table, ['Edit']));
+ console.log('saved');
Sidebar.updateTotals();
}
@@ -62,9 +63,9 @@ export async function deleteAllTables(){
export function loadTableData(name){
const data = localStorage.getItem(name);
- if ( data == '' ) {
- return '';
- }
+ if ( data == '' || data == '[]' ) {
+ return 0;
+ };
return JSON.parse(data);
}
diff --git a/src/js/views/02_baseline_landing_page/helpers.js b/src/js/views/02_baseline_landing_page/helpers.js
index a1d60e4..fc90e45 100644
--- a/src/js/views/02_baseline_landing_page/helpers.js
+++ b/src/js/views/02_baseline_landing_page/helpers.js
@@ -23,7 +23,7 @@ export function preparePageView(){
// TODO: update to make dynamic
Prompt.Text.update(`We will now ask you a series of questions about your BASELINE budget request.
At the end, we will ask you about any new initiatives (ie. supplemental requests).
- Select one of your funds to begin.`);
+ Select one of your funds then click continue.`);
}
function allowRowSelection(){
diff --git a/src/js/views/03_revenue/helpers.js b/src/js/views/03_revenue/helpers.js
index 4c27354..a0f7d88 100644
--- a/src/js/views/03_revenue/helpers.js
+++ b/src/js/views/03_revenue/helpers.js
@@ -51,7 +51,7 @@ export async function initializeRevTable(){
// enable editing
Table.Buttons.Edit.init(revRowOnEdit, Table.save);
// show info boxes on click
- Tooltip.linkAllRevenue();
+ Tooltip.linkAll();
} else {
Prompt.Text.update('No revenues for this fund.')
}
diff --git a/src/js/views/04_personnel/helpers.js b/src/js/views/04_personnel/helpers.js
index 5dc5a59..d63fb8d 100644
--- a/src/js/views/04_personnel/helpers.js
+++ b/src/js/views/04_personnel/helpers.js
@@ -8,9 +8,8 @@ import Modal from "../../components/modal/modal.js";
import Prompt from "../../components/prompt/prompt.js";
import Table from '../../components/table/table.js'
import Sidebar from "../../components/sidebar/sidebar.js";
-import { Services } from "../../utils/data_utils/budget_data_handlers.js";
-import Tooltip from "../../components/tooltip/tooltip.js";
-
+import { AccountString, FundLookupTable, Services } from "../../utils/data_utils/budget_data_handlers.js";
+import { unformatCurrency } from "../../utils/common_utils.js";
export function preparePageView(){
// prepare page view
@@ -20,6 +19,10 @@ export function preparePageView(){
Table.adjustWidth('90%');
NavButtons.Next.enable();
+ // new row button
+ Table.Buttons.AddRow.updateText("Add new job");
+ Table.Buttons.AddRow.show();
+
// update page text
Subtitle.update('Personnel');
Prompt.Text.update(`
@@ -43,7 +46,9 @@ function assignClasses() {
// hidden columns needed for calculations
{ title: 'Fringe Benefits Rate', className: 'fringe', hide: true },
{ title: 'Appropriation Name', className: 'approp-name', hide: true },
+ { title: 'Appropriation', className: 'approp', hide: true },
{ title: 'Cost Center Name', className: 'cc-name', hide: true },
+ { title: 'Cost Center', className: 'cc', hide: true },
{ title: 'General Increase Rate', className: 'general-increase-rate', hide: true},
{ title: 'Step/Merit Increase Rate', className: 'merit-increase-rate', hide: true},
{ title: `Average Salary/Wage as of 9/1/20${FISCAL_YEAR-2}`, className: 'current-salary', isCost: true, hide: true},
@@ -69,19 +74,11 @@ export async function initializePersonnelTable(){
updateDisplayandTotals();
// activate edit buttons
Table.Buttons.Edit.init(personnelRowOnEdit, updateDisplayandTotals);
- initializeRowAddition();
- // Link up tooltips to display more info on hover
- Tooltip.linkAllPersonnel();
} else {
Prompt.Text.update('No personnel expenditures for this fund.')
}
}
-function initializeRowAddition(){
- Table.Buttons.AddRow.updateText("Add new job");
- Table.Buttons.AddRow.show();
-}
-
// update sidebar and also cost totals when the FTEs are edited
function updateDisplayandTotals(){
// calculate for each row
@@ -114,8 +111,13 @@ export function setUpModal() {
export function setUpForm() {
// Set up form
Form.new('modal-body');
- Form.NewField.shortText('Job Name:', 'job-name', true);
- Form.NewField.shortText('Account String:', 'account-string', true);
+ Form.NewField.shortText('Job Title:', 'job-name', true);
+ Form.NewField.dropdown('Appropriation:', 'approp-name', FundLookupTable.getApprops(), true);
+ Form.NewField.dropdown('Cost Center:', 'cc-name', FundLookupTable.getCostCenters(), true);
+ Form.NewField.dropdown('Service', 'service', Services.list(), true);
+ Form.NewField.shortText('Number of FTEs requested:', 'baseline-ftes', true);
+ Form.NewField.shortText(`Projected average salary IN FISCAL YEAR ${FISCAL_YEAR}:`, 'avg-salary', true);
+ Form.NewField.shortText(`Expected fringe rate (as a percentage)`, 'fringe', true);
Form.SubmitButton.add();
// Initialize form submission to table data
Modal.Submit.init(handleSubmitNewJob);
@@ -124,17 +126,21 @@ export function setUpForm() {
function handleSubmitNewJob(event){
// get answers from form, hide form, show answers in table
const responses = Form.fetchAllResponses(event);
+ // edit inputs from modal
+ responses['avg-salary'] = unformatCurrency(responses['avg-salary']);
+ responses['fringe'] = parseFloat(responses['fringe']) / 100;
+ responses['account-string'] = AccountString.build(responses['approp-name'], responses['cc-name'])
+ responses['approp'] = AccountString.getNumber(responses['approp-name']);
+ responses['cc'] = AccountString.getNumber(responses['cc-name']);
// make sure it's not an empty response
if (Object.values(responses)[0] != ''){
// change page view
Modal.hide();
-
// add data to table
Table.Rows.add(responses);
- Table.show();
- Table.Buttons.AddRow.show();
- // TODO: save table data
- // TODO: edit cost to show currency correctly
- }
+ Table.save();
+ initializePersonnelTable();
+
+ }
}
diff --git a/src/js/views/05_overtime/helpers.js b/src/js/views/05_overtime/helpers.js
index f1874d4..189878b 100644
--- a/src/js/views/05_overtime/helpers.js
+++ b/src/js/views/05_overtime/helpers.js
@@ -5,8 +5,12 @@ import NavButtons from '../../components/nav_buttons/nav_buttons.js';
import Subtitle from '../../components/header/header.js';
import Sidebar from '../../components/sidebar/sidebar.js';
import Table from '../../components/table/table.js';
-import { Services } from '../../utils/data_utils/budget_data_handlers.js';
-import Tooltip from '../../components/tooltip/tooltip.js';
+import { AccountString, Services } from '../../utils/data_utils/budget_data_handlers.js';
+import Modal from '../../components/modal/modal.js';
+import Form from '../../components/form/form.js';
+import { unformatCurrency } from '../../utils/common_utils.js';
+import { FundLookupTable } from "../../utils/data_utils/budget_data_handlers.js";
+
export function preparePageView(){
// prepare page view
@@ -24,6 +28,21 @@ export function preparePageView(){
initializeOTTable();
Prompt.Text.update(`Please see your baseline overtime / holiday pay / shift premiums in the table below.
Make any edits and continue.`);
+
+ // form for new row
+ setUpModal();
+ setUpForm();
+
+ // show new row button
+ Table.Buttons.AddRow.updateText("Add new cost center");
+ Table.Buttons.AddRow.show();
+}
+
+function setUpModal() {
+ // Initialize modal
+ Modal.clear();
+ Modal.Link.add('add-btn');
+ Modal.Title.update('New cost center for overtime');
}
function assignClasses() {
@@ -40,6 +59,9 @@ function assignClasses() {
{ title: 'Edit', className: 'edit'},
// calc columns
{ title: 'FICA Rate', className: 'fica', hide: true},
+ { title: 'Account String', className: 'account-string', hide: true},
+ { title: `Cost Center`, className: 'cc', hide: true },
+ { title: 'Appropriation', className: 'approp', hide: true},
];
// assign cost classes
@@ -64,8 +86,6 @@ export async function initializeOTTable(){
updateDisplayandTotals();
// activate edit buttons
Table.Buttons.Edit.init(OTRowOnEdit, updateDisplayandTotals);
- // wire up tooltips to show info on click
- Tooltip.linkAllOvertime();
} else {
Prompt.Text.update('No overtime expenditures for this fund.')
}
@@ -95,4 +115,42 @@ function updateDisplayandTotals(){
//save data
Table.save();
}
+}
+
+export function setUpForm() {
+ // Set up form
+ Form.new('modal-body');
+ Form.NewField.dropdown('Appropriation:', 'approp-name', FundLookupTable.getApprops(), true);
+ Form.NewField.dropdown('Cost Center:', 'cc-name', FundLookupTable.getCostCenters(), true);
+ Form.NewField.dropdown('Service', 'service', Services.list(), true);
+ Form.NewField.dropdown('Recurring or One-Time', 'recurring', ['Recurring', 'One-Time'], true);
+ Form.NewField.shortText('Overtime amount requested:', 'OT-wages', true);
+ Form.SubmitButton.add();
+ // Initialize form submission to table data
+ Modal.Submit.init(handleSubmitNewRow);
+}
+
+function handleSubmitNewRow(event){
+ // get answers from form, hide form, show answers in table
+ const responses = Form.fetchAllResponses(event);
+
+ // edit inputs from modal
+ responses['OT-wages'] = unformatCurrency(responses['OT-wages']);
+ responses['fica'] = 0.0765;
+ // create account string
+ responses['account-string'] = AccountString.build(responses['approp-name'], responses['cc-name']);
+ responses['approp'] = AccountString.getNumber(responses['approp-name']);
+ responses['cc'] = AccountString.getNumber(responses['cc-name']);
+
+ // make sure it's not an empty response
+ if (Object.values(responses)[0] != ''){
+ // change page view
+ Modal.hide();
+ // add data to table
+ Table.Rows.add(responses);
+ Table.save();
+ initializeOTTable();
+
+ }
+
}
\ No newline at end of file
diff --git a/src/js/views/06_nonpersonnel/helpers.js b/src/js/views/06_nonpersonnel/helpers.js
index 000078d..c6ea724 100644
--- a/src/js/views/06_nonpersonnel/helpers.js
+++ b/src/js/views/06_nonpersonnel/helpers.js
@@ -5,6 +5,11 @@ import Body from "../../components/body/body.js";
import NavButtons from "../../components/nav_buttons/nav_buttons.js";
import Subtitle from "../../components/header/header.js";
import Tooltip from "../../components/tooltip/tooltip.js";
+import Modal from "../../components/modal/modal.js";
+import Form from "../../components/form/form.js";
+import { ObjectCategories, Services, AccountString } from "../../utils/data_utils/budget_data_handlers.js";
+import { FundLookupTable } from "../../utils/data_utils/budget_data_handlers.js";
+import { unformatCurrency } from "../../utils/common_utils.js";
const nonPersonnelColumns = [
{ title: 'FY26 Request', className: 'request', isCost: true },
@@ -18,9 +23,12 @@ const nonPersonnelColumns = [
// hidden columns used for calcs and info boxes
{ title: 'Appropriation Name', className: 'approp-name', hide: true },
{ title: 'Cost Center Name', className: 'cc-name', hide: true },
- { title : 'Contract End Date', className : 'contract-end', hide:true},
+ { title: 'Appropriation', className: 'approp', hide: true },
+ { title: 'Cost Center', className: 'cc', hide: true },
+ { title: 'Contract End Date', className: 'contract-end', hide:true},
{ title: 'Amount Remaining on Contract', className: 'remaining', isCost: true , hide: true},
{ title: 'Object Name', className: 'object-name', hide: true},
+ { title: 'Object', className: 'object', hide: true},
{ title: 'Vendor Name', className: 'vendor', hide: true},
{ title: 'Object Category', className: 'object-category', hide: true},
{ title: 'BPA/CPA Description', className: 'cpa-description', hide: true}
@@ -36,6 +44,14 @@ export function preparePageView(){
Subtitle.update('Non-Personnel');
Prompt.Text.update('Review and edit non-personnel line items.');
NavButtons.Next.enable();
+
+ // form for new row
+ setUpModal();
+ setUpForm();
+
+ // show new row button
+ Table.Buttons.AddRow.updateText("Add new non-personnel item");
+ Table.Buttons.AddRow.show()
}
export async function initializeNonpersonnelTable(){
@@ -48,8 +64,8 @@ export async function initializeNonpersonnelTable(){
Table.Columns.assignClasses(nonPersonnelColumns);
// enable editing
Table.Buttons.Edit.init(nonPersonnelRowOnEdit, Table.save);
- // show info boxes on click
- Tooltip.linkAllNP();
+ // show detail buttons
+ Tooltip.linkAll();
} else {
Prompt.Text.update('No non-personnel expenditures for this fund.')
}
@@ -62,3 +78,53 @@ function nonPersonnelRowOnEdit(){
Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']);
}
+export function setUpModal() {
+ // Initialize modal
+ Modal.clear();
+ Modal.Link.add('add-btn');
+ Modal.Title.update('New non-personnel item');
+}
+
+export function setUpForm() {
+ // Set up form
+ Form.new('modal-body');
+ Form.NewField.dropdown('Appropriation:', 'approp-name', FundLookupTable.getApprops(), true);
+ Form.NewField.dropdown('Cost Center:', 'cc-name', FundLookupTable.getCostCenters(), true);
+ Form.NewField.dropdown('Object Category:', 'object-category', ObjectCategories.list, true);
+ // TODO: maybe give dropdown based on selected obj category
+ Form.NewField.shortText('Object Number (if known):', 'object', false);
+ Form.NewField.dropdown('Service', 'service', Services.list(), true);
+ Form.NewField.longText('Describe your new request:', 'description', true);
+ Form.NewField.dropdown('Recurring or One-Time', 'recurring', ['Recurring', 'One-Time'], true);
+ Form.NewField.shortText('Amount requested:', 'request', true);
+ Form.SubmitButton.add();
+ // Initialize form submission to table data
+ Modal.Submit.init(submitNewRow);
+}
+
+
+function submitNewRow(event){
+ // get answers from form, hide form, show answers in table
+ const responses = Form.fetchAllResponses(event);
+ // edit inputs from modal
+ responses['avg-salary'] = unformatCurrency(responses['avg-salary']);
+ responses['fringe'] = parseFloat(responses['fringe']) / 100;
+ // create account string
+ responses['account-string'] = AccountString.build(responses['approp-name'], responses['cc-name'], responses['object']);
+ responses['approp'] = AccountString.getNumber(responses['approp-name']);
+ responses['cc'] = AccountString.getNumber(responses['cc-name']);
+ // TODO: build out lookup table from meta.obj tab from detail sheet?
+ responses['object-name'] = responses['object'];
+
+ // make sure it's not an empty response
+ if (Object.values(responses)[0] != ''){
+ // change page view
+ Modal.hide();
+ // add data to table
+ Table.Rows.add(responses);
+ Table.save();
+ initializeNonpersonnelTable();
+
+ }
+
+}
diff --git a/src/js/views/07_new_initiatives/helpers.js b/src/js/views/07_new_initiatives/helpers.js
index 183b83a..a3d1723 100644
--- a/src/js/views/07_new_initiatives/helpers.js
+++ b/src/js/views/07_new_initiatives/helpers.js
@@ -8,12 +8,37 @@ import NavButtons from '../../components/nav_buttons/nav_buttons.js'
import { nextPage } from '../view_logic.js'
import Subtitle from '../../components/header/header.js'
import Sidebar from '../../components/sidebar/sidebar.js'
+import { FundLookupTable, AccountString } from '../../utils/data_utils/budget_data_handlers.js'
const explanation = `New initiative submissions will count as supplemental line items and will be the starting
point for a conversation with both OB and ODFS, who will help with the details.`
const dropdownOptions = ['N/A', 'One-Time', 'Recurring']
+const initiativesCols = [
+ { title: 'Initiative Name', className: 'init-name' },
+ { title: 'Account String', className: 'account-string' },
+ { title: 'Ballpark Total Expenses', className: 'total', isCost: true },
+ { title: 'Personnel Cost', className: 'personnel', isCost: true },
+ { title: 'Non-personnel Cost', className: 'nonpersonnel', isCost: true },
+ { title: 'Revenue', className: 'revenue', isCost: true },
+ { title: 'Revenue Type', className: 'rev-type' },
+ { title: 'Edit', className : 'edit' },
+
+ // hide the explanation columns
+ { title: 'Q1', className: 'q1', hide: true },
+ { title: 'Q2', className: 'q2', hide: true },
+ { title: 'Q3', className: 'q3', hide: true },
+
+ // hide the account string breakdown columns too
+ { title: 'Appropriation Name', className: 'approp-name', hide: true },
+ { title: 'Cost Center Name', className: 'cc-name', hide: true },
+ { title: 'Appropriation', className: 'approp', hide: true },
+ { title: 'Cost Center', className: 'cc', hide: true },
+ { title: 'Fund Name', className: 'fund-name', hide: true },
+ { title: 'Fund', className: 'fund', hide: true }
+];
+
export function initializePageView() {
// Prepare page view
Body.reset();
@@ -53,63 +78,51 @@ export function setUpForm() {
Form.new('modal-body');
// general questions
- Form.NewField.shortText('Initiative Name:', 'Initiative Name', true);
- Form.NewField.longText('What is the business case for the Initiative?', 'Q1', true);
+ Form.NewField.shortText('Initiative Name:', 'init-name', true);
+ Form.NewField.longText('What is the business case for the Initiative?', 'q1', true);
Form.NewField.longText(`Why is the initiative needed? What is the value-add to residents?
- What is the Department’s plan for implementing the Initiative?`, 'Q2', true);
- Form.NewField.longText(`Why can’t the Initiative be funded with the Department’s baseline budget?`, 'Q3', true);
+ What is the Department’s plan for implementing the Initiative?`, 'q2', true);
+ Form.NewField.longText(`Why can’t the Initiative be funded with the Department’s baseline budget?`, 'q3', true);
// TODO: Edit to drop down
- Form.NewField.shortText('Relevant account string (if known)?', 'Account String', false);
+ Form.NewField.dropdown('Fund:', 'fund-name', FundLookupTable.listFundNames(), true);
+ Form.NewField.dropdown('Appropriation (if known):', 'approp-name', FundLookupTable.getApprops(), true);
+ Form.NewField.dropdown('Cost Center (if known):', 'cc-name', FundLookupTable.getCostCenters(), true);
// Numbers
Form.NewField.numericInput('What is your ballpark estimate of TOTAL ADDITONAL expenses associated with this initiative?',
- 'Ballpark Total Expenses', false);
- Form.NewField.numericInput('Estimate of ADDITONAL personnel cost?', 'Personnel Cost', false);
- Form.NewField.numericInput('Estimate of ADDITONAL nonpersonnel cost?', 'Non-personnel Cost', false);
- Form.NewField.numericInput('Estimate of ADDITONAL revenue (if applicable)?', 'Revenue', false);
+ 'total', false);
+ Form.NewField.numericInput('Estimate of ADDITONAL personnel cost?', 'personnel', false);
+ Form.NewField.numericInput('Estimate of ADDITONAL nonpersonnel cost?', 'nonpersonnel', false);
+ Form.NewField.numericInput('Estimate of ADDITONAL revenue (if applicable)?', 'revenue', false);
Form.NewField.dropdown(`If there will be revenue, is it one-time or recurring?`,
- 'One-time v. Recurring', dropdownOptions);
+ 'rev-type', dropdownOptions);
Form.SubmitButton.add();
// Initialize form submission to table data
- Modal.Submit.init(handleNewInitSubmission);
+ Modal.Submit.init(submitNewRow);
}
function assignClasses() {
- // record columns and their classes
- const initiativesCols = [
- { title: 'Initiative Name', className: 'init-name' },
- { title: 'Account String', className: 'account-string' },
- { title: 'Ballpark Total Expenses', className: 'total', isCost: true },
- { title: 'Revenue', className: 'revenue', isCost: true },
- { title: 'Personnel Cost', className: 'personnel', isCost: true },
- { title: 'Non-personnel Cost', className: 'nonpersonnel', isCost: true },
- { title: 'One-time v. Recurring', className: 'rev-type' },
- { title: 'Edit', className : 'edit' },
-
- // hide the explanation columns
- { title: 'Q1', className: 'q1', hide: true },
- { title: 'Q2', className: 'q2', hide: true },
- { title: 'Q3', className: 'q3', hide: true },
- ];
-
// assign cost classes
Table.Columns.assignClasses(initiativesCols)
}
export async function initializeInitTable(){
- Table.clear();
+
// load table data from storage
if(await Table.Data.load()) {
- //after table is loaded, fill it
+ // after table is loaded, fill it
Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, "Edit");
assignClasses();
- // enable editing
- Table.Buttons.Edit.init(rowOnEdit, Table.save);
// show table
Table.show();
+ // enable editing
+ Table.Buttons.Edit.init(rowOnEdit, Table.save);
+ } else {
+ Table.clear();
+ console.log('no data');
}
}
@@ -118,23 +131,28 @@ function rowOnEdit(){
Table.Cell.createTextbox('revenue', true);
Table.Cell.createTextbox('personnel', true);
Table.Cell.createTextbox('nonpersonnel', true);
- Table.Cell.createTextbox('account-string');
Table.Cell.createTextbox('init-name');
Table.Cell.createDropdown('rev-type', dropdownOptions);
}
-function handleNewInitSubmission(event){
+function submitNewRow(event){
// get answers from form, hide form, show answers in table
const responses = Form.fetchAllResponses(event);
+ console.log(responses);
+
+ // create account string columns
+ responses['approp'] = AccountString.getNumber(responses['approp-name']);
+ responses['cc'] = AccountString.getNumber(responses['cc-name']);
+ responses['fund'] = AccountString.getNumber(responses['fund-name']);
+ responses['account-string'] = AccountString.build(responses['approp-name'], responses['cc-name'], null, responses['fund']);
+
// make sure it's not an empty response
if (Object.values(responses)[0] != ''){
+ Modal.hide();
// add data to table
- Table.Rows.add(responses);
- // save it
+ Table.Rows.add(responses, initiativesCols);
Table.save();
- // show updated table
initializeInitTable();
- Modal.hide();
Table.Buttons.AddRow.updateText('Add another new initiative');
}
}
diff --git a/src/js/views/view_logic.js b/src/js/views/view_logic.js
index 48febd6..4d20432 100644
--- a/src/js/views/view_logic.js
+++ b/src/js/views/view_logic.js
@@ -85,6 +85,12 @@ export function lastPage(){
// clean up current page
if (CLEANUP[page_state]) { CLEANUP[page_state]() };
+
+ // if on new-inits, circle back to fund selection
+ if (CurrentPage.load() == 'new-inits'){
+ visitPage('baseline-landing');
+ return;
+ }
// Check if there is a next key
if (currentIndex >= 1) {
|