diff --git a/src/js/components/accordion/accordion.js b/src/js/components/accordion/accordion.js index c66d348..a6b7228 100644 --- a/src/js/components/accordion/accordion.js +++ b/src/js/components/accordion/accordion.js @@ -1,11 +1,11 @@ import './accordion.css' -import { Baseline, CurrentFund, Fund, Supplemental } from "../../utils/data_utils/local_storage_handlers.js"; + +import {Baseline, CurrentFund, Fund, Supplemental, FundLookupTable} from '../../models' import { formatCurrency, cleanString } from "../../utils/common_utils.js"; import Table from "../table/table.js"; -import { FundLookupTable } from "../../utils/data_utils/budget_data_handlers.js"; import { visitPage } from '../../views/view_logic.js'; -import { TARGET } from '../../init.js'; +import { TARGET } from '../../constants/'; function redirectForEdit(){ const row = document.querySelector(`.active-editing`); diff --git a/src/js/components/body/body.js b/src/js/components/body/body.js index 028bd84..37b688a 100644 --- a/src/js/components/body/body.js +++ b/src/js/components/body/body.js @@ -22,8 +22,9 @@ function resetPage() { Accordion.hide(); FileUpload.hide(); Tooltip.hide(); - // disable next button - NavButtons.Next.disable(); + + Table.adjustWidth('100%'); + Prompt.Buttons.reset(); // disable submit button Modal.Submit.deinit(); diff --git a/src/js/components/file_upload/file_upload.js b/src/js/components/file_upload/file_upload.js index 43cc3d4..719ba59 100644 --- a/src/js/components/file_upload/file_upload.js +++ b/src/js/components/file_upload/file_upload.js @@ -1,6 +1,6 @@ import './file_upload.css'; -import { processWorkbook } from "../../utils/data_utils/XLSX_handlers.js"; +import { processWorkbook } from "../../utils/XLSX_handlers.js"; import Sidebar from '../sidebar/sidebar.js'; export const FileUpload = { diff --git a/src/js/components/header/header.js b/src/js/components/header/header.js index d3f7b71..53ab0de 100644 --- a/src/js/components/header/header.js +++ b/src/js/components/header/header.js @@ -1,6 +1,6 @@ import './header.css'; -import { CurrentFund } from "../../utils/data_utils/local_storage_handlers.js"; +import CurrentFund from '../../models/current_fund'; export const Subtitle = { update : function(subtitle){ diff --git a/src/js/components/modal/modal.js b/src/js/components/modal/modal.js index 3099ca3..14fd47b 100644 --- a/src/js/components/modal/modal.js +++ b/src/js/components/modal/modal.js @@ -4,7 +4,6 @@ import './modal.css'; function clearModal(){ updateModalTitle(''); document.getElementById('modal-body').innerHTML = ''; - //removeAllModalLinks() } function hideModal(modal_id) { diff --git a/src/js/components/nav_buttons/nav_buttons.js b/src/js/components/nav_buttons/nav_buttons.js index 4d9d362..6e8b329 100644 --- a/src/js/components/nav_buttons/nav_buttons.js +++ b/src/js/components/nav_buttons/nav_buttons.js @@ -9,7 +9,6 @@ function initializeNavButtons(){ // initialize next button const next_btn = document.getElementById('btn-next'); next_btn.addEventListener('click', nextPage); - disable('btn-next'); } function hideNavButtons() { diff --git a/src/js/components/sidebar/sidebar.js b/src/js/components/sidebar/sidebar.js index 2b06b33..bc21eef 100644 --- a/src/js/components/sidebar/sidebar.js +++ b/src/js/components/sidebar/sidebar.js @@ -1,8 +1,9 @@ import './sidebar.css' import { formatCurrency } from "../../utils/common_utils.js"; -import { TARGET } from "../../init.js"; -import { Baseline, Supplemental } from "../../utils/data_utils/local_storage_handlers.js"; +import { TARGET } from '../../constants/'; +import {Baseline, Supplemental} from '../../models/'; + // Assuming you have a CSS variable --main-color defined on the :root const root = document.documentElement; diff --git a/src/js/components/table/subcomponents/cells.js b/src/js/components/table/subcomponents/cells.js index 73e8eec..67d13b0 100644 --- a/src/js/components/table/subcomponents/cells.js +++ b/src/js/components/table/subcomponents/cells.js @@ -1,5 +1,5 @@ import { formatCurrency, displayWithCommas } from "../../../utils/common_utils.js"; -import { Services } from "../../../utils/data_utils/budget_data_handlers.js"; +import Services from "../../../models/services.js"; import Dropdown from "../../form/subcomponents/dropdown.js"; // return cell value attribute or 0 if it does not exist diff --git a/src/js/components/table/subcomponents/data.js b/src/js/components/table/subcomponents/data.js index 67c4b23..23c96dc 100644 --- a/src/js/components/table/subcomponents/data.js +++ b/src/js/components/table/subcomponents/data.js @@ -1,5 +1,6 @@ -import { FundLookupTable } from "../../../utils/data_utils/budget_data_handlers.js"; -import { CurrentFund, CurrentPage, loadTableData, saveTableData } from "../../../utils/data_utils/local_storage_handlers.js"; +import FundLookupTable from '../../../models/fund_lookup_table.js'; +import CurrentFund from '../../../models/current_fund.js' +import CurrentPage from '../../../models/current_page.js' function fillTable(data) { try { @@ -33,24 +34,24 @@ function fillTable(data) { } catch(error) { console.error('No table saved in localStorage:', error); } - saveTableData(); } async function loadFromStorage(){ - // look up table in storage and pass to table load function\ + // look up table name in storage if (CurrentFund.number()){ var key = `${CurrentPage.load()}_${CurrentFund.number()}`; } else { var key = CurrentPage.load(); } - const data = await loadTableData(key); - if (!data){ - // if no table in storage, return 0 + // load from local storage + const data = localStorage.getItem(key); + // if nothing in storage, return a zero + if ( !data ) { return 0; - } else { - fillTable(data); - return 1; - } + }; + // otherwise, fill table in HTML and return success (1) + fillTable(await JSON.parse(data)); + return 1; } diff --git a/src/js/components/table/table.js b/src/js/components/table/table.js index 94f29dd..8ecf231 100644 --- a/src/js/components/table/table.js +++ b/src/js/components/table/table.js @@ -6,8 +6,11 @@ import Columns from './subcomponents/columns.js' 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'; +import { convertToJSON } from "../../utils/JSON_data_handlers.js"; +import Sidebar from '../sidebar/sidebar.js'; +import CurrentFund from '../../models/current_fund.js'; +import CurrentPage from '../../models/current_page.js'; function adjustTableWidth(width_pct){ const table = document.getElementById('main-table'); @@ -31,6 +34,24 @@ function hideTable(){ Buttons.AddRow.hide(); } +function saveTableData() { + // remove the detail text + Tooltip.unlink(); + // get table + var table = document.getElementById('main-table'); + // determine save_as name + if (CurrentFund.number()) { + var save_as = `${CurrentPage.load()}_${CurrentFund.number()}`; + } else { + var save_as = CurrentPage.load(); + } + localStorage.setItem(save_as, convertToJSON(table, ['Edit'])); + // update sidebar with new data + Sidebar.updateTotals(); + // relink, depending on page + Tooltip.linkAll(); +} + const Table = { Buttons : Buttons, Cell : Cell, @@ -45,13 +66,7 @@ const Table = { clear : clearTable, hide : hideTable, show : showTable, - save : async function() { - // remove the detail text - Tooltip.unlink(); - saveTableData(); - // relink, depending on page - Tooltip.linkAll(); - } + save : saveTableData } 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 c86dd88..6967848 100644 --- a/src/js/components/tooltip/tooltip.js +++ b/src/js/components/tooltip/tooltip.js @@ -1,8 +1,9 @@ -import { FISCAL_YEAR } from '../../init'; +import { FISCAL_YEAR } from '../../constants/'; import Cell from '../table/subcomponents/cells'; import { formatCurrency } from '../../utils/common_utils'; +import CurrentPage from '../../models/current_page'; + import './tooltip.css' -import { CurrentFund, CurrentPage } from '../../utils/data_utils/local_storage_handlers'; function hideTooltip() { document.getElementById('tooltip').style.visibility = 'hidden'; diff --git a/src/js/constants/app_constants.js b/src/js/constants/app_constants.js new file mode 100644 index 0000000..225fd4d --- /dev/null +++ b/src/js/constants/app_constants.js @@ -0,0 +1,5 @@ +// temporary hard-coding +export let TARGET = 10000000; + +// Set to equal current fiscal year +export var FISCAL_YEAR = '26'; \ No newline at end of file diff --git a/src/js/constants/excel_constants.js b/src/js/constants/excel_constants.js new file mode 100644 index 0000000..0034e4c --- /dev/null +++ b/src/js/constants/excel_constants.js @@ -0,0 +1,21 @@ +// sheets to expect on detail sheet +export const SHEETS = { + 'FTE, Salary-Wage, & Benefits' : 'personnel' , + 'Overtime & Other Personnel' : 'overtime', + 'Non-Personnel Operating' : 'nonpersonnel', + 'Revenue' : 'revenue' +} + +export const OBJ_CATEGORIES = { + list : [ + // 'Salaries & Wages', + // 'Employee Benefits', + 'Professional & Contractual Services', + 'Operating Supplies', + 'Operating Services', + 'Equipment Acquisition', + 'Capital Outlays', + 'Fixed Charges', + 'Other Expenses' + ] +} \ No newline at end of file diff --git a/src/js/constants/index.js b/src/js/constants/index.js new file mode 100644 index 0000000..06b365c --- /dev/null +++ b/src/js/constants/index.js @@ -0,0 +1,2 @@ +export * from './app_constants'; +export * from './excel_constants'; \ No newline at end of file diff --git a/src/js/init.js b/src/js/init.js index 3be21f6..9e4153f 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -2,21 +2,7 @@ import '../css/common.css'; // import functions -import { CurrentPage } from './utils/data_utils/local_storage_handlers.js'; - -// temporary hard-coding -export let REVENUE = 0; -export let TARGET = 10000000; -// Set to equal current fiscal year -export var FISCAL_YEAR = '26'; - -// sheets to expect on detail sheet -export const SHEETS = { - 'FTE, Salary-Wage, & Benefits' : 'personnel' , - 'Overtime & Other Personnel' : 'overtime', - 'Non-Personnel Operating' : 'nonpersonnel', - 'Revenue' : 'revenue' -} +import CurrentPage from './models/current_page.js'; document.addEventListener('DOMContentLoaded', function () { CurrentPage.visit(); diff --git a/src/js/models/account_string.js b/src/js/models/account_string.js new file mode 100644 index 0000000..cf8095a --- /dev/null +++ b/src/js/models/account_string.js @@ -0,0 +1,43 @@ + + +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) + }, +} + +export default AccountString; \ No newline at end of file diff --git a/src/js/models/baseline.js b/src/js/models/baseline.js new file mode 100644 index 0000000..4532899 --- /dev/null +++ b/src/js/models/baseline.js @@ -0,0 +1,45 @@ +import Fund from "./fund.js"; +import FundLookupTable from "./fund_lookup_table.js"; + + +export class Baseline { + // baseline will just contain a list of funds, each with + // running tallies for their budgets + constructor() { + const allFunds = FundLookupTable.listFunds(); + this.funds = []; + allFunds.forEach((fund) => { + this.funds.push(new Fund(fund)); + }); + } + + personnel() { + let total = 0; + this.funds.forEach(fund => { + total += fund.getPersonnelCost(); + }); + return total; + } + + nonpersonnel() { + let total = 0; + this.funds.forEach(fund => { + total += fund.getNonPersonnelCost(); + }); + return total; + } + + revenue() { + let total = 0; + this.funds.forEach(fund => { + total += fund.getRevenue(); + }); + return total; + } + + total() { + return this.nonpersonnel() + this.personnel() - this.revenue(); + } +} + +export default Baseline; \ No newline at end of file diff --git a/src/js/models/current_fund.js b/src/js/models/current_fund.js new file mode 100644 index 0000000..b040e90 --- /dev/null +++ b/src/js/models/current_fund.js @@ -0,0 +1,19 @@ + +import FundLookupTable from "./fund_lookup_table"; + +export const CurrentFund = { + update : function(fund){ + localStorage.setItem('fund', fund); + }, + number : function(){ + return localStorage.getItem("fund"); + }, + name : function(){ + return FundLookupTable.getName( this.number()); + }, + reset : function() { + this.update(''); + } +} + +export default CurrentFund; \ No newline at end of file diff --git a/src/js/models/current_page.js b/src/js/models/current_page.js new file mode 100644 index 0000000..b3a5144 --- /dev/null +++ b/src/js/models/current_page.js @@ -0,0 +1,16 @@ +import { visitPage } from "../views/view_logic"; + +export const CurrentPage = { + update : function(page){ + localStorage.setItem('page_state', page); + }, + load : function(){ + const pageState = localStorage.getItem('page_state'); + return pageState !== null ? pageState : 'welcome'; + }, + visit : function(){ + visitPage(this.load()); + } +} + +export default CurrentPage; \ No newline at end of file diff --git a/src/js/models/fund.js b/src/js/models/fund.js new file mode 100644 index 0000000..848df5a --- /dev/null +++ b/src/js/models/fund.js @@ -0,0 +1,61 @@ + +import { colSum } from "../utils/common_utils"; +import { FISCAL_YEAR } from '../constants/'; + +// Class to hold information on a specific fund and table +class StoredTable { + constructor(page, fund){ + this.name = `${page}_${fund}`; + this.page = page; + this.table = JSON.parse(localStorage.getItem(this.name)); + } + + totalCol() { + switch(this.page){ + case 'personnel': + return 'Total Cost'; + case 'overtime': + return 'Total Cost (including benefits)'; + case 'nonpersonnel': + return `FY${FISCAL_YEAR} Request`; + case 'revenue': + return `Departmental Request Total`; + default: + break; + } + } + getSum() { + // fill with zero until there is something saved in storage + return colSum(this.table, this.totalCol(), this.name); + } + +} + +// Holds all the detailed data for one fund's budget +export class Fund { + constructor(fund){ + this.fund = fund; + this.personnel = new StoredTable('personnel', fund); + this.overtime = new StoredTable('overtime', fund); + this.nonpersonnel = new StoredTable('nonpersonnel', fund); + this.revenue = new StoredTable('revenue', fund); + } + + getPersonnelCost() { + return this.personnel.getSum() + this.overtime.getSum(); + } + + getNonPersonnelCost() { + return this.nonpersonnel.getSum(); + } + + getRevenue() { + return this.revenue.getSum(); + } + + getTotal() { + return this.getNonPersonnelCost() + this.getPersonnelCost() - this.getRevenue() + } +} + +export default Fund; \ No newline at end of file diff --git a/src/js/utils/data_utils/budget_data_handlers.js b/src/js/models/fund_lookup_table.js similarity index 60% rename from src/js/utils/data_utils/budget_data_handlers.js rename to src/js/models/fund_lookup_table.js index 187a046..37b90dc 100644 --- a/src/js/utils/data_utils/budget_data_handlers.js +++ b/src/js/models/fund_lookup_table.js @@ -1,9 +1,5 @@ -import { CurrentFund } from "./local_storage_handlers"; - -function getUniqueValues(data, key) { - const values = data.map(obj => obj[key]); - return Array.from(new Set(values)); -} +import CurrentFund from "./current_fund.js"; +import { getUniqueValues } from "../utils/common_utils.js"; export const FundLookupTable = { retrieve : function() { @@ -114,66 +110,4 @@ export const FundLookupTable = { } } -// data structure to save the possible service options for the department -export const Services = { - save : function(services){ - localStorage.setItem('services-list', JSON.stringify(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 +export default FundLookupTable \ No newline at end of file diff --git a/src/js/models/index.js b/src/js/models/index.js new file mode 100644 index 0000000..f2025bf --- /dev/null +++ b/src/js/models/index.js @@ -0,0 +1,11 @@ +// models/index.js + +export { default as AccountString } from './account_string.js'; +export { default as Baseline } from './baseline.js'; +export { default as CurrentFund } from './current_fund.js'; +export { default as CurrentPage } from './current_page.js'; +export { default as Fund } from './fund.js'; +export { default as FundLookupTable } from './fund_lookup_table.js'; +export { default as Initiative } from './initiative.js'; +export { default as Services } from './services.js'; +export { default as Supplemental } from './supplemental.js'; diff --git a/src/js/models/initiative.js b/src/js/models/initiative.js new file mode 100644 index 0000000..1d5e3c0 --- /dev/null +++ b/src/js/models/initiative.js @@ -0,0 +1,31 @@ + +// data structure to hold information on new initiatives + +export class Initiative { + + constructor(row) { + this.data = row; + this.name = row['Initiative Name']; + } + + expenses() { + if (this.data['Ballpark Total Expenses']) { + return this.data['Ballpark Total Expenses']; + } else { + return 0; + } + } + + revenue() { + if (this.data['Revenue']) { + return this.data['Revenue']; + } else { + return 0; + } + } + + net() { return this.expenses() - this.revenue() } + +} + +export default Initiative; \ No newline at end of file diff --git a/src/js/models/services.js b/src/js/models/services.js new file mode 100644 index 0000000..9cf60e0 --- /dev/null +++ b/src/js/models/services.js @@ -0,0 +1,11 @@ +// data structure to save the possible service options for the department +export const Services = { + save : function(services){ + localStorage.setItem('services-list', JSON.stringify(services)); + }, + list : function(){ + return JSON.parse(localStorage.getItem('services-list')) || {}; + } +} + +export default Services; \ No newline at end of file diff --git a/src/js/models/supplemental.js b/src/js/models/supplemental.js new file mode 100644 index 0000000..5126324 --- /dev/null +++ b/src/js/models/supplemental.js @@ -0,0 +1,35 @@ + +import Initiative from "./initiative.js"; +import { colSum } from "../utils/common_utils.js"; + +// data structure to hold supplemental requests +export class Supplemental { + constructor() { + this.table = JSON.parse(localStorage.getItem(this.name)); + this.initiatives = []; + if(this.table){ + this.table.forEach((row) => { + this.initiatives.push(new Initiative(row)); + }); + } + } + + getInits() { + return this.table.map((item) => { return item['Initiative Name'] }); + } + + expenses() { + return colSum(this.table, 'Ballpark Total Expenses'); + } + + revenue() { + return colSum(this.table, 'Revenue'); + } + + total(){ + return this.expenses() - this.revenue(); + } + +} + +export default Supplemental; \ No newline at end of file diff --git a/src/js/utils/data_utils/JSON_data_handlers.js b/src/js/utils/JSON_data_handlers.js similarity index 100% rename from src/js/utils/data_utils/JSON_data_handlers.js rename to src/js/utils/JSON_data_handlers.js diff --git a/src/js/utils/data_utils/XLSX_handlers.js b/src/js/utils/XLSX_handlers.js similarity index 95% rename from src/js/utils/data_utils/XLSX_handlers.js rename to src/js/utils/XLSX_handlers.js index 1b16ec3..405dccb 100644 --- a/src/js/utils/data_utils/XLSX_handlers.js +++ b/src/js/utils/XLSX_handlers.js @@ -1,9 +1,10 @@ -import { SHEETS } from '../../init.js'; -import { FundLookupTable, Services } from './budget_data_handlers.js'; -import { removeNewLines } from '../common_utils.js'; -import { Baseline } from './local_storage_handlers.js'; +import { SHEETS } from '../constants/'; +import FundLookupTable from '../models/fund_lookup_table.js'; +import { removeNewLines } from './common_utils.js'; +import Baseline from '../models/baseline.js'; +import Services from '../models/services.js'; function deleteTopRowsUntilFullData(data) { // function to try to find the top of the usable data diff --git a/src/js/utils/common_utils.js b/src/js/utils/common_utils.js index 814fadf..b32552c 100644 --- a/src/js/utils/common_utils.js +++ b/src/js/utils/common_utils.js @@ -44,4 +44,30 @@ export function removeNewLines(str){ str = str.replaceAll(' ', ' '); str = str.replace(/^\s+|\s+$/g, ''); return str; -} \ No newline at end of file +} + +export function colSum(table, colName) { + // fill with zero until there is something saved in storage + if(!table || table == ''){ + return 0; + } + const headers = Object.keys(table[0]); + if (headers.includes(colName)) { + let sum = 0; + for (let i = 0; i < table.length; i++){ + var value = Math.round(parseFloat(table[i][colName])); + // treat NaN (non-numerics) as zeroes + if (value) { sum += value; } + } + return sum; + } else { + // console.error(`Could not find expected total column in saved data for ${name}. Returning 0. See StoredTable.totalCol() switch.`); + return 0; + } + +} + +export function getUniqueValues(data, key) { + const values = data.map(obj => obj[key]); + return Array.from(new Set(values)); +} diff --git a/src/js/utils/data_utils/local_storage_handlers.js b/src/js/utils/data_utils/local_storage_handlers.js deleted file mode 100644 index dd5e9b1..0000000 --- a/src/js/utils/data_utils/local_storage_handlers.js +++ /dev/null @@ -1,241 +0,0 @@ -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"; -import { FundLookupTable } from "./budget_data_handlers.js"; -import { convertToJSON } from "./JSON_data_handlers.js"; - -export const CurrentPage = { - update : function(page){ - localStorage.setItem('page_state', page); - }, - load : function(){ - const pageState = localStorage.getItem('page_state'); - return pageState !== null ? pageState : 'welcome'; - }, - visit : function(){ - visitPage(this.load()); - } -} - -export const CurrentFund = { - update : function(fund){ - localStorage.setItem('fund', fund); - }, - number : function(){ - return localStorage.getItem("fund"); - }, - name : function(){ - return FundLookupTable.getName( this.number()); - }, - reset : function() { - this.update(''); - } -} - -// TODO: consider moving this into a const for Current Table (or to the table component) -export function saveTableData() { - var table = document.getElementById('main-table'); - if (CurrentFund.number()) { - var save_as = `${CurrentPage.load()}_${CurrentFund.number()}`; - } else { - var save_as = CurrentPage.load(); - } - localStorage.setItem(save_as, convertToJSON(table, ['Edit'])); - console.log('saved'); - Sidebar.updateTotals(); -} - -function deleteTable(name){ - localStorage.setItem(name, ''); -} - -export async function deleteAllTables(){ - var funds = await fetchJSON(DATA_ROOT + 'funds.json'); - funds = funds.map((item) => { return item.Name }); - for (const page in PAGES){ - for(const i in funds){ - deleteTable(`${page}_${funds[i]}`); - } - } - deleteTable('new-inits'); -} - -export function loadTableData(name){ - const data = localStorage.getItem(name); - if ( data == '' || data == '[]' ) { - return 0; - }; - return JSON.parse(data); -} - -// Class to hold information on a specific fund and table -class StoredTable { - constructor(page, fund){ - this.name = `${page}_${fund}`; - this.page = page; - this.table = loadTableData(this.name); - } - - totalCol() { - switch(this.page){ - case 'personnel': - return 'Total Cost'; - case 'overtime': - return 'Total Cost (including benefits)'; - case 'nonpersonnel': - return `FY${FISCAL_YEAR} Request`; - case 'revenue': - return `Departmental Request Total`; - default: - break; - } - } - getSum() { - // fill with zero until there is something saved in storage - return colSum(this.table, this.totalCol(), this.name); - } - -} - -function colSum(table, colName) { - // fill with zero until there is something saved in storage - if(!table || table == ''){ - return 0; - } - const headers = Object.keys(table[0]); - if (headers.includes(colName)) { - let sum = 0; - for (let i = 0; i < table.length; i++){ - var value = Math.round(parseFloat(table[i][colName])); - // treat NaN (non-numerics) as zeroes - if (value) { sum += value; } - } - return sum; - } else { - // console.error(`Could not find expected total column in saved data for ${name}. Returning 0. See StoredTable.totalCol() switch.`); - return 0; - } - -} - -// Holds all the detailed data for one fund's budget -export class Fund { - constructor(fund){ - this.fund = fund; - this.personnel = new StoredTable('personnel', fund); - this.overtime = new StoredTable('overtime', fund); - this.nonpersonnel = new StoredTable('nonpersonnel', fund); - this.revenue = new StoredTable('revenue', fund); - } - - getPersonnelCost() { - return this.personnel.getSum() + this.overtime.getSum(); - } - - getNonPersonnelCost() { - return this.nonpersonnel.getSum(); - } - - getRevenue() { - return this.revenue.getSum(); - } - - getTotal() { - return this.getNonPersonnelCost() + this.getPersonnelCost() - this.getRevenue() - } -} - -export class Baseline { - // baseline will just contain a list of funds - constructor() { - const allFunds = FundLookupTable.listFunds(); - this.funds = []; - allFunds.forEach((fund) => { - this.funds.push(new Fund(fund)); - }); - } - - personnel() { - let total = 0; - this.funds.forEach(fund => { - total += fund.getPersonnelCost(); - }); - return total; - } - - nonpersonnel() { - let total = 0; - this.funds.forEach(fund => { - total += fund.getNonPersonnelCost(); - }); - return total; - } - - revenue() { - let total = 0; - this.funds.forEach(fund => { - total += fund.getRevenue(); - }); - return total; - } - - total() { - return this.nonpersonnel() + this.personnel() - this.revenue(); - } -} - -export class Initiative { - constructor(row) { - this.data = row; - this.name = row['Initiative Name']; - } - - expenses() { - if (this.data['Ballpark Total Expenses']) { - return this.data['Ballpark Total Expenses']; - } else { - return 0; - } - } - - revenue() { - if (this.data['Revenue']) { - return this.data['Revenue']; - } else { - return 0; - } - } - - net() { return this.expenses() - this.revenue() } - -} - -export class Supplemental { - constructor() { - this.table = loadTableData('new-inits'); - this.initiatives = []; - if(this.table){ - this.table.forEach((row) => { - this.initiatives.push(new Initiative(row)); - }); - } - } - - getInits() { - return this.table.map((item) => { return item['Initiative Name'] }); - } - - expenses() { - return colSum(this.table, 'Ballpark Total Expenses'); - } - - revenue() { - return colSum(this.table, 'Revenue'); - } - - total(){ - return this.expenses() - this.revenue(); - } - -} diff --git a/src/js/views/00_welcome.js b/src/js/views/00_welcome.js new file mode 100644 index 0000000..c16b037 --- /dev/null +++ b/src/js/views/00_welcome.js @@ -0,0 +1,32 @@ + +import Welcome from '../components/welcome/welcome.js'; +import { View } from './view_class.js'; +import { visitPage } from './view_logic.js'; + +export class WelcomeView extends View { + + constructor() { + super(); + this.page_state = 'welcome'; + this.subtitle = 'Welcome'; + this.sidebar = false; + this.navButtons = false; + } + + visit() { + super.visit(); + + // show welcome section + Welcome.show(); + + // initialize links in buttons + document.getElementById('step-upload').addEventListener('click', () => visitPage('upload')); + document.getElementById('step-initiatives').addEventListener('click', () => visitPage('new-inits')); + document.getElementById('step-revenue').addEventListener('click', () => visitPage('revenue')); + document.getElementById('step-finish').addEventListener('click', () => visitPage('summary')); + + } + +} + +export default WelcomeView; diff --git a/src/js/views/00_welcome/helpers.js b/src/js/views/00_welcome/helpers.js deleted file mode 100644 index e8364d5..0000000 --- a/src/js/views/00_welcome/helpers.js +++ /dev/null @@ -1,24 +0,0 @@ -import Subtitle from '../../components/header/header.js' -import Welcome from '../../components/welcome/welcome.js' -import Body from '../../components/body/body.js' - -import { loadNewInitiatives } from '../07_new_initiatives/main.js' -import { loadSummaryPage } from '../08_summary/main.js' -import { loadBaselineLandingPage } from '../02_baseline_landing_page/main.js' -import { loadUploadPage } from '../01_upload/main.js' - -export function initializePageView(){ - // page set up - Body.reset(); - Subtitle.update("Welcome"); - Welcome.show(); -} - -export function addLinks(){ - // initialize links in buttons - document.getElementById('step-upload').addEventListener('click', loadUploadPage) - document.getElementById('step-initiatives').addEventListener('click', loadNewInitiatives) - document.getElementById('step-revenue').addEventListener('click', loadBaselineLandingPage) - document.getElementById('step-finish').addEventListener('click', loadSummaryPage) - -} diff --git a/src/js/views/00_welcome/main.js b/src/js/views/00_welcome/main.js deleted file mode 100644 index e4f4c11..0000000 --- a/src/js/views/00_welcome/main.js +++ /dev/null @@ -1,11 +0,0 @@ - -import { CurrentPage } from '../../utils/data_utils/local_storage_handlers.js' -import { initializePageView, addLinks } from './helpers.js' - -export function initializeWelcomePage(){ - - CurrentPage.update('welcome'); - initializePageView(); - addLinks(); - -} \ No newline at end of file diff --git a/src/js/views/01_upload.js b/src/js/views/01_upload.js new file mode 100644 index 0000000..9285c74 --- /dev/null +++ b/src/js/views/01_upload.js @@ -0,0 +1,26 @@ +import { View } from './view_class.js' + +import FileUpload from "../components/file_upload/file_upload.js"; +import NavButtons from "../components/nav_buttons/nav_buttons.js"; + +export class UploadView extends View { + + constructor() { + super(); + this.page_state = 'upload'; + this.prompt = `Upload the baseline detail sheet given by your budget analyst.`; + this.subtitle = 'Excel Upload'; + this.sidebar = false; + } + + visit() { + super.visit(); + // disable continue button until Excel file is uploaded and read + NavButtons.Next.disable(); + FileUpload.show(); + FileUpload.init(); + NavButtons.Next.enable(); + } +} + +export default UploadView; \ No newline at end of file diff --git a/src/js/views/01_upload/helpers.js b/src/js/views/01_upload/helpers.js deleted file mode 100644 index 292657f..0000000 --- a/src/js/views/01_upload/helpers.js +++ /dev/null @@ -1,24 +0,0 @@ -import Subtitle from '../../components/header/header.js' -import Prompt from '../../components/prompt/prompt.js' -import NavButtons from '../../components/nav_buttons/nav_buttons.js' -import Body from "../../components/body/body.js"; -import { FileUpload } from '../../components/file_upload/file_upload.js'; - -export function initializePageView() { - - // remove fund selection - localStorage.setItem("fund", ''); - - // prepare page view - Body.reset(); - NavButtons.show(); - FileUpload.show(); - - // update page text - Subtitle.update('Excel Upload'); - Prompt.Text.update(`Upload the baseline detail sheet given by your budget analyst.`); - - // show and initialize file upload; enable continue after file saved in local storage - FileUpload.init(); - NavButtons.Next.enable(); -} \ No newline at end of file diff --git a/src/js/views/01_upload/main.js b/src/js/views/01_upload/main.js deleted file mode 100644 index 102fd45..0000000 --- a/src/js/views/01_upload/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { initializePageView } from "./helpers.js"; - -export function loadUploadPage(){ - //update page state - CurrentPage.update('upload'); - initializePageView(); - -} \ No newline at end of file diff --git a/src/js/views/02_baseline_landing.js b/src/js/views/02_baseline_landing.js new file mode 100644 index 0000000..53f9681 --- /dev/null +++ b/src/js/views/02_baseline_landing.js @@ -0,0 +1,91 @@ +import NavButtons from "../components/nav_buttons/nav_buttons.js"; +import Table from "../components/table/table.js"; +import { View, ViewTable } from './view_class.js' +import CurrentFund from "../models/current_fund.js"; + +export class FundView extends View { + + constructor() { + super(); + this.page_state = 'baseline-landing'; + this.prompt = `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 then click continue.`; + this.subtitle = 'Baseline Budget Request'; + this.table = new FundTable(); + this.sidebar = false; + } + + visit() { + // remove fund selection + localStorage.setItem("fund", ''); + super.visit(); + } +} + +class FundTable extends ViewTable { + + constructor() { + super(); + + // add additional revenue columns to the table + this.columns = [ + { title: 'Fund', className: 'fund-name' } + ]; + + this.noDataMessage = 'No funds found.' + this.addEdit = false; + } + + build(){ + // load in fund data + Table.Data.loadFunds(); + Table.show(); + Table.Columns.assignClasses(this.columns); + Table.adjustWidth('30%'); + + // disable next button until a fund is selected + NavButtons.Next.disable(); + allowRowSelection(); + } +} + + +function allowRowSelection(){ + + var tableRows = document.querySelectorAll("tbody tr"); + + // enable highlight on hover and on select + tableRows.forEach(function(row) { + row.addEventListener('mouseover', function() { + this.classList.add('hover-effect'); + }); + row.addEventListener('mouseout', function() { + this.classList.remove('hover-effect'); + }); + row.addEventListener('click', function() { + selectFund(tableRows, this); + }); + }); +} + +function selectFund(tableRows, selected_row){ + + // remove selected class from any other rows + tableRows.forEach(function(tableRow) { + tableRow.classList = ''; + }); + + // add selected class to clicked row + selected_row.classList.add('selected'); + + // get fund and save selected fund + var fund = selected_row.querySelector('.fund-name').textContent; + var fundNumber = parseInt(fund); + CurrentFund.update(fundNumber); + + // enable next step + NavButtons.Next.enable(); +} + +export default FundView; \ No newline at end of file diff --git a/src/js/views/02_baseline_landing_page/helpers.js b/src/js/views/02_baseline_landing_page/helpers.js deleted file mode 100644 index fc90e45..0000000 --- a/src/js/views/02_baseline_landing_page/helpers.js +++ /dev/null @@ -1,65 +0,0 @@ - -import Subtitle from '../../components/header/header.js' -import Prompt from '../../components/prompt/prompt.js' -import NavButtons from '../../components/nav_buttons/nav_buttons.js' -import Table from "../../components/table/table.js"; -import Body from "../../components/body/body.js"; -import { CurrentFund } from '../../utils/data_utils/local_storage_handlers.js'; - -const fundCols = [ - { title: 'Fund', className: 'fund-name' }, -]; - -export function preparePageView(){ - - CurrentFund.reset(); - - // prepare page view - Body.reset(); - NavButtons.show(); - - // update page text - Subtitle.update('Baseline Budget Request'); - // 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 then click continue.`); -} - -function allowRowSelection(){ - var tableRows = document.querySelectorAll("tbody tr"); - tableRows.forEach(function(row) { - row.addEventListener('mouseover', function() { - this.classList.add('hover-effect'); - }); - row.addEventListener('mouseout', function() { - this.classList.remove('hover-effect'); - }); - row.addEventListener('click', function() { - selectFund(tableRows, this); - }); - }); -} - -export function initializeFundTable(){ - Table.Data.loadFunds(); - Table.adjustWidth('30%'); - Table.show(); - Table.Columns.assignClasses(fundCols); - allowRowSelection(); -} - -function selectFund(tableRows, selected_row){ - // remove selected class from any other rows - tableRows.forEach(function(tableRow) { - tableRow.classList = ''; - }); - // add selected class to clicked row - selected_row.classList.add('selected'); - // get fund and save selected fund - var fund = selected_row.querySelector('.fund-name').textContent; - var fundNumber = parseInt(fund); - CurrentFund.update(fundNumber); - // enable next step - NavButtons.Next.enable(); -} \ No newline at end of file diff --git a/src/js/views/02_baseline_landing_page/main.js b/src/js/views/02_baseline_landing_page/main.js deleted file mode 100644 index 270c9ce..0000000 --- a/src/js/views/02_baseline_landing_page/main.js +++ /dev/null @@ -1,10 +0,0 @@ -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { preparePageView, initializeFundTable } from "../02_baseline_landing_page/helpers.js"; - - -export function loadBaselineLandingPage(){ - //update page state - CurrentPage.update('baseline-landing'); - preparePageView(); - initializeFundTable(); -} diff --git a/src/js/views/03_revenue.js b/src/js/views/03_revenue.js new file mode 100644 index 0000000..9b2b172 --- /dev/null +++ b/src/js/views/03_revenue.js @@ -0,0 +1,40 @@ +import { View, ViewTable } from './view_class.js' + +import Table from '../components/table/table.js'; + +export class RevenueView extends View { + + constructor() { + super(); + this.page_state = 'revenue'; + this.prompt = 'Review and edit revenue line items.'; + this.subtitle = 'Revenues'; + this.table = new RevenueTable(); + } +} + +class RevenueTable extends ViewTable { + + constructor() { + super(); + + // add additional revenue columns to the table + this.columns = this.columns.concat([ + { title: 'Recurring or One-Time', className: 'recurring'}, + { title: 'Object Category', className: 'object-category'}, + { title: 'Departmental Request Total', className: 'request', isCost: true}, + { title: 'Departmental Request Notes', className: 'notes'}, + ]); + + this.noDataMessage = 'No revenues for this fund.' + } + + // action on row edit click: make cells editable + actionOnEdit() { + Table.Cell.createTextbox('request', true); + Table.Cell.createTextbox('notes'); + Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); + } +} + +export default RevenueView; \ No newline at end of file diff --git a/src/js/views/03_revenue/helpers.js b/src/js/views/03_revenue/helpers.js deleted file mode 100644 index a0f7d88..0000000 --- a/src/js/views/03_revenue/helpers.js +++ /dev/null @@ -1,65 +0,0 @@ -import Prompt from '../../components/prompt/prompt.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 Sidebar from '../../components/sidebar/sidebar.js' -import Table from '../../components/table/table.js' -import Tooltip from '../../components/tooltip/tooltip.js' - -const revenueColumns = [ - { title: 'Edit', className : 'edit' }, - { title : 'Account String', className : 'account-string'}, - { title: 'Recurring or One-Time', className: 'recurring'}, - { title: 'Object Category', className: 'object-category'}, - { title: 'Departmental Request Total', className: 'request', isCost: true}, - { title: 'Departmental Request Notes', className: 'notes'}, - - // 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: 'Object Name', className: 'object-name', hide: true} -]; - -export function preparePageView(){ - // prepare page view - Body.reset(); - NavButtons.show(); - Sidebar.show(); - Table.adjustWidth('100%'); - - // update page text - Subtitle.update('Revenues'); - - // set up table - initializeRevTable() - - // enable continue button - NavButtons.Next.enable(); - - Prompt.Text.update('Review and edit revenue line items.'); - -} - -export async function initializeRevTable(){ - // load table data from storage - if(await Table.Data.load()) { - //after table is loaded, fill it - Table.show(); - Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, "Edit"); - // assign cost classes - Table.Columns.assignClasses(revenueColumns); - // enable editing - Table.Buttons.Edit.init(revRowOnEdit, Table.save); - // show info boxes on click - Tooltip.linkAll(); - } else { - Prompt.Text.update('No revenues for this fund.') - } -} - -function revRowOnEdit(){ - // make it editable - Table.Cell.createTextbox('request', true); - Table.Cell.createTextbox('notes'); - Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); -} \ No newline at end of file diff --git a/src/js/views/03_revenue/main.js b/src/js/views/03_revenue/main.js deleted file mode 100644 index 36960e6..0000000 --- a/src/js/views/03_revenue/main.js +++ /dev/null @@ -1,10 +0,0 @@ -import { CurrentPage } from '../../utils/data_utils/local_storage_handlers.js' -import { preparePageView } from './helpers.js' - -export function loadRevenuePage() { - - //update page state - CurrentPage.update('revenue'); - preparePageView(); -} - diff --git a/src/js/views/04_personnel.js b/src/js/views/04_personnel.js new file mode 100644 index 0000000..00deb60 --- /dev/null +++ b/src/js/views/04_personnel.js @@ -0,0 +1,94 @@ +import { View, ViewTable } from './view_class.js' + +import Table from "../components/table/table.js"; +import Form from "../components/form/form.js"; + +import { Services, FundLookupTable } from '../models/'; +import { unformatCurrency } from "../utils/common_utils.js"; + +export class PersonnelView extends View { + + constructor(fiscal_year) { + super(); + this.page_state = 'personnel'; + this.prompt = ` + This table displays the number of FTEs in each job code for in your department's + current (amended) FY25 budget. To make edits to the number of positions, click the + "Edit" button on the row you would like to edit. The "Total Cost" column and the + summary sidebar will also update to reflect any edits.`; + this.subtitle = 'Personnel'; + this.table = new PersonnelTable(fiscal_year); + } +} + + +class PersonnelTable extends ViewTable { + + constructor(fiscal_year) { + super(); + this.fiscal_year = fiscal_year; + // add additional personnel columns to the table + this.columns = this.columns.concat([ + { title: 'Job Title', className: 'job-name' }, + { title: 'Service', className: 'service' }, + { title: `FY${this.fiscal_year} Requested FTE`, className: 'baseline-ftes' }, + { title: `FY${this.fiscal_year} Average Projected Salary/Wage`, className: 'avg-salary', isCost: true }, + { title: 'Total Cost', className: 'total-baseline', isCost: true }, + // hidden columns + { title: 'Fringe Benefits Rate', className: 'fringe', 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${this.fiscal_year-2}`, className: 'current-salary', isCost: true, hide: true} + ]); + + this.noDataMessage = 'No personnel expenditures for this fund.' + this.addButtonText = 'Add new job' ; + } + + // action on row edit click: make cells editable + actionOnEdit() { + Table.Cell.createTextbox('baseline-ftes'); + Table.Cell.createServiceDropdown(Services.list()); + } + + updateTable(){ + // calculate for each row + let rows = document.getElementsByTagName('tr'); + 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 = avg_salary * baseline_ftes * (1 + fringe); + + // update total column + Table.Cell.updateValue(rows[i], 'total-baseline', total_baseline_cost); + } + + // Save the table after all updates are done + Table.save(); + } + + addCustomQuestions(){ + // form questions to add a new job + 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 ${this.fiscal_year}:`, 'avg-salary', true); + Form.NewField.shortText(`Expected fringe rate (as a percentage)`, 'fringe', true); + } + + editColumns(responses){ + responses = super.editColumns(responses); + // edit inputs from modal + responses['avg-salary'] = unformatCurrency(responses['avg-salary']); + responses['fringe'] = parseFloat(responses['fringe']) / 100; + return responses; + } +} + +export default PersonnelView; \ No newline at end of file diff --git a/src/js/views/04_personnel/helpers.js b/src/js/views/04_personnel/helpers.js deleted file mode 100644 index d63fb8d..0000000 --- a/src/js/views/04_personnel/helpers.js +++ /dev/null @@ -1,146 +0,0 @@ - -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"; -import Form from "../../components/form/form.js"; -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 { AccountString, FundLookupTable, Services } from "../../utils/data_utils/budget_data_handlers.js"; -import { unformatCurrency } from "../../utils/common_utils.js"; - -export function preparePageView(){ - // prepare page view - Body.reset(); - NavButtons.show(); - Sidebar.show(); - 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(` - This table displays the number of FTEs in each job code for in your department's - current (amended) FY25 budget. To make edits to the number of positions, click the - "Edit" button on the row you would like to edit. The "Total Cost" column and the - summary sidebar will also update to reflect any edits. - `); -} - -function assignClasses() { - // record columns and their classes - const personnelColumns = [ - { title: 'Job Title', className: 'job-name' }, - { 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' }, - // 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}, - ]; - - // assign cost classes - Table.Columns.assignClasses(personnelColumns) -} - -function personnelRowOnEdit(){ - Table.Cell.createTextbox('baseline-ftes'); - Table.Cell.createServiceDropdown(Services.list()); -} - -export async function initializePersonnelTable(){ - // load table data from local storage - if(await Table.Data.load()) { - //after table is loaded, show it - Table.show(); - 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(personnelRowOnEdit, updateDisplayandTotals); - } else { - Prompt.Text.update('No personnel expenditures for this fund.') - } -} - -// update sidebar and also cost totals when the FTEs are edited -function updateDisplayandTotals(){ - // calculate for each row - let rows = document.getElementsByTagName('tr'); - 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 = avg_salary * baseline_ftes * (1 + fringe); - - // update total column - Table.Cell.updateValue(rows[i], 'total-baseline', total_baseline_cost); - } - - // Save the table after all updates are done - Table.save(); - -} - -export function setUpModal() { - // Initialize modal - Modal.clear(); - Modal.Link.add('add-btn'); - Modal.Title.update('New job'); -} - -export function setUpForm() { - // Set up form - Form.new('modal-body'); - 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); -} - -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.save(); - initializePersonnelTable(); - - } - -} diff --git a/src/js/views/04_personnel/main.js b/src/js/views/04_personnel/main.js deleted file mode 100644 index e567206..0000000 --- a/src/js/views/04_personnel/main.js +++ /dev/null @@ -1,13 +0,0 @@ -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { preparePageView, initializePersonnelTable, setUpModal, setUpForm } from "./helpers.js"; - -export function loadPersonnelPage(){ - - CurrentPage.update('personnel'); - preparePageView(); - initializePersonnelTable(); - - setUpModal(); - setUpForm(); -} - diff --git a/src/js/views/05_overtime.js b/src/js/views/05_overtime.js new file mode 100644 index 0000000..44a9237 --- /dev/null +++ b/src/js/views/05_overtime.js @@ -0,0 +1,95 @@ + + +import { View, ViewTable } from './view_class.js' +import Table from '../components/table/table.js'; +import Form from '../components/form/form.js'; + +import { FundLookupTable, Services } from '../models/'; +import { unformatCurrency } from '../utils/common_utils.js'; + +export class OvertimeView extends View { + + constructor() { + super(); + this.page_state = 'overtime'; + this.prompt = ` + Please see your baseline overtime / holiday pay / shift premiums in the table below. + Make any edits and continue.`; + this.subtitle = 'Overtime Estimates'; + this.table = new OvertimeTable(); + } +} + +class OvertimeTable extends ViewTable { + + constructor() { + super(); + + // add additional OT columns to the table + this.columns = this.columns.concat([ + { 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}, + // hidden columns + { title: 'FICA Rate', className: 'fica', hide: true}, + ]); + + this.noDataMessage = 'No overtime expenditures for this fund.' + this.addButtonText = 'Add new cost center' ; + } + + // action on row edit click: make cells editable + actionOnEdit() { + Table.Cell.createTextbox('OT-wages', true); + Table.Cell.createTextbox('OT-salary', true); + Table.Cell.createServiceDropdown(Services.list()); + Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); + } + + updateTable(){ + + function calculateTotalCost(salary, wages, fica_rate){ + fica_rate = parseFloat(fica_rate); + return (wages + salary) * (1 + fica_rate) ; + } + + // calculate for each row + let rows = document.getElementsByTagName('tr'); + for (let i = 1; i < rows.length; i++){ + // 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, fica_rate); + + // update total + Table.Cell.updateValue(rows[i], 'total', row_total); + } + + // Save the table after all updates are done + Table.save(); + } + + addCustomQuestions(){ + // form questions to add a new job + 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); + } + + editColumns(responses){ + responses = super.editColumns(responses); + // edit inputs from modal + responses['OT-wages'] = unformatCurrency(responses['OT-wages']); + responses['fica'] = 0.0765; + return responses; + } +} + +export default OvertimeView; \ No newline at end of file diff --git a/src/js/views/05_overtime/helpers.js b/src/js/views/05_overtime/helpers.js deleted file mode 100644 index 189878b..0000000 --- a/src/js/views/05_overtime/helpers.js +++ /dev/null @@ -1,156 +0,0 @@ - -import Prompt from '../../components/prompt/prompt.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 Sidebar from '../../components/sidebar/sidebar.js'; -import Table from '../../components/table/table.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 - Body.reset(); - NavButtons.show(); - Sidebar.show(); - - // enable next button - NavButtons.Next.enable(); - - // update page text - Subtitle.update('Overtime Estimates'); - - // activate table - 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() { - // record columns and their classes - const OT_cols = [ - // { 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'}, - // 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 - Table.Columns.assignClasses(OT_cols) -} - -function OTRowOnEdit(){ - Table.Cell.createTextbox('OT-wages', true); - Table.Cell.createTextbox('OT-salary', true); - Table.Cell.createServiceDropdown(Services.list()); - Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); -} - -export async function initializeOTTable(){ - // load table data from local storage - if(await Table.Data.load()) { - //after table is loaded, fill it - Table.show(); - 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); - } else { - Prompt.Text.update('No overtime expenditures for this fund.') - } -} - -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 -function updateDisplayandTotals(){ - // calculate for each row - let rows = document.getElementsByTagName('tr'); - for (let i = 1; i < rows.length; i++){ - // 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, fica_rate); - - // update total - Table.Cell.updateValue(rows[i], 'total', row_total); - - //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/05_overtime/main.js b/src/js/views/05_overtime/main.js deleted file mode 100644 index 1be9a1b..0000000 --- a/src/js/views/05_overtime/main.js +++ /dev/null @@ -1,10 +0,0 @@ - -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { preparePageView } from './helpers.js'; - -export function loadOTPage(){ - //update page state - CurrentPage.update('overtime'); - preparePageView(); - -} \ No newline at end of file diff --git a/src/js/views/06_nonpersonnel.js b/src/js/views/06_nonpersonnel.js new file mode 100644 index 0000000..16bbeab --- /dev/null +++ b/src/js/views/06_nonpersonnel.js @@ -0,0 +1,72 @@ +import { View, ViewTable } from './view_class.js' +import Form from '../components/form/form.js'; +import Table from '../components/table/table.js'; +import { FundLookupTable, Services } from '../models/'; +import { unformatCurrency } from '../utils/common_utils.js'; +import { OBJ_CATEGORIES } from '../constants/'; + +export class NonPersonnelView extends View { + + constructor(fiscal_year) { + super(); + this.page_state = 'nonpersonnel'; + this.prompt = 'Review and edit non-personnel line items.'; + this.subtitle = 'Non-Personnel'; + this.table = new NonPersonnelTable(fiscal_year); + } +} + +class NonPersonnelTable extends ViewTable { + + constructor(fiscal_year) { + super(); + + // add additional personnel columns to the table + this.columns = this.columns.concat([ + { title: `FY${fiscal_year} Request`, className: 'request', isCost: true }, + { title: 'Service', className : 'service' }, + { title: 'Recurring or One-Time', className: 'recurring'}, + { title : 'CPA #', className : 'cpa'}, + // hidden columns + { 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} + ]); + + this.noDataMessage = 'No personnel expenditures for this fund.' + this.addButtonText = 'Add new job' ; + } + + // action on row edit click: make cells editable + actionOnEdit() { + Table.Cell.createTextbox('request', true); + Table.Cell.createServiceDropdown(); + Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); + } + + addCustomQuestions(){ + // form questions to add a new row + 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', OBJ_CATEGORIES.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:', 'cpa-description', true); + Form.NewField.dropdown('Recurring or One-Time', 'recurring', ['Recurring', 'One-Time'], true); + Form.NewField.shortText('Amount requested:', 'request', true); + } + + editColumns(responses){ + responses = super.editColumns(responses); + responses['avg-salary'] = unformatCurrency(responses['avg-salary']); + responses['fringe'] = parseFloat(responses['fringe']) / 100; + return responses; + } +} + +export default NonPersonnelView; \ No newline at end of file diff --git a/src/js/views/06_nonpersonnel/helpers.js b/src/js/views/06_nonpersonnel/helpers.js deleted file mode 100644 index c6ea724..0000000 --- a/src/js/views/06_nonpersonnel/helpers.js +++ /dev/null @@ -1,130 +0,0 @@ -import Prompt from "../../components/prompt/prompt.js"; -import Sidebar from "../../components/sidebar/sidebar.js"; -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 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 }, - { title: 'Service', className : 'service' }, - { title: 'Edit', className : 'edit' }, - { title : 'Account String', className : 'account-string'}, - { title: 'Recurring or One-Time', className: 'recurring'}, - - { 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: '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} -]; - -export function preparePageView(){ - // prepare page view - Body.reset(); - NavButtons.show(); - Sidebar.show(); - Table.adjustWidth('100%'); - // update page text - 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(){ - // load table data from storage - if(await Table.Data.load()) { - //after table is loaded, fill it - Table.show(); - Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, "Edit"); - // assign cost classes - Table.Columns.assignClasses(nonPersonnelColumns); - // enable editing - Table.Buttons.Edit.init(nonPersonnelRowOnEdit, Table.save); - // show detail buttons - Tooltip.linkAll(); - } else { - Prompt.Text.update('No non-personnel expenditures for this fund.') - } -} - -function nonPersonnelRowOnEdit(){ - // make it editable - Table.Cell.createTextbox('request', true); - Table.Cell.createServiceDropdown(); - 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/06_nonpersonnel/main.js b/src/js/views/06_nonpersonnel/main.js deleted file mode 100644 index 2976c97..0000000 --- a/src/js/views/06_nonpersonnel/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { preparePageView, initializeNonpersonnelTable } from "./helpers.js"; - -export function loadNonpersonnelPage(){ - - CurrentPage.update('nonpersonnel'); - preparePageView(); - initializeNonpersonnelTable() -} diff --git a/src/js/views/07_new_initiatives.js b/src/js/views/07_new_initiatives.js new file mode 100644 index 0000000..421f1ce --- /dev/null +++ b/src/js/views/07_new_initiatives.js @@ -0,0 +1,92 @@ + +import { View, ViewTable } from './view_class.js' +import Table from "../components/table/table.js"; +import Form from "../components/form/form.js"; +import FundLookupTable from '../models/fund_lookup_table.js'; +import { FISCAL_YEAR } from '../constants/'; + + +const dropdownOptions = ['N/A', 'One-Time', 'Recurring'] + +// set up page and initialize all buttons +export class InitiativesView extends View { + + constructor() { + super(); + this.page_state = 'new-inits'; + this.prompt = ` + This is the place to propose new initiatives for FY${FISCAL_YEAR}. + 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.`; + this.subtitle = 'New Initiatives'; + this.table = new InitiativesTable(); + } + + visit() { + super.visit(); + // remove fund selection + localStorage.setItem("fund", ''); + } + +} + +class InitiativesTable extends ViewTable { + + constructor() { + super(); + + // add additional columns to the table + this.columns = this.columns.concat([ + { title: 'Initiative Name', className: 'init-name' }, + { 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' }, + + // hide the explanation columns + { title: 'Q1', className: 'q1', hide: true }, + { title: 'Q2', className: 'q2', hide: true }, + { title: 'Q3', className: 'q3', hide: true } + ]); + + this.addButtonText = 'Add new initiative' ; + } + + addCustomQuestions(){ + + // general questions + 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); + + // TODO: Edit to drop down + 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?', + '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?`, + 'rev-type', dropdownOptions); + } + + // action on row edit click: make cells editable + actionOnEdit() { + Table.Cell.createTextbox('total', true); + Table.Cell.createTextbox('revenue', true); + Table.Cell.createTextbox('personnel', true); + Table.Cell.createTextbox('nonpersonnel', true); + Table.Cell.createTextbox('init-name'); + Table.Cell.createDropdown('rev-type', dropdownOptions); + } + +} + +export default InitiativesView; \ No newline at end of file diff --git a/src/js/views/07_new_initiatives/helpers.js b/src/js/views/07_new_initiatives/helpers.js deleted file mode 100644 index a3d1723..0000000 --- a/src/js/views/07_new_initiatives/helpers.js +++ /dev/null @@ -1,169 +0,0 @@ - -import Prompt from '../../components/prompt/prompt.js' -import Modal from '../../components/modal/modal.js' -import Form from '../../components/form/form.js' -import Table from '../../components/table/table.js' -import Body from '../../components/body/body.js' -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(); - NavButtons.show(); - Sidebar.show(); - NavButtons.Next.enable(); - - //table appearance - Table.adjustWidth('100%'); - Table.Buttons.AddRow.updateText('Add new initiative'); - - // remove fund selection - localStorage.setItem("fund", ''); - - // Load text - Subtitle.update('New Initiatives'); - Prompt.Text.update('This is the place to propose new initiatives for FY26. ' + explanation); - NavButtons.Next.enable - // Prompt.Buttons.Left.updateText('Yes, propose a new initiative'); - // Prompt.Buttons.Right.updateText('No new initiatives'); - // clicking 'no new initialitives' will also take us to the next page - Table.Buttons.AddRow.show(); - // Prompt.Buttons.Right.addAction(nextPage); - // Prompt.Buttons.Left.addAction(NavButtons.Next.enable); -} - -export function setUpModal() { - // Initialize modal - Modal.clear(); - Modal.Link.add('option1'); - Modal.Title.update('New initiative'); - Modal.Link.add('add-btn'); -} - -export function setUpForm() { - // Set up form - Form.new('modal-body'); - - // general questions - 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); - - // TODO: Edit to drop down - 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?', - '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?`, - 'rev-type', dropdownOptions); - - - Form.SubmitButton.add(); - // Initialize form submission to table data - Modal.Submit.init(submitNewRow); -} - -function assignClasses() { - // assign cost classes - Table.Columns.assignClasses(initiativesCols) -} - -export async function initializeInitTable(){ - - // load table data from storage - if(await Table.Data.load()) { - // after table is loaded, fill it - Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, "Edit"); - assignClasses(); - // show table - Table.show(); - // enable editing - Table.Buttons.Edit.init(rowOnEdit, Table.save); - } else { - Table.clear(); - console.log('no data'); - } -} - -function rowOnEdit(){ - Table.Cell.createTextbox('total', true); - Table.Cell.createTextbox('revenue', true); - Table.Cell.createTextbox('personnel', true); - Table.Cell.createTextbox('nonpersonnel', true); - Table.Cell.createTextbox('init-name'); - Table.Cell.createDropdown('rev-type', dropdownOptions); -} - -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, initiativesCols); - Table.save(); - initializeInitTable(); - Table.Buttons.AddRow.updateText('Add another new initiative'); - } -} - -export function removeModalLinks(){ - Modal.Link.remove('option1'); - Modal.Link.remove('add-btn'); -} - -export function removePromptButtonListeners(){ - Prompt.Buttons.Right.removeAction(nextPage); - Prompt.Buttons.Left.removeAction(NavButtons.Next.enable); - Modal.clear(); -} \ No newline at end of file diff --git a/src/js/views/07_new_initiatives/main.js b/src/js/views/07_new_initiatives/main.js deleted file mode 100644 index 90bf05c..0000000 --- a/src/js/views/07_new_initiatives/main.js +++ /dev/null @@ -1,19 +0,0 @@ - -import { initializePageView, setUpModal, setUpForm, removeModalLinks, removePromptButtonListeners, initializeInitTable } from './helpers.js' -import { CurrentPage } from '../../utils/data_utils/local_storage_handlers.js' - - -// set up page and initialize all buttons -export function loadNewInitiatives() { - CurrentPage.update('new-inits'); - initializePageView(); - setUpModal(); - setUpForm(); - initializeInitTable(); -} - -export function cleanUpInitiativesPage() { - removeModalLinks(); - // remove event listeners on prompt buttons - removePromptButtonListeners(); -} \ No newline at end of file diff --git a/src/js/views/08_summary.js b/src/js/views/08_summary.js new file mode 100644 index 0000000..dabcc18 --- /dev/null +++ b/src/js/views/08_summary.js @@ -0,0 +1,71 @@ +import CurrentFund from '../models/current_fund.js'; +import Baseline from '../models/baseline.js'; +import { Accordion } from "../components/accordion/accordion.js"; +import { visitPage } from "./view_logic.js"; +import { formatCurrency } from '../utils/common_utils.js'; +import { View } from "./view_class.js"; +import Prompt from "../components/prompt/prompt.js"; +import { downloadXLSX } from "../utils/XLSX_handlers.js"; +import WelcomeView from './00_welcome.js'; +import { TARGET } from '../constants/app_constants.js'; + +export function compareToTarget(){ + const baseline = new Baseline; + if (baseline.total() <= TARGET){ + Prompt.Text.update(`Congrats! Your budget is below your target! + Edit any line items below or download your completed Excel.`); + } else { + Prompt.Text.update(`Your budget is above your target of ${formatCurrency(TARGET)}. + Please expand the summary table below and edit line items until you meet your target. + When you meet the target, you will be able to download the Excel sheet.`); + Prompt.Buttons.Right.disable(); + } + Prompt.show(); +} + +function returnToWelcome() { + const welcome = new WelcomeView(); + const left = document.getElementById('option1'); + console.log(left.l) + welcome.visit(); +}; + +export class SummaryView extends View { + + constructor() { + super(); + this.page_state = 'summary'; + this.subtitle = 'Summary'; + this.sidebar = false; + } + + visit() { + super.visit(); + + // reset fund + CurrentFund.reset(); + + // show summary accordion + Accordion.build(); + Accordion.show(); + + // add prompt buttons + Prompt.Buttons.Right.updateText('Download Excel'); + Prompt.Buttons.Left.updateText('Start over with new Excel upload'); + // add button links + Prompt.Buttons.Left.addAction(returnToWelcome); + Prompt.Buttons.Right.addAction(downloadXLSX); + + // update prompt text depending on target matching + compareToTarget(); + } + + cleanup() { + // delete event listeners + Prompt.Buttons.Left.removeAction(returnToWelcome); + Prompt.Buttons.Right.removeAction(downloadXLSX); + Prompt.Buttons.Right.enable(); + } +} + +export default SummaryView; \ No newline at end of file diff --git a/src/js/views/08_summary/helpers.js b/src/js/views/08_summary/helpers.js deleted file mode 100644 index 4c07102..0000000 --- a/src/js/views/08_summary/helpers.js +++ /dev/null @@ -1,55 +0,0 @@ - -import Prompt from '../../components/prompt/prompt.js' -import Body from "../../components/body/body.js"; -import Subtitle from "../../components/header/header.js"; -import { visitPage } from "../view_logic.js"; -import { Accordion } from "../../components/accordion/accordion.js"; -import { downloadXLSX } from "../../utils/data_utils/XLSX_handlers.js"; -import { Baseline, CurrentFund } from '../../utils/data_utils/local_storage_handlers.js'; -import { TARGET } from '../../init.js'; -import { formatCurrency } from '../../utils/common_utils.js'; - -export function summaryView(){ - - // show/hide elements - Body.reset(); - Accordion.build(); - Accordion.show(); - - // set fund to none - CurrentFund.reset(); - - // prompt buttons - Prompt.Buttons.Right.updateText('Download Excel'); - 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() -} - -function compareToTarget(){ - const baseline = new Baseline; - if (baseline.total() <= TARGET){ - Prompt.Text.update(`Congrats! Your budget is below your target! - Edit any line items below or download your completed Excel.`); - Prompt.show(); - } else { - Prompt.Text.update(`Your budget is above your target of ${formatCurrency(TARGET)}. - Please expand the summary table below and edit line items until you meet your target. - When you meet the target, you will be able to download the Excel sheet.`); - Prompt.Buttons.Right.disable(); - Prompt.show(); - } -} - -const returnToWelcome = () => {visitPage('welcome')} - -export function disablePromptButtons(){ - Prompt.Buttons.Left.removeAction(returnToWelcome); - Prompt.Buttons.Right.removeAction(downloadXLSX); - Prompt.Buttons.Right.enable(); -} \ No newline at end of file diff --git a/src/js/views/08_summary/main.js b/src/js/views/08_summary/main.js deleted file mode 100644 index 655c035..0000000 --- a/src/js/views/08_summary/main.js +++ /dev/null @@ -1,13 +0,0 @@ -import { CurrentPage } from "../../utils/data_utils/local_storage_handlers.js"; -import { summaryView, disablePromptButtons } from "./helpers.js"; - -export function loadSummaryPage(){ - //update page state - CurrentPage.update('summary'); - summaryView(); -} - -export function cleanUpSummaryPage(){ - disablePromptButtons(); -} - diff --git a/src/js/views/view_class.js b/src/js/views/view_class.js new file mode 100644 index 0000000..6a4b0d3 --- /dev/null +++ b/src/js/views/view_class.js @@ -0,0 +1,204 @@ +import Prompt from "../components/prompt/prompt.js"; +import Sidebar from "../components/sidebar/sidebar.js"; +import NavButtons from "../components/nav_buttons/nav_buttons.js"; +import Body from "../components/body/body.js"; +import Subtitle from "../components/header/header.js"; +import Table from "../components/table/table.js"; +import Form from "../components/form/form.js"; +import Modal from "../components/modal/modal.js"; + +import { CurrentPage, AccountString } from '../models/' + + +export class View { + + constructor() { + // page state in local storage + this.page_state = ''; + + // whether to display + this.navButtons = true; + this.sidebar = true; + + // text to show in the prompt area + this.prompt = null; + + // subtitle text + this.subtitle = ''; + + // table object of class ViewTable or null + this.table = null; + } + + visit() { + // update page state + CurrentPage.update(this.page_state); + + // start with a blank page + Body.reset(); + + // default to showing navbuttons + if (this.navButtons) { NavButtons.show(); }; + + // default to showing sidebar + if (this.sidebar) { Sidebar.show() }; + + // initialize prompt text and buttons + if (this.prompt) { Prompt.Text.update(this.prompt) }; + + // initialize table + if (this.table) { this.table.build(); } + + // show page subtitle + if (this.subtitle) { Subtitle.update(this.subtitle) }; + } + + cleanup() { + if (this.table) { Table.clear() } + } + +} + +export class ViewTable { + + constructor(){ + // Ensure methods retain the correct `this` context + this.submitNewRow = this.submitNewRow.bind(this); + + this.columns = [ + { title: 'Account String', className: 'account-string' }, + { 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: 'Fund Name', className: 'fund-name', hide: true }, + { title: 'Fund', className: 'fund', hide: true }, + { title: 'Edit', className: 'edit' }, + ]; + + // whether to add an edit column + this.addEdit = true ; + + // message to show if there's no saved data + this.noDataMessage = null; + + // text to show for new row button + this.addButtonText = null ; + } + + async build() { + // build table from local storage and initialize edit buttons + + // add the add new row button if needed + if (this.addButtonText) { + this.setUpForm(); + } + + // delete any residual data + // TODO: delete + Table.clear(); + + // fill with new data from local storage + if(await Table.Data.load()) { + + //after table is loaded, show it + Table.show(); + + // add an edit column if needed + if (this.addEdit) { + Table.Columns.addAtEnd(Table.Buttons.edit_confirm_btns, 'Edit'); + // activate edit buttons + Table.Buttons.Edit.init(this.actionOnEdit, this.updateTable); + } + + // assign the correct classes based on the table columns + Table.Columns.assignClasses(this.columns); + + // Apply any update function to make sure sidebar is up to date + this.updateTable(); + + } else { + + // show a message if there's no saved table data for the selected fund + if (this.noDataMessage) { + Prompt.Text.update(this.noDataMessage); + } + } + } + + // placeholder for action on row edit click + actionOnEdit() { return } + + // update function for the sidebar; default to just saving the table + updateTable() { Table.save() } + + // extra questions of the form to add a new row + addCustomQuestions() { return }; + + setUpForm() { + // show add button + Table.Buttons.AddRow.show(); + Table.Buttons.AddRow.updateText(this.addButtonText); + + // set up modal for form when add button is pressed + Modal.clear(); + Modal.Link.add('add-btn'); + Modal.Title.update(this.addButtonText); + + // create form + Form.new('modal-body'); + + // add custom questions + this.addCustomQuestions(); + // add submit button + Form.SubmitButton.add(); + + // Initialize form submission to table data + Modal.Submit.init(this.submitNewRow); + } + + editColumns(responses) { + // get numbers from account string names + if(responses['fund-name']){ + responses['fund'] = AccountString.getNumber(responses['fund-name']); + }; + if(responses['approp-name']){ + responses['approp'] = AccountString.getNumber(responses['approp-name']); + }; + if(responses['cc-name']){ + responses['cc'] = AccountString.getNumber(responses['cc-name']); + }; + if(responses['object-name']){ + responses['object'] = AccountString.getNumber(responses['object-name']); + }; + responses['account-string'] = + AccountString.build(responses['approp-name'], + responses['cc-name'], + responses['object-name'], + responses['fund']); + return responses; + } + + submitNewRow(event) { + // get answers from form, hide form, show answers in table + var responses = Form.fetchAllResponses(event); + + // edit inputs from modal + responses = this.editColumns(responses); + + // 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, this.columns); + Table.save(); + + // rebuild table + this.build(); + } + } + +} \ No newline at end of file diff --git a/src/js/views/view_logic.js b/src/js/views/view_logic.js index 4d20432..d72d739 100644 --- a/src/js/views/view_logic.js +++ b/src/js/views/view_logic.js @@ -1,54 +1,57 @@ -import { initializeWelcomePage } from './00_welcome/main.js'; -import { cleanUpInitiativesPage, loadNewInitiatives } from './07_new_initiatives/main.js' -import { loadRevenuePage, cleanupRevenuePage } from './03_revenue/main.js' -import { loadPersonnelPage } from './04_personnel/main.js'; -import { loadOTPage } from './05_overtime/main.js'; -import { loadNonpersonnelPage } from './06_nonpersonnel/main.js'; -import { loadBaselineLandingPage } from './02_baseline_landing_page/main.js'; -import { cleanUpSummaryPage, loadSummaryPage } from './08_summary/main.js'; -import { loadUploadPage } from './01_upload/main.js'; -import { CurrentPage, CurrentFund } from '../utils/data_utils/local_storage_handlers.js'; -import { FundLookupTable } from '../utils/data_utils/budget_data_handlers.js'; +import WelcomeView from './00_welcome.js'; +import UploadView from './01_upload.js'; +import FundView from './02_baseline_landing.js'; +import RevenueView from './03_revenue.js'; +import PersonnelView from './04_personnel.js'; +import OvertimeView from './05_overtime.js'; +import NonPersonnelView from './06_nonpersonnel.js'; +import InitiativesView from './07_new_initiatives.js'; +import SummaryView from './08_summary.js'; -export let PAGES = { - 'welcome' : initializeWelcomePage, - 'upload' : loadUploadPage, - 'baseline-landing' : loadBaselineLandingPage, - 'revenue' : loadRevenuePage, - 'personnel' : loadPersonnelPage, - 'overtime' : loadOTPage, - 'nonpersonnel' : loadNonpersonnelPage, - 'new-inits' : loadNewInitiatives, - 'summary' : loadSummaryPage -} +import { FundLookupTable, CurrentFund, CurrentPage } from '../models/'; +import { FISCAL_YEAR } from '../constants/'; -export let CLEANUP = { - 'new-inits' : cleanUpInitiativesPage, - 'summary' : cleanUpSummaryPage +export function initializePages() { + const PAGES = { + 'welcome': new WelcomeView(), + 'upload': new UploadView(), + 'baseline-landing': new FundView(), + 'revenue': new RevenueView(), + 'personnel': new PersonnelView(FISCAL_YEAR), + 'overtime': new OvertimeView(), + 'nonpersonnel': new NonPersonnelView(FISCAL_YEAR), + 'new-inits': new InitiativesView(), + 'summary': new SummaryView(), + }; + return PAGES; } export function visitPage(new_page_key){ + + const PAGES = initializePages(); + // clean up from current page var page_state = CurrentPage.load(); - if (CLEANUP[page_state]) { CLEANUP[page_state]() }; + PAGES[page_state].cleanup(); + // Use the page_state to access and call the corresponding function from PAGES if (PAGES[new_page_key]) { - PAGES[new_page_key](); // Invokes the function if it exists in the PAGES map + // Invokes the function if it exists in the PAGES map + PAGES[new_page_key].visit(); } else { console.error(`No page initializer found for state: ${new_page_key}`); }} export function nextPage(){ + const PAGES = initializePages(); + var page_state = CurrentPage.load(); const keys = Object.keys(PAGES); // Find the index of the current key const currentIndex = keys.indexOf(page_state); - // clean up current page - if (CLEANUP[page_state]) { CLEANUP[page_state]() }; - // unless on personnel (which will go to overtime), return to summary if all funds are viewed const returnPages = ['revenue', 'nonpersonnel', 'new-inits', 'overtime']; if (!FundLookupTable.fundsLeft() && returnPages.includes(CurrentPage.load())) { @@ -77,15 +80,14 @@ export function nextPage(){ export function lastPage(){ + const PAGES = initializePages(); + var page_state = CurrentPage.load(); const keys = Object.keys(PAGES); // Find the index of the current key const currentIndex = keys.indexOf(page_state); - // 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');