mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 03:23:51 -05:00
* Fix bug with spending report cumulative totals * release notes --------- Co-authored-by: Joel Rich <joelrich@protonmail.com>
259 lines
7.8 KiB
TypeScript
259 lines
7.8 KiB
TypeScript
// @ts-strict-ignore
|
|
import keyBy from 'lodash/keyBy';
|
|
|
|
import { runQuery } from 'loot-core/src/client/query-helpers';
|
|
import { type useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';
|
|
import { send } from 'loot-core/src/platform/client/fetch';
|
|
import * as monthUtils from 'loot-core/src/shared/months';
|
|
import { q } from 'loot-core/src/shared/query';
|
|
import { integerToAmount } from 'loot-core/src/shared/util';
|
|
import { type RuleConditionEntity } from 'loot-core/src/types/models';
|
|
import {
|
|
type SpendingMonthEntity,
|
|
type SpendingEntity,
|
|
} from 'loot-core/src/types/models/reports';
|
|
|
|
import { makeQuery } from './makeQuery';
|
|
|
|
type createSpendingSpreadsheetProps = {
|
|
conditions?: RuleConditionEntity[];
|
|
conditionsOp?: string;
|
|
compare?: string;
|
|
compareTo?: string;
|
|
};
|
|
|
|
export function createSpendingSpreadsheet({
|
|
conditions = [],
|
|
conditionsOp,
|
|
compare,
|
|
compareTo,
|
|
}: createSpendingSpreadsheetProps) {
|
|
const startDate = monthUtils.subMonths(compare, 3) + '-01';
|
|
const endDate = monthUtils.getMonthEnd(compare + '-01');
|
|
const startDateTo = compareTo + '-01';
|
|
const endDateTo = monthUtils.getMonthEnd(compareTo + '-01');
|
|
const interval = 'Daily';
|
|
const compareInterval = monthUtils.dayRangeInclusive(
|
|
compare + '-01',
|
|
endDate,
|
|
);
|
|
|
|
return async (
|
|
spreadsheet: ReturnType<typeof useSpreadsheet>,
|
|
setData: (data: SpendingEntity) => void,
|
|
) => {
|
|
const { filters } = await send('make-filters-from-conditions', {
|
|
conditions: conditions.filter(cond => !cond.customName),
|
|
});
|
|
const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
|
|
|
|
const [assets, debts] = await Promise.all([
|
|
runQuery(
|
|
makeQuery(
|
|
'assets',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
conditionsOpKey,
|
|
filters,
|
|
),
|
|
).then(({ data }) => data),
|
|
runQuery(
|
|
makeQuery(
|
|
'debts',
|
|
startDate,
|
|
endDate,
|
|
interval,
|
|
conditionsOpKey,
|
|
filters,
|
|
),
|
|
).then(({ data }) => data),
|
|
]);
|
|
|
|
const [assetsTo, debtsTo] = await Promise.all([
|
|
runQuery(
|
|
makeQuery(
|
|
'assets',
|
|
startDateTo,
|
|
endDateTo,
|
|
interval,
|
|
conditionsOpKey,
|
|
filters,
|
|
),
|
|
).then(({ data }) => data),
|
|
runQuery(
|
|
makeQuery(
|
|
'debts',
|
|
startDateTo,
|
|
endDateTo,
|
|
interval,
|
|
conditionsOpKey,
|
|
filters,
|
|
),
|
|
).then(({ data }) => data),
|
|
]);
|
|
|
|
const overlapAssets =
|
|
endDateTo < startDate || startDateTo > endDate ? assetsTo : [];
|
|
const overlapDebts =
|
|
endDateTo < startDate || startDateTo > endDate ? debtsTo : [];
|
|
|
|
const combineAssets = [...assets, ...overlapAssets];
|
|
const combineDebts = [...debts, ...overlapDebts];
|
|
|
|
const budgetMonth = parseInt(compare.replace('-', ''));
|
|
const [budgets] = await Promise.all([
|
|
runQuery(
|
|
q('zero_budgets')
|
|
.filter({
|
|
$and: [{ month: { $eq: budgetMonth } }],
|
|
})
|
|
.filter({
|
|
[conditionsOpKey]: filters.filter(filter => filter.category),
|
|
})
|
|
.groupBy([{ $id: '$category' }])
|
|
.select([
|
|
{ category: { $id: '$category' } },
|
|
{ amount: { $sum: '$amount' } },
|
|
]),
|
|
).then(({ data }) => data),
|
|
]);
|
|
|
|
const dailyBudget =
|
|
budgets &&
|
|
integerToAmount(budgets.reduce((a, v) => (a = a + v.amount), 0)) /
|
|
compareInterval.length;
|
|
|
|
const intervals = monthUtils.dayRangeInclusive(startDate, endDate);
|
|
if (endDateTo < startDate || startDateTo > endDate) {
|
|
intervals.push(...monthUtils.dayRangeInclusive(startDateTo, endDateTo));
|
|
}
|
|
|
|
const days = [...Array(29).keys()]
|
|
.filter(f => f > 0)
|
|
.map(n => n.toString().padStart(2, '0'));
|
|
|
|
let totalAssets = 0;
|
|
let totalDebts = 0;
|
|
let totalBudget = 0;
|
|
|
|
const months = monthUtils.rangeInclusive(startDate, endDate).map(month => {
|
|
return { month, perMonthAssets: 0, perMonthDebts: 0 };
|
|
});
|
|
|
|
if (endDateTo < startDate || startDateTo > endDate) {
|
|
months.unshift({
|
|
month: compareTo,
|
|
perMonthAssets: 0,
|
|
perMonthDebts: 0,
|
|
});
|
|
}
|
|
|
|
const intervalData = days.map(day => {
|
|
let averageSum = 0;
|
|
let monthCount = 0;
|
|
const dayData = months.map(month => {
|
|
const data = intervals.reduce((arr, intervalItem) => {
|
|
const offsetDay =
|
|
Number(intervalItem.substring(8, 10)) >= 28
|
|
? '28'
|
|
: intervalItem.substring(8, 10);
|
|
let perIntervalAssets = 0;
|
|
let perIntervalDebts = 0;
|
|
|
|
if (
|
|
month.month === monthUtils.getMonth(intervalItem) &&
|
|
day === offsetDay
|
|
) {
|
|
const intervalAssets = combineAssets
|
|
.filter(e => !e.categoryIncome && !e.accountOffBudget)
|
|
.filter(asset => asset.date === intervalItem)
|
|
.reduce((a, v) => (a = a + v.amount), 0);
|
|
perIntervalAssets += intervalAssets;
|
|
|
|
const intervalDebts = combineDebts
|
|
.filter(e => !e.categoryIncome && !e.accountOffBudget)
|
|
.filter(debt => debt.date === intervalItem)
|
|
.reduce((a, v) => (a = a + v.amount), 0);
|
|
perIntervalDebts += intervalDebts;
|
|
|
|
totalAssets += perIntervalAssets;
|
|
totalDebts += perIntervalDebts;
|
|
|
|
let cumulativeAssets = 0;
|
|
let cumulativeDebts = 0;
|
|
|
|
if (month.month === compare) {
|
|
totalBudget -= dailyBudget;
|
|
}
|
|
|
|
months.map(m => {
|
|
if (m.month === month.month) {
|
|
cumulativeAssets = m.perMonthAssets += perIntervalAssets;
|
|
cumulativeDebts = m.perMonthDebts += perIntervalDebts;
|
|
}
|
|
return null;
|
|
});
|
|
if (month.month >= startDate && month.month < compare) {
|
|
if (day === '28') {
|
|
if (monthUtils.getMonthEnd(intervalItem) === intervalItem) {
|
|
averageSum += cumulativeAssets + cumulativeDebts;
|
|
monthCount += 1;
|
|
}
|
|
} else {
|
|
averageSum += cumulativeAssets + cumulativeDebts;
|
|
monthCount += 1;
|
|
}
|
|
}
|
|
|
|
arr.push({
|
|
date: intervalItem,
|
|
totalDebts: integerToAmount(perIntervalDebts),
|
|
totalAssets: integerToAmount(perIntervalAssets),
|
|
totalTotals: integerToAmount(
|
|
perIntervalDebts + perIntervalAssets,
|
|
),
|
|
cumulative:
|
|
intervalItem <= monthUtils.currentDay()
|
|
? integerToAmount(cumulativeDebts + cumulativeAssets)
|
|
: null,
|
|
});
|
|
}
|
|
|
|
return arr;
|
|
}, []);
|
|
const maxCumulative = data.reduce((a, b) =>
|
|
b.cumulative === null ? a : b,
|
|
).cumulative;
|
|
|
|
const totalDaily = data.reduce((a, v) => (a = a + v.totalTotals), 0);
|
|
|
|
return {
|
|
date: data[0].date,
|
|
cumulative: maxCumulative,
|
|
daily: totalDaily,
|
|
month: month.month,
|
|
};
|
|
});
|
|
const indexedData: SpendingMonthEntity = keyBy(dayData, 'month');
|
|
return {
|
|
months: indexedData,
|
|
day,
|
|
average: integerToAmount(averageSum) / monthCount,
|
|
compare: dayData.filter(c => c.month === compare)[0].cumulative,
|
|
compareTo: dayData.filter(c => c.month === compareTo)[0].cumulative,
|
|
budget: totalBudget,
|
|
};
|
|
});
|
|
|
|
setData({
|
|
intervalData,
|
|
startDate,
|
|
endDate,
|
|
totalDebts: integerToAmount(totalDebts),
|
|
totalAssets: integerToAmount(totalAssets),
|
|
totalTotals: integerToAmount(totalAssets + totalDebts),
|
|
});
|
|
};
|
|
}
|