diff --git a/main/moneymoney/handlers.ts b/main/moneymoney/handlers.ts index 99802bd..e653d98 100644 --- a/main/moneymoney/handlers.ts +++ b/main/moneymoney/handlers.ts @@ -105,6 +105,54 @@ function withRetry Promise>( }) as any; } +function extractTransactions(val: unknown): unknown[] { + if ( + typeof val === 'object' && + val !== null && + Array.isArray((val as any).transactions) + ) { + return (val as any).transactions; + } + + throw new Error('Unexpected transactions object'); +} + +async function exportTransactions( + _: any, + accountNumbers: string[], + startDate: string, +): Promise; +async function exportTransactions(_: any): Promise; +async function exportTransactions( + _: any, + accountNumbers?: string[], + startDate?: string, +): Promise { + if (accountNumbers && startDate) { + const transactions = await Promise.all( + accountNumbers.map(async (accountNumber) => { + return parse( + await osascript( + join(scriptsDir, 'exportTransactions.applescript'), + accountNumber, + startDate, + ), + ); + }), + ); + + return transactions + .map(extractTransactions) + .reduce((m, ts) => m.concat(ts), []); + } + + return extractTransactions( + parse( + await osascript(join(scriptsDir, 'exportAllTransactions.applescript')), + ), + ); +} + export default function moneymoneyHandlers(ipcMain: IpcMain) { ipcMain.handle( 'MM_EXPORT_ACCOUNTS', @@ -119,31 +167,7 @@ export default function moneymoneyHandlers(ipcMain: IpcMain) { }), ); - ipcMain.handle( - 'MM_EXPORT_TRANSACTIONS', - withRetry(async (_, accountNumbers: string[], startDate: string) => { - return Promise.all( - accountNumbers.map(async (accountNumber) => { - return parse( - await osascript( - join(scriptsDir, 'exportTransactions.applescript'), - accountNumber, - startDate, - ), - ); - }), - ); - }), - ); - - ipcMain.handle( - 'MM_EXPORT_ALL_TRANSACTIONS', - withRetry(async () => { - return parse( - await osascript(join(scriptsDir, 'exportAllTransactions.applescript')), - ); - }), - ); + ipcMain.handle('MM_EXPORT_TRANSACTIONS', withRetry(exportTransactions)); ipcMain.handle( 'MM_EXPORT_CATEGORIES', diff --git a/main/scripts/exportAllTransactions.applescript b/main/scripts/exportAllTransactions.applescript index 771c113..c24c1e9 100644 --- a/main/scripts/exportAllTransactions.applescript +++ b/main/scripts/exportAllTransactions.applescript @@ -1 +1 @@ -tell application "MoneyMoney" to export transactions from date 1900 - 1 - 1 as "plist" +tell application "MoneyMoney" to export transactions from date 1900-1-1 as "plist" diff --git a/src/App.tsx b/src/App.tsx index 32a4495..a941057 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,19 @@ import './theme.scss'; -import React, { Suspense, useState, useCallback, ReactNode } from 'react'; +import React, { + Suspense, + useState, + useCallback, + ReactNode, + Dispatch, + SetStateAction, +} from 'react'; import classNames from 'classnames'; import { InitRes, getInitData, useBudgetReducer, initialInitDataRes, + InitDataWithState, } from './budget'; import { ErrorBoundary, Startup } from './components'; import styles from './App.module.scss'; @@ -16,29 +24,24 @@ const Welcome = React.lazy(() => import('./views/Welcome')); const NewBudget = React.lazy(() => import('./views/NewBudget')); const Main = React.lazy(() => import('./views/Main')); -function App({ readInitialView }: { readInitialView: InitRes }) { - const [initialView, initialState] = readInitialView(); - const [view, setView] = useState('new' as typeof initialView); - const [moneyMoney, updateSettings] = useMoneyMoney(); - const [state, dispatch] = useBudgetReducer(initialState, updateSettings); +function App(initData: InitDataWithState) { + const [view, setView] = useState(initData.view); + const [moneyMoney, updateSettings] = useMoneyMoney(initData.res); + const [state, dispatch] = useBudgetReducer(initData.state, updateSettings); const numberFormatter = useNumberFormatter(state.settings.fractionDigits); const openBudget = useCallback(() => { setView('budget'); - }, []); - const openNew = useCallback(() => { - setView('new'); - }, []); + }, [setView]); return ( }> {((): ReactNode => { switch (view) { - case 'welcome': - return ; case 'new': return ( >; +}; +function AppWelcomeSwitch({ + readInitialView, + setInitRes, +}: AppWelcomeSwitchProps) { + const initData = readInitialView(); + const openNew = useCallback(() => { + // setInitRes + // setView('new'); + }, []); + + switch (initData.view) { + case 'welcome': + return ; + default: + return ; + } +} + export default function AppWrapper() { const [readInit, setInitRes] = useState(() => initialInitDataRes); const retryReadInit = useRetryResource( @@ -79,7 +104,10 @@ export default function AppWrapper() { > }> - + diff --git a/src/budget/Types.ts b/src/budget/Types.ts index 694c61b..2592e56 100644 --- a/src/budget/Types.ts +++ b/src/budget/Types.ts @@ -122,15 +122,19 @@ export type OverspendRollover = { [key: string]: boolean }; export type Rollover = { total: number; [key: string]: number }; export type InterMonthData = { + startBalance?: number; uncategorized: AmountWithTransactions; categories: (BudgetCategoryRow | BudgetCategoryGroup)[]; toBudget: number; total: BudgetRow; income: AmountWithPartialTransactions; - overspendPrevMonth: number; + prevMonth: { + overspend: number; + startBalance?: number; + toBudget: number; + }; overspendRolloverState: OverspendRollover; available: AmountWithPartialTransactions[]; - availableThisMonth: AmountWithPartialTransactions; rollover: Rollover; }; export type MonthData = { diff --git a/src/budget/createInitialState.ts b/src/budget/createInitialState.ts new file mode 100644 index 0000000..2cc9917 --- /dev/null +++ b/src/budget/createInitialState.ts @@ -0,0 +1,108 @@ +import startOfMonth from 'date-fns/startOfMonth'; +import subMonths from 'date-fns/subMonths'; +import isAfter from 'date-fns/isAfter'; +import { getToday } from '../lib'; +import { + getAccounts, + getTransactions, + filterAccounts, + Transaction, + Account, +} from '../moneymoney'; + +import { BudgetState, IncomeCategory, VERSION } from './Types'; + +function getStartBalance( + startDate: Date, + transactions: Transaction[], + accounts: Account[], +): number { + const transactionsSinceStart = transactions.filter(({ bookingDate }) => + isAfter(bookingDate, startDate), + ); + const transactionBal = transactionsSinceStart.reduce( + (m, { amount }) => m + amount, + 0, + ); + const accountsBal = accounts.reduce((m, { balance }) => m + balance, 0); + + return accountsBal + transactionBal * -1; +} + +function isLaterHalfOfMonth({ bookingDate }: Transaction) { + return bookingDate.getDate() >= 15; +} + +function getIncomeCategories(transactions: Transaction[]): IncomeCategory[] { + const transactionsByCat = transactions.reduce((memo, transaction) => { + const { categoryUuid, amount } = transaction; + + if (!memo[categoryUuid]) { + memo[categoryUuid] = { transactions: [], balance: 0, hasNegative: false }; + } + + memo[categoryUuid].transactions.push(transaction); + memo[categoryUuid].balance += amount; + if (amount < 0) { + memo[categoryUuid].hasNegative = true; + } + + return memo; + }, {} as { [key: string]: { transactions: Transaction[]; balance: number; hasNegative: boolean } }); + + const positiveCats = Object.entries(transactionsByCat) + .filter(([_, { hasNegative }]) => !hasNegative) + .sort(([_, { balance: a }], [__, { balance: b }]) => b - a); + + return positiveCats.map(([id, { transactions }]) => ({ + id, + availableIn: transactions.some(isLaterHalfOfMonth) ? 1 : 0, + })); +} + +export default async function createInitialState(): Promise { + const [allAccounts, allTransactions] = await Promise.all([ + getAccounts(), + getTransactions(), + ]); + const currenciesWithUsage = allAccounts.reduce( + (memo, { group, currency }) => { + if (group) { + return memo; + } + memo[currency] = (memo[currency] || 0) + 1; + return memo; + }, + { USD: 1 } as { [key: string]: number }, + ); + const currenciesByUsage = Object.entries(currenciesWithUsage) + .sort(([_, a], [__, b]) => b - a) + .map(([c]) => c); + const currency = currenciesByUsage[0]; + const accounts = filterAccounts(currency, allAccounts).filter( + ({ group, portfolio }) => !group && !portfolio, + ); + const accountUuids = accounts.map(({ uuid }) => uuid); + const transactionsOfAccounts = allTransactions.filter(({ accountUuid }) => + accountUuids.includes(accountUuid), + ); + const startDate = startOfMonth(subMonths(getToday(), 1)); + + return { + name: '', + version: VERSION, + budgets: {}, + settings: { + currency, + incomeCategories: getIncomeCategories(transactionsOfAccounts), + accounts: accountUuids, + fractionDigits: 2, + startDate: startDate.getTime(), + startBalance: getStartBalance( + startDate, + transactionsOfAccounts, + accounts, + ), + }, + }; +} diff --git a/src/budget/getInitData.ts b/src/budget/getInitData.ts index 05c9ab5..585f4c3 100644 --- a/src/budget/getInitData.ts +++ b/src/budget/getInitData.ts @@ -2,12 +2,24 @@ import { ipcRenderer } from 'electron'; import { readFile as rf } from 'fs'; import { promisify } from 'util'; import { createResource, Resource } from '../lib'; -import { View } from '../shared/types'; -import { INITIAL_STATE } from './budgetReducer'; +import { InitialRes, createInitialRes } from '../moneymoney'; +import { + ViewBudget, + ViewNew, + ViewSettings, + ViewWelcome, + View, +} from '../shared/types'; import { validateBudgetState, BudgetState } from './Types'; +import createInitialState from './createInitialState'; const readFile = promisify(rf); -type InitData = [View['type'], BudgetState]; +export type InitDataWithState = { + view: (ViewBudget | ViewNew | ViewSettings)['type']; + state: BudgetState; + res: InitialRes; +}; +export type InitData = InitDataWithState | { view: ViewWelcome['type'] }; export type InitRes = Resource; async function getInitData(): Promise { @@ -15,14 +27,26 @@ async function getInitData(): Promise { switch (init.type) { case 'welcome': - case 'new': - return [init.type, INITIAL_STATE]; + return { view: init.type }; + case 'new': { + const initialState = await createInitialState(); + return { + view: init.type, + state: initialState, + res: createInitialRes(init.type, initialState.settings), + }; + } case 'budget': - case 'settings': - return [ - init.type, - validateBudgetState(JSON.parse((await readFile(init.file)).toString())), - ]; + case 'settings': { + const state = validateBudgetState( + JSON.parse((await readFile(init.file)).toString()), + ); + return { + view: init.type, + state, + res: createInitialRes(init.type, state.settings), + }; + } } } diff --git a/src/budget/getMonthData.ts b/src/budget/getMonthData.ts index 079a3b1..aca4f59 100644 --- a/src/budget/getMonthData.ts +++ b/src/budget/getMonthData.ts @@ -46,24 +46,6 @@ function assignAvailable( }); return available; } -function addBudgeted( - toBudget: number, - available: AmountWithPartialTransactions, -): AmountWithPartialTransactions { - if (toBudget === 0) { - return available; - } - return { - amount: available.amount + toBudget, - transactions: [ - { - amount: toBudget, - name: `${toBudget > 0 ? 'Not budgeted' : 'Overbudgeted'} last month`, - }, - ...available.transactions, - ], - }; -} function emptyBudgetRow(): BudgetRow { return { budgeted: 0, spend: 0, balance: 0 }; @@ -171,6 +153,7 @@ const calcMonth: MonthDataGetter = function calcMonth( round, ): InterMonthData { const { + startBalance, overspendRolloverState: prevOverspendRolloverState, toBudget: prevToBudget, available: prevAvailable, @@ -181,7 +164,7 @@ const calcMonth: MonthDataGetter = function calcMonth( amount: 0, transactions: [], }; - const availableThisMonth = addBudgeted(prevToBudget, income); + const { rollover, categories: budgetCategories, @@ -200,7 +183,9 @@ const calcMonth: MonthDataGetter = function calcMonth( transactions: [], }; const toBudget = round( - availableThisMonth.amount - + (startBalance || 0) + + income.amount + + prevToBudget - total.budgeted + overspendPrevMonth + uncategorized.amount, @@ -212,9 +197,12 @@ const calcMonth: MonthDataGetter = function calcMonth( total, available, income, - availableThisMonth, rollover, - overspendPrevMonth, + prevMonth: { + startBalance, + toBudget: prevToBudget, + overspend: overspendPrevMonth, + }, categories: budgetCategories, uncategorized, }; diff --git a/src/budget/index.ts b/src/budget/index.ts index aed6997..ba757ce 100644 --- a/src/budget/index.ts +++ b/src/budget/index.ts @@ -9,7 +9,10 @@ import { BudgetCategoryGroup as BudgetCategoryGroupT, } from './Types'; import { Action as ActionT } from './budgetReducer'; -import { InitRes as InitResT } from './getInitData'; +import { + InitRes as InitResT, + InitDataWithState as InitDataWithStateT, +} from './getInitData'; export * from './budgetReducer'; export { VERSION } from './Types'; @@ -22,6 +25,7 @@ export { } from './getInitData'; export type InitRes = InitResT; +export type InitDataWithState = InitDataWithStateT; export type Action = ActionT; export type BudgetState = BudgetStateT; export type BudgetListEntry = BudgetListEntryT; diff --git a/src/budget/useBudgetData.ts b/src/budget/useBudgetData.ts index 48927a6..7ba5b8e 100644 --- a/src/budget/useBudgetData.ts +++ b/src/budget/useBudgetData.ts @@ -1,14 +1,8 @@ -import type { Transaction, MoneyMoneyRes } from '../moneymoney'; +import type { MoneyMoneyRes } from '../moneymoney'; import { BudgetState } from './Types'; import useBudgets from './useBudgets'; import useFilteredCategories from './useFilteredCategories'; -function transactionsLoaded( - transactions: Transaction[] | Error | null, -): transactions is Transaction[] { - return Array.isArray(transactions); -} - export default function useBudgetData( state: BudgetState, { readCategories, readTransactions }: MoneyMoneyRes, @@ -20,7 +14,7 @@ export default function useBudgetData( const usableCategories = useFilteredCategories(incomeCategories, categories); const [months, extendFuture] = useBudgets( - transactionsLoaded(transactions) ? transactions : undefined, + transactions, usableCategories, defaultCategories, state, diff --git a/src/budget/useBudgets.ts b/src/budget/useBudgets.ts index 5c32aa7..9ed2b2b 100644 --- a/src/budget/useBudgets.ts +++ b/src/budget/useBudgets.ts @@ -28,24 +28,19 @@ export default function useBudgets( ); const getInitial = useMemo(() => { const initial: InterMonthData = { + startBalance, uncategorized: { amount: 0, transactions: [] }, total: { budgeted: 0, spend: 0, balance: 0 }, categories: [], - overspendPrevMonth: 0, + prevMonth: { + toBudget: 0, + overspend: 0, + }, toBudget: 0, income: { amount: 0, transactions: [] }, overspendRolloverState: {}, rollover: { total: 0 }, - availableThisMonth: { - amount: 0, - transactions: [], - }, - available: [ - { - amount: startBalance, - transactions: [], - }, - ], + available: [], }; return () => initial; }, [startBalance]); diff --git a/src/lib/__test__/factories/monthData.ts b/src/lib/__test__/factories/monthData.ts index 19a4df0..5634e12 100644 --- a/src/lib/__test__/factories/monthData.ts +++ b/src/lib/__test__/factories/monthData.ts @@ -9,11 +9,10 @@ export function createMonthData( categories = [], toBudget = 0, total = { spend: 0, balance: 0, budgeted: 0 }, - overspendPrevMonth = 0, + prevMonth = { overspend: 0, toBudget: 0 }, overspendRolloverState = {}, available = [], income = { amount: 0, transactions: [] }, - availableThisMonth = { amount: 0, transactions: [] }, rollover = { total: 0 }, }: Partial = {}, ): MonthData { @@ -27,10 +26,9 @@ export function createMonthData( toBudget, total, income, - overspendPrevMonth, + prevMonth, overspendRolloverState, available, - availableThisMonth, rollover, }), }; diff --git a/src/lib/parseBudgetInput.ts b/src/lib/parseBudgetInput.ts index 7e70103..61970f0 100644 --- a/src/lib/parseBudgetInput.ts +++ b/src/lib/parseBudgetInput.ts @@ -185,9 +185,6 @@ export default function parseBudgetInput( parser.functions.balance = createGetter( (data, catInput) => getCatByInput(data, catInput).balance * -1, ); - parser.functions.available = createGetter( - (data) => data.availableThisMonth.amount, - ).bind(null, total); parser.functions.income = createGetter((data) => data.income.amount).bind( null, total, diff --git a/src/moneymoney/Types.ts b/src/moneymoney/Types.ts index 7b4ffeb..93bd2e6 100644 --- a/src/moneymoney/Types.ts +++ b/src/moneymoney/Types.ts @@ -57,16 +57,8 @@ const transactionShape = t.intersection( ], 'transaction', ); -const transactionsShape = t.type( - { - transactions: t.array(transactionShape), - }, - 'accounts', -); -const transactionsByAccountShape = t.array( - transactionsShape, - 'transactionsByAccount', -); +const transactionsShape = t.array(transactionShape, 'transactions'); + const interopAccountShape = t.type( { accountNumber: t.string, @@ -108,7 +100,6 @@ export type Category = t.TypeOf; export type Transaction = t.TypeOf; export type Transactions = t.TypeOf; export type InteropAccount = t.TypeOf; -export type TransactionsByAccount = t.TypeOf; export type Account = { name: string; balance: number; @@ -127,15 +118,6 @@ export function validateTransactions(data: unknown): Transactions { } return c.right; } -export function validateTransactionByAccount( - data: unknown, -): TransactionsByAccount { - const c = transactionsByAccountShape.decode(data); - if (isLeft(c)) { - throw ThrowReporter.report(c); - } - return c.right; -} export function validateAccount(data: unknown): InteropAccount { const c = interopAccountShape.decode(data); if (isLeft(c)) { diff --git a/src/moneymoney/getAllTransactions.ts b/src/moneymoney/getAllTransactions.ts deleted file mode 100644 index 7eaa05f..0000000 --- a/src/moneymoney/getAllTransactions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Transaction, validateTransactions } from './Types'; -import { ipcRenderer } from 'electron'; -import { createResource } from '../lib'; -import { TransactionsResource } from './getTransactions'; -import memoizeOne from 'memoize-one'; - -export async function getAllTransactions(): Promise { - return validateTransactions( - await ipcRenderer.invoke('MM_EXPORT_ALL_TRANSACTIONS'), - ).transactions; -} -const getAllTransactionsMemo = memoizeOne((cacheToken: symbol) => - getAllTransactions(), -); - -function getAllTransactionsResource(cacheToken: symbol): TransactionsResource { - return createResource(() => getAllTransactionsMemo(cacheToken)); -} - -export default memoizeOne(getAllTransactionsResource); diff --git a/src/moneymoney/getTransactions.ts b/src/moneymoney/getTransactions.ts index 44f8f6d..741f622 100644 --- a/src/moneymoney/getTransactions.ts +++ b/src/moneymoney/getTransactions.ts @@ -1,4 +1,4 @@ -import { Transaction, validateTransactionByAccount } from './Types'; +import { Transaction, validateTransactions } from './Types'; import format from 'date-fns/format'; import { ipcRenderer } from 'electron'; import { createResource, Resource } from '../lib'; @@ -11,37 +11,41 @@ export const filterCurrency = memoizeOne( transactions.filter(({ currency: c }) => c === currency), ); -async function getTransactions( +export async function getTransactions(): Promise; +export async function getTransactions( accountNumbers: string[], startDateTimestamp: number, +): Promise; +export async function getTransactions( + accountNumbers?: string[], + startDateTimestamp?: number, ): Promise { + if (!accountNumbers || !startDateTimestamp) { + return validateTransactions( + await ipcRenderer.invoke('MM_EXPORT_TRANSACTIONS'), + ); + } + if (accountNumbers.length === 0) { return []; } const startDate = format(new Date(startDateTimestamp), 'yyyy-MM-dd'); - const transactionsByAccount = validateTransactionByAccount( + + return validateTransactions( await ipcRenderer.invoke( 'MM_EXPORT_TRANSACTIONS', accountNumbers, startDate, ), ); - - return transactionsByAccount.reduce( - (memo, { transactions }) => memo.concat(transactions), - [] as Transaction[], - ); } -export default function getTransactionsResource( - p: Promise<{ - accounts: string[]; - startDate: number; - }>, -): TransactionsResource { - return createResource(async () => { - const { accounts, startDate } = await p; - return getTransactions(accounts, startDate); - }); +export default function getTransactionsResource(settings: { + accounts: string[]; + startDate: number; +}): TransactionsResource { + return createResource(() => + getTransactions(settings.accounts, settings.startDate), + ); } diff --git a/src/moneymoney/index.ts b/src/moneymoney/index.ts index a9950af..e5937f9 100644 --- a/src/moneymoney/index.ts +++ b/src/moneymoney/index.ts @@ -7,10 +7,14 @@ import { Balance as BalanceT, } from './Types'; import { AccountsResource as AccountsResourceT } from './getAccounts'; -import { MoneyMoneyRes as MoneyMoneyResT } from './useMoneyMoney'; -export { getAccounts } from './getAccounts'; -export { default as getTransactionsRes } from './getTransactions'; -export { useMoneyMoney } from './useMoneyMoney'; +import { + MoneyMoneyRes as MoneyMoneyResT, + InitialRes as InitialResT, +} from './useMoneyMoney'; +export { getAccounts, filterAccounts } from './getAccounts'; +export { getTransactions } from './getTransactions'; +export { getCategories } from './getCategories'; +export { useMoneyMoney, createInitialRes } from './useMoneyMoney'; export { default as calculateBalances } from './calculateBalances'; export * from './errors'; export type AccountsResource = AccountsResourceT; @@ -21,3 +25,4 @@ export type Balances = BalancesT; export type Balance = BalanceT; export type AmountWithTransactions = AmountWithTransactionsT; export type MoneyMoneyRes = MoneyMoneyResT; +export type InitialRes = InitialResT; diff --git a/src/moneymoney/useMoneyMoney.ts b/src/moneymoney/useMoneyMoney.ts index 88d86c4..221cf49 100644 --- a/src/moneymoney/useMoneyMoney.ts +++ b/src/moneymoney/useMoneyMoney.ts @@ -9,8 +9,8 @@ import getCategories, { } from './getCategories'; import getAccounts, { AccountsResource, filterAccounts } from './getAccounts'; import { BudgetState } from '../budget'; -import { initialInitData } from '../budget/getInitData'; -import { useRetryResource } from '../lib'; +import { useRetryResource, Resource } from '../lib'; +import { Category, InteropAccount, Transaction } from './Types'; export type MoneyMoneyRes = { readCategories: CategoryResource; @@ -24,58 +24,57 @@ type RequiredSettings = Pick< 'accounts' | 'currency' | 'startDate' >; -let initialSettings: RequiredSettings | null; -const pSettings = initialInitData.then(([_, { settings }]) => { - initialSettings = settings; - return settings; -}); -const initialAccountsRes = getAccounts(); -const initialCategoriesRes = getCategories(); -const initialTransactionsRes = getTransactions(pSettings); +export type InitialRes = { + settings: RequiredSettings; + accounts: Resource; + categories: Resource; + transactions: Resource; +}; + +export function createInitialRes( + view: 'budget' | 'settings' | 'new', + settings: RequiredSettings, +): InitialRes { + const init: InitialRes = { + settings, + accounts: getAccounts(), + categories: getCategories(), + transactions: getTransactions(settings), + }; -function ignoreError(cb: () => void) { try { - cb(); + switch (view) { + case 'budget': + init.categories(); + init.transactions(); + break; + case 'settings': + case 'new': + init.accounts(); + break; + } } catch (err) { /* ¯\_(ツ)_/¯ */ } -} -initialInitData.then(([initialView]) => { - switch (initialView) { - case 'budget': - ignoreError(() => initialCategoriesRes()); - ignoreError(() => initialTransactionsRes()); - break; - case 'settings': - case 'new': - ignoreError(() => initialAccountsRes()); - break; - } -}); - -export function useMoneyMoney(): [ - MoneyMoneyRes, - (settings: RequiredSettings) => void, -] { - if (!initialSettings) { - throw new Error( - 'Unexpected useMoneyMoney call before initialSettings are resolved', - ); - } + return init; +} +export function useMoneyMoney( + initialRes: InitialRes, +): [MoneyMoneyRes, (settings: RequiredSettings) => void] { const [currency, setCurrency] = useState( - initialSettings.currency, + initialRes.settings.currency, ); - const settingsRef = useRef(initialSettings); + const settingsRef = useRef(initialRes.settings); - const [readAccounts, setAccountsRes] = useState(() => initialAccountsRes); + const [readAccounts, setAccountsRes] = useState(() => initialRes.accounts); const [readCategories, setCategoriesRes] = useState( - () => initialCategoriesRes, + () => initialRes.categories, ); const [readTransactions, setTransactionsRes] = useState( - () => initialTransactionsRes, + () => initialRes.transactions, ); const refreshAll = useCallback(() => { @@ -85,9 +84,7 @@ export function useMoneyMoney(): [ const newCategoriesRes = getCategories(); setCategoriesRes(() => newCategoriesRes); - const newTransactionsRes = getTransactions( - Promise.resolve(settingsRef.current), - ); + const newTransactionsRes = getTransactions(settingsRef.current); setTransactionsRes(() => newTransactionsRes); }, []); @@ -117,9 +114,7 @@ export function useMoneyMoney(): [ oldSettings.accounts !== newSettings.accounts || oldSettings.startDate !== newSettings.startDate ) { - const newTransactionsRes = getTransactions( - Promise.resolve(settingsRef.current), - ); + const newTransactionsRes = getTransactions(settingsRef.current); setTimeout(() => { setTransactionsRes(() => newTransactionsRes); }, 10); diff --git a/src/shared/types.ts b/src/shared/types.ts index 9acea3b..7edf140 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,5 +1,6 @@ -export type View = - | { type: 'new'; file?: never } - | { type: 'welcome'; file?: never } - | { type: 'budget'; file: string } - | { type: 'settings'; file: string }; +export type ViewNew = { type: 'new'; file?: never }; +export type ViewWelcome = { type: 'welcome'; file?: never }; +export type ViewBudget = { type: 'budget'; file: string }; +export type ViewSettings = { type: 'settings'; file: string }; + +export type View = ViewNew | ViewWelcome | ViewBudget | ViewSettings; diff --git a/src/views/Month/Overview.tsx b/src/views/Month/Overview.tsx index 3738c4f..4f37a44 100644 --- a/src/views/Month/Overview.tsx +++ b/src/views/Month/Overview.tsx @@ -68,14 +68,34 @@ export default function Overview({ {data && ( <>
    + {data.prevMonth.startBalance !== undefined && ( + + )} + {data.prevMonth.toBudget > 0 && ( + + )} + {data.prevMonth.toBudget < 0 && ( + + )} - + {data.prevMonth.overspend !== 0 && ( + + )} {data.uncategorized.amount !== 0 && ( = 0 ? 'To Budget' : 'Overbudgeted'} />
diff --git a/src/views/NewBudget/03_fillCategories.tsx b/src/views/NewBudget/03_fillCategories.tsx index 568c8e4..98e7d8b 100644 --- a/src/views/NewBudget/03_fillCategories.tsx +++ b/src/views/NewBudget/03_fillCategories.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useRef, useMemo } from 'react'; +import React, { Suspense, useRef } from 'react'; import cx from 'classnames'; import { ErrorBoundary, @@ -12,7 +12,6 @@ import CategorySidebar from './components/CategorySidebar'; import SingleBudget from './components/SingleBudget'; import { Step } from './Types'; import styles from './NewBudget.module.scss'; -import { createNumberFormatter } from '../../lib'; const IntroIncomeCats: Step = { title: 'Fill Categories', @@ -22,11 +21,6 @@ const IntroIncomeCats: Step = { Comp(props) { const sidebarRef = useRef(null); const budgetRef = useRef(null); - const { fractionDigits, numberLocale } = props.state.settings; - const numberFormatter = useMemo( - () => createNumberFormatter(fractionDigits, numberLocale), - [fractionDigits, numberLocale], - ); return ( <> @@ -84,10 +78,12 @@ const IntroIncomeCats: Step = {

This Money is taken from "To Budget"

But currently there are{' '} - {numberFormatter.format(0)} "Available Funds" and - thereby no money available to budget. + {props.numberFormatter.format(0)} "Available + Funds" and thereby no money available to budget.

- + diff --git a/src/views/NewBudget/04_availableFunds.tsx b/src/views/NewBudget/04_availableFunds.tsx index ce6d2ec..d7a2254 100644 --- a/src/views/NewBudget/04_availableFunds.tsx +++ b/src/views/NewBudget/04_availableFunds.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import cx from 'classnames'; import SingleBudget from './components/SingleBudget'; @@ -11,9 +11,6 @@ const AvailableFunds: Step = { return true; }, Comp(props) { - const sidebarRef = useRef(null); - const budgetRef = useRef(null); - return ( <> diff --git a/src/views/NewBudget/NewBudget.module.scss b/src/views/NewBudget/NewBudget.module.scss index b1b63f5..403eba4 100644 --- a/src/views/NewBudget/NewBudget.module.scss +++ b/src/views/NewBudget/NewBudget.module.scss @@ -118,6 +118,7 @@ } .singleBudgetWrap { @extend .singleBudgetDimensions; + background: var(--background-color); overflow: scroll; display: flex; } diff --git a/src/views/NewBudget/NewBudget.tsx b/src/views/NewBudget/NewBudget.tsx index f0ab56c..4f35391 100644 --- a/src/views/NewBudget/NewBudget.tsx +++ b/src/views/NewBudget/NewBudget.tsx @@ -9,7 +9,7 @@ import { Content, Button, Header, HeaderSpacer } from '../../components'; import useMenu from '../../lib/useMenu'; import { OK, Step } from './Types'; import { MoneyMoneyRes } from '../../moneymoney'; -import { HeaderHeightProvider } from '../../lib'; +import { HeaderHeightProvider, NumberFormatter } from '../../lib'; import useSelectAllAccounts from './useSelectAllAccounts'; import Welcome from './01_welcome'; import Categories from './02_categories'; @@ -19,6 +19,7 @@ import AvailableFunds from './04_availableFunds'; const STEPS: Step[] = [Welcome, Categories, FillCategories, AvailableFunds]; type Props = { + numberFormatter: NumberFormatter; state: BudgetState; dispatch: Dispatch; moneyMoney: MoneyMoneyRes; @@ -31,7 +32,12 @@ function getProgress(i: number) { return (100 / (STEPS.length - 1)) * i; } -export default function NewBudget({ state, dispatch, moneyMoney }: Props) { +export default function NewBudget({ + state, + dispatch, + moneyMoney, + numberFormatter, +}: Props) { const [ { step: { Comp }, @@ -112,6 +118,7 @@ export default function NewBudget({ state, dispatch, moneyMoney }: Props) { dispatch={dispatch} moneyMoney={moneyMoney} nextPage={nextPage} + numberFormatter={numberFormatter} /> diff --git a/src/views/NewBudget/Types.ts b/src/views/NewBudget/Types.ts index 185abfe..d30b2d6 100644 --- a/src/views/NewBudget/Types.ts +++ b/src/views/NewBudget/Types.ts @@ -1,5 +1,6 @@ import { Dispatch, ReactElement } from 'react'; import { BudgetState, Action } from '../../budget'; +import { NumberFormatter } from '../../lib'; import { MoneyMoneyRes } from '../../moneymoney'; export type StepCompProps = { @@ -8,6 +9,7 @@ export type StepCompProps = { dispatch: Dispatch; moneyMoney: MoneyMoneyRes; setOk: (ok: OK) => void; + numberFormatter: NumberFormatter; }; export type OK = boolean | 'primary'; export type Step = { diff --git a/src/views/NewBudget/components/SingleBudget.tsx b/src/views/NewBudget/components/SingleBudget.tsx index ab0defb..cd3c7d4 100644 --- a/src/views/NewBudget/components/SingleBudget.tsx +++ b/src/views/NewBudget/components/SingleBudget.tsx @@ -6,46 +6,53 @@ import { MonthData, useBudgetData } from '../../../budget'; import Month from '../../Month'; import { useSyncScrollY } from '../../../lib'; +type InterMonthData = ReturnType; + +function zeroAll(data: InterMonthData): InterMonthData { + return { + income: { amount: 0, transactions: [] }, + prevMonth: { overspend: 0, toBudget: 0 }, + overspendRolloverState: {}, + rollover: { total: 0 }, + toBudget: 0, + available: [], + uncategorized: { amount: 0, transactions: [] }, + total: { + balance: 0, + budgeted: 0, + spend: 0, + }, + categories: data.categories.map((cat) => ({ + ...cat, + balance: 0, + budgeted: 0, + spend: 0, + })), + }; +} + export default function SingleBudget({ state, moneyMoney, innerRef, syncScrollY, small, + numberFormatter, + mapMonthData = zeroAll, }: StepCompProps & { + mapMonthData?: (data: InterMonthData) => InterMonthData; small?: boolean; innerRef?: MutableRefObject; syncScrollY?: MutableRefObject; }) { - const { months, numberFormatter, categories } = useBudgetData( - state, - moneyMoney, - ); + const { months, categories } = useBudgetData(state, moneyMoney); const syncScroll = useSyncScrollY(syncScrollY); const month = useMemo(() => { return { ...months[0], - get: (): ReturnType => { - const data = months[0].get(); - - return { - ...data, - uncategorized: { amount: 0, transactions: [] }, - total: { - balance: 0, - budgeted: 0, - spend: 0, - }, - categories: data.categories.map((cat) => ({ - ...cat, - balance: 0, - budgeted: 0, - spend: 0, - })), - }; - }, + get: () => mapMonthData(months[0].get()), }; - }, [months]); + }, [months, mapMonthData]); return (