-
Matiss Janis Aboltins authoredMatiss Janis Aboltins authored
cash-flow-spreadsheet.tsx 5.99 KiB
// @ts-strict-ignore
import React from 'react';
import * as d from 'date-fns';
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 { integerToCurrency, integerToAmount } from 'loot-core/src/shared/util';
import { type RuleConditionEntity } from 'loot-core/types/models';
import { AlignedText } from '../../common/AlignedText';
import { runAll, indexCashFlow } from '../util';
export function simpleCashFlow(start, end) {
return async (spreadsheet, setData) => {
function makeQuery() {
return q('transactions')
.filter({
$and: [{ date: { $gte: start } }, { date: { $lte: end } }],
'account.offbudget': false,
$or: [
{
'payee.transfer_acct.offbudget': true,
'payee.transfer_acct': null,
},
],
})
.calculate({ $sum: '$amount' });
}
return runAll(
[
makeQuery().filter({ amount: { $gt: 0 } }),
makeQuery().filter({ amount: { $lt: 0 } }),
],
data => {
setData({
graphData: {
income: data[0],
expense: data[1],
},
});
},
);
};
}
export function cashFlowByDate(
start: string,
end: string,
isConcise: boolean,
conditions: RuleConditionEntity[] = [],
conditionsOp: 'and' | 'or',
) {
return async (spreadsheet, setData) => {
const { filters } = await send('make-filters-from-conditions', {
conditions: conditions.filter(cond => !cond.customName),
});
const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
function makeQuery() {
const query = q('transactions')
.filter({
[conditionsOpKey]: filters,
})
.filter({
$and: [
{ date: { $transform: '$month', $gte: start } },
{ date: { $transform: '$month', $lte: end } },
],
'account.offbudget': false,
});
if (isConcise) {
return query
.groupBy([{ $month: '$date' }, 'payee.transfer_acct'])
.select([
{ date: { $month: '$date' } },
{ isTransfer: 'payee.transfer_acct' },
{ amount: { $sum: '$amount' } },
]);
}
return query
.groupBy(['date', 'payee.transfer_acct'])
.select([
'date',
{ isTransfer: 'payee.transfer_acct' },
{ amount: { $sum: '$amount' } },
]);
}
return runAll(
[
q('transactions')
.filter({
[conditionsOpKey]: filters,
date: { $transform: '$month', $lt: start },
'account.offbudget': false,
})
.calculate({ $sum: '$amount' }),
makeQuery().filter({ amount: { $gt: 0 } }),
makeQuery().filter({ amount: { $lt: 0 } }),
],
data => {
setData(recalculate(data, start, end, isConcise));
},
);
};
}
function recalculate(data, start, end, isConcise) {
const [startingBalance, income, expense] = data;
const convIncome = income.map(t => {
return { ...t, isTransfer: t.isTransfer !== null };
});
const convExpense = expense.map(t => {
return { ...t, isTransfer: t.isTransfer !== null };
});
const dates = isConcise
? monthUtils.rangeInclusive(
monthUtils.getMonth(start),
monthUtils.getMonth(end),
)
: monthUtils.dayRangeInclusive(start, end);
const incomes = indexCashFlow(convIncome, 'date', 'isTransfer');
const expenses = indexCashFlow(convExpense, 'date', 'isTransfer');
let balance = startingBalance;
let totalExpenses = 0;
let totalIncome = 0;
let totalTransfers = 0;
const graphData = dates.reduce(
(res, date) => {
let income = 0;
let expense = 0;
let creditTransfers = 0;
let debitTransfers = 0;
if (incomes[date]) {
income = !incomes[date].false ? 0 : incomes[date].false;
creditTransfers = !incomes[date].true ? 0 : incomes[date].true;
}
if (expenses[date]) {
expense = !expenses[date].false ? 0 : expenses[date].false;
debitTransfers = !expenses[date].true ? 0 : expenses[date].true;
}
totalExpenses += expense;
totalIncome += income;
balance += income + expense + creditTransfers + debitTransfers;
totalTransfers += creditTransfers + debitTransfers;
const x = d.parseISO(date);
const label = (
<div>
<div style={{ marginBottom: 10 }}>
<strong>
{d.format(x, isConcise ? 'MMMM yyyy' : 'MMMM d, yyyy')}
</strong>
</div>
<div style={{ lineHeight: 1.5 }}>
<AlignedText left="Income:" right={integerToCurrency(income)} />
<AlignedText left="Expenses:" right={integerToCurrency(expense)} />
<AlignedText
left="Change:"
right={<strong>{integerToCurrency(income + expense)}</strong>}
/>
{creditTransfers + debitTransfers !== 0 && (
<AlignedText
left="Transfers:"
right={integerToCurrency(creditTransfers + debitTransfers)}
/>
)}
<AlignedText left="Balance:" right={integerToCurrency(balance)} />
</div>
</div>
);
res.income.push({ x, y: integerToAmount(income) });
res.expenses.push({ x, y: integerToAmount(expense) });
res.transfers.push({
x,
y: integerToAmount(creditTransfers + debitTransfers),
});
res.balances.push({
x,
y: integerToAmount(balance),
premadeLabel: label,
amount: balance,
});
return res;
},
{ expenses: [], income: [], transfers: [], balances: [] },
);
const { balances } = graphData;
return {
graphData,
balance: balances[balances.length - 1].amount,
totalExpenses,
totalIncome,
totalTransfers,
totalChange: balances[balances.length - 1].amount - balances[0].amount,
};
}