diff --git a/index.html b/index.html deleted file mode 100644 index 15327c2..0000000 --- a/index.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - -Demo Budget Form - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -

FY2026 Budget Form

-

-
- - -
- - -
- - - - -
- - -
-
-

Baseline

-
-
-
-

Suplemental

-
-
-
- - -
-

-
- - - - - -
- - -
- - - - -
- -
- - -
-
- -
-
- - -
-
- -
-
- -
- - - - - - - - - \ No newline at end of file diff --git a/src/index.html b/src/index.html index 8e1d261..028589c 100644 --- a/src/index.html +++ b/src/index.html @@ -15,11 +15,10 @@ - - + @@ -71,6 +70,9 @@

+ +
+
@@ -78,7 +80,6 @@

-
@@ -100,7 +101,6 @@

-
diff --git a/src/js/components/body/body.js b/src/js/components/body/body.js index 83df631..028bd84 100644 --- a/src/js/components/body/body.js +++ b/src/js/components/body/body.js @@ -1,13 +1,14 @@ import './body.css'; import Welcome from '../../components/welcome/welcome.js' -import { Accordion } from '../accordion/accordion.js'; -import { FileUpload } from '../file_upload/file_upload.js'; +import Accordion from '../accordion/accordion.js'; +import FileUpload from '../file_upload/file_upload.js'; import Modal from '../modal/modal.js'; import NavButtons from '../nav_buttons/nav_buttons.js'; import Prompt from '../prompt/prompt.js'; import Sidebar from '../sidebar/sidebar.js'; import Table from '../table/table.js'; +import Tooltip from '../tooltip/tooltip.js'; function resetPage() { // hide everything in the body @@ -20,6 +21,7 @@ function resetPage() { Sidebar.hide(); Accordion.hide(); FileUpload.hide(); + Tooltip.hide(); // disable next button NavButtons.Next.disable(); Prompt.Buttons.reset(); diff --git a/src/js/components/file_upload/file_upload.js b/src/js/components/file_upload/file_upload.js index 7fe2b28..43cc3d4 100644 --- a/src/js/components/file_upload/file_upload.js +++ b/src/js/components/file_upload/file_upload.js @@ -37,4 +37,6 @@ function readXL(event) { }; reader.readAsArrayBuffer(file); // Read the file as an ArrayBuffer } -} \ No newline at end of file +} + +export default FileUpload; \ No newline at end of file diff --git a/src/js/components/table/subcomponents/cells.js b/src/js/components/table/subcomponents/cells.js index fe061d7..50697b1 100644 --- a/src/js/components/table/subcomponents/cells.js +++ b/src/js/components/table/subcomponents/cells.js @@ -12,7 +12,12 @@ function getCellValue(row, className) { // return text in cell function getCellText(row, className) { var cell = row.querySelector(`.${className}`); - return cell.textContent; + if (cell) { + return cell.textContent; + } else { + //console.log(`Error retrieving cell text for class ${className}`); + return ''; + } } function updateTableCell(row, col_class, new_value){ diff --git a/src/js/components/table/subcomponents/columns.js b/src/js/components/table/subcomponents/columns.js index c7746f7..d6caaab 100644 --- a/src/js/components/table/subcomponents/columns.js +++ b/src/js/components/table/subcomponents/columns.js @@ -114,7 +114,9 @@ function assignColumnClasses(columnDefinitions) { } // show the column - showColumnByTitle(column.title); + if (!column.hide){ + showColumnByTitle(column.title); + } }); } diff --git a/src/js/components/tooltip/tooltip.css b/src/js/components/tooltip/tooltip.css new file mode 100644 index 0000000..78a4ad8 --- /dev/null +++ b/src/js/components/tooltip/tooltip.css @@ -0,0 +1,37 @@ +#tooltip { + position: absolute; + background-color: black; + color: white; + padding: 5px; + border-radius: 3px; + visibility: hidden; + white-space: nowrap; + font-size: 14px; + z-index: 1000; + max-width: 300px; + word-wrap: break-word; + white-space: normal; +} + +.tooltip-cell { + /* color: blue; */ + /* text-decoration: underline; */ + cursor: pointer; +} + +.tooltip-cell .info-icon { + margin-left: 5px; + color: var(--spiritgreen); + font-size: 15px; +} + +.tooltip-cell:hover { + background-color: #f0f0f0; /* Change background on hover */ +} + +.detail { + color: blue; + color: var(--spiritgreen); + text-decoration: underline; + margin-left: 5px; +} \ No newline at end of file diff --git a/src/js/components/tooltip/tooltip.js b/src/js/components/tooltip/tooltip.js new file mode 100644 index 0000000..8901107 --- /dev/null +++ b/src/js/components/tooltip/tooltip.js @@ -0,0 +1,182 @@ +import { FISCAL_YEAR } from '../../init'; +import Cell from '../table/subcomponents/cells'; +import { formatCurrency } from '../../utils/common_utils'; +import './tooltip.css' + +function hideTooltip() { + document.getElementById('tooltip').style.visibility = 'hidden'; +} + +function showTooltip() { + document.getElementById('tooltip').style.visibility = 'visible'; +} + +function editTooltipText(newText){ + // edit text to display inside tooltip + const tooltip = document.getElementById('tooltip'); + tooltip.innerHTML = newText; +} + +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}`; + if (obj) { message += `
Object: ${obj}`} + editTooltipText(message); +} + +function showSalaryProjection(row){ + const general_increase = Cell.getText(row, 'general-increase-rate'); + const merit_increase = Cell.getText(row, 'merit-increase-rate'); + const current_salary = Cell.getValue(row, 'current-salary'); + 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}.`; + } 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.` + } + + editTooltipText(message); +} + +function showFinalPersonnelCost(row){ + const proj_salary = Cell.getValue(row, 'avg-salary'); + const ftes = Cell.getText(row, 'baseline-ftes'); + const fringe = parseFloat(Cell.getText(row, 'fringe')); + const avg_benefits = proj_salary * fringe; + const message = `The total cost captures ${ftes} position(s) at + an annual salary/wage of ${formatCurrency(proj_salary)}, + plus fringe benefits that cost ${formatCurrency(avg_benefits)} + per position per year, on average.` + editTooltipText(message); +} + +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, + which is ${ficaPercentage}% for this cost center.` + editTooltipText(message); +} + +function showCPA(row){ + const cpa = parseFloat(Cell.getText(row, 'cpa')); + const description = Cell.getText(row, 'cpa-description'); + const vendor = Cell.getText(row, 'vendor'); + const contract_end = Cell.getText(row, 'contract-end'); + const remaining = Cell.getValue(row, 'remaining'); + if (cpa) { + var message = `CPA #${cpa}`; + } else { + var message = `No CPA`; + } + if (vendor) {message += `
Vendor: ${vendor}`}; + if (description) {message += `
Description: ${description}`}; + if (contract_end) {message += `
Contract End Date: ${contract_end}`} + if (remaining) {message += `
Amount Remaining on Contract: ${formatCurrency(remaining)}`} + + editTooltipText(message); +} + +export const Tooltip = { + + hide : hideTooltip, + show : showTooltip, + + 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); + }) + }, + + linkSalaryCol : function() { + // get all relevant cells + document.querySelectorAll('.avg-salary').forEach( (cell) => { + this.link(cell, showSalaryProjection); + }) + }, + + linkTotalPersonnelCostCol : function() { + // get all relevant cells + document.querySelectorAll('.total-baseline').forEach( (cell) => { + this.link(cell, showFinalPersonnelCost); + }) + }, + + linkTotalOTCol : function() { + // get all relevant cells + document.querySelectorAll('.total').forEach( (cell) => { + this.link(cell, showFICA); + }) + }, + + linkCPACol : function() { + // get all relevant cells + document.querySelectorAll('.cpa').forEach( (cell) => { + this.link(cell, showCPA); + }) + }, + + linkAllPersonnel : function() { + this.linkAccountStringCol(); + this.linkSalaryCol(); + this.linkTotalPersonnelCostCol(); + }, + + linkAllOvertime : function() { + // this.linkAccountStringCol(); + this.linkTotalOTCol(); + }, + + linkAllNP : function() { + this.linkAccountStringCol(); + this.linkCPACol(); + } +} + +export default Tooltip \ No newline at end of file diff --git a/src/js/init.js b/src/js/init.js index 5bc282c..ee24013 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -4,20 +4,11 @@ import '../css/common.css'; // import functions import { CurrentPage } from './utils/data_utils/local_storage_handlers.js'; -// path for my laptop -export let DATA_ROOT = '../../../data/law_dept_sample/' -// github path -// export let DATA_ROOT = '../../budget-request-demo/data/law_dept_sample/' - +// temporary hard-coding export let REVENUE = 0; export let TARGET = 14000000; +// Set to equal current fiscal year export var FISCAL_YEAR = '26'; -export var OT_FRINGE = 0.0765; - -// variables on the salary -export var fringe = 0.36 -export var cola = 0.02 -export var merit = 0.02 // sheets to expect on detail sheet export const SHEETS = { diff --git a/src/js/utils/common_utils.js b/src/js/utils/common_utils.js index 3e92ad2..eb154cf 100644 --- a/src/js/utils/common_utils.js +++ b/src/js/utils/common_utils.js @@ -37,6 +37,10 @@ export function cleanString(str){ } export function removeNewLines(str){ - // TODO: ensure there is a space between words on new lines - return str.replaceAll(/[\r\n]+/g, ""); + // replace all new lines with spaces + str = str.replaceAll(/[\r\n]+/g, " "); + // remove any extra spaces or trailing/leading whitespace + str = str.replaceAll(' ', ' '); + str = str.replace(/^\s+|\s+$/g, ''); + return str; } \ 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 b92d3ab..6918d89 100644 --- a/src/js/utils/data_utils/local_storage_handlers.js +++ b/src/js/utils/data_utils/local_storage_handlers.js @@ -1,4 +1,4 @@ -import { FISCAL_YEAR, DATA_ROOT } from "../../init.js"; +import { FISCAL_YEAR } from "../../init.js"; import Sidebar from "../../components/sidebar/sidebar.js"; import { PAGES, visitPage } from "../../views/view_logic.js"; import { fetchJSON } from "./JSON_data_handlers.js"; diff --git a/src/js/views/04_personnel/helpers.js b/src/js/views/04_personnel/helpers.js index 9291b79..5dc5a59 100644 --- a/src/js/views/04_personnel/helpers.js +++ b/src/js/views/04_personnel/helpers.js @@ -1,5 +1,5 @@ -import { FISCAL_YEAR, fringe, cola, merit } from "../../init.js" +import { FISCAL_YEAR } from "../../init.js" import Body from "../../components/body/body.js"; import NavButtons from "../../components/nav_buttons/nav_buttons.js"; import Subtitle from "../../components/header/header.js"; @@ -9,9 +9,8 @@ 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 { convertToJSON } from "../../utils/data_utils/JSON_data_handlers.js"; +import Tooltip from "../../components/tooltip/tooltip.js"; -import { Baseline, loadTableData } from "../../utils/data_utils/local_storage_handlers.js"; export function preparePageView(){ // prepare page view @@ -35,12 +34,19 @@ function assignClasses() { // record columns and their classes const personnelColumns = [ { title: 'Job Title', className: 'job-name' }, - { title: 'Account String', className: 'string' }, + { title: 'Account String', className: 'account-string' }, { title: 'Service', className: 'service' }, { title: `FY${FISCAL_YEAR} Requested FTE`, className: 'baseline-ftes' }, { title: `FY${FISCAL_YEAR} Average Projected Salary/Wage`, className: 'avg-salary', isCost: true }, { title: 'Total Cost', className: 'total-baseline', isCost: true }, - { title: 'Edit', className: 'edit' } + { title: 'Edit', className: 'edit' }, + // hidden columns needed for calculations + { title: 'Fringe Benefits Rate', className: 'fringe', hide: true }, + { title: 'Appropriation Name', className: 'approp-name', hide: true }, + { title: 'Cost Center Name', className: 'cc-name', 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}, ]; // assign cost classes @@ -64,6 +70,8 @@ export async function initializePersonnelTable(){ // 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.') } @@ -74,10 +82,6 @@ function initializeRowAddition(){ Table.Buttons.AddRow.show(); } -function calculateTotalCost(ftes, avg_salary, fringe, cola, merit){ - return ftes * avg_salary * (1 + fringe) * (1 + cola) * (1 + merit); -} - // update sidebar and also cost totals when the FTEs are edited function updateDisplayandTotals(){ // calculate for each row @@ -85,10 +89,11 @@ function updateDisplayandTotals(){ for (let i = 1; i < rows.length; i++){ // fetch values for calculations let avg_salary = Table.Cell.getValue(rows[i], 'avg-salary'); + let fringe = parseFloat(Table.Cell.getText(rows[i], 'fringe')); let baseline_ftes = Table.Cell.getText(rows[i], 'baseline-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_baseline_cost = avg_salary * baseline_ftes * (1 + fringe); // update total column Table.Cell.updateValue(rows[i], 'total-baseline', total_baseline_cost); @@ -99,7 +104,6 @@ function updateDisplayandTotals(){ } - export function setUpModal() { // Initialize modal Modal.clear(); diff --git a/src/js/views/05_overtime/helpers.js b/src/js/views/05_overtime/helpers.js index ac477f0..70b8082 100644 --- a/src/js/views/05_overtime/helpers.js +++ b/src/js/views/05_overtime/helpers.js @@ -6,7 +6,7 @@ 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 { OT_FRINGE } from '../../init.js'; +import Tooltip from '../../components/tooltip/tooltip.js'; export function preparePageView(){ // prepare page view @@ -29,14 +29,17 @@ export function preparePageView(){ function assignClasses() { // record columns and their classes const OT_cols = [ - { title: 'Account String', className: 'string' }, - { title: `Cost Center Name`, className: 'cc' }, + // { title: 'Account String', className: 'account-string' }, + { title: `Cost Center Name`, className: 'cc-name' }, + { title: 'Appropriation Name', className: 'approp-name'}, { title: 'Service', className: 'service' }, { title: 'Recurring or One-Time', className: 'recurring'}, { title: 'Hourly Employee Overtime (Wages)', className: 'OT-wages', isCost: true }, { title: 'Salaried Employee Overtime (Salary)', className: 'OT-salary', isCost: true }, { title: 'Total Cost (including benefits)', className : 'total', isCost: true}, - { title: 'Edit', className: 'edit'} + { title: 'Edit', className: 'edit'}, + // calc columns + { title: 'FICA Rate', className: 'fica', hide: true}, ]; // assign cost classes @@ -54,22 +57,22 @@ export async function initializeOTTable(){ if(await Table.Data.load()) { //after table is loaded, fill it Table.show(); - Table.Columns.addAtEnd( '0', 'Hourly Employee Overtime (Wages)'); - Table.Columns.addAtEnd( '0', 'Salaried Employee Overtime (Salary)'); - // Table.Columns.addAtEnd( '0', 'Total Cost (including benefits)'); Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, 'Edit');; assignClasses(); // add up the baseline costs and update sidebar 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.') } } -function calculateTotalCost(wages, salary, fringe){ - return (wages + salary) * (1 + fringe) ; +function calculateTotalCost(salary, wages, fica_rate){ + fica_rate = parseFloat(fica_rate); + return (wages + salary) * (1 + fica_rate) ; } // update sidebar and also cost totals when the FTEs are edited @@ -80,9 +83,10 @@ function updateDisplayandTotals(){ // fetch values for calculations let OT_salary = Table.Cell.getValue(rows[i], 'OT-salary'); let OT_wages = Table.Cell.getValue(rows[i], 'OT-wages'); + let fica_rate = Table.Cell.getText(rows[i], 'fica'); // add salary and wages and fringe benefits (FICA) - let row_total = calculateTotalCost(OT_salary, OT_wages, OT_FRINGE); + let row_total = calculateTotalCost(OT_salary, OT_wages, fica_rate); // update total Table.Cell.updateValue(rows[i], 'total', row_total); diff --git a/src/js/views/06_nonpersonnel/helpers.js b/src/js/views/06_nonpersonnel/helpers.js index a24a960..e021b60 100644 --- a/src/js/views/06_nonpersonnel/helpers.js +++ b/src/js/views/06_nonpersonnel/helpers.js @@ -4,19 +4,26 @@ import Table from "../../components/table/table.js"; import Body from "../../components/body/body.js"; import NavButtons from "../../components/nav_buttons/nav_buttons.js"; import Subtitle from "../../components/header/header.js"; -import { FundLookupTable } from "../../utils/data_utils/budget_data_handlers.js"; -import { CurrentFund } from "../../utils/data_utils/local_storage_handlers.js"; +import Tooltip from "../../components/tooltip/tooltip.js"; const nonPersonnelColumns = [ { title: 'FY26 Request', className: 'request', isCost: true }, - { title: 'Amount Remaining on Contract', className: 'remaining', isCost: true }, { title: 'Service', className : 'service' }, { title: 'Edit', className : 'edit' }, { title : 'Account String', className : 'account-string'}, - { title : 'CPA #', className : 'cpa'}, - { title : 'Contract End Date', className : 'contract-end'}, { title: 'Recurring or One-Time', className: 'recurring'}, - { title: 'Object Name', className: 'object'} + + { title : 'CPA #', className : 'cpa'}, + + // 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: 'Amount Remaining on Contract', className: 'remaining', isCost: true , hide: true}, + { title: 'Object Name', className: 'object-name', 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} ]; export function preparePageView(){ @@ -41,6 +48,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(); } else { Prompt.Text.update('No non-personnel expenditures for this fund.') } diff --git a/src/js/views/08_summary/helpers.js b/src/js/views/08_summary/helpers.js index 21dd1ea..4c07102 100644 --- a/src/js/views/08_summary/helpers.js +++ b/src/js/views/08_summary/helpers.js @@ -21,14 +21,13 @@ export function summaryView(){ // prompt buttons Prompt.Buttons.Right.updateText('Download Excel'); - Prompt.Buttons.Left.updateText('Start over'); + Prompt.Buttons.Left.updateText('Start over with new Excel upload'); // add button links Prompt.Buttons.Left.addAction(returnToWelcome); Prompt.Buttons.Right.addAction(downloadXLSX); // update page text Subtitle.update('Summary'); - compareToTarget() }