Skip to content
Snippets Groups Projects
MobileAccount.js 6.17 KiB
import React, { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams, useNavigate } from 'react-router-dom';

import debounce from 'debounce';
import memoizeOne from 'memoize-one';
import { bindActionCreators } from 'redux';

import * as actions from 'loot-core/src/client/actions';
import {
  SchedulesProvider,
  useCachedSchedules,
} from 'loot-core/src/client/data-hooks/schedules';
import * as queries from 'loot-core/src/client/queries';
import { pagedQuery } from 'loot-core/src/client/query-helpers';
import { listen } from 'loot-core/src/platform/client/fetch';
import {
  isPreviewId,
  ungroupTransactions,
} from 'loot-core/src/shared/transactions';

import useCategories from '../../hooks/useCategories';
import { useSetThemeColor } from '../../hooks/useSetThemeColor';
import { theme } from '../../style';

import AccountDetails from './MobileAccountDetails';

const getSchedulesTransform = memoizeOne((id, hasSearch) => {
  let filter = queries.getAccountFilter(id, '_account');

  // Never show schedules on these pages
  if (hasSearch) {
    filter = { id: null };
  }

  return q => {
    q = q.filter({ $and: [filter, { '_account.closed': false }] });
    return q.orderBy({ next_date: 'desc' });
  };
});

function PreviewTransactions({ accountId, children }) {
  let scheduleData = useCachedSchedules();

  if (scheduleData == null) {
    return children(null);
  }

  let schedules = scheduleData.schedules.filter(
    s =>
      !s.completed &&
      ['due', 'upcoming', 'missed'].includes(scheduleData.statuses.get(s.id)),
  );

  return children(
    schedules.map(schedule => ({
      id: 'preview/' + schedule.id,
      payee: schedule._payee,
      account: schedule._account,
      amount: schedule._amount,
      date: schedule.next_date,
      notes: scheduleData.statuses.get(schedule.id),
      schedule: schedule.id,
    })),
  );
}

let paged;

export default function Account(props) {
  const accounts = useSelector(state => state.queries.accounts);

  const navigate = useNavigate();
  const [transactions, setTransactions] = useState([]);
  const [searchText, setSearchText] = useState('');
  const [currentQuery, setCurrentQuery] = useState();

  let state = useSelector(state => ({
    payees: state.queries.payees,
    newTransactions: state.queries.newTransactions,
    prefs: state.prefs.local,
    dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
  }));

  let dispatch = useDispatch();
  let actionCreators = useMemo(
    () => bindActionCreators(actions, dispatch),
    [dispatch],
  );

  const { id: accountId } = useParams();

  const makeRootQuery = () => queries.makeTransactionsQuery(accountId);

  const updateQuery = query => {
    if (paged) {
      paged.unsubscribe();
    }

    paged = pagedQuery(
      query.options({ splits: 'grouped' }).select('*'),
      data => setTransactions(data),
      { pageCount: 150, mapper: ungroupTransactions },
    );
  };

  const fetchTransactions = async () => {
    let query = makeRootQuery();
    setCurrentQuery(query);
    updateQuery(query);
  };

  useEffect(() => {
    let unlisten;

    async function setUpAccount() {
      unlisten = listen('sync-event', ({ type, tables }) => {
        if (type === 'applied') {
          if (
            tables.includes('transactions') ||
            tables.includes('category_mapping') ||
            tables.includes('payee_mapping')
          ) {
            paged?.run();
          }

          if (tables.includes('payees') || tables.includes('payee_mapping')) {
            actionCreators.getPayees();
          }
        }
      });

      if (accounts.length === 0) {
        await actionCreators.getAccounts();
      }

      await actionCreators.initiallyLoadPayees();
      await fetchTransactions();

      actionCreators.markAccountRead(accountId);
    }

    setUpAccount();

    return () => unlisten();
  }, []);

  // Load categories if necessary.
  const categories = useCategories();

  const updateSearchQuery = debounce(() => {
    if (searchText === '' && currentQuery) {
      updateQuery(currentQuery);
    } else if (searchText && currentQuery) {
      updateQuery(
        queries.makeTransactionSearchQuery(
          currentQuery,
          searchText,
          state.dateFormat,
        ),
      );
    }
  }, 150);

  useEffect(updateSearchQuery, [searchText, currentQuery, state.dateFormat]);

  useSetThemeColor(theme.mobileAccountViewTheme);

  if (!accounts || !accounts.length) {
    return null;
  }

  const account = accounts.find(acct => acct.id === accountId);

  const isNewTransaction = id => {
    return state.newTransactions.includes(id);
  };

  const onSearch = async text => {
    paged.unsubscribe();
    setSearchText(text);
  };

  const onSelectTransaction = transaction => {
    // details of how the native app used to handle preview transactions here can be found at commit 05e58279
    if (!isPreviewId(transaction.id)) {
      navigate(`transactions/${transaction.id}`);
    }
  };

  let balance = queries.accountBalance(account);
  let numberFormat = state.prefs.numberFormat || 'comma-dot';
  let hideFraction = state.prefs.hideFraction || false;

  return (
    <SchedulesProvider
      transform={getSchedulesTransform(accountId, searchText !== '')}
    >
      <PreviewTransactions accountId={props.accountId}>
        {prependTransactions =>
          prependTransactions == null ? null : (
            <AccountDetails
              // This key forces the whole table rerender when the number
              // format changes
              {...state}
              {...actionCreators}
              key={numberFormat + hideFraction}
              account={account}
              accounts={accounts}
              categories={categories.list}
              payees={state.payees}
              transactions={transactions}
              prependTransactions={prependTransactions || []}
              balance={balance}
              isNewTransaction={isNewTransaction}
              onLoadMore={() => {
                paged?.fetchNext();
              }}
              onSearch={onSearch}
              onSelectTransaction={onSelectTransaction}
            />
          )
        }
      </PreviewTransactions>
    </SchedulesProvider>
  );
}