From df5aa3186c354eae177e096b8c92c1c6fd75ef66 Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 8 Nov 2023 12:44:45 -0700 Subject: [PATCH] Goals: Save full goal value and add an indicator of goal status (#1780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * first pass at progress bar * db migration / enter goal in db * add getGoal function * stabilize * whoops * TS * reset goal in db if no template found * reconfirm * release note * typo * rename migration * to ms * move priority logic, consistent variable names, * fixup * clear goal if template removed * Visual goals (#40) * 🔥 removing privacyMode feature flag (#1688) * :art: fix multiline label in schedules modal (#1687) * Update Visual Regression README File (#1689) * Fix typo in GoCardlessLink.js (#1684) happend -> happened * queried cleared balance for tooltip (#1678) * Dark Theme Reports/Settings (#1512) * :bug: Mobile account transaction list: Fix sticky date section headers (#1698) * :construction_worker: do not cancel github ci jobs on master branch (#1692) * Sidebar Account Fix (#1703) * Dark Theme Final (#1513) * Category autocomplete should only search selectable categories (#1681) * set colors based on a goal value * extra comment --------- Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 Co-authored-by: Ikko Eltociear Ashimine Co-authored-by: Shaan Khosla <35707672+shaankhosla@users.noreply.github.com> Co-authored-by: Neil <55785687+carkom@users.noreply.github.com> Co-authored-by: Trevor Farlow * update release note * lint * use null as cleared state * show goal status via colors (#41) * cleanup * I think its working * lint * fix report budget, by adding in the goal coloring * fix the error by adding colors to the report side (#42) * [refactor] Migrate Schedules Table to typescript (#1691) * :wrench: removing unnecessary manual module resolution (#1707) * :bug: (mobile) scrolling in lists with pull-to-refresh (#1706) * :lipstick: (mobile) updating apple home-screen icon (#1705) * Enhance Y-Axis Scaling on Net Worth Graph (#1709) * fix report budget, by adding in the goal coloring --------- Co-authored-by: Mohamed Muhsin <62111075+muhsinkamil@users.noreply.github.com> Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 * report budget database updates * Goal progress bar (#1734) * first pass at progress bar * db migration / enter goal in db * add getGoal function * stabilize * whoops * TS * reset goal in db if no template found * reconfirm * release note * typo * rename migration * to ms * move priority logic, consistent variable names, * fixup * clear goal if template removed * Visual goals (#40) * 🔥 removing privacyMode feature flag (#1688) * :art: fix multiline label in schedules modal (#1687) * Update Visual Regression README File (#1689) * Fix typo in GoCardlessLink.js (#1684) happend -> happened * queried cleared balance for tooltip (#1678) * Dark Theme Reports/Settings (#1512) * :bug: Mobile account transaction list: Fix sticky date section headers (#1698) * :construction_worker: do not cancel github ci jobs on master branch (#1692) * Sidebar Account Fix (#1703) * Dark Theme Final (#1513) * Category autocomplete should only search selectable categories (#1681) * set colors based on a goal value * extra comment --------- Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 Co-authored-by: Ikko Eltociear Ashimine Co-authored-by: Shaan Khosla <35707672+shaankhosla@users.noreply.github.com> Co-authored-by: Neil <55785687+carkom@users.noreply.github.com> Co-authored-by: Trevor Farlow * update release note * lint * use null as cleared state * show goal status via colors (#41) * cleanup * I think its working * lint * fix the error by adding colors to the report side (#42) * [refactor] Migrate Schedules Table to typescript (#1691) * :wrench: removing unnecessary manual module resolution (#1707) * :bug: (mobile) scrolling in lists with pull-to-refresh (#1706) * :lipstick: (mobile) updating apple home-screen icon (#1705) * Enhance Y-Axis Scaling on Net Worth Graph (#1709) * fix report budget, by adding in the goal coloring --------- Co-authored-by: Mohamed Muhsin <62111075+muhsinkamil@users.noreply.github.com> Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 * report budget database updates * Fix schedule searchbar (#1729) --------- Co-authored-by: youngcw Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 Co-authored-by: Ikko Eltociear Ashimine Co-authored-by: Shaan Khosla <35707672+shaankhosla@users.noreply.github.com> Co-authored-by: Neil <55785687+carkom@users.noreply.github.com> Co-authored-by: Trevor Farlow Co-authored-by: Mohamed Muhsin <62111075+muhsinkamil@users.noreply.github.com> * working dynamic colors. Need to figure out what changes are actually needed * cleanup * more cleanup * lint * reset the goal when applying a single template * make getCategory function * remove some unneeded changes * actually remove the changes, not just comment * cleanup some unneeded code that was causing some bugs. Works for me, but should be vetted more * lint * add json definitions to database * use template feature flag to enable colors * some fixes * don't set goals for remainders, remove unneeded change * lint * release note * lint again * fix mobile crash * undo changes in CellValue.tsx * lint * use getStyle * move status calc to helper * lint * recommendations * suggestion Co-authored-by: Joel Jeremy Marquez --------- Co-authored-by: shall0pass <20625555+shall0pass@users.noreply.github.com> Co-authored-by: Matiss Janis Aboltins Co-authored-by: Crazypkr1099 Co-authored-by: Ikko Eltociear Ashimine Co-authored-by: Shaan Khosla <35707672+shaankhosla@users.noreply.github.com> Co-authored-by: Neil <55785687+carkom@users.noreply.github.com> Co-authored-by: Trevor Farlow Co-authored-by: Mohamed Muhsin <62111075+muhsinkamil@users.noreply.github.com> Co-authored-by: Joel Jeremy Marquez --- .../budget/BalanceWithCarryover.tsx | 19 +- .../components/budget/MobileBudgetTable.js | 2 + .../components/budget/report/components.tsx | 2 + .../budget/rollover/rollover-components.tsx | 2 + .../src/components/budget/util.ts | 23 +- .../1694438752000_add_goal_targets.sql | 7 + packages/loot-core/src/client/queries.ts | 2 + .../loot-core/src/server/budget/actions.ts | 18 ++ packages/loot-core/src/server/budget/base.ts | 1 + .../src/server/budget/goaltemplates.ts | 284 +++++++++++------- .../src/server/budget/types/handlers.d.ts | 8 +- packages/loot-core/src/server/sheet.ts | 1 + upcoming-release-notes/1780.md | 6 + 13 files changed, 252 insertions(+), 123 deletions(-) create mode 100644 packages/loot-core/migrations/1694438752000_add_goal_targets.sql create mode 100644 upcoming-release-notes/1780.md diff --git a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx index 22e500cb673..f0c29ee5283 100644 --- a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx +++ b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx @@ -1,5 +1,6 @@ import React, { type ComponentProps } from 'react'; +import useFeatureFlag from '../../hooks/useFeatureFlag'; import ArrowThinRight from '../../icons/v1/ArrowThinRight'; import { type CSSProperties } from '../../style'; import View from '../common/View'; @@ -11,6 +12,8 @@ import { makeAmountStyle } from './util'; type BalanceWithCarryoverProps = { carryover: ComponentProps['binding']; balance: ComponentProps['binding']; + goal?: ComponentProps['binding']; + budgeted?: ComponentProps['binding']; disabled?: boolean; style?: CSSProperties; balanceStyle?: CSSProperties; @@ -19,6 +22,8 @@ type BalanceWithCarryoverProps = { export default function BalanceWithCarryover({ carryover, balance, + goal, + budgeted, disabled, style, balanceStyle, @@ -26,13 +31,21 @@ export default function BalanceWithCarryover({ }: BalanceWithCarryoverProps) { let carryoverValue = useSheetValue(carryover); let balanceValue = useSheetValue(balance); - + let goalValue = useSheetValue(goal); + let budgetedValue = useSheetValue(budgeted); + let isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( + makeAmountStyle( + value, + isGoalTemplatesEnabled ? goalValue : null, + budgetedValue, + ) + } style={{ textAlign: 'right', ...(!disabled && { @@ -58,7 +71,7 @@ export default function BalanceWithCarryover({ )} diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.js b/packages/desktop-client/src/components/budget/MobileBudgetTable.js index 90c2da02953..ccb8f974d05 100644 --- a/packages/desktop-client/src/components/budget/MobileBudgetTable.js +++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.js @@ -446,6 +446,8 @@ const ExpenseCategory = memo(function ExpenseCategory({ {balanceTooltip.isOpen && ( diff --git a/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx b/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx index 8d1f2c3610d..8be56b0bbf8 100644 --- a/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx +++ b/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx @@ -320,6 +320,8 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ {balanceTooltip.isOpen && ( diff --git a/packages/desktop-client/src/components/budget/util.ts b/packages/desktop-client/src/components/budget/util.ts index bda8e59e227..38d571e2fd3 100644 --- a/packages/desktop-client/src/components/budget/util.ts +++ b/packages/desktop-client/src/components/budget/util.ts @@ -37,15 +37,26 @@ export function makeAmountGrey(value: number | string) { : null; } -export function makeAmountStyle(value: number) { - const greyed = makeAmountGrey(value); - if (greyed) { - return greyed; - } - +export function makeAmountStyle( + value: number, + goalValue?: number, + budgetedValue?: number, +) { if (value < 0) { return { color: theme.errorText }; } + + if (goalValue == null) { + const greyed = makeAmountGrey(value); + if (greyed) { + return greyed; + } + } else { + if (budgetedValue < goalValue) { + return { color: theme.warningText }; + } + return { color: theme.noticeText }; + } } export function makeAmountFullStyle(value: number) { diff --git a/packages/loot-core/migrations/1694438752000_add_goal_targets.sql b/packages/loot-core/migrations/1694438752000_add_goal_targets.sql new file mode 100644 index 00000000000..d4f62f969d4 --- /dev/null +++ b/packages/loot-core/migrations/1694438752000_add_goal_targets.sql @@ -0,0 +1,7 @@ +BEGIN TRANSACTION; + +ALTER TABLE zero_budgets ADD column goal INTEGER DEFAULT null; +ALTER TABLE reflect_budgets ADD column goal INTEGER DEFAULT null; +ALTER TABLE categories ADD column goal_def TEXT DEFAULT null; + +COMMIT; diff --git a/packages/loot-core/src/client/queries.ts b/packages/loot-core/src/client/queries.ts index 2e5852f25f2..0da752bb3fc 100644 --- a/packages/loot-core/src/client/queries.ts +++ b/packages/loot-core/src/client/queries.ts @@ -177,6 +177,7 @@ export const rolloverBudget = { catSumAmount: id => `sum-amount-${id}`, catBalance: id => `leftover-${id}`, catCarryover: id => `carryover-${id}`, + catGoal: id => `goal-${id}`, }; export const reportBudget = { @@ -199,4 +200,5 @@ export const reportBudget = { catSumAmount: id => `sum-amount-${id}`, catBalance: id => `leftover-${id}`, catCarryover: id => `carryover-${id}`, + catGoal: id => `goal-${id}`, }; diff --git a/packages/loot-core/src/server/budget/actions.ts b/packages/loot-core/src/server/budget/actions.ts index 62caada230b..4b7d1244b64 100644 --- a/packages/loot-core/src/server/budget/actions.ts +++ b/packages/loot-core/src/server/budget/actions.ts @@ -113,6 +113,24 @@ export function setBudget({ }); } +export function setGoal({ month, category, goal }): Promise { + const table = getBudgetTable(); + let existing = db.firstSync( + `SELECT id FROM ${table} WHERE month = ? AND category = ?`, + [dbMonth(month), category], + ); + if (existing) { + return db.update(table, { + id: existing.id, + goal: goal, + }); + } + return db.insert(table, { + id: month, + goal: goal, + }); +} + export function setBuffer(month: string, amount: unknown): Promise { let existing = db.firstSync( `SELECT id FROM zero_budget_months WHERE id = ?`, diff --git a/packages/loot-core/src/server/budget/base.ts b/packages/loot-core/src/server/budget/base.ts index 6e4ba5b4f87..a380d1ffb19 100644 --- a/packages/loot-core/src/server/budget/base.ts +++ b/packages/loot-core/src/server/budget/base.ts @@ -318,6 +318,7 @@ function handleBudgetChange(budget) { `${sheetName}!carryover-${budget.category}`, budget.carryover === 1 ? true : false, ); + sheet.get().set(`${sheetName}!goal-${budget.category}`, budget.goal); } } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 9f3b415450b..178061d49ee 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -6,16 +6,20 @@ import * as db from '../db'; import { getRuleForSchedule, getNextDate } from '../schedules/app'; import { batchMessages } from '../sync'; -import { setBudget, getSheetValue, isReflectBudget } from './actions'; +import { setBudget, getSheetValue, isReflectBudget, setGoal } from './actions'; import { parse } from './goal-template.pegjs'; export async function applyTemplate({ month }) { - let category_templates = await getCategoryTemplates(null); + await storeTemplates(); + let category_templates = await getTemplates(null); + await resetCategoryTargets({ month, category: null }); return processTemplate(month, false, category_templates); } export async function overwriteTemplate({ month }) { - let category_templates = await getCategoryTemplates(null); + await storeTemplates(); + let category_templates = await getTemplates(null); + await resetCategoryTargets({ month, category: null }); return processTemplate(month, true, category_templates); } @@ -23,19 +27,22 @@ export async function applySingleCategoryTemplate({ month, category }) { let categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [ category, ]); - let category_templates = await getCategoryTemplates(categories[0]); - await setBudget({ - category: category, - month, - amount: 0, - }); - return processTemplate(month, false, category_templates); + await storeTemplates(); + let category_templates = await getTemplates(categories[0]); + await resetCategoryTargets({ month, category: categories }); + return processTemplate(month, true, category_templates); } export function runCheckTemplates() { return checkTemplates(); } +async function getCategories() { + return await db.all( + 'SELECT * FROM v_categories WHERE tombstone = 0 AND hidden = 0', + ); +} + function checkScheduleTemplates(template) { let lowPriority = template[0].priority; let errorNotice = false; @@ -60,16 +67,91 @@ async function setGoalBudget({ month, templateBudget }) { }); } -async function processTemplate(month, force, category_templates) { +async function setCategoryTargets({ month, idealTemplate }) { + await batchMessages(async () => { + idealTemplate.forEach(element => { + setGoal({ + category: element.category, + goal: element.amount, + month: month, + }); + }); + }); +} + +async function resetCategoryTargets({ month, category }) { + let categories; + if (category === null) { + categories = await getCategories(); + } else { + categories = category; + } + await batchMessages(async () => { + categories.forEach(element => { + setGoal({ + category: element.id, + goal: null, + month: month, + }); + }); + }); +} + +async function storeTemplates() { + //stores the template definitions to the database + let templates = await getCategoryTemplates(null); + let categories = await getCategories(); + + for (let c = 0; c < categories.length; c++) { + let template = templates[categories[c].id]; + if (template) { + await db.update('categories', { + id: categories[c].id, + goal_def: JSON.stringify(template), + }); + } else { + await db.update('categories', { + id: categories[c].id, + goal_def: null, + }); + } + } +} + +async function getTemplates(category) { + //retrieves template definitions from the database + const goal_def = await db.all( + 'SELECT * FROM categories WHERE goal_def IS NOT NULL', + ); + + let templates = []; + for (let ll = 0; ll < goal_def.length; ll++) { + templates[goal_def[ll].id] = JSON.parse(goal_def[ll].goal_def); + } + if (category) { + let singleCategoryTemplate = {}; + if (templates[category.id] !== undefined) { + singleCategoryTemplate[category.id] = templates[category.id]; + } + return singleCategoryTemplate; + } else { + return templates; + } +} + +async function processTemplate( + month, + force, + category_templates, +): Promise { let num_applied = 0; let errors = []; let originalCategoryBalance = []; + let idealTemplate = []; let setToZero = []; let priority_list = []; - let categories = await db.all( - 'SELECT * FROM v_categories WHERE tombstone = 0 AND hidden = 0', - ); + let categories = await getCategories(); //clears templated categories for (let c = 0; c < categories.length; c++) { @@ -134,7 +216,7 @@ async function processTemplate(month, force, category_templates) { let sheetName = monthUtils.sheetForMonth(month); let available_start = await getSheetValue(sheetName, `to-budget`); - let available_remaining = isReflectBudget() + let budgetAvailable = isReflectBudget() ? await getSheetValue(sheetName, `total-saved`) : await getSheetValue(sheetName, `to-budget`); for (let ii = 0; ii < priority_list.length; ii++) { @@ -150,20 +232,20 @@ async function processTemplate(month, force, category_templates) { for (let c = 0; c < categories.length; c++) { let category = categories[c]; - let template = category_templates[category.id]; - if (template) { + let template_lines = category_templates[category.id]; + if (template_lines) { //check that all schedule and by lines have the same priority level let skipSchedule = false; let isScheduleOrBy = false; let priorityCheck = 0; if ( - template.filter( + template_lines.filter( t => (t.type === 'schedule' || t.type === 'by') && t.priority === priority, ).length > 0 ) { - template = template.filter( + template_lines = template_lines.filter( t => (t.priority === priority && (t.type !== 'schedule' || t.type !== 'by')) || @@ -171,7 +253,7 @@ async function processTemplate(month, force, category_templates) { t.type === 'by', ); let { lowPriority, errorNotice } = await checkScheduleTemplates( - template, + template_lines, ); priorityCheck = lowPriority; skipSchedule = priorityCheck !== priority ? true : false; @@ -186,11 +268,13 @@ async function processTemplate(month, force, category_templates) { } if (!skipSchedule) { if (!isScheduleOrBy) { - template = template.filter(t => t.priority === priority); + template_lines = template_lines.filter( + t => t.priority === priority, + ); } - if (template.length > 0) { + if (template_lines.length > 0) { errors = errors.concat( - template + template_lines .filter(t => t.type === 'error') .map(({ line, error }) => [ @@ -209,25 +293,46 @@ async function processTemplate(month, force, category_templates) { let { amount: to_budget, errors: applyErrors } = await applyCategoryTemplate( category, - template, + template_lines, month, - priority, remainder_scale, available_start, - available_remaining, + budgetAvailable, prev_budgeted, force, ); if (to_budget != null) { num_applied++; - if (to_budget > available_remaining && priority > 0) { - to_budget = available_remaining; + //only store goals from non remainder templates + if (priority !== remainder_priority) { + if ( + idealTemplate.filter(c => c.category === category.id).length > + 0 + ) { + idealTemplate.filter( + c => c.category === category.id, + )[0].amount += to_budget; + } else { + idealTemplate.push({ + category: category.id, + amount: to_budget, + }); + } } - templateBudget.push({ - category: category.id, - amount: to_budget + prev_budgeted, - }); - available_remaining -= to_budget; + if (to_budget <= budgetAvailable || !priority) { + templateBudget.push({ + category: category.id, + amount: to_budget + prev_budgeted, + }); + } else if (to_budget > budgetAvailable && budgetAvailable >= 0) { + to_budget = budgetAvailable; + errors.push(`Insufficient funds.`); + templateBudget.push({ + category: category.id, + amount: to_budget + prev_budgeted, + }); + } + budgetAvailable -= to_budget; } if (applyErrors != null) { errors = errors.concat( @@ -240,7 +345,7 @@ async function processTemplate(month, force, category_templates) { } await setGoalBudget({ month, templateBudget }); } - + await setCategoryTargets({ month, idealTemplate }); if (!force) { //if overwrite is not preferred, set cell to original value; originalCategoryBalance = originalCategoryBalance.filter( @@ -268,7 +373,6 @@ async function processTemplate(month, force, category_templates) { } } } - if (num_applied === 0) { if (errors.length) { return { @@ -331,11 +435,10 @@ async function applyCategoryTemplate( category, template_lines, month, - priority, remainder_scale, available_start, budgetAvailable, - budgeted, + prev_budgeted, force, ) { let current_month = `${month}-01`; @@ -413,9 +516,10 @@ async function applyCategoryTemplate( let spent = await getSheetValue(sheetName, `sum-amount-${category.id}`); let balance = await getSheetValue(sheetName, `leftover-${category.id}`); let to_budget = 0; - let limit; - let hold; - let last_month_balance = balance - spent - budgeted; + let limit = 0; + let hold = false; + let limitCheck = false; + let last_month_balance = balance - spent - prev_budgeted; let remainder = 0; for (let l = 0; l < template_lines.length; l++) { let template = template_lines[l]; @@ -423,10 +527,11 @@ async function applyCategoryTemplate( case 'simple': { // simple has 'monthly' and/or 'limit' params if (template.limit != null) { - if (limit != null) { + if (limitCheck) { errors.push(`More than one “up to” limit found.`); return { errors }; } else { + limitCheck = true; limit = amountToInteger(template.limit.amount); hold = template.limit.hold; } @@ -438,12 +543,7 @@ async function applyCategoryTemplate( } else { increment = limit; } - if (to_budget + increment < budgetAvailable || !priority) { - to_budget += increment; - } else { - if (budgetAvailable > 0) to_budget += budgetAvailable; - errors.push(`Insufficient funds.`); - } + to_budget += increment; break; } case 'by': { @@ -475,16 +575,9 @@ async function applyCategoryTemplate( target = 0; remainder = Math.abs(remainder); } - let diff = + let increment = num_months >= 0 ? Math.round(target / (num_months + 1)) : 0; - if (diff >= 0) { - if (to_budget + diff < budgetAvailable || !priority) { - to_budget += diff; - } else { - if (budgetAvailable > 0) to_budget += budgetAvailable; - errors.push(`Insufficient funds.`); - } - } + to_budget += increment; } else { errors.push(`by templates are not supported in Report budgets`); } @@ -499,6 +592,7 @@ async function applyCategoryTemplate( errors.push(`More than one “up to” limit found.`); return { errors }; } else { + limitCheck = true; limit = amountToInteger(template.limit.amount); hold = template.limit.hold; } @@ -508,12 +602,7 @@ async function applyCategoryTemplate( while (w < next_month) { if (w >= current_month) { - if (to_budget + amount < budgetAvailable || !priority) { - to_budget += amount; - } else { - if (budgetAvailable > 0) to_budget += budgetAvailable; - errors.push(`Insufficient funds.`); - } + to_budget += amount; } w = monthUtils.addWeeks(w, weeks); } @@ -570,12 +659,7 @@ async function applyCategoryTemplate( (target - already_budgeted) / (num_months + 1), ); } - if (increment < budgetAvailable || !priority) { - to_budget = increment; - } else { - if (budgetAvailable > 0) to_budget = budgetAvailable; - errors.push(`Insufficient funds.`); - } + to_budget = increment; break; } case 'percentage': { @@ -626,12 +710,7 @@ async function applyCategoryTemplate( 0, Math.round(monthlyIncome * (percent / 100)), ); - if (increment + to_budget <= budgetAvailable || !priority) { - to_budget += increment; - } else { - if (budgetAvailable > 0) to_budget = budgetAvailable; - errors.push(`Insufficient funds.`); - } + to_budget += increment; break; } case 'schedule': { @@ -714,7 +793,7 @@ async function applyCategoryTemplate( t = t.filter(t => t.completed === 0); t = t.sort((a, b) => b.target - a.target); - let diff = 0; + let increment = 0; if (balance >= totalScheduledGoal) { for (let ll = 0; ll < t.length; ll++) { if (t[ll].num_months < 0) { @@ -728,11 +807,11 @@ async function applyCategoryTemplate( t[ll].target_frequency === 'weekly' || t[ll].target_frequency === 'daily' ) { - diff += t[ll].target; + increment += t[ll].target; } else if (t[ll].template.full && t[ll].num_months > 0) { - diff += 0; + increment += 0; } else { - diff += t[ll].target / t[ll].target_interval; + increment += t[ll].target / t[ll].target_interval; } } } else if (balance < totalScheduledGoal) { @@ -776,25 +855,17 @@ async function applyCategoryTemplate( t[ll].target_frequency === 'weekly' || t[ll].target_frequency === 'daily' ) { - diff += tg; + increment += tg; } else if (t[ll].template.full && t[ll].num_months > 0) { - diff += 0; + increment += 0; } else { - diff += tg / (t[ll].num_months + 1); + increment += tg / (t[ll].num_months + 1); } } } } - diff = Math.round(diff); - if ((diff > 0 && to_budget + diff <= budgetAvailable) || !priority) { - to_budget += diff; - } else if ( - to_budget + diff > budgetAvailable && - budgetAvailable >= 0 - ) { - to_budget = budgetAvailable; - errors.push(`Insufficient funds.`); - } + increment = Math.round(increment); + to_budget += increment; } break; } @@ -820,32 +891,23 @@ async function applyCategoryTemplate( } } - if (limit != null) { + if (limitCheck) { if (hold && balance > limit) { to_budget = 0; } else if (to_budget + balance > limit) { to_budget = limit - balance; } } - if ( - ((category.budgeted != null && category.budgeted !== 0) || - to_budget === 0) && - !force - ) { - return { errors }; - } else if (category.budgeted === to_budget) { - return null; - } else { - let str = category.name + ': ' + integerToAmount(last_month_balance); - str += - ' + ' + - integerToAmount(to_budget) + - ' = ' + - integerToAmount(last_month_balance + to_budget); - str += ' ' + template_lines.map(x => x.line).join('\n'); - console.log(str); - return { amount: to_budget, errors }; - } + // setup notifications + let str = category.name + ': ' + integerToAmount(last_month_balance); + str += + ' + ' + + integerToAmount(to_budget) + + ' = ' + + integerToAmount(last_month_balance + to_budget); + str += ' ' + template_lines.map(x => x.line).join('\n'); + console.log(str); + return { amount: to_budget, errors }; } async function checkTemplates(): Promise { diff --git a/packages/loot-core/src/server/budget/types/handlers.d.ts b/packages/loot-core/src/server/budget/types/handlers.d.ts index 3c44a0a4ba3..bb3f6964080 100644 --- a/packages/loot-core/src/server/budget/types/handlers.d.ts +++ b/packages/loot-core/src/server/budget/types/handlers.d.ts @@ -15,11 +15,13 @@ export interface BudgetHandlers { 'budget/check-templates': () => Promise; - 'budget/apply-goal-template': (arg: { month: string }) => Promise; + 'budget/apply-goal-template': (arg: { + month: string; + }) => Promise; 'budget/overwrite-goal-template': (arg: { month: string; - }) => Promise; + }) => Promise; 'budget/cleanup-goal-template': (arg: { month: string; @@ -40,7 +42,7 @@ export interface BudgetHandlers { 'budget/apply-single-template': (arg: { month: string; category: string; //category id - }) => Promise; + }) => Promise; 'budget/set-n-month-avg': (arg: { month: string; diff --git a/packages/loot-core/src/server/sheet.ts b/packages/loot-core/src/server/sheet.ts index 180cbaf13d8..0420f27dc2e 100644 --- a/packages/loot-core/src/server/sheet.ts +++ b/packages/loot-core/src/server/sheet.ts @@ -213,6 +213,7 @@ export async function loadUserBudgets(db): Promise { `${sheetName}!carryover-${budget.category}`, budget.carryover === 1 ? true : false, ); + sheet.set(`${sheetName}!goal-${budget.category}`, budget.goal); } } diff --git a/upcoming-release-notes/1780.md b/upcoming-release-notes/1780.md new file mode 100644 index 00000000000..a3005704cf1 --- /dev/null +++ b/upcoming-release-notes/1780.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [shall0pass, youngcw] +--- + +Goals: Add indicator of goal status. Add db entries for saving the goal, and for the template json.