diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png index 5574f459f07..97d5dec1aa3 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png index 996545fab4e..9d912171d47 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png index 64807bb85f5..1d67bf60131 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png index e37fc6eeda8..42dbf39a157 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png index 5f3d2ce786f..8ecab730c23 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png index 4b35b1090e0..ab76fcd48d9 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 3b7b4b2f096..e2e201169a7 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -9,7 +9,6 @@ import useCategories from '../hooks/useCategories'; import useSyncServerStatus from '../hooks/useSyncServerStatus'; import { type CommonModalProps } from '../types/modals'; -import BudgetSummary from './modals/BudgetSummary'; import CloseAccount from './modals/CloseAccount'; import ConfirmCategoryDelete from './modals/ConfirmCategoryDelete'; import CreateAccount from './modals/CreateAccount'; @@ -25,8 +24,11 @@ import LoadBackup from './modals/LoadBackup'; import ManageRulesModal from './modals/ManageRulesModal'; import MergeUnusedPayees from './modals/MergeUnusedPayees'; import PlaidExternalMsg from './modals/PlaidExternalMsg'; +import ReportBudgetSummary from './modals/ReportBudgetSummary'; +import RolloverBudgetSummary from './modals/RolloverBudgetSummary'; import SelectLinkedAccounts from './modals/SelectLinkedAccounts'; import SingleInput from './modals/SingleInput'; +import SwitchBudgetType from './modals/SwitchBudgetType'; import DiscoverSchedules from './schedules/DiscoverSchedules'; import ScheduleDetails from './schedules/EditSchedule'; import ScheduleLink from './schedules/LinkSchedule'; @@ -247,9 +249,19 @@ export default function Modals() { /> ); - case 'budget-summary': + case 'rollover-budget-summary': return ( - + ); + + case 'report-budget-summary': + return ( + ); + case 'switch-budget-type': + return ( + + ); + default: console.error('Unknown modal:', name); return null; diff --git a/packages/desktop-client/src/components/Page.tsx b/packages/desktop-client/src/components/Page.tsx index 77d27bbba75..6f666400f7d 100644 --- a/packages/desktop-client/src/components/Page.tsx +++ b/packages/desktop-client/src/components/Page.tsx @@ -6,13 +6,21 @@ import { theme, styles, type CSSProperties } from '../style'; import Text from './common/Text'; import View from './common/View'; -function PageTitle({ - name, - style, -}: { +type PageHeaderProps = { name: ReactNode; style?: CSSProperties; -}) { + leftContent?: ReactNode; + rightContent?: ReactNode; +}; + +const HEADER_HEIGHT = 50; + +function PageHeader({ + name, + style, + leftContent, + rightContent, +}: PageHeaderProps) { const { isNarrowWidth } = useResponsive(); if (isNarrowWidth) { @@ -23,16 +31,42 @@ function PageTitle({ backgroundColor: theme.mobilePageBackground, color: theme.mobileModalText, flexDirection: 'row', - flex: '1 0 auto', - fontSize: 18, - fontWeight: 500, - height: 50, - justifyContent: 'center', - overflowY: 'auto', + flexShrink: 0, + height: HEADER_HEIGHT, ...style, }} > - {name} + + {leftContent} + + + {name} + + + {rightContent} + ); } @@ -51,25 +85,33 @@ function PageTitle({ ); } +type PageProps = { + title: ReactNode; + titleStyle?: CSSProperties; + headerLeftContent?: ReactNode; + headerRightContent?: ReactNode; + children: ReactNode; +}; + export function Page({ title, - children, titleStyle, -}: { - title: ReactNode; - children: ReactNode; - titleStyle?: CSSProperties; -}) { + headerLeftContent, + headerRightContent, + children, +}: PageProps) { let { isNarrowWidth } = useResponsive(); let HORIZONTAL_PADDING = isNarrowWidth ? 10 : 20; return ( - - - - - - - - Back - - - - - - - {account.name} - - - - - - - - - + ); } + +function AccountDetailsHeader({ account }) { + return ( + + + + + + + Back + + + + + + + {account.name} + + + + + + + + + + ); +} diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.js b/packages/desktop-client/src/components/accounts/MobileAccounts.js index 186583d3a73..eda891234a0 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.js +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.js @@ -7,6 +7,7 @@ import { useActions } from '../../hooks/useActions'; import useCategories from '../../hooks/useCategories'; import useNavigate from '../../hooks/useNavigate'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; +import Add from '../../icons/v1/Add'; import { theme, styles } from '../../style'; import Button from '../common/Button'; import Text from '../common/Text'; @@ -124,7 +125,7 @@ function AccountCard({ account, updated, getBalanceQuery, onSelect }) { ); } -function EmptyMessage({ onAdd }) { +function EmptyMessage() { return ( @@ -132,22 +133,6 @@ function EmptyMessage({ onAdd }) { account to automatically download transactions, or manage it locally yourself. - - - alert( - 'Account creation is not supported on mobile on the self-hosted service yet', - ) - } - > - Add Account - - - - In the future, you can add accounts using the add button in the header. - ); } @@ -160,16 +145,14 @@ function AccountList({ getOffBudgetBalance, onAddAccount, onSelectAccount, + onSync, }) { - const { syncAndDownload } = useActions(); - const budgetedAccounts = accounts.filter(account => account.offbudget === 0); const offbudgetAccounts = accounts.filter(account => account.offbudget === 1); - - // If there are no accounts, show a helpful message - if (accounts.length === 0) { - return ; - } + const noBackgroundColorStyle = { + backgroundColor: 'transparent', + color: 'white', + }; return ( @@ -180,9 +163,27 @@ function AccountList({ color: theme.mobileHeaderText, fontSize: 16, }} + headerRightContent={ + + + + } > - - + {accounts.length === 0 && } + + {budgetedAccounts.length > 0 && ( + + )} {budgetedAccounts.map(acct => ( ))} - + {offbudgetAccounts.length > 0 && ( + + )} {offbudgetAccounts.map(acct => ( {}} // () => navigate('AddAccountModal') + onAddAccount={() => replaceModal('add-account')} onSelectAccount={onSelectAccount} onSelectTransaction={onSelectTransaction} + onSync={syncAndDownload} /> ); diff --git a/packages/desktop-client/src/components/budget/BudgetSummaries.tsx b/packages/desktop-client/src/components/budget/BudgetSummaries.tsx index cd4bfed88ac..11dc77f5bc2 100644 --- a/packages/desktop-client/src/components/budget/BudgetSummaries.tsx +++ b/packages/desktop-client/src/components/budget/BudgetSummaries.tsx @@ -15,8 +15,8 @@ import useResizeObserver from '../../hooks/useResizeObserver'; import View from '../common/View'; import { MonthsContext } from './MonthsContext'; -import { type BudgetSummary as ReportBudgetSummary } from './report/BudgetSummary'; -import { type BudgetSummary as RolloverBudgetSummary } from './rollover/BudgetSummary'; +import type ReportBudgetSummary from './report/budgetsummary/BudgetSummary'; +import type RolloverBudgetSummary from './rollover/budgetsummary/BudgetSummary'; type BudgetSummariesProps = { SummaryComponent: typeof ReportBudgetSummary | typeof RolloverBudgetSummary; diff --git a/packages/desktop-client/src/components/budget/MobileBudget.js b/packages/desktop-client/src/components/budget/MobileBudget.js index 0f3d5268617..f937f5f9a1b 100644 --- a/packages/desktop-client/src/components/budget/MobileBudget.js +++ b/packages/desktop-client/src/components/budget/MobileBudget.js @@ -24,6 +24,7 @@ import View from '../common/View'; import SyncRefresh from '../SyncRefresh'; import { BudgetTable } from './MobileBudgetTable'; +import { prewarmMonth, switchBudgetType } from './util'; class Budget extends Component { constructor(props) { @@ -54,7 +55,13 @@ class Budget extends Component { const { start, end } = await send('get-budget-bounds'); this.setState({ bounds: { start, end } }); - this.prewarmMonth(this.state.currentMonth); + await prewarmMonth( + this.props.budgetType, + this.props.spreadsheet, + this.state.currentMonth, + ); + + this.setState({ initialized: true }); let unlisten = listen('sync-event', ({ type, tables }) => { if ( @@ -78,27 +85,19 @@ class Budget extends Component { this.cleanup?.(); } - prewarmMonth = async (month, type = null) => { - type = type || this.props.budgetType; - - let method = - type === 'report' ? 'report-budget-month' : 'rollover-budget-month'; - - let values = await send(method, { month }); - - for (let value of values) { - this.props.spreadsheet.prewarmCache(value.name, value); - } - - if (!this.state.initialized) { - this.setState({ initialized: true }); + onShowBudgetSummary = () => { + if (this.props.budgetType === 'report') { + this.props.pushModal('report-budget-summary', { + month: this.state.currentMonth, + }); + } else { + this.props.pushModal('rollover-budget-summary', { + month: this.state.currentMonth, + onBudgetAction: this.props.applyBudgetAction, + }); } }; - onShowBudgetDetails = () => { - this.props.pushModal('budget-summary', { month: this.state.currentMonth }); - }; - onBudgetAction = type => { const { currentMonth } = this.state; this.props.applyBudgetAction(currentMonth, type, this.state.bounds); @@ -267,15 +266,17 @@ class Budget extends Component { }; onPrevMonth = async () => { + let { spreadsheet, budgetType } = this.props; let month = monthUtils.subMonths(this.state.currentMonth, 1); - await this.prewarmMonth(month); - this.setState({ currentMonth: month }); + await prewarmMonth(budgetType, spreadsheet, month); + this.setState({ currentMonth: month, initialized: true }); }; onNextMonth = async () => { + let { spreadsheet, budgetType } = this.props; let month = monthUtils.addMonths(this.state.currentMonth, 1); - await this.prewarmMonth(month); - this.setState({ currentMonth: month }); + await prewarmMonth(budgetType, spreadsheet, month); + this.setState({ currentMonth: month, initialized: true }); }; onOpenActionSheet = () => { @@ -321,6 +322,19 @@ class Budget extends Component { ); }; + onSwitchBudgetType = async () => { + const { spreadsheet, budgetType, loadPrefs } = this.props; + const { bounds, currentMonth } = this.state; + + this.setState({ initialized: false }); + + await switchBudgetType(budgetType, spreadsheet, bounds, currentMonth, () => + loadPrefs(), + ); + + this.setState({ initialized: true }); + }; + render() { const { currentMonth, bounds, editMode, initialized } = this.state; const { @@ -331,6 +345,7 @@ class Budget extends Component { budgetType, navigation, applyBudgetAction, + pushModal, } = this.props; let numberFormat = prefs.numberFormat || 'comma-dot'; let hideFraction = prefs.hideFraction || false; @@ -369,7 +384,7 @@ class Budget extends Component { // } editMode={editMode} onEditMode={flag => this.setState({ editMode: flag })} - onShowBudgetDetails={this.onShowBudgetDetails} + onShowBudgetSummary={this.onShowBudgetSummary} onPrevMonth={this.onPrevMonth} onNextMonth={this.onNextMonth} onSaveGroup={this.onSaveGroup} @@ -383,7 +398,9 @@ class Budget extends Component { onOpenActionSheet={() => {}} //this.onOpenActionSheet} onBudgetAction={applyBudgetAction} onRefresh={onRefresh} + onSwitchBudgetType={this.onSwitchBudgetType} savePrefs={savePrefs} + pushModal={pushModal} /> )} diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.js b/packages/desktop-client/src/components/budget/MobileBudgetTable.js index 36c18087a43..b5a684c6b81 100644 --- a/packages/desktop-client/src/components/budget/MobileBudgetTable.js +++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.js @@ -6,6 +6,7 @@ import memoizeOne from 'memoize-one'; import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries'; import * as monthUtils from 'loot-core/src/shared/months'; +import useFeatureFlag from '../../hooks/useFeatureFlag'; import ArrowThinLeft from '../../icons/v1/ArrowThinLeft'; import ArrowThinRight from '../../icons/v1/ArrowThinRight'; import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple'; @@ -35,7 +36,8 @@ import { AmountInput } from '../util/AmountInput'; // import { DragDrop, Draggable, Droppable, DragDropHighlight } from './dragdrop'; import BalanceWithCarryover from './BalanceWithCarryover'; import { ListItem, ROW_HEIGHT } from './MobileTable'; -import BalanceTooltip from './rollover/BalanceTooltip'; +import ReportBudgetBalanceTooltip from './report/BalanceTooltip'; +import RolloverBudgetBalanceTooltip from './rollover/BalanceTooltip'; import { makeAmountGrey } from './util'; function ToBudget({ toBudget, onClick }) { @@ -68,7 +70,7 @@ function ToBudget({ toBudget, onClick }) { ); } -function Saved({ projected }) { +function Saved({ projected, onClick }) { let binding = projected ? reportBudget.totalBudgetedSaved : reportBudget.totalSaved; @@ -77,21 +79,32 @@ function Saved({ projected }) { let isNegative = saved < 0; return ( - {projected ? ( ) : ( )} @@ -108,7 +121,7 @@ function Saved({ projected }) { : theme.formInputText, }} /> - + ); } @@ -219,7 +232,13 @@ function ExpenseCategoryPreview({ name, pending, style }) { } const ExpenseCategory = memo(function ExpenseCategory({ + type, category, + goal, + budgeted, + spent, + balance, + carryover, index, // gestures, blank, @@ -244,9 +263,6 @@ const ExpenseCategory = memo(function ExpenseCategory({ let [categoryName, setCategoryName] = useState(category.name); let [isHidden, setIsHidden] = useState(category.hidden); - let budgeted = rolloverBudget.catBudgeted(category.id); - let spent = rolloverBudget.catSumAmount(category.id); - let tooltip = useTooltip(); let balanceTooltip = useTooltip(); @@ -295,6 +311,14 @@ const ExpenseCategory = memo(function ExpenseCategory({ let listItemRef = useRef(); let inputRef = useRef(); + let _onBudgetAction = (monthIndex, action, arg) => { + onBudgetAction?.( + monthUtils.getMonthFromIndex(monthUtils.getYear(month), monthIndex), + action, + arg, + ); + }; + let content = ( e.preventDefault()} > - {balanceTooltip.isOpen && ( - { - onBudgetAction?.( - monthUtils.getMonthFromIndex( - monthUtils.getYear(month), - monthIndex, - ), - action, - arg, - ); - }} - onClose={() => { - onOpenBudgetActionMenu?.(null); - }} - /> - )} + {balanceTooltip.isOpen && + (type === 'report' ? ( + { + onOpenBudgetActionMenu?.(null); + }} + /> + ) : ( + { + onOpenBudgetActionMenu?.(null); + }} + /> + ))} @@ -516,6 +543,9 @@ const ExpenseCategory = memo(function ExpenseCategory({ const ExpenseGroupTotals = memo(function ExpenseGroupTotals({ group, + budgeted, + spent, + balance, editMode, isEditing, onEdit, @@ -690,7 +720,7 @@ const ExpenseGroupTotals = memo(function ExpenseGroupTotals({ }} > - {budget && ( + {budgeted && ( - {budget && ( + {budgeted && ( - + {/* + /> */} )} @@ -1350,7 +1441,7 @@ function IncomeGroup({ ); })} @@ -1448,6 +1543,7 @@ function BudgetGroups({ return ( )} @@ -1526,11 +1626,13 @@ export function BudgetTable(props) { onEditMode, onReorderCategory, onReorderGroup, - onShowBudgetDetails, + onShowBudgetSummary, // onOpenActionSheet, onBudgetAction, onRefresh, + onSwitchBudgetType, savePrefs, + pushModal, } = props; const GROUP_EDIT_ACTION = 'group'; @@ -1627,6 +1729,8 @@ export function BudgetTable(props) { onNextMonth={onNextMonth} showHiddenCategories={showHiddenCategories} savePrefs={savePrefs} + pushModal={pushModal} + onSwitchBudgetType={onSwitchBudgetType} /> {type === 'report' ? ( - = currentMonth} /> + = currentMonth} + onClick={onShowBudgetSummary} + /> ) : ( )} @@ -1676,7 +1783,11 @@ export function BudgetTable(props) { style={{ color: theme.buttonNormalText }} /> { tooltip.close(); @@ -1871,6 +1993,11 @@ function BudgetHeader({ case 'toggle-hidden-categories': toggleHiddenCategories(); break; + case 'switch-budget-type': + pushModal('switch-budget-type', { + onSwitch: onSwitchBudgetType, + }); + break; default: throw new Error(`Unrecognized menu option: ${name}`); } @@ -2007,6 +2134,10 @@ function BudgetHeader({ name: 'toggle-hidden-categories', text: 'Toggle hidden categories', }, + isReportBudgetEnabled && { + name: 'switch-budget-type', + text: 'Switch budget type', + }, ]} /> diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index dbc3f1bf594..12c70c57065 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -28,7 +28,6 @@ import { deleteGroup, } from 'loot-core/src/shared/categories'; import * as monthUtils from 'loot-core/src/shared/months'; -import { type Handlers } from 'loot-core/src/types/handlers'; import { type GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs'; import { type BoundActions, useActions } from '../../hooks/useActions'; @@ -40,11 +39,11 @@ import View from '../common/View'; import { TitlebarContext, type TitlebarContextValue } from '../Titlebar'; import DynamicBudgetTable from './DynamicBudgetTable'; -import { getValidMonthBounds } from './MonthsContext'; -import * as report from './report/components'; +import * as report from './report/ReportComponents'; import { ReportProvider } from './report/ReportContext'; -import * as rollover from './rollover/rollover-components'; +import * as rollover from './rollover/RolloverComponents'; import { RolloverContext } from './rollover/RolloverContext'; +import { prewarmAllMonths, prewarmMonth, switchBudgetType } from './util'; type ReportComponents = { SummaryComponent: typeof report.BudgetSummary; @@ -130,7 +129,14 @@ function Budget(props: BudgetProps) { let { start, end } = await send('get-budget-bounds'); setBounds({ start, end }); - prewarmAllMonths({ start, end }, budgetType); + await prewarmAllMonths( + budgetType, + props.spreadsheet, + { start, end }, + prewarmStartMonth, + ); + + setInitialized(true); } run(); @@ -184,36 +190,6 @@ function Budget(props: BudgetProps) { }); }, [props.accountId]); - const prewarmMonth = async (month, type = null) => { - type = type || props.budgetType; - - let method: keyof Handlers = - type === 'report' ? 'report-budget-month' : 'rollover-budget-month'; - - let values = await send(method, { month }); - - for (let value of values) { - props.spreadsheet.prewarmCache(value.name, value); - } - }; - - async function prewarmAllMonths(bounds, type = null) { - let numMonths = 3; - - const startMonth = props.startMonth || currentMonth; - - bounds = getValidMonthBounds( - bounds, - monthUtils.subMonths(startMonth, 1), - monthUtils.addMonths(startMonth, numMonths + 1), - ); - let months = monthUtils.rangeInclusive(bounds.start, bounds.end); - - await Promise.all(months.map(month => prewarmMonth(month, type))); - - setInitialized(true); - } - const onMonthSelect = async (month, numDisplayed) => { setPrewarmStartMonth(month); @@ -229,10 +205,18 @@ function Budget(props: BudgetProps) { // but it will just load in some unnecessary data. if (month < startMonth) { // pre-warm prev month - await prewarmMonth(monthUtils.subMonths(month, 1)); + await prewarmMonth( + props.budgetType, + props.spreadsheet, + monthUtils.subMonths(month, 1), + ); } else if (month > startMonth) { // pre-warm next month - await prewarmMonth(monthUtils.addMonths(month, numDisplayed)); + await prewarmMonth( + props.budgetType, + props.spreadsheet, + monthUtils.addMonths(month, numDisplayed), + ); } if (warmingMonth === month) { @@ -388,14 +372,13 @@ function Budget(props: BudgetProps) { const onTitlebarEvent = async msg => { switch (msg) { case 'budget/switch-type': { - let type = props.budgetType; - let newType = type === 'rollover' ? 'report' : 'rollover'; - - props.spreadsheet.disableObservers(); - await send('budget-set-type', { type: newType }); - await prewarmAllMonths(bounds, newType); - props.spreadsheet.enableObservers(); - props.loadPrefs(); + await switchBudgetType( + props.budgetType, + props.spreadsheet, + bounds, + prewarmStartMonth, + () => props.loadPrefs(), + ); break; } default: diff --git a/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx new file mode 100644 index 00000000000..4d34915a5e3 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { reportBudget } from 'loot-core/src/client/queries'; + +import Menu from '../../common/Menu'; +import useSheetValue from '../../spreadsheet/useSheetValue'; +import { Tooltip } from '../../tooltips'; + +type BalanceTooltipProps = { + categoryId: string; + tooltip: { close: () => void }; + monthIndex: number; + onBudgetAction: (idx: number, action: string, arg: unknown) => void; + onClose?: () => void; +}; + +export default function BalanceTooltip({ + categoryId, + tooltip, + monthIndex, + onBudgetAction, + onClose, + ...tooltipProps +}: BalanceTooltipProps) { + let carryover = useSheetValue(reportBudget.catCarryover(categoryId)); + + let _onClose = () => { + tooltip.close(); + onClose?.(); + }; + + return ( + + { + onBudgetAction(monthIndex, 'carryover', { + category: categoryId, + flag: !carryover, + }); + _onClose(); + }} + items={[ + { + name: 'carryover', + text: carryover + ? 'Remove overspending rollover' + : 'Rollover overspending', + }, + ]} + /> + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx deleted file mode 100644 index c8f95b3ac96..00000000000 --- a/packages/desktop-client/src/components/budget/report/BudgetSummary.tsx +++ /dev/null @@ -1,489 +0,0 @@ -import React, { - type ComponentProps, - type ComponentType, - type ReactNode, - useState, - type SVGProps, -} from 'react'; - -import { css } from 'glamor'; - -import { reportBudget } from 'loot-core/src/client/queries'; -import * as monthUtils from 'loot-core/src/shared/months'; - -import useFeatureFlag from '../../../hooks/useFeatureFlag'; -import DotsHorizontalTriple from '../../../icons/v1/DotsHorizontalTriple'; -import ArrowButtonDown1 from '../../../icons/v2/ArrowButtonDown1'; -import ArrowButtonUp1 from '../../../icons/v2/ArrowButtonUp1'; -import { theme, type CSSProperties, styles } from '../../../style'; -import AlignedText from '../../common/AlignedText'; -import Button from '../../common/Button'; -import HoverTarget from '../../common/HoverTarget'; -import Menu from '../../common/Menu'; -import Stack from '../../common/Stack'; -import Text from '../../common/Text'; -import View from '../../common/View'; -import NotesButton from '../../NotesButton'; -import PrivacyFilter from '../../PrivacyFilter'; -import CellValue from '../../spreadsheet/CellValue'; -import NamespaceContext from '../../spreadsheet/NamespaceContext'; -import useFormat from '../../spreadsheet/useFormat'; -import useSheetValue from '../../spreadsheet/useSheetValue'; -import { Tooltip } from '../../tooltips'; -import { makeAmountFullStyle } from '../util'; - -import { useReport } from './ReportContext'; - -type PieProgressProps = { - style?: SVGProps['style']; - progress: number; - color: string; - backgroundColor: string; -}; -function PieProgress({ - style, - progress, - color, - backgroundColor, -}: PieProgressProps) { - let radius = 4; - let circum = 2 * Math.PI * radius; - let dash = progress * circum; - let gap = circum; - - return ( - - - {' '} - - ); -} - -function fraction(num, denom) { - if (denom === 0) { - if (num > 0) { - return 1; - } - return 0; - } - - return num / denom; -} - -type IncomeProgressProps = { - current: ComponentProps['binding']; - target: ComponentProps['binding']; -}; -function IncomeProgress({ current, target }: IncomeProgressProps) { - let totalIncome = useSheetValue(current) || 0; - let totalBudgeted = useSheetValue(target) || 0; - - let over = false; - - if (totalIncome < 0) { - over = true; - totalIncome = -totalIncome; - } - - let frac = fraction(totalIncome, totalBudgeted); - - return ( - - ); -} - -type ExpenseProgressProps = { - current: ComponentProps['binding']; - target: ComponentProps['binding']; -}; -function ExpenseProgress({ current, target }: ExpenseProgressProps) { - let totalSpent = useSheetValue(current) || 0; - let totalBudgeted = useSheetValue(target) || 0; - - // Reverse total spent, and also set a bottom boundary of 0 (in case - // income goes into an expense category and it's "positive", don't - // show that in the graph) - totalSpent = Math.max(-totalSpent, 0); - - let frac; - let over = false; - - if (totalSpent > totalBudgeted) { - frac = (totalSpent - totalBudgeted) / totalBudgeted; - over = true; - } else { - frac = fraction(totalSpent, totalBudgeted); - } - - return ( - - ); -} - -type BudgetTotalProps = { - title: ReactNode; - current: ComponentProps['binding']; - target: ComponentProps['binding']; - ProgressComponent: ComponentType<{ current; target }>; - style?: CSSProperties; -}; -function BudgetTotal({ - title, - current, - target, - ProgressComponent, - style, -}: BudgetTotalProps) { - return ( - - - - - - {title} - - - - - - {' of '} - - - - - - ); -} - -type IncomeTotalProps = { - style?: CSSProperties; -}; -function IncomeTotal({ style }: IncomeTotalProps) { - return ( - - ); -} - -type ExpenseTotalProps = { - style?: CSSProperties; -}; -function ExpenseTotal({ style }: ExpenseTotalProps) { - return ( - - ); -} - -type SavedProps = { - projected: boolean; - style?: CSSProperties; -}; -function Saved({ projected, style }: SavedProps) { - let budgetedSaved = useSheetValue(reportBudget.totalBudgetedSaved) || 0; - let totalSaved = useSheetValue(reportBudget.totalSaved) || 0; - let format = useFormat(); - let saved = projected ? budgetedSaved : totalSaved; - let isNegative = saved < 0; - - return ( - - {projected ? ( - Projected Savings: - ) : ( - - {isNegative ? 'Overspent:' : 'Saved:'} - - )} - - { - if (!projected) { - let diff = totalSaved - budgetedSaved; - return ( - - - {format(budgetedSaved, 'financial-with-sign')} - - } - /> - - {format(diff, 'financial-with-sign')} - - } - /> - - ); - } - return null; - }} - > - - - {format(saved, 'financial')} - - - - - ); -} - -type BudgetSummaryProps = { - month?: string; -}; -export function BudgetSummary({ month }: BudgetSummaryProps) { - let { - currentMonth, - summaryCollapsed: collapsed, - onBudgetAction, - onToggleSummaryCollapse, - } = useReport(); - - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); - - let [menuOpen, setMenuOpen] = useState(false); - function onMenuOpen() { - setMenuOpen(true); - } - - function onMenuClose() { - setMenuOpen(false); - } - - let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1; - - return ( - - - - - - - - - - - {monthUtils.format(month, 'MMMM')} - - - - - - - - - - - {menuOpen && ( - - { - onMenuClose(); - onBudgetAction(month, type); - }} - items={[ - { name: 'copy-last', text: 'Copy last month’s budget' }, - { name: 'set-zero', text: 'Set budgets to zero' }, - { - name: 'set-3-avg', - text: 'Set budgets to 3 month average', - }, - isGoalTemplatesEnabled && { - name: 'check-templates', - text: 'Check templates', - }, - isGoalTemplatesEnabled && { - name: 'apply-goal-template', - text: 'Apply budget template', - }, - isGoalTemplatesEnabled && { - name: 'overwrite-goal-template', - text: 'Overwrite with budget template', - }, - ]} - /> - - )} - - - - - {!collapsed && ( - - - - - )} - - {collapsed ? ( - - = currentMonth} /> - - ) : ( - = currentMonth} - style={{ marginTop: 13, marginBottom: 20 }} - /> - )} - - - ); -} diff --git a/packages/desktop-client/src/components/budget/report/components.tsx b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx similarity index 90% rename from packages/desktop-client/src/components/budget/report/components.tsx rename to packages/desktop-client/src/components/budget/report/ReportComponents.tsx index 5162866fb63..1d3d03bb8d3 100644 --- a/packages/desktop-client/src/components/budget/report/components.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx @@ -13,13 +13,12 @@ import Text from '../../common/Text'; import View from '../../common/View'; import CellValue from '../../spreadsheet/CellValue'; import useFormat from '../../spreadsheet/useFormat'; -import useSheetValue from '../../spreadsheet/useSheetValue'; import { Field, SheetCell } from '../../table'; import { Tooltip, useTooltip } from '../../tooltips'; import BalanceWithCarryover from '../BalanceWithCarryover'; import { makeAmountGrey } from '../util'; -export { BudgetSummary } from './BudgetSummary'; +import BalanceTooltip from './BalanceTooltip'; let headerLabelStyle: CSSProperties = { flex: 1, @@ -141,48 +140,6 @@ export const GroupMonth = memo(function GroupMonth({ group }: GroupMonthProps) { ); }); -type BalanceTooltipProps = { - categoryId: string; - tooltip: { close: () => void }; - monthIndex: number; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; -}; -function BalanceTooltip({ - categoryId, - tooltip, - monthIndex, - onBudgetAction, -}: BalanceTooltipProps) { - let carryover = useSheetValue(reportBudget.catCarryover(categoryId)); - - return ( - - { - onBudgetAction(monthIndex, 'carryover', { - category: categoryId, - flag: !carryover, - }); - tooltip.close(); - }} - items={[ - { - name: 'carryover', - text: carryover - ? 'Remove overspending rollover' - : 'Rollover overspending', - }, - ]} - /> - - ); -} - type CategoryMonthProps = { monthIndex: number; category: { id: string; name: string; is_income: boolean }; @@ -387,6 +344,8 @@ export const CategoryMonth = memo(function CategoryMonth({ ); }); +export { default as BudgetSummary } from './budgetsummary/BudgetSummary'; + export const ExpenseGroupMonth = GroupMonth; export const ExpenseCategoryMonth = CategoryMonth; diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx new file mode 100644 index 00000000000..9a5bdb12cf9 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetSummary.tsx @@ -0,0 +1,215 @@ +import React, { useState } from 'react'; + +import { css } from 'glamor'; + +import * as monthUtils from 'loot-core/src/shared/months'; + +import useFeatureFlag from '../../../../hooks/useFeatureFlag'; +import DotsHorizontalTriple from '../../../../icons/v1/DotsHorizontalTriple'; +import ArrowButtonDown1 from '../../../../icons/v2/ArrowButtonDown1'; +import ArrowButtonUp1 from '../../../../icons/v2/ArrowButtonUp1'; +import { theme, styles } from '../../../../style'; +import Button from '../../../common/Button'; +import Menu from '../../../common/Menu'; +import Stack from '../../../common/Stack'; +import View from '../../../common/View'; +import NotesButton from '../../../NotesButton'; +import NamespaceContext from '../../../spreadsheet/NamespaceContext'; +import { Tooltip } from '../../../tooltips'; +import { useReport } from '../ReportContext'; + +import ExpenseTotal from './ExpenseTotal'; +import IncomeTotal from './IncomeTotal'; +import Saved from './Saved'; + +type BudgetSummaryProps = { + month?: string; +}; +export default function BudgetSummary({ month }: BudgetSummaryProps) { + let { + currentMonth, + summaryCollapsed: collapsed, + onBudgetAction, + onToggleSummaryCollapse, + } = useReport(); + + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + + let [menuOpen, setMenuOpen] = useState(false); + function onMenuOpen() { + setMenuOpen(true); + } + + function onMenuClose() { + setMenuOpen(false); + } + + let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1; + + return ( + + + + + + + + + + + {monthUtils.format(month, 'MMMM')} + + + + + + + + + + + {menuOpen && ( + + { + onMenuClose(); + onBudgetAction(month, type); + }} + items={[ + { name: 'copy-last', text: 'Copy last month’s budget' }, + { name: 'set-zero', text: 'Set budgets to zero' }, + { + name: 'set-3-avg', + text: 'Set budgets to 3 month average', + }, + isGoalTemplatesEnabled && { + name: 'check-templates', + text: 'Check templates', + }, + isGoalTemplatesEnabled && { + name: 'apply-goal-template', + text: 'Apply budget template', + }, + isGoalTemplatesEnabled && { + name: 'overwrite-goal-template', + text: 'Overwrite with budget template', + }, + ]} + /> + + )} + + + + + {!collapsed && ( + + + + + )} + + {collapsed ? ( + + = currentMonth} /> + + ) : ( + = currentMonth} + style={{ marginTop: 13, marginBottom: 20 }} + /> + )} + + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetTotal.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetTotal.tsx new file mode 100644 index 00000000000..c41763d94f9 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/BudgetTotal.tsx @@ -0,0 +1,58 @@ +import React, { + type CSSProperties, + type ComponentProps, + type ComponentType, + type ReactNode, +} from 'react'; + +import { theme, styles } from '../../../../style'; +import Text from '../../../common/Text'; +import View from '../../../common/View'; +import CellValue from '../../../spreadsheet/CellValue'; + +type BudgetTotalProps = { + title: ReactNode; + current: ComponentProps['binding']; + target: ComponentProps['binding']; + ProgressComponent: ComponentType<{ current; target }>; + style?: CSSProperties; +}; +export default function BudgetTotal({ + title, + current, + target, + ProgressComponent, + style, +}: BudgetTotalProps) { + return ( + + + + + + {title} + + + + + + {' of '} + + + + + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseProgress.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseProgress.tsx new file mode 100644 index 00000000000..5f5120cdeb0 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseProgress.tsx @@ -0,0 +1,41 @@ +import React, { type ComponentProps } from 'react'; + +import { theme } from '../../../../style'; +import type CellValue from '../../../spreadsheet/CellValue'; +import useSheetValue from '../../../spreadsheet/useSheetValue'; + +import fraction from './fraction'; +import PieProgress from './PieProgress'; + +type ExpenseProgressProps = { + current: ComponentProps['binding']; + target: ComponentProps['binding']; +}; +export function ExpenseProgress({ current, target }: ExpenseProgressProps) { + let totalSpent = useSheetValue(current) || 0; + let totalBudgeted = useSheetValue(target) || 0; + + // Reverse total spent, and also set a bottom boundary of 0 (in case + // income goes into an expense category and it's "positive", don't + // show that in the graph) + totalSpent = Math.max(-totalSpent, 0); + + let frac; + let over = false; + + if (totalSpent > totalBudgeted) { + frac = (totalSpent - totalBudgeted) / totalBudgeted; + over = true; + } else { + frac = fraction(totalSpent, totalBudgeted); + } + + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseTotal.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseTotal.tsx new file mode 100644 index 00000000000..3188c48bf3d --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/ExpenseTotal.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { reportBudget } from 'loot-core/src/client/queries'; + +import { type CSSProperties } from '../../../../style'; + +import BudgetTotal from './BudgetTotal'; +import { ExpenseProgress } from './ExpenseProgress'; + +type ExpenseTotalProps = { + style?: CSSProperties; +}; +export default function ExpenseTotal({ style }: ExpenseTotalProps) { + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx new file mode 100644 index 00000000000..7a13d213d5a --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeProgress.tsx @@ -0,0 +1,38 @@ +import React, { type ComponentProps } from 'react'; + +import { theme } from '../../../../style'; +import type CellValue from '../../../spreadsheet/CellValue'; +import useSheetValue from '../../../spreadsheet/useSheetValue'; + +import fraction from './fraction'; +import PieProgress from './PieProgress'; + +type IncomeProgressProps = { + current: ComponentProps['binding']; + target: ComponentProps['binding']; +}; +export default function IncomeProgress({ + current, + target, +}: IncomeProgressProps) { + let totalIncome = useSheetValue(current) || 0; + let totalBudgeted = useSheetValue(target) || 0; + + let over = false; + + if (totalIncome < 0) { + over = true; + totalIncome = -totalIncome; + } + + let frac = fraction(totalIncome, totalBudgeted); + + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeTotal.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeTotal.tsx new file mode 100644 index 00000000000..26f9fc88399 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/IncomeTotal.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { reportBudget } from 'loot-core/src/client/queries'; + +import { type CSSProperties } from '../../../../style'; + +import BudgetTotal from './BudgetTotal'; +import IncomeProgress from './IncomeProgress'; + +type IncomeTotalProps = { + style?: CSSProperties; +}; +export default function IncomeTotal({ style }: IncomeTotalProps) { + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/PieProgress.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/PieProgress.tsx new file mode 100644 index 00000000000..32cba3df5cb --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/PieProgress.tsx @@ -0,0 +1,35 @@ +import React, { type SVGProps } from 'react'; + +type PieProgressProps = { + style?: SVGProps['style']; + progress: number; + color: string; + backgroundColor: string; +}; +export default function PieProgress({ + style, + progress, + color, + backgroundColor, +}: PieProgressProps) { + let radius = 4; + let circum = 2 * Math.PI * radius; + let dash = progress * circum; + let gap = circum; + + return ( + + + {' '} + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/Saved.tsx b/packages/desktop-client/src/components/budget/report/budgetsummary/Saved.tsx new file mode 100644 index 00000000000..8c8768e3e72 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/Saved.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { css } from 'glamor'; + +import { reportBudget } from 'loot-core/src/client/queries'; + +import { theme, type CSSProperties, styles } from '../../../../style'; +import AlignedText from '../../../common/AlignedText'; +import HoverTarget from '../../../common/HoverTarget'; +import Text from '../../../common/Text'; +import View from '../../../common/View'; +import PrivacyFilter from '../../../PrivacyFilter'; +import useFormat from '../../../spreadsheet/useFormat'; +import useSheetValue from '../../../spreadsheet/useSheetValue'; +import { Tooltip } from '../../../tooltips'; +import { makeAmountFullStyle } from '../../util'; + +type SavedProps = { + projected: boolean; + style?: CSSProperties; +}; +export default function Saved({ projected, style }: SavedProps) { + let budgetedSaved = useSheetValue(reportBudget.totalBudgetedSaved) || 0; + let totalSaved = useSheetValue(reportBudget.totalSaved) || 0; + let format = useFormat(); + let saved = projected ? budgetedSaved : totalSaved; + let isNegative = saved < 0; + + return ( + + {projected ? ( + Projected Savings: + ) : ( + + {isNegative ? 'Overspent:' : 'Saved:'} + + )} + + { + if (!projected) { + let diff = totalSaved - budgetedSaved; + return ( + + + {format(budgetedSaved, 'financial-with-sign')} + + } + /> + + {format(diff, 'financial-with-sign')} + + } + /> + + ); + } + return null; + }} + > + + + {format(saved, 'financial')} + + + + + ); +} diff --git a/packages/desktop-client/src/components/budget/report/budgetsummary/fraction.ts b/packages/desktop-client/src/components/budget/report/budgetsummary/fraction.ts new file mode 100644 index 00000000000..6fce8d5a2e6 --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/budgetsummary/fraction.ts @@ -0,0 +1,10 @@ +export default function fraction(num, denom) { + if (denom === 0) { + if (num > 0) { + return 1; + } + return 0; + } + + return num / denom; +} diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx deleted file mode 100644 index b6b0a4142e4..00000000000 --- a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import React, { useState } from 'react'; - -import { css } from 'glamor'; - -import { rolloverBudget } from 'loot-core/src/client/queries'; -import * as monthUtils from 'loot-core/src/shared/months'; - -import DotsHorizontalTriple from '../../../icons/v1/DotsHorizontalTriple'; -import ArrowButtonDown1 from '../../../icons/v2/ArrowButtonDown1'; -import ArrowButtonUp1 from '../../../icons/v2/ArrowButtonUp1'; -import { theme, styles } from '../../../style'; -import AlignedText from '../../common/AlignedText'; -import Block from '../../common/Block'; -import Button from '../../common/Button'; -import HoverTarget from '../../common/HoverTarget'; -import Menu from '../../common/Menu'; -import View from '../../common/View'; -import NotesButton from '../../NotesButton'; -import PrivacyFilter from '../../PrivacyFilter'; -import CellValue from '../../spreadsheet/CellValue'; -import NamespaceContext from '../../spreadsheet/NamespaceContext'; -import useFormat from '../../spreadsheet/useFormat'; -import useSheetName from '../../spreadsheet/useSheetName'; -import useSheetValue from '../../spreadsheet/useSheetValue'; -import { Tooltip } from '../../tooltips'; - -import HoldTooltip from './HoldTooltip'; -import { useRollover } from './RolloverContext'; -import TransferTooltip from './TransferTooltip'; - -type TotalsListProps = { - prevMonthName: string; - collapsed?: boolean; -}; -function TotalsList({ prevMonthName, collapsed }: TotalsListProps) { - const format = useFormat(); - return ( - - - ( - - - } - /> - - } - /> - - )} - > - - - - { - let v = format(value, 'financial'); - return value > 0 ? '+' + v : value === 0 ? '-' + v : v; - }} - style={{ fontWeight: 600, ...styles.tnum }} - /> - - { - let v = format(value, 'financial'); - return value > 0 ? '+' + v : value === 0 ? '-' + v : v; - }} - style={{ fontWeight: 600, ...styles.tnum }} - /> - - { - let n = parseInt(value) || 0; - let v = format(Math.abs(n), 'financial'); - return n >= 0 ? '-' + v : '+' + v; - }} - style={{ fontWeight: 600, ...styles.tnum }} - /> - - - - Available Funds - Overspent in {prevMonthName} - Budgeted - For Next Month - - - ); -} - -type ToBudgetProps = { - month: string | number; - prevMonthName?: string; - collapsed?: boolean; - onBudgetAction: (idx: string | number, action: string, arg?: unknown) => void; -}; -function ToBudget({ - month, - prevMonthName, - collapsed, - onBudgetAction, -}: ToBudgetProps) { - let [menuOpen, setMenuOpen] = useState(null); - let sheetName = useSheetName(rolloverBudget.toBudget); - let sheetValue = useSheetValue({ - name: rolloverBudget.toBudget, - value: 0, - }); - let format = useFormat(); - let availableValue = parseInt(sheetValue); - let num = isNaN(availableValue) ? 0 : availableValue; - let isNegative = num < 0; - - return ( - - {isNegative ? 'Overbudgeted:' : 'To Budget:'} - - ( - - - - )} - > - - setMenuOpen('actions')} - data-cellname={sheetName} - className={`${css([ - styles.veryLargeText, - { - fontWeight: 400, - userSelect: 'none', - cursor: 'pointer', - color: isNegative ? theme.errorText : theme.pageTextPositive, - marginBottom: -1, - borderBottom: '1px solid transparent', - ':hover': { - borderColor: isNegative - ? theme.errorBorder - : theme.pageTextPositive, - }, - }, - ])}`} - > - {format(num, 'financial')} - - - - {menuOpen === 'actions' && ( - setMenuOpen(null)} - > - { - if (type === 'reset-buffer') { - onBudgetAction(month, 'reset-hold'); - setMenuOpen(null); - } else { - setMenuOpen(type); - } - }} - items={[ - { - name: 'transfer', - text: 'Move to a category', - }, - { - name: 'buffer', - text: 'Hold for next month', - }, - { - name: 'reset-buffer', - text: 'Reset next month’s buffer', - }, - ]} - /> - - )} - {menuOpen === 'buffer' && ( - setMenuOpen(null)} - onSubmit={amount => { - onBudgetAction(month, 'hold', { amount }); - }} - /> - )} - {menuOpen === 'transfer' && ( - setMenuOpen(null)} - onSubmit={(amount, category) => { - onBudgetAction(month, 'transfer-available', { - amount, - category, - }); - }} - /> - )} - - - ); -} - -type BudgetSummaryProps = { - month: string; - isGoalTemplatesEnabled?: boolean; -}; -export function BudgetSummary({ - month, - isGoalTemplatesEnabled, -}: BudgetSummaryProps) { - let { - currentMonth, - summaryCollapsed: collapsed, - onBudgetAction, - onToggleSummaryCollapse, - } = useRollover(); - - let [menuOpen, setMenuOpen] = useState(false); - function onMenuOpen(e) { - setMenuOpen(true); - } - - function onMenuClose() { - setMenuOpen(false); - } - - let prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM'); - - let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1; - - return ( - - - - - - - - - - - {monthUtils.format(month, 'MMMM')} - - - - - - - - - - - {menuOpen && ( - - { - onMenuClose(); - onBudgetAction(month, type); - }} - items={[ - { name: 'copy-last', text: 'Copy last month’s budget' }, - { name: 'set-zero', text: 'Set budgets to zero' }, - { - name: 'set-3-avg', - text: 'Set budgets to 3 month average', - }, - isGoalTemplatesEnabled && { - name: 'check-templates', - text: 'Check templates', - }, - isGoalTemplatesEnabled && { - name: 'apply-goal-template', - text: 'Apply budget template', - }, - isGoalTemplatesEnabled && { - name: 'overwrite-goal-template', - text: 'Overwrite with budget template', - }, - isGoalTemplatesEnabled && { - name: 'cleanup-goal-template', - text: 'End of month cleanup', - }, - ]} - /> - - )} - - - - - {collapsed ? ( - - - - ) : ( - <> - - - - - > - )} - - - ); -} diff --git a/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx index 0399184d59d..35f2ad1b39c 100644 --- a/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/HoldTooltip.tsx @@ -3,6 +3,7 @@ import React, { useContext, useEffect, type ChangeEvent, + type ComponentPropsWithoutRef, } from 'react'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; @@ -16,11 +17,15 @@ import View from '../../common/View'; import NamespaceContext from '../../spreadsheet/NamespaceContext'; import { Tooltip } from '../../tooltips'; -type HoldTooltipProps = { +type HoldTooltipProps = ComponentPropsWithoutRef & { onSubmit: (amount: number) => void; - onClose: () => void; }; -export default function HoldTooltip({ onSubmit, onClose }: HoldTooltipProps) { +export default function HoldTooltip({ + onSubmit, + onClose, + position = 'bottom-right', + ...props +}: HoldTooltipProps) { const spreadsheet = useSpreadsheet(); const sheetName = useContext(NamespaceContext); @@ -48,10 +53,11 @@ export default function HoldTooltip({ onSubmit, onClose }: HoldTooltipProps) { return ( Hold this amount: diff --git a/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx similarity index 99% rename from packages/desktop-client/src/components/budget/rollover/rollover-components.tsx rename to packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx index e11021468ed..747770a7d86 100644 --- a/packages/desktop-client/src/components/budget/rollover/rollover-components.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx @@ -20,8 +20,6 @@ import { makeAmountGrey } from '../util'; import BalanceTooltip from './BalanceTooltip'; -export { BudgetSummary } from './BudgetSummary'; - let headerLabelStyle: CSSProperties = { flex: 1, padding: '0 5px', @@ -402,3 +400,5 @@ export function IncomeCategoryMonth({ ); } + +export { default as BudgetSummary } from './budgetsummary/BudgetSummary'; diff --git a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx index 76a4d3b83e7..4aa10527c9e 100644 --- a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx @@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect, - type ComponentProps, + type ComponentPropsWithoutRef, } from 'react'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; @@ -19,21 +19,20 @@ import NamespaceContext from '../../spreadsheet/NamespaceContext'; import { Tooltip } from '../../tooltips'; import { addToBeBudgetedGroup } from '../util'; -type TransferTooltipProps = { +type TransferTooltipProps = ComponentPropsWithoutRef & { initialAmount?: number; initialAmountName?: string; showToBeBudgeted?: boolean; - tooltipProps?: ComponentProps; onSubmit: (amount: number, category: unknown) => void; - onClose: () => void; }; export default function TransferTooltip({ initialAmount, initialAmountName, showToBeBudgeted, - tooltipProps, onSubmit, onClose, + position = 'bottom-right', + ...props }: TransferTooltipProps) { let spreadsheet = useSpreadsheet(); let sheetName = useContext(NamespaceContext); @@ -76,11 +75,11 @@ export default function TransferTooltip({ return ( Transfer this amount: diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx new file mode 100644 index 00000000000..8c7c3bb0381 --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/BudgetSummary.tsx @@ -0,0 +1,223 @@ +import React, { useState } from 'react'; + +import { css } from 'glamor'; + +import * as monthUtils from 'loot-core/src/shared/months'; + +import DotsHorizontalTriple from '../../../../icons/v1/DotsHorizontalTriple'; +import ArrowButtonDown1 from '../../../../icons/v2/ArrowButtonDown1'; +import ArrowButtonUp1 from '../../../../icons/v2/ArrowButtonUp1'; +import { theme, styles } from '../../../../style'; +import Button from '../../../common/Button'; +import Menu from '../../../common/Menu'; +import View from '../../../common/View'; +import NotesButton from '../../../NotesButton'; +import NamespaceContext from '../../../spreadsheet/NamespaceContext'; +import { Tooltip } from '../../../tooltips'; +import { useRollover } from '../RolloverContext'; + +import ToBudget from './ToBudget'; +import TotalsList from './TotalsList'; + +type BudgetSummaryProps = { + month: string; + isGoalTemplatesEnabled?: boolean; +}; +export default function BudgetSummary({ + month, + isGoalTemplatesEnabled, +}: BudgetSummaryProps) { + let { + currentMonth, + summaryCollapsed: collapsed, + onBudgetAction, + onToggleSummaryCollapse, + } = useRollover(); + + let [menuOpen, setMenuOpen] = useState(false); + function onMenuOpen(e) { + setMenuOpen(true); + } + + function onMenuClose() { + setMenuOpen(false); + } + + let prevMonthName = monthUtils.format(monthUtils.prevMonth(month), 'MMM'); + + let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1; + + return ( + + + + + + + + + + + {monthUtils.format(month, 'MMMM')} + + + + + + + + + + + {menuOpen && ( + + { + onMenuClose(); + onBudgetAction(month, type); + }} + items={[ + { name: 'copy-last', text: 'Copy last month’s budget' }, + { name: 'set-zero', text: 'Set budgets to zero' }, + { + name: 'set-3-avg', + text: 'Set budgets to 3 month average', + }, + isGoalTemplatesEnabled && { + name: 'check-templates', + text: 'Check templates', + }, + isGoalTemplatesEnabled && { + name: 'apply-goal-template', + text: 'Apply budget template', + }, + isGoalTemplatesEnabled && { + name: 'overwrite-goal-template', + text: 'Overwrite with budget template', + }, + isGoalTemplatesEnabled && { + name: 'cleanup-goal-template', + text: 'End of month cleanup', + }, + ]} + /> + + )} + + + + + {collapsed ? ( + + + + ) : ( + <> + + + + + > + )} + + + ); +} diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx new file mode 100644 index 00000000000..e024e322d92 --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx @@ -0,0 +1,159 @@ +import React, { useState, type ComponentPropsWithoutRef } from 'react'; + +import { css } from 'glamor'; + +import { rolloverBudget } from 'loot-core/src/client/queries'; + +import { theme, styles, type CSSProperties } from '../../../../style'; +import Block from '../../../common/Block'; +import HoverTarget from '../../../common/HoverTarget'; +import Menu from '../../../common/Menu'; +import View from '../../../common/View'; +import PrivacyFilter from '../../../PrivacyFilter'; +import useFormat from '../../../spreadsheet/useFormat'; +import useSheetName from '../../../spreadsheet/useSheetName'; +import useSheetValue from '../../../spreadsheet/useSheetValue'; +import { Tooltip } from '../../../tooltips'; +import HoldTooltip from '../HoldTooltip'; +import TransferTooltip from '../TransferTooltip'; + +import TotalsList from './TotalsList'; + +type ToBudgetProps = { + month: string | number; + onBudgetAction: (idx: string | number, action: string, arg?: unknown) => void; + prevMonthName?: string; + showTotalsTooltipOnHover?: boolean; + style?: CSSProperties; + amountStyle?: CSSProperties; + menuTooltipProps?: ComponentPropsWithoutRef; + totalsTooltipProps?: ComponentPropsWithoutRef; + holdTooltipProps?: ComponentPropsWithoutRef; + transferTooltipProps?: ComponentPropsWithoutRef; +}; +export default function ToBudget({ + month, + prevMonthName, + showTotalsTooltipOnHover, + onBudgetAction, + style, + amountStyle, + menuTooltipProps, + totalsTooltipProps, + holdTooltipProps, + transferTooltipProps, +}: ToBudgetProps) { + let [menuOpen, setMenuOpen] = useState(null); + let sheetName = useSheetName(rolloverBudget.toBudget); + let sheetValue = useSheetValue({ + name: rolloverBudget.toBudget, + value: 0, + }); + let format = useFormat(); + let availableValue = parseInt(sheetValue); + let num = isNaN(availableValue) ? 0 : availableValue; + let isNegative = num < 0; + + return ( + + {isNegative ? 'Overbudgeted:' : 'To Budget:'} + + ( + + + + )} + > + + setMenuOpen('actions')} + data-cellname={sheetName} + className={`${css([ + styles.veryLargeText, + { + fontWeight: 400, + userSelect: 'none', + cursor: 'pointer', + color: isNegative ? theme.errorText : theme.pageTextPositive, + marginBottom: -1, + borderBottom: '1px solid transparent', + ':hover': { + borderColor: isNegative + ? theme.errorBorder + : theme.pageTextPositive, + }, + }, + amountStyle, + ])}`} + > + {format(num, 'financial')} + + + + {menuOpen === 'actions' && ( + setMenuOpen(null)} + {...menuTooltipProps} + > + { + if (type === 'reset-buffer') { + onBudgetAction(month, 'reset-hold'); + setMenuOpen(null); + } else { + setMenuOpen(type); + } + }} + items={[ + { + name: 'transfer', + text: 'Move to a category', + }, + { + name: 'buffer', + text: 'Hold for next month', + }, + { + name: 'reset-buffer', + text: 'Reset next month’s buffer', + }, + ]} + /> + + )} + {menuOpen === 'buffer' && ( + setMenuOpen(null)} + onSubmit={amount => { + onBudgetAction(month, 'hold', { amount }); + }} + {...holdTooltipProps} + /> + )} + {menuOpen === 'transfer' && ( + setMenuOpen(null)} + onSubmit={(amount, category) => { + onBudgetAction(month, 'transfer-available', { + amount, + category, + }); + }} + {...transferTooltipProps} + /> + )} + + + ); +} diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/TotalsList.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/TotalsList.tsx new file mode 100644 index 00000000000..0805b8bacf0 --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/TotalsList.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +import { rolloverBudget } from 'loot-core/src/client/queries'; + +import { styles, type CSSProperties } from '../../../../style'; +import AlignedText from '../../../common/AlignedText'; +import Block from '../../../common/Block'; +import HoverTarget from '../../../common/HoverTarget'; +import View from '../../../common/View'; +import CellValue from '../../../spreadsheet/CellValue'; +import useFormat from '../../../spreadsheet/useFormat'; +import { Tooltip } from '../../../tooltips'; + +type TotalsListProps = { + prevMonthName: string; + style?: CSSProperties; +}; +export default function TotalsList({ prevMonthName, style }: TotalsListProps) { + const format = useFormat(); + return ( + + + ( + + + } + /> + + } + /> + + )} + > + + + + { + let v = format(value, 'financial'); + return value > 0 ? '+' + v : value === 0 ? '-' + v : v; + }} + style={{ fontWeight: 600, ...styles.tnum }} + /> + + { + let v = format(value, 'financial'); + return value > 0 ? '+' + v : value === 0 ? '-' + v : v; + }} + style={{ fontWeight: 600, ...styles.tnum }} + /> + + { + let n = parseInt(value) || 0; + let v = format(Math.abs(n), 'financial'); + return n >= 0 ? '-' + v : '+' + v; + }} + style={{ fontWeight: 600, ...styles.tnum }} + /> + + + + Available Funds + Overspent in {prevMonthName} + Budgeted + For Next Month + + + ); +} diff --git a/packages/desktop-client/src/components/budget/util.ts b/packages/desktop-client/src/components/budget/util.ts index f01d4f6edcb..ba590982b1b 100644 --- a/packages/desktop-client/src/components/budget/util.ts +++ b/packages/desktop-client/src/components/budget/util.ts @@ -1,8 +1,15 @@ +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 { type Handlers } from 'loot-core/src/types/handlers'; import { type CategoryGroupEntity } from 'loot-core/src/types/models'; +import { type LocalPrefs } from 'loot-core/src/types/prefs'; import { styles, theme } from '../../style'; import { type DropPosition } from '../sort'; +import { getValidMonthBounds } from './MonthsContext'; + export function addToBeBudgetedGroup(groups: CategoryGroupEntity[]) { return [ { @@ -121,3 +128,55 @@ export function findSortUp( export function getScrollbarWidth() { return Math.max(styles.scrollbarWidth - 2, 0); } + +export async function prewarmMonth( + budgetType: LocalPrefs['budgetType'], + spreadsheet: ReturnType, + month: string, +) { + let method: keyof Handlers = + budgetType === 'report' ? 'report-budget-month' : 'rollover-budget-month'; + + let values = await send(method, { month }); + + for (let value of values) { + spreadsheet.prewarmCache(value.name, value); + } +} + +export async function prewarmAllMonths( + budgetType: LocalPrefs['budgetType'], + spreadsheet: ReturnType, + bounds: { start: string; end: string }, + startMonth: string, +) { + let numMonths = 3; + + bounds = getValidMonthBounds( + bounds, + monthUtils.subMonths(startMonth, 1), + monthUtils.addMonths(startMonth, numMonths + 1), + ); + let months = monthUtils.rangeInclusive(bounds.start, bounds.end); + + await Promise.all( + months.map(month => prewarmMonth(budgetType, spreadsheet, month)), + ); +} + +export async function switchBudgetType( + budgetType: LocalPrefs['budgetType'], + spreadsheet: ReturnType, + bounds: { start: string; end: string }, + startMonth: string, + onSuccess: () => Promise | undefined, +) { + let newType: 'rollover' | 'report' = + budgetType === 'rollover' ? 'report' : 'rollover'; + + spreadsheet.disableObservers(); + await send('budget-set-type', { type: newType }); + await prewarmAllMonths(newType, spreadsheet, bounds, startMonth); + spreadsheet.enableObservers(); + await onSuccess?.(); +} diff --git a/packages/desktop-client/src/components/common/HoverTarget.tsx b/packages/desktop-client/src/components/common/HoverTarget.tsx index d22bd57958f..bbaa27b87d4 100644 --- a/packages/desktop-client/src/components/common/HoverTarget.tsx +++ b/packages/desktop-client/src/components/common/HoverTarget.tsx @@ -21,13 +21,13 @@ export default function HoverTarget({ }: HoverTargetProps) { let [hovered, setHovered] = useState(false); - const onMouseEnter = useCallback(() => { + const onPointerEnter = useCallback(() => { if (!disabled) { setHovered(true); } }, [disabled]); - const onMouseLeave = useCallback(() => { + const onPointerLeave = useCallback(() => { if (!disabled) { setHovered(false); } @@ -42,8 +42,8 @@ export default function HoverTarget({ return ( {children} diff --git a/packages/desktop-client/src/components/modals/BudgetSummary.tsx b/packages/desktop-client/src/components/modals/BudgetSummary.tsx deleted file mode 100644 index a65834f1f40..00000000000 --- a/packages/desktop-client/src/components/modals/BudgetSummary.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; - -import { rolloverBudget } from 'loot-core/src/client/queries'; -import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; - -import { theme, styles } from '../../style'; -import { type CommonModalProps } from '../../types/modals'; -import Modal from '../common/Modal'; -import Text from '../common/Text'; -import View from '../common/View'; -import CellValue from '../spreadsheet/CellValue'; -import NamespaceContext from '../spreadsheet/NamespaceContext'; -import useFormat from '../spreadsheet/useFormat'; -import useSheetValue from '../spreadsheet/useSheetValue'; - -function ToBudget({ toBudget }) { - let budgetAmount = useSheetValue(toBudget); - let format = useFormat(); - return ( - - - {budgetAmount < 0 ? 'Overbudget:' : 'To Budget:'} - - - {format(budgetAmount, 'financial')} - - - ); -} - -type BudgetSummaryProps = { - modalProps: CommonModalProps; - month: string; -}; - -function BudgetSummary({ month, modalProps }: BudgetSummaryProps) { - const prevMonthName = format(prevMonth(month), 'MMM'); - - return ( - - {() => ( - - - - - - - - - - - Available Funds - Overspent in {prevMonthName} - Budgeted - For Next Month - - - - - )} - - ); -} - -export default BudgetSummary; diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx index 44984c491de..7b4ea942dff 100644 --- a/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx +++ b/packages/desktop-client/src/components/modals/CreateLocalAccount.tsx @@ -132,6 +132,7 @@ function CreateLocalAccount({ modalProps, actions }: CreateLocalAccountProps) { setBalance(event.target.value)} onBlur={event => { diff --git a/packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx b/packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx new file mode 100644 index 00000000000..f895ecb3ee0 --- /dev/null +++ b/packages/desktop-client/src/components/modals/ReportBudgetSummary.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { sheetForMonth } from 'loot-core/src/shared/months'; +import * as monthUtils from 'loot-core/src/shared/months'; + +import { styles } from '../../style'; +import { type CommonModalProps } from '../../types/modals'; +import ExpenseTotal from '../budget/report/budgetsummary/ExpenseTotal'; +import IncomeTotal from '../budget/report/budgetsummary/IncomeTotal'; +import Saved from '../budget/report/budgetsummary/Saved'; +import Modal from '../common/Modal'; +import Stack from '../common/Stack'; +import NamespaceContext from '../spreadsheet/NamespaceContext'; + +type ReportBudgetSummaryProps = { + modalProps: CommonModalProps; + month: string; +}; + +export default function ReportBudgetSummary({ + month, + modalProps, +}: ReportBudgetSummaryProps) { + const currentMonth = monthUtils.currentMonth(); + return ( + + {() => ( + + + + + + = currentMonth} + style={{ ...styles.mediumText, marginTop: 20 }} + /> + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx new file mode 100644 index 00000000000..a942d30f35f --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummary.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; + +import { styles } from '../../style'; +import { type CommonModalProps } from '../../types/modals'; +import ToBudget from '../budget/rollover/budgetsummary/ToBudget'; +import TotalsList from '../budget/rollover/budgetsummary/TotalsList'; +import Modal from '../common/Modal'; +import NamespaceContext from '../spreadsheet/NamespaceContext'; + +type RolloverBudgetSummaryProps = { + modalProps: CommonModalProps; + onBudgetAction: (idx: string | number, action: string, arg: unknown) => void; + month: string; +}; + +export default function RolloverBudgetSummary({ + month, + onBudgetAction, + modalProps, +}: RolloverBudgetSummaryProps) { + const prevMonthName = format(prevMonth(month), 'MMM'); + + return ( + + {() => ( + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx new file mode 100644 index 00000000000..814d8c75573 --- /dev/null +++ b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { type CommonModalProps } from '../../types/modals'; +import Button from '../common/Button'; +import ExternalLink from '../common/ExternalLink'; +import Modal from '../common/Modal'; +import Paragraph from '../common/Paragraph'; +import Text from '../common/Text'; + +type SwitchBudgetTypeProps = { + modalProps: CommonModalProps; + onSwitch: () => void; +}; + +export default function SwitchBudgetType({ + modalProps, + onSwitch, +}: SwitchBudgetTypeProps) { + const budgetType = useSelector(state => state.prefs.local.budgetType); + return ( + + {() => ( + <> + + You are currently using a{' '} + + {budgetType === 'report' ? 'Report budget' : 'Rollover budget'}. + {' '} + Switching will not lose any data and you can always switch back. + + { + onSwitch(); + modalProps.onClose?.(); + }} + > + Switch to a{' '} + {budgetType === 'report' ? 'Rollover budget' : 'Report budget'} + + + + How do these types of budgeting work? + + + > + )} + + ); +} diff --git a/packages/desktop-client/src/components/spreadsheet/NamespaceContext.ts b/packages/desktop-client/src/components/spreadsheet/NamespaceContext.ts index d05f5af3b76..f9241513802 100644 --- a/packages/desktop-client/src/components/spreadsheet/NamespaceContext.ts +++ b/packages/desktop-client/src/components/spreadsheet/NamespaceContext.ts @@ -1,3 +1,3 @@ import { createContext } from 'react'; -export default createContext(undefined); +export default createContext(undefined); diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.js b/packages/desktop-client/src/components/transactions/MobileTransaction.js index 688ff6f294e..0ff83a3aae9 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.js +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.js @@ -362,6 +362,7 @@ class TransactionEditInner extends PureComponent { void }; }; export type PushModalAction = { diff --git a/upcoming-release-notes/1853.md b/upcoming-release-notes/1853.md new file mode 100644 index 00000000000..ce7a4e57e8b --- /dev/null +++ b/upcoming-release-notes/1853.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Mobile create account. diff --git a/upcoming-release-notes/1880.md b/upcoming-release-notes/1880.md new file mode 100644 index 00000000000..603c5953cb8 --- /dev/null +++ b/upcoming-release-notes/1880.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Mobile report budget