diff --git a/index.html b/index.html
deleted file mode 100644
index 15327c2..0000000
--- a/index.html
+++ /dev/null
@@ -1,194 +0,0 @@
-
-
-
-
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()
}