diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 883f90f59d7..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,6 +24,8 @@ 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'; @@ -248,9 +249,19 @@ export default function Modals() { /> ); - case 'budget-summary': + case 'rollover-budget-summary': return ( - + ); + + case 'report-budget-summary': + return ( + { - this.props.pushModal('budget-summary', { month: this.state.currentMonth }); + 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, + }); + } }; onBudgetAction = type => { @@ -375,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} diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.js b/packages/desktop-client/src/components/budget/MobileBudgetTable.js index e2aa67da4de..b5a684c6b81 100644 --- a/packages/desktop-client/src/components/budget/MobileBudgetTable.js +++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.js @@ -70,7 +70,7 @@ function ToBudget({ toBudget, onClick }) { ); } -function Saved({ projected }) { +function Saved({ projected, onClick }) { let binding = projected ? reportBudget.totalBudgetedSaved : reportBudget.totalSaved; @@ -79,21 +79,32 @@ function Saved({ projected }) { let isNegative = saved < 0; return ( - {projected ? ( + ); } @@ -1615,7 +1626,7 @@ export function BudgetTable(props) { onEditMode, onReorderCategory, onReorderGroup, - onShowBudgetDetails, + onShowBudgetSummary, // onOpenActionSheet, onBudgetAction, onRefresh, @@ -1733,11 +1744,14 @@ export function BudgetTable(props) { }} > {type === 'report' ? ( - = currentMonth} /> + = currentMonth} + onClick={onShowBudgetSummary} + /> ) : ( )} diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index 02259f24e51..12c70c57065 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -39,9 +39,9 @@ import View from '../common/View'; import { TitlebarContext, type TitlebarContextValue } from '../Titlebar'; import DynamicBudgetTable from './DynamicBudgetTable'; -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'; 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 99% rename from packages/desktop-client/src/components/budget/report/components.tsx rename to packages/desktop-client/src/components/budget/report/ReportComponents.tsx index 1c03a443860..1d3d03bb8d3 100644 --- a/packages/desktop-client/src/components/budget/report/components.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportComponents.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', @@ -346,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/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/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/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);