From 2b7ea7ba501caa30d2d9372876acb8a4b6770c93 Mon Sep 17 00:00:00 2001 From: Steve Schafer Date: Mon, 19 Jan 2026 11:02:49 -0700 Subject: [PATCH] Add budgets to report. --- src/Categorize.jsx | 2 +- src/Report.jsx | 145 +++++++++++++++++++++++++++++++++++++++++++-- src/apiService.jsx | 19 ++++++ src/main.css | 3 + 4 files changed, 163 insertions(+), 6 deletions(-) diff --git a/src/Categorize.jsx b/src/Categorize.jsx index 3f6da66..e26e915 100644 --- a/src/Categorize.jsx +++ b/src/Categorize.jsx @@ -16,7 +16,7 @@ const Categorize = (props) => { useEffect(() => { console.log("Categorize: useEffect"); console.log("params", params); - let redo = params.redo == 'true'; + let redo = params.redo === 'true'; categorize(redo).then( data => { diff --git a/src/Report.jsx b/src/Report.jsx index 3b7cb19..dd1ae38 100644 --- a/src/Report.jsx +++ b/src/Report.jsx @@ -1,7 +1,7 @@ import React from 'react'; import {useState, useEffect} from "react"; import {useSearchParams, useNavigate, useLocation} from 'react-router-dom'; -import {report} from "./apiService"; +import {report, saveBudget} from "./apiService"; const Report = (props) => { const [error, setError] = useState(null); @@ -129,7 +129,7 @@ const Report = (props) => { const numberFormat = new Intl.NumberFormat("en-US", { style: 'currency', currency: 'USD' }); - let renderMonths = (category) => { + let renderMonths = (year, category) => { let cols = []; for(let monthNum = 0; monthNum < category.monthCount; monthNum++) { let amount = category.monthGrandTotals[monthNum]; @@ -140,11 +140,80 @@ const Report = (props) => { if(category.largestMonth === monthNum) { style["color"] = "RED"; } - cols.push({numberFormat.format(amount)}); + rememberBudget(year, category, monthNum); + cols.push( + + + {numberFormat.format(amount)} + + ); } return cols; }; + const rememberBudget = (year, category, monthNum) => { + if(document.yearBudgets == null) { + document.yearBudgets = {}; + } + let categoryBudgets = document.yearBudgets[year]; + if(categoryBudgets == null) { + categoryBudgets = {}; + document.yearBudgets[year] = categoryBudgets; + } + let monthBudgets = categoryBudgets[category.id]; + if(monthBudgets == null) { + monthBudgets = {}; + categoryBudgets[category.id] = monthBudgets; + } + monthBudgets[monthNum] = category.budgetAmounts.monthBudgets[monthNum]; + }; + + const getRememberedBudget = (year, categoryId, monthNum) => { + let yearBudgets = document.yearBudgets; + if(yearBudgets == null) { + return null; + } + let categoryBudgets = yearBudgets[year]; + if(categoryBudgets == null) { + return null; + } + let monthBudgets = categoryBudgets[categoryId]; + if(monthBudgets == null) { + return null; + } + return monthBudgets[monthNum]; + }; + + const enableSubmitButton = (e) => { + let re = /budget\/([0-9]+)\/([0-9-]+)\/([0-9-]+)/; + let inputs = document.body.querySelectorAll(".budget"); + let enabled = false; + for(let input of inputs) { + let name = input.name; + let value = input.value; + let reResult = re.exec(name); + if(reResult == null) { + console.log("regexp did not match", name); + continue; + } + let year = +reResult[1]; + let categoryId = +reResult[2]; + let monthNum = +reResult[3]; + let rememberedBudget = getRememberedBudget(year, categoryId, monthNum); + if(rememberedBudget != value) { + enabled = true; + break; + } + } + let button = document.body.querySelector("button.budget"); + button.disabled = !enabled; + }; + let renderDetail = (key, category) => { let rows = []; for(let detail of category.details) { @@ -174,6 +243,7 @@ const Report = (props) => { let indent = level * 10; let result = []; let key = "key-" + year + "-" + category.id; + rememberBudget(year, category, -1); result.push( <> @@ -182,9 +252,15 @@ const Report = (props) => { { numberFormat.format(category.grandAverage) } + { numberFormat.format(category.grandTotal) } - { renderMonths(category) } + { renderMonths(year, category) } @@ -226,6 +302,13 @@ const Report = (props) => { }}); }; + const showInputs = (e) => { + let cols = document.body.querySelectorAll(".budget"); + for(let col of cols) { + col.style.display = e.target.checked ? "" : "none"; + } + }; + const showColumn = (e, name, doNotCheckAll) => { let cols = document.body.querySelectorAll("table.report td." + name + ",th." + name); for(let col of cols) { @@ -266,6 +349,52 @@ const Report = (props) => { showColumn(e, "month11", true); }; + const handleSubmit = e => { + e.preventDefault(); + let yearMap = {}; + let re = /budget\/([0-9]+)\/([0-9-]+)\/([0-9-]+)/; + let inputs = document.body.querySelectorAll(".budget"); + for(let input of inputs) { + let name = input.name; + let reResult = re.exec(name); + if(reResult == null) { + console.log("regexp did not match", name); + continue; + } + let year = +reResult[1]; + let categoryId = +reResult[2]; + let monthNum = +reResult[3]; + // console.log(year, categoryId, monthNum); + let categoryMap = yearMap[year]; + if(!categoryMap) { + categoryMap = {}; + yearMap[year] = categoryMap; + } + let monthMap = categoryMap[categoryId]; + if(!monthMap) { + monthMap = {}; + categoryMap[categoryId] = monthMap; + } + monthMap[monthNum] = input.value; + } + console.log(yearMap); + saveBudget(yearMap).then( + data => { + // console.log('saveRegexes data', data); + if(data.status !== 200) { + alert(data.message); + return; + } + let button = document.body.querySelector("button.budget"); + button.disabled = true; + }, + error => { + alert(error.message); + console.error('error object', error); + } + ); + }; + console.log("report render", report); return ( <> @@ -319,10 +448,11 @@ const Report = (props) => { return ( <>

{yearReport.year}

+
- + @@ -343,6 +473,11 @@ const Report = (props) => { { renderCategory(0, yearReport.year, yearReport.rootCategory) }
CategoryCategory Average Total Jan
+ +
); }) diff --git a/src/apiService.jsx b/src/apiService.jsx index b42208e..66a3ec3 100644 --- a/src/apiService.jsx +++ b/src/apiService.jsx @@ -171,6 +171,25 @@ export const deleteRegex = regexId => { }); }; +export const saveBudget = yearMap => { + if(yearMap == null) { + return new Promise((resolve, reject) => { + reject('yearMap is null'); + }); + } + let url = `${baseUrl}/budget`; + let method = 'PUT'; + return fetch(url, fetchOptions(method, yearMap)).then(response => { + console.log(url, "response", response); + if(response.status !== 200) { + return new Promise((resolve, reject) => { + reject({status: response.status}); + }); + } + return response.json(); // promise + }); +} + export const saveRegex = regex => { if(regex == null) { return new Promise((resolve, reject) => { diff --git a/src/main.css b/src/main.css index 5cc79bc..66c2cbf 100644 --- a/src/main.css +++ b/src/main.css @@ -39,6 +39,9 @@ div.header-date { table.detail tr th,td { padding-right: 20px; } +table.report tr th,td { + padding-right: 20px; +} @media only screen and (max-width: 640px) { body { padding: 0px;