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) {