diff --git a/sample_data/FY26 Detail Sheet - v3.0 Prototype.xlsx b/sample_data/FY26 Detail Sheet - v3.0 Prototype.xlsx new file mode 100644 index 0000000..b4e24f8 Binary files /dev/null and b/sample_data/FY26 Detail Sheet - v3.0 Prototype.xlsx differ diff --git a/src/js/components/accordion/accordion.js b/src/js/components/accordion/accordion.js index a6b7228..f340719 100644 --- a/src/js/components/accordion/accordion.js +++ b/src/js/components/accordion/accordion.js @@ -5,7 +5,6 @@ import {Baseline, CurrentFund, Fund, Supplemental, FundLookupTable} from '../../ import { formatCurrency, cleanString } from "../../utils/common_utils.js"; import Table from "../table/table.js"; import { visitPage } from '../../views/view_logic.js'; -import { TARGET } from '../../constants/'; function redirectForEdit(){ const row = document.querySelector(`.active-editing`); @@ -161,7 +160,7 @@ export const Accordion = { const suppAmount = document.querySelector('#supp-title .top-line-amount') suppAmount.textContent = formatCurrency(supp.total()); // color-code baseline - if (baseline.total() <= TARGET){ + if (baseline.total() <= Baseline.target()){ baselineAmount.style.color = 'green'; } else { baselineAmount.style.color = 'red'; diff --git a/src/js/components/form/subcomponents/dropdown.js b/src/js/components/form/subcomponents/dropdown.js index 411f70f..ad611b6 100644 --- a/src/js/components/form/subcomponents/dropdown.js +++ b/src/js/components/form/subcomponents/dropdown.js @@ -3,6 +3,9 @@ function createDropdown(dataArray) { // Creating a select element const selectElement = document.createElement('select'); + // add a default blank option to the dataArray + dataArray = [''].concat(dataArray); + // Looping through the array and creating an option for each element dataArray.forEach(item => { const optionElement = document.createElement('option'); diff --git a/src/js/components/sidebar/sidebar.js b/src/js/components/sidebar/sidebar.js index bc21eef..03bac8a 100644 --- a/src/js/components/sidebar/sidebar.js +++ b/src/js/components/sidebar/sidebar.js @@ -1,7 +1,6 @@ import './sidebar.css' import { formatCurrency } from "../../utils/common_utils.js"; -import { TARGET } from '../../constants/'; import {Baseline, Supplemental} from '../../models/'; @@ -28,7 +27,7 @@ function showSidebar() { header.style.width = `${contentWidth - parseInt(sideBarWidth, 10)}px`; // add target to sidebar - addTarget(TARGET); + addTarget(Baseline.target()); // add event listener to resize content if window is adjusted window.addEventListener('resize', showSidebar); diff --git a/src/js/components/tooltip/tooltip.js b/src/js/components/tooltip/tooltip.js index 6967848..2627b6a 100644 --- a/src/js/components/tooltip/tooltip.js +++ b/src/js/components/tooltip/tooltip.js @@ -2,6 +2,7 @@ import { FISCAL_YEAR } from '../../constants/'; import Cell from '../table/subcomponents/cells'; import { formatCurrency } from '../../utils/common_utils'; import CurrentPage from '../../models/current_page'; +import { excelSerialDateToJSDate } from '../../utils/XLSX_handlers'; import './tooltip.css' @@ -68,7 +69,7 @@ function showFinalPersonnelCost(row){ function showFICA(row){ const fica = parseFloat(Cell.getText(row, 'fica')); const ficaPercentage = (fica * 100).toFixed(2); - const message = `This total is overtime wages plus overtime salary plus FICA (payroll tax), + const message = `This total is overtime / holiday / shift premium pay, plus FICA (payroll tax), which is ${ficaPercentage}% for this cost center.` editTooltipText(message); } @@ -77,7 +78,9 @@ function showCPA(row){ const cpa = parseFloat(Cell.getText(row, 'cpa')); const description = Cell.getText(row, 'cpa-description'); const vendor = Cell.getText(row, 'vendor'); - const contract_end = Cell.getText(row, 'contract-end'); + var contract_end = Cell.getText(row, 'contract-end'); + // convert to normal date format from excel + contract_end = excelSerialDateToJSDate(contract_end); const remaining = Cell.getValue(row, 'remaining'); if (cpa) { var message = `CPA #${cpa}`; @@ -162,25 +165,27 @@ export const Tooltip = { show : showTooltip, linkAll : () => { + linkAccountStringCol(); switch(CurrentPage.load()){ case 'personnel' : - linkAccountStringCol(); + // linkAccountStringCol(); linkSalaryCol(); linkTotalPersonnelCostCol(); break; case 'overtime': linkTotalOTCol(); + // linkAccountStringCol(); break; case 'nonpersonnel': - linkAccountStringCol(); + // linkAccountStringCol(); linkCPACol(); break; - case 'revenue': - linkAccountStringCol(); - break; - case 'new-inits': - linkAccountStringCol(); - break; + // case 'revenue': + // linkAccountStringCol(); + // break; + // case 'new-inits': + // linkAccountStringCol(); + // break; default: break; diff --git a/src/js/constants/app_constants.js b/src/js/constants/app_constants.js deleted file mode 100644 index 225fd4d..0000000 --- a/src/js/constants/app_constants.js +++ /dev/null @@ -1,5 +0,0 @@ -// 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/budget_constants.js b/src/js/constants/budget_constants.js new file mode 100644 index 0000000..6a4838d --- /dev/null +++ b/src/js/constants/budget_constants.js @@ -0,0 +1,36 @@ +// Set to equal current fiscal year +export var FISCAL_YEAR = '26'; + +// object categories (from obj part of account string) +export const OBJ_CATEGORIES = { + list : [ + // 'Salaries & Wages', + // 'Employee Benefits', + 'Professional & Contractual Services', + 'Operating Supplies', + 'Operating Services', + 'Equipment Acquisition', + 'Capital Outlays', + 'Fixed Charges', + 'Other Expenses' + ] +} + +// from the drop-down menu +export const EMPLOYEE_TYPES = [ + 'Regular', + 'TASS', + 'Seasonal', + 'Uniform Fire', + 'Uniform Police', + 'Appointed', + 'Elected', + 'Long Term Disability' +] + +export const OT_OBJECTS = [ + '601300 - Salar-Overtime-Gen City', + '601305 - Salaries-Overtime-Police Unif', + '601310 - Salaries-Overtime-Fire Unif', + '602300 - Wages-Overtime-Gen City' +] \ No newline at end of file diff --git a/src/js/constants/excel_constants.js b/src/js/constants/excel_constants.js index 0034e4c..e49a456 100644 --- a/src/js/constants/excel_constants.js +++ b/src/js/constants/excel_constants.js @@ -1,21 +1,19 @@ +import { FISCAL_YEAR } from "./budget_constants"; + // sheets to expect on detail sheet export const SHEETS = { 'FTE, Salary-Wage, & Benefits' : 'personnel' , 'Overtime & Other Personnel' : 'overtime', - 'Non-Personnel Operating' : 'nonpersonnel', + 'Non-Personnel' : '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 +// where to find the general fund target +export const TARGET_CELL_ADDRESS = 'C14' + +export const TOTAL_COLUMNS = { + 'personnel': 'Total Sal/Wag & Ben Request', + 'overtime':`FY${FISCAL_YEAR} Total OT/SP/Hol + FICA Request`, + 'nonpersonnel': `FY${FISCAL_YEAR} Departmental Request Total`, + 'revenue': `FY${FISCAL_YEAR} Departmental Estimate` +}; diff --git a/src/js/constants/index.js b/src/js/constants/index.js index 06b365c..4a580ca 100644 --- a/src/js/constants/index.js +++ b/src/js/constants/index.js @@ -1,2 +1,2 @@ -export * from './app_constants'; +export * from './budget_constants'; export * from './excel_constants'; \ No newline at end of file diff --git a/src/js/models/account_string.js b/src/js/models/account_string.js index cf8095a..b7f7e55 100644 --- a/src/js/models/account_string.js +++ b/src/js/models/account_string.js @@ -1,4 +1,4 @@ - +import CurrentFund from "./current_fund"; export const AccountString = { getNumber: function(input) { diff --git a/src/js/models/baseline.js b/src/js/models/baseline.js index 4532899..2590493 100644 --- a/src/js/models/baseline.js +++ b/src/js/models/baseline.js @@ -13,6 +13,8 @@ export class Baseline { }); } + static target() { return localStorage.getItem('target') }; + personnel() { let total = 0; this.funds.forEach(fund => { diff --git a/src/js/models/fund.js b/src/js/models/fund.js index 848df5a..6eed831 100644 --- a/src/js/models/fund.js +++ b/src/js/models/fund.js @@ -1,6 +1,6 @@ import { colSum } from "../utils/common_utils"; -import { FISCAL_YEAR } from '../constants/'; +import { TOTAL_COLUMNS } from '../constants/'; // Class to hold information on a specific fund and table class StoredTable { @@ -11,18 +11,7 @@ class StoredTable { } 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; - } + return TOTAL_COLUMNS[this.page]; } getSum() { // fill with zero until there is something saved in storage diff --git a/src/js/utils/XLSX_handlers.js b/src/js/utils/XLSX_handlers.js index 405dccb..81568af 100644 --- a/src/js/utils/XLSX_handlers.js +++ b/src/js/utils/XLSX_handlers.js @@ -1,6 +1,6 @@ -import { SHEETS } from '../constants/'; +import { SHEETS, TARGET_CELL_ADDRESS } from '../constants/'; import FundLookupTable from '../models/fund_lookup_table.js'; import { removeNewLines } from './common_utils.js'; import Baseline from '../models/baseline.js'; @@ -81,7 +81,7 @@ export function processWorkbook(arrayBuffer) { } // But also save the possible services - else if (sheetName == 'Drop-Downs'){ + else if (sheetName == 'Drop-Down Menus'){ const sheet = workbook.Sheets[sheetName]; // Convert the sheet to JSON to easily manipulate data const sheetData = XLSX.utils.sheet_to_json(sheet, { header: 1 }); @@ -100,6 +100,17 @@ export function processWorkbook(arrayBuffer) { Services.save(cleanedServicesColumn); } } + + else if(sheetName == 'Dept Summary'){ + const sheet = workbook.Sheets[sheetName]; + // get and save TARGET for general fund + if(sheet[TARGET_CELL_ADDRESS]) { + const cellValue = sheet[TARGET_CELL_ADDRESS].v; // Access the cell value + localStorage.setItem('target', cellValue); + } else { + console.error(`Cell ${TARGET_CELL_ADDRESS} not found in sheet ${sheetName}`); + } + } }); console.log('all excel data saved'); @@ -154,4 +165,22 @@ export function downloadXLSX() { document.body.appendChild(link); link.click(); document.body.removeChild(link); -} \ No newline at end of file +} + +export function excelSerialDateToJSDate(serial) { + + if (!serial) { return null }; + // Excel considers 1900-01-01 as day 1, but JavaScript's Date considers + // 1970-01-01 as day 0. Therefore, we calculate the number of milliseconds + // between 1900-01-01 and 1970-01-01. + const excelEpoch = new Date(Date.UTC(1899, 11, 30)); // JavaScript Consider December month as '11' + + // Calculate the JS date by adding serial days to the epoch date + const date = new Date(excelEpoch.getTime() + (serial * 24 * 60 * 60 * 1000)); + + // Set the time part to zero (midnight) + date.setUTCHours(0, 0, 0, 0); + + // Return the date part of the ISO string + return date.toISOString().split('T')[0]; +} diff --git a/src/js/utils/common_utils.js b/src/js/utils/common_utils.js index b32552c..78c908e 100644 --- a/src/js/utils/common_utils.js +++ b/src/js/utils/common_utils.js @@ -42,6 +42,7 @@ export function removeNewLines(str){ str = str.replaceAll(/[\r\n]+/g, " "); // remove any extra spaces or trailing/leading whitespace str = str.replaceAll(' ', ' '); + str = str.replaceAll(' ', ' '); str = str.replace(/^\s+|\s+$/g, ''); return str; } diff --git a/src/js/views/00_welcome.js b/src/js/views/00_welcome.js index c16b037..eb6b5ae 100644 --- a/src/js/views/00_welcome.js +++ b/src/js/views/00_welcome.js @@ -22,7 +22,7 @@ export class WelcomeView extends View { // 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-revenue').addEventListener('click', () => visitPage('baseline-landing')); document.getElementById('step-finish').addEventListener('click', () => visitPage('summary')); } diff --git a/src/js/views/03_revenue.js b/src/js/views/03_revenue.js index 9b2b172..22d5cb7 100644 --- a/src/js/views/03_revenue.js +++ b/src/js/views/03_revenue.js @@ -4,26 +4,28 @@ import Table from '../components/table/table.js'; export class RevenueView extends View { - constructor() { + constructor(fiscal_year) { super(); this.page_state = 'revenue'; - this.prompt = 'Review and edit revenue line items.'; + this.prompt = `Review and edit revenue line items. If you change the estimate or + notice an error in an account string, please note it in the notes column. Click edit + to change values in a row.`; this.subtitle = 'Revenues'; - this.table = new RevenueTable(); + this.table = new RevenueTable(fiscal_year); } } class RevenueTable extends ViewTable { - constructor() { + constructor(fiscal_year) { 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'}, + { title: `FY${fiscal_year} Departmental Estimate`, className: 'request', isCost: true}, + { title: 'Departmental Estimate Notes', className: 'notes'}, ]); this.noDataMessage = 'No revenues for this fund.' diff --git a/src/js/views/04_personnel.js b/src/js/views/04_personnel.js index 00deb60..8293e38 100644 --- a/src/js/views/04_personnel.js +++ b/src/js/views/04_personnel.js @@ -5,6 +5,7 @@ import Form from "../components/form/form.js"; import { Services, FundLookupTable } from '../models/'; import { unformatCurrency } from "../utils/common_utils.js"; +import { EMPLOYEE_TYPES } from '../constants/budget_constants.js'; export class PersonnelView extends View { @@ -30,10 +31,11 @@ class PersonnelTable extends ViewTable { // add additional personnel columns to the table this.columns = this.columns.concat([ { title: 'Job Title', className: 'job-name' }, + { title: 'Employee Type', className: 'employee-type'}, { 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 }, + { title: `FY${this.fiscal_year} Projected Average Salary/Wage`, className: 'avg-salary', isCost: true }, + { title: 'Total Sal/Wag & Ben Request', 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}, @@ -74,6 +76,7 @@ class PersonnelTable extends ViewTable { addCustomQuestions(){ // form questions to add a new job Form.NewField.shortText('Job Title:', 'job-name', true); + Form.NewField.dropdown('Employee Type:', 'employee-type', EMPLOYEE_TYPES, 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); diff --git a/src/js/views/05_overtime.js b/src/js/views/05_overtime.js index 44a9237..5f0622f 100644 --- a/src/js/views/05_overtime.js +++ b/src/js/views/05_overtime.js @@ -3,35 +3,35 @@ 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'; +import { OT_OBJECTS } from '../constants/'; export class OvertimeView extends View { - constructor() { + constructor(fiscal_year) { 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(); + this.table = new OvertimeTable(fiscal_year); } } class OvertimeTable extends ViewTable { - constructor() { + constructor(fiscal_year) { 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}, + { title: 'Departmental Request OT/SP/Hol', className: 'OT-pay', isCost: true }, + { title: `FY${fiscal_year} Total OT/SP/Hol + FICA Request`, className : 'total', isCost: true}, + { title: 'Object Name', className: 'object-name'}, // hidden columns { title: 'FICA Rate', className: 'fica', hide: true}, ]); @@ -42,29 +42,23 @@ class OvertimeTable extends ViewTable { // action on row edit click: make cells editable actionOnEdit() { - Table.Cell.createTextbox('OT-wages', true); - Table.Cell.createTextbox('OT-salary', true); + Table.Cell.createTextbox('OT-pay', true); Table.Cell.createServiceDropdown(Services.list()); Table.Cell.createDropdown('recurring', ['One-Time', 'Recurring']); + Table.Cell.createDropdown('object-name', OT_OBJECTS); } 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 OT_salary = Table.Cell.getValue(rows[i], 'OT-pay'); 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); + let row_total = OT_salary * (1 + parseFloat(fica_rate)); // update total Table.Cell.updateValue(rows[i], 'total', row_total); @@ -77,16 +71,17 @@ class OvertimeTable extends ViewTable { 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('Cost Center:', 'cc-name', FundLookupTable.getCostCenters(), true); + Form.NewField.dropdown('Object (salary or wage):', 'object-name', OT_OBJECTS, 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.NewField.shortText('Overtime amount requested:', 'OT-pay', true); } editColumns(responses){ responses = super.editColumns(responses); // edit inputs from modal - responses['OT-wages'] = unformatCurrency(responses['OT-wages']); + responses['OT-pay'] = unformatCurrency(responses['OT-pay']); responses['fica'] = 0.0765; return responses; } diff --git a/src/js/views/06_nonpersonnel.js b/src/js/views/06_nonpersonnel.js index 16bbeab..6cc24b2 100644 --- a/src/js/views/06_nonpersonnel.js +++ b/src/js/views/06_nonpersonnel.js @@ -23,13 +23,13 @@ class NonPersonnelTable extends ViewTable { // add additional personnel columns to the table this.columns = this.columns.concat([ - { title: `FY${fiscal_year} Request`, className: 'request', isCost: true }, + { title: `FY${fiscal_year} Departmental Request Total`, 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: 'End Date', className: 'contract-end', hide: true}, + { title: 'BPA/CPA Amount Remaining', 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}, @@ -37,7 +37,7 @@ class NonPersonnelTable extends ViewTable { { title: 'BPA/CPA Description', className: 'cpa-description', hide: true} ]); - this.noDataMessage = 'No personnel expenditures for this fund.' + this.noDataMessage = 'No non-personnel expenditures for this fund.' this.addButtonText = 'Add new job' ; } diff --git a/src/js/views/08_summary.js b/src/js/views/08_summary.js index dabcc18..21f0df3 100644 --- a/src/js/views/08_summary.js +++ b/src/js/views/08_summary.js @@ -1,21 +1,19 @@ 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){ + if (baseline.total() <= Baseline.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)}. + Prompt.Text.update(`Your budget is above your target of ${formatCurrency(Baseline.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(); diff --git a/src/js/views/view_logic.js b/src/js/views/view_logic.js index d72d739..6fcf078 100644 --- a/src/js/views/view_logic.js +++ b/src/js/views/view_logic.js @@ -16,9 +16,9 @@ export function initializePages() { 'welcome': new WelcomeView(), 'upload': new UploadView(), 'baseline-landing': new FundView(), - 'revenue': new RevenueView(), + 'revenue': new RevenueView(FISCAL_YEAR), 'personnel': new PersonnelView(FISCAL_YEAR), - 'overtime': new OvertimeView(), + 'overtime': new OvertimeView(FISCAL_YEAR), 'nonpersonnel': new NonPersonnelView(FISCAL_YEAR), 'new-inits': new InitiativesView(), 'summary': new SummaryView(),