Skip to content
Snippets Groups Projects
  • Neil's avatar
    1f105999
    Customizeable Reports (#1791) · 1f105999
    Neil authored
    * Reorganize and add graphs
    
    * Create Customizable Chart
    
    * Notes
    
    * Hide Menu update Donut
    
    * lint fixes
    
    * Organize Menus
    
    * Change Title
    
    * UI changes
    
    * UI updates
    
    * Add Data Table
    
    * Functionality additions and Privacy Filters
    
    * Date filters working and formatting changes
    
    * Fix default spreadsheet and add tableGraph
    
    * Integrate Summary Data and Split Legend
    
    * started adding functionality on charts
    
    * list fix
    
    * Enabling more graphs, fixing errors
    
    * Legend, interactions, Empty Rows Filter
    
    * fixes for EmptyRows/interactions/legends
    
    * formatting UI and filtering data
    
    * format date
    
    * fix errors
    
    * Fix Legend Order
    
    * lint fixes
    
    * Add tooltips
    
    * Feature Flag
    
    * fix overview card, fix offbudget checkbox
    
    * Revamped dataType, added scrollBars
    
    * data display adjustments
    
    * data spreadsheet updates/groups added to matrix
    
    * Add Category Selector
    
    * Add Labels Button
    
    * formatting fixes
    
    * Add Averages to dataTable
    
    * data bug fix
    
    * Added all type back in with exceptions
    
    * formatting
    
    * split assets/debts, add Uncategorized
    
    * bug fixes and UI updates
    
    * add scrollbars to table
    
    * formatting dataTable
    
    * tooltips, navigation and graph labels
    
    * Code clean-up and re-org
    
    * revert color change
    
    * Change labels name
    
    * organize files
    
    * code cleanup
    
    * Tooltip Colors
    
    * Descoping legend for future PR
    
    * descope legend & rename split
    
    * rename type variable to be more descriptive
    
    * adjustments for sankey and eslint merges
    
    * notes update
    
    * code review fixes
    
    * code fixes
    
    * fix date selections
    Customizeable Reports (#1791)
    Neil authored
    * Reorganize and add graphs
    
    * Create Customizable Chart
    
    * Notes
    
    * Hide Menu update Donut
    
    * lint fixes
    
    * Organize Menus
    
    * Change Title
    
    * UI changes
    
    * UI updates
    
    * Add Data Table
    
    * Functionality additions and Privacy Filters
    
    * Date filters working and formatting changes
    
    * Fix default spreadsheet and add tableGraph
    
    * Integrate Summary Data and Split Legend
    
    * started adding functionality on charts
    
    * list fix
    
    * Enabling more graphs, fixing errors
    
    * Legend, interactions, Empty Rows Filter
    
    * fixes for EmptyRows/interactions/legends
    
    * formatting UI and filtering data
    
    * format date
    
    * fix errors
    
    * Fix Legend Order
    
    * lint fixes
    
    * Add tooltips
    
    * Feature Flag
    
    * fix overview card, fix offbudget checkbox
    
    * Revamped dataType, added scrollBars
    
    * data display adjustments
    
    * data spreadsheet updates/groups added to matrix
    
    * Add Category Selector
    
    * Add Labels Button
    
    * formatting fixes
    
    * Add Averages to dataTable
    
    * data bug fix
    
    * Added all type back in with exceptions
    
    * formatting
    
    * split assets/debts, add Uncategorized
    
    * bug fixes and UI updates
    
    * add scrollbars to table
    
    * formatting dataTable
    
    * tooltips, navigation and graph labels
    
    * Code clean-up and re-org
    
    * revert color change
    
    * Change labels name
    
    * organize files
    
    * code cleanup
    
    * Tooltip Colors
    
    * Descoping legend for future PR
    
    * descope legend & rename split
    
    * rename type variable to be more descriptive
    
    * adjustments for sankey and eslint merges
    
    * notes update
    
    * code review fixes
    
    * code fixes
    
    * fix date selections
cash-flow-spreadsheet.tsx 5.74 KiB
import React from 'react';

import * as d from 'date-fns';

import q from 'loot-core/src/client/query-helpers';
import { send } from 'loot-core/src/platform/client/fetch';
import * as monthUtils from 'loot-core/src/shared/months';
import { integerToCurrency, integerToAmount } from 'loot-core/src/shared/util';

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,
  end,
  isConcise,
  conditions = [],
  conditionsOp,
) {
  return async (spreadsheet, setData) => {
    let { filters } = await send('make-filters-from-conditions', {
      conditions: conditions.filter(cond => !cond.customName),
    });
    const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';

    function makeQuery(where) {
      let 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('amount > 0').filter({ amount: { $gt: 0 } }),
        makeQuery('amount < 0').filter({ amount: { $lt: 0 } }),
      ],
      data => {
        setData(recalculate(data, start, end, isConcise));
      },
    );
  };
}

function recalculate(data, start, end, isConcise) {
  let [startingBalance, income, expense] = data;
  let convIncome = income.map(t => {
    return { ...t, isTransfer: t.isTransfer !== null };
  });
  let 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.balances.push({
        x,
        y: integerToAmount(balance),
        premadeLabel: label,
        amount: balance,
      });
      return res;
    },
    { expenses: [], income: [], balances: [] },
  );

  const { balances } = graphData;

  return {
    graphData,
    balance: balances[balances.length - 1].amount,
    totalExpenses,
    totalIncome,
    totalTransfers,
    totalChange: balances[balances.length - 1].amount - balances[0].amount,
  };
}