diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png index 1aeaa14f98c0ff96efcef64c1c7140790a70c2b9..ce13427af8795856513e8632eae1a58bbaf84b51 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png index 76c562bd18862ad1d2d184f238c63e7b8637d512..4b22f0e2f6e3adb5c0540b60bacb0671b6f6234f 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png index 39f7c042e38adbc962d73ff15431208691c065a5..85905bc0ee1f8c18b55c53d7593a2d689f1d611b 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png index 8320db5365131c4ee874af81ed3de17debb9d0db..efc32336eed6f6bad4ba415c8afe1432f6d9a628 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png index 591897b5a477a03f07a5eb5bd71c3f579149e409..42cb054ab58c74803ee0ac0725c0239056e3ec0c 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png index 3d879590fd4e43c2ed1047d53c5cbffec3df7211..034509229c4b9a828780a71321629b83f118e725 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-4-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 95e0713602f7b90a0f5da55b77169b8decf0d230..824a59460bf84a4343f4cac9dea6c594ac43d598 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -14,7 +14,7 @@ import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; import { ModalTitle } from './common/Modal'; import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal'; import { AccountMenuModal } from './modals/AccountMenuModal'; -import { BudgetMenuModal } from './modals/BudgetMenuModal'; +import { BudgetMonthMenuModal } from './modals/BudgetMonthMenuModal'; import { CategoryAutocompleteModal } from './modals/CategoryAutocompleteModal'; import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; import { CategoryMenuModal } from './modals/CategoryMenuModal'; @@ -40,8 +40,10 @@ import { Notes } from './modals/Notes'; import { PayeeAutocompleteModal } from './modals/PayeeAutocompleteModal'; import { PlaidExternalMsg } from './modals/PlaidExternalMsg'; import { ReportBalanceMenuModal } from './modals/ReportBalanceMenuModal'; +import { ReportBudgetMenuModal } from './modals/ReportBudgetMenuModal'; import { ReportBudgetSummaryModal } from './modals/ReportBudgetSummaryModal'; import { RolloverBalanceMenuModal } from './modals/RolloverBalanceMenuModal'; +import { RolloverBudgetMenuModal } from './modals/RolloverBudgetMenuModal'; import { RolloverBudgetSummaryModal } from './modals/RolloverBudgetSummaryModal'; import { RolloverToBudgetMenuModal } from './modals/RolloverToBudgetMenuModal'; import { ScheduledTransactionMenuModal } from './modals/ScheduledTransactionMenuModal'; @@ -429,7 +431,6 @@ export function Modals() { key={name} modalProps={modalProps} categoryId={options.categoryId} - categoryGroup={options.categoryGroup} onSave={options.onSave} onEditNotes={options.onEditNotes} onDelete={options.onDelete} @@ -437,6 +438,40 @@ export function Modals() { /> ); + case 'rollover-budget-menu': + return ( + <NamespaceContext.Provider + key={name} + value={monthUtils.sheetForMonth(options.month)} + > + <RolloverBudgetMenuModal + modalProps={modalProps} + categoryId={options.categoryId} + onUpdateBudget={options.onUpdateBudget} + onCopyLastMonthAverage={options.onCopyLastMonthAverage} + onSetMonthsAverage={options.onSetMonthsAverage} + onApplyBudgetTemplate={options.onApplyBudgetTemplate} + /> + </NamespaceContext.Provider> + ); + + case 'report-budget-menu': + return ( + <NamespaceContext.Provider + key={name} + value={monthUtils.sheetForMonth(options.month)} + > + <ReportBudgetMenuModal + modalProps={modalProps} + categoryId={options.categoryId} + onUpdateBudget={options.onUpdateBudget} + onCopyLastMonthAverage={options.onCopyLastMonthAverage} + onSetMonthsAverage={options.onSetMonthsAverage} + onApplyBudgetTemplate={options.onApplyBudgetTemplate} + /> + </NamespaceContext.Provider> + ); + case 'category-group-menu': return ( <CategoryGroupMenuModal @@ -479,7 +514,7 @@ export function Modals() { </NamespaceContext.Provider> ); - case 'rollover-to-budget-menu': + case 'rollover-summary-to-budget-menu': return ( <NamespaceContext.Provider key={name} @@ -552,13 +587,13 @@ export function Modals() { /> ); - case 'budget-menu': + case 'budget-month-menu': return ( <NamespaceContext.Provider key={name} value={monthUtils.sheetForMonth(options.month)} > - <BudgetMenuModal + <BudgetMonthMenuModal modalProps={modalProps} month={options.month} onToggleHiddenCategories={options.onToggleHiddenCategories} diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx index cc61c3d8b5ee522f43ecfe0f785f868c7492f8df..f4d45208dce1279046207f58526fcce41911d98d 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx @@ -1,7 +1,5 @@ import React, { useRef, useState } from 'react'; -import * as monthUtils from 'loot-core/src/shared/months'; - import { useCategories } from '../../hooks/useCategories'; import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; @@ -40,8 +38,8 @@ export function BudgetTable(props) { ); const [editing, setEditing] = useState(null); - const onEditMonth = (id, monthIndex) => { - setEditing(id ? { id, cell: monthIndex } : null); + const onEditMonth = (id, month) => { + setEditing(id ? { id, cell: month } : null); }; const onEditName = id => { @@ -134,18 +132,6 @@ export function BudgetTable(props) { } }; - const resolveMonth = monthIndex => { - return monthUtils.addMonths(startMonth, monthIndex); - }; - - const _onShowActivity = (catId, monthIndex) => { - onShowActivity(catId, resolveMonth(monthIndex)); - }; - - const _onBudgetAction = (monthIndex, type, args) => { - onBudgetAction(resolveMonth(monthIndex), type, args); - }; - const onCollapse = collapsedIds => { setCollapsedPref(collapsedIds); }; @@ -244,8 +230,8 @@ export function BudgetTable(props) { onDeleteGroup={onDeleteGroup} onReorderCategory={_onReorderCategory} onReorderGroup={_onReorderGroup} - onBudgetAction={_onBudgetAction} - onShowActivity={_onShowActivity} + onBudgetAction={onBudgetAction} + onShowActivity={onShowActivity} /> </View> </View> diff --git a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx index 5007b81c8d12d51d609ae28724b2b5b17dadf0e2..bb9da828c58ab549cd78f3322195ddac400b5a06 100644 --- a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx +++ b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx @@ -28,12 +28,12 @@ type ExpenseCategoryProps = { dragState: DragState<CategoryEntity>; MonthComponent: ComponentProps<typeof RenderMonths>['component']; onEditName?: ComponentProps<typeof SidebarCategory>['onEditName']; - onEditMonth?: (id: string, monthIndex: number) => void; + onEditMonth?: (id: string, month: string) => void; onSave?: ComponentProps<typeof SidebarCategory>['onSave']; onDelete?: ComponentProps<typeof SidebarCategory>['onDelete']; onDragChange: OnDragChangeCallback<CategoryEntity>; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onBudgetAction: (month: number, action: string, arg: unknown) => void; + onShowActivity: (id: string, month: string) => void; onReorder: OnDropCallback; }; @@ -101,7 +101,7 @@ export function ExpenseCategory({ <RenderMonths component={MonthComponent} - editingIndex={ + editingMonth={ editingCell && editingCell.id === cat.id && editingCell.cell } args={{ diff --git a/packages/desktop-client/src/components/budget/IncomeCategory.tsx b/packages/desktop-client/src/components/budget/IncomeCategory.tsx index f240d72b3d614b0baec5c4b6fa6827bf59a5e13e..97f8dec2b987bc12da56503dd913ba8d0add5858 100644 --- a/packages/desktop-client/src/components/budget/IncomeCategory.tsx +++ b/packages/desktop-client/src/components/budget/IncomeCategory.tsx @@ -21,13 +21,13 @@ type IncomeCategoryProps = { editingCell: { id: string; cell: string } | null; MonthComponent: ComponentProps<typeof RenderMonths>['component']; onEditName: ComponentProps<typeof SidebarCategory>['onEditName']; - onEditMonth?: (id: string, monthIndex: number) => void; + onEditMonth?: (id: string, month: string) => void; onSave: ComponentProps<typeof SidebarCategory>['onSave']; onDelete: ComponentProps<typeof SidebarCategory>['onDelete']; onDragChange: OnDragChangeCallback<CategoryEntity>; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onReorder: OnDropCallback; - onShowActivity: (id: string, idx: number) => void; + onShowActivity: (id: string, month: string) => void; }; export function IncomeCategory({ @@ -76,7 +76,7 @@ export function IncomeCategory({ /> <RenderMonths component={MonthComponent} - editingIndex={ + editingMonth={ editingCell && editingCell.id === cat.id && editingCell.cell } args={{ diff --git a/packages/desktop-client/src/components/budget/RenderMonths.tsx b/packages/desktop-client/src/components/budget/RenderMonths.tsx index 20aceb19922c953dfaf9d2c47af22003e41efe3d..f2968d3f714138dceddc7e5e8de7c66f1265d456 100644 --- a/packages/desktop-client/src/components/budget/RenderMonths.tsx +++ b/packages/desktop-client/src/components/budget/RenderMonths.tsx @@ -14,22 +14,22 @@ import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { MonthsContext } from './MonthsContext'; type RenderMonthsProps = { - component?: ComponentType<{ monthIndex: number; editing: boolean }>; - editingIndex?: string | number; + component?: ComponentType<{ month: string; editing: boolean }>; + editingMonth?: string; args?: object; style?: CSSProperties; }; export function RenderMonths({ component: Component, - editingIndex, + editingMonth, args, style, }: RenderMonthsProps) { const { months } = useContext(MonthsContext); return months.map((month, index) => { - const editing = editingIndex === index; + const editing = editingMonth === month; return ( <NamespaceContext.Provider @@ -43,7 +43,7 @@ export function RenderMonths({ ...style, }} > - <Component monthIndex={index} editing={editing} {...args} /> + <Component month={month} editing={editing} {...args} /> </View> </NamespaceContext.Provider> ); diff --git a/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx index 66999627bf7800736b0683af2adeabb17325721d..91aa1ade6d195009ca33a92fc92ee09d4ac59694 100644 --- a/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx +++ b/packages/desktop-client/src/components/budget/report/BalanceTooltip.tsx @@ -7,15 +7,15 @@ import { BalanceMenu } from './BalanceMenu'; type BalanceTooltipProps = { categoryId: string; tooltip: { close: () => void }; - monthIndex: number; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + month: string; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onClose?: () => void; }; export function BalanceTooltip({ categoryId, tooltip, - monthIndex, + month, onBudgetAction, onClose, ...tooltipProps @@ -36,7 +36,7 @@ export function BalanceTooltip({ <BalanceMenu categoryId={categoryId} onCarryover={carryover => { - onBudgetAction?.(monthIndex, 'carryover', { + onBudgetAction?.(month, 'carryover', { category: categoryId, flag: carryover, }); diff --git a/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx b/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aeed3cddd88c1f867b6b5b7bb41ff4b664eeb49a --- /dev/null +++ b/packages/desktop-client/src/components/budget/report/BudgetMenu.tsx @@ -0,0 +1,75 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; +import { Menu } from '../../common/Menu'; + +type BudgetMenuProps = Omit< + ComponentPropsWithoutRef<typeof Menu>, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; +}; +export function BudgetMenu({ + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, + ...props +}: BudgetMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + const onMenuSelect = (name: string) => { + switch (name) { + case 'copy-single-last': + onCopyLastMonthAverage?.(); + break; + case 'set-single-3-avg': + onSetMonthsAverage?.(3); + break; + case 'set-single-6-avg': + onSetMonthsAverage?.(6); + break; + case 'set-single-12-avg': + onSetMonthsAverage?.(12); + break; + case 'apply-single-category-template': + onApplyBudgetTemplate?.(); + break; + default: + throw new Error(`Unrecognized menu item: ${name}`); + } + }; + + return ( + <Menu + {...props} + onMenuSelect={onMenuSelect} + items={[ + { + name: 'copy-single-last', + text: 'Copy last month’s budget', + }, + { + name: 'set-single-3-avg', + text: 'Set to 3 month average', + }, + { + name: 'set-single-6-avg', + text: 'Set to 6 month average', + }, + { + name: 'set-single-12-avg', + text: 'Set to yearly average', + }, + ...(isGoalTemplatesEnabled + ? [ + { + name: 'apply-single-category-template', + text: 'Apply budget template', + }, + ] + : []), + ]} + /> + ); +} diff --git a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx index 3181c8da7cb5554f7b7df4cb4f083ae6967d2729..5d09472c8ffc755b9d8aaa5312eb25293cae213c 100644 --- a/packages/desktop-client/src/components/budget/report/ReportComponents.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportComponents.tsx @@ -5,11 +5,9 @@ import { reportBudget } from 'loot-core/src/client/queries'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; -import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button'; -import { Menu } from '../../common/Menu'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { CellValue } from '../../spreadsheet/CellValue'; @@ -20,6 +18,7 @@ import { BalanceWithCarryover } from '../BalanceWithCarryover'; import { makeAmountGrey } from '../util'; import { BalanceTooltip } from './BalanceTooltip'; +import { BudgetMenu } from './BudgetMenu'; const headerLabelStyle: CSSProperties = { flex: 1, @@ -142,15 +141,15 @@ export const GroupMonth = memo(function GroupMonth({ group }: GroupMonthProps) { }); type CategoryMonthProps = { - monthIndex: number; + month: string; category: { id: string; name: string; is_income: boolean }; editing: boolean; - onEdit: (id: string | null, idx?: number) => void; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onEdit: (id: string | null, month?: string) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; + onShowActivity: (id: string, month: string) => void; }; export const CategoryMonth = memo(function CategoryMonth({ - monthIndex, + month, category, editing, onEdit, @@ -160,7 +159,6 @@ export const CategoryMonth = memo(function CategoryMonth({ const balanceTooltip = useTooltip(); const [menuOpen, setMenuOpen] = useState(false); const [hover, setHover] = useState(false); - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( <View @@ -221,33 +219,34 @@ export const CategoryMonth = memo(function CategoryMonth({ style={{ padding: 0 }} onClose={() => setMenuOpen(false)} > - <Menu - onMenuSelect={type => { - onBudgetAction(monthIndex, type, { category: category.id }); - setMenuOpen(false); + <BudgetMenu + onCopyLastMonthAverage={() => { + onBudgetAction?.(month, 'copy-single-last', { + category: category.id, + }); + }} + onSetMonthsAverage={numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction?.( + month, + `set-single-${numberOfMonths}-avg`, + { + category: category.id, + }, + ); + }} + onApplyBudgetTemplate={() => { + onBudgetAction?.(month, 'apply-single-category-template', { + category: category.id, + }); }} - items={[ - { - name: 'copy-single-last', - text: 'Copy last month’s budget', - }, - { - name: 'set-single-3-avg', - text: 'Set to 3 month average', - }, - { - name: 'set-single-6-avg', - text: 'Set to 6 month average', - }, - { - name: 'set-single-12-avg', - text: 'Set to yearly average', - }, - isGoalTemplatesEnabled && { - name: 'apply-single-category-template', - text: 'Apply budget template', - }, - ]} /> </Tooltip> )} @@ -258,7 +257,7 @@ export const CategoryMonth = memo(function CategoryMonth({ exposed={editing} focused={editing} width="flex" - onExpose={() => onEdit(category.id, monthIndex)} + onExpose={() => onEdit(category.id, month)} style={{ ...(editing && { zIndex: 100 }), ...styles.tnum }} textAlign="right" valueStyle={{ @@ -291,7 +290,7 @@ export const CategoryMonth = memo(function CategoryMonth({ }, }} onSave={amount => { - onBudgetAction(monthIndex, 'budget-amount', { + onBudgetAction(month, 'budget-amount', { category: category.id, amount, }); @@ -301,7 +300,7 @@ export const CategoryMonth = memo(function CategoryMonth({ <Field name="spent" width="flex" style={{ textAlign: 'right' }}> <span data-testid="category-month-spent" - onClick={() => onShowActivity(category.id, monthIndex)} + onClick={() => onShowActivity(category.id, month)} > <CellValue binding={reportBudget.catSumAmount(category.id)} @@ -337,7 +336,7 @@ export const CategoryMonth = memo(function CategoryMonth({ <BalanceTooltip categoryId={category.id} tooltip={balanceTooltip} - monthIndex={monthIndex} + month={month} onBudgetAction={onBudgetAction} /> )} diff --git a/packages/desktop-client/src/components/budget/report/ReportContext.tsx b/packages/desktop-client/src/components/budget/report/ReportContext.tsx index c1c90e93d725f0947bc7f73fc9e13a954c40fa5e..d93d8cbe66be4321d48a1cae5e758fc4372f5381 100644 --- a/packages/desktop-client/src/components/budget/report/ReportContext.tsx +++ b/packages/desktop-client/src/components/budget/report/ReportContext.tsx @@ -7,7 +7,7 @@ const Context = createContext(null); type ReportProviderProps = { summaryCollapsed: boolean; - onBudgetAction: (idx: number, action: string, arg: unknown) => void; + onBudgetAction: (month: string, action: string, arg: unknown) => void; onToggleSummaryCollapse: () => void; children: ReactNode; }; diff --git a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx index 66b8f86ba30538d0de14b7dd7e1c597b4bb744ed..1432556c096ce5f58dbc5ed931cb397345e2bf6e 100644 --- a/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/BalanceTooltip.tsx @@ -12,15 +12,15 @@ import { TransferTooltip } from './TransferTooltip'; type BalanceTooltipProps = { categoryId: string; tooltip: { close: () => void }; - monthIndex: number; - onBudgetAction: (idx: number, action: string, arg?: unknown) => void; + month: string; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; onClose?: () => void; }; export function BalanceTooltip({ categoryId, tooltip, - monthIndex, + month, onBudgetAction, onClose, ...tooltipProps @@ -46,7 +46,7 @@ export function BalanceTooltip({ <BalanceMenu categoryId={categoryId} onCarryover={carryover => { - onBudgetAction(monthIndex, 'carryover', { + onBudgetAction(month, 'carryover', { category: categoryId, flag: carryover, }); @@ -64,7 +64,7 @@ export function BalanceTooltip({ showToBeBudgeted={true} onClose={_onClose} onSubmit={(amount, toCategoryId) => { - onBudgetAction(monthIndex, 'transfer-category', { + onBudgetAction(month, 'transfer-category', { amount, from: categoryId, to: toCategoryId, @@ -77,7 +77,7 @@ export function BalanceTooltip({ <CoverTooltip onClose={_onClose} onSubmit={fromCategoryId => { - onBudgetAction(monthIndex, 'cover', { + onBudgetAction(month, 'cover', { to: categoryId, from: fromCategoryId, }); diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aeed3cddd88c1f867b6b5b7bb41ff4b664eeb49a --- /dev/null +++ b/packages/desktop-client/src/components/budget/rollover/BudgetMenu.tsx @@ -0,0 +1,75 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; + +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; +import { Menu } from '../../common/Menu'; + +type BudgetMenuProps = Omit< + ComponentPropsWithoutRef<typeof Menu>, + 'onMenuSelect' | 'items' +> & { + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; +}; +export function BudgetMenu({ + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, + ...props +}: BudgetMenuProps) { + const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + const onMenuSelect = (name: string) => { + switch (name) { + case 'copy-single-last': + onCopyLastMonthAverage?.(); + break; + case 'set-single-3-avg': + onSetMonthsAverage?.(3); + break; + case 'set-single-6-avg': + onSetMonthsAverage?.(6); + break; + case 'set-single-12-avg': + onSetMonthsAverage?.(12); + break; + case 'apply-single-category-template': + onApplyBudgetTemplate?.(); + break; + default: + throw new Error(`Unrecognized menu item: ${name}`); + } + }; + + return ( + <Menu + {...props} + onMenuSelect={onMenuSelect} + items={[ + { + name: 'copy-single-last', + text: 'Copy last month’s budget', + }, + { + name: 'set-single-3-avg', + text: 'Set to 3 month average', + }, + { + name: 'set-single-6-avg', + text: 'Set to 6 month average', + }, + { + name: 'set-single-12-avg', + text: 'Set to yearly average', + }, + ...(isGoalTemplatesEnabled + ? [ + { + name: 'apply-single-category-template', + text: 'Apply budget template', + }, + ] + : []), + ]} + /> + ); +} diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx index a8e326f960180b5d7069b64572aa1fc592eb5bd8..b1ba0ff719eb6072fcb3657e83a5787ff435ca79 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverComponents.tsx @@ -1,14 +1,13 @@ -import React, { memo, useState } from 'react'; +import type React from 'react'; +import { memo, useState } from 'react'; import { rolloverBudget } from 'loot-core/src/client/queries'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/src/shared/util'; -import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../../icons/v1'; import { styles, theme, type CSSProperties } from '../../../style'; import { Button } from '../../common/Button'; -import { Menu } from '../../common/Menu'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; import { CellValue } from '../../spreadsheet/CellValue'; @@ -19,6 +18,7 @@ import { BalanceWithCarryover } from '../BalanceWithCarryover'; import { makeAmountGrey } from '../util'; import { BalanceTooltip } from './BalanceTooltip'; +import { BudgetMenu } from './BudgetMenu'; const headerLabelStyle: CSSProperties = { flex: 1, @@ -137,15 +137,15 @@ export const ExpenseGroupMonth = memo(function ExpenseGroupMonth({ }); type ExpenseCategoryMonthProps = { - monthIndex: number; + month: string; category: { id: string; name: string; is_income: boolean }; editing: boolean; - onEdit: (id: string | null, idx?: number) => void; - onBudgetAction: (idx: number, action: string, arg?: unknown) => void; - onShowActivity: (id: string, idx: number) => void; + onEdit: (id: string | null, month?: string) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; + onShowActivity: (id: string, month: string) => void; }; export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ - monthIndex, + month, category, editing, onEdit, @@ -155,7 +155,6 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ const balanceTooltip = useTooltip(); const [menuOpen, setMenuOpen] = useState(false); const [hover, setHover] = useState(false); - const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); return ( <View @@ -216,37 +215,34 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ style={{ padding: 0 }} onClose={() => setMenuOpen(false)} > - <Menu - onMenuSelect={type => { - onBudgetAction(monthIndex, type, { category: category.id }); - setMenuOpen(false); + <BudgetMenu + onCopyLastMonthAverage={() => { + onBudgetAction?.(month, 'copy-single-last', { + category: category.id, + }); + }} + onSetMonthsAverage={numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction?.( + month, + `set-single-${numberOfMonths}-avg`, + { + category: category.id, + }, + ); + }} + onApplyBudgetTemplate={() => { + onBudgetAction?.(month, 'apply-single-category-template', { + category: category.id, + }); }} - items={[ - { - name: 'copy-single-last', - text: 'Copy last month’s budget', - }, - { - name: 'set-single-3-avg', - text: 'Set to 3 month average', - }, - { - name: 'set-single-6-avg', - text: 'Set to 6 month average', - }, - { - name: 'set-single-12-avg', - text: 'Set to yearly average', - }, - ...(isGoalTemplatesEnabled - ? [ - { - name: 'apply-single-category-template', - text: 'Apply budget template', - }, - ] - : []), - ]} /> </Tooltip> )} @@ -257,7 +253,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ exposed={editing} focused={editing} width="flex" - onExpose={() => onEdit(category.id, monthIndex)} + onExpose={() => onEdit(category.id, month)} style={{ ...(editing && { zIndex: 100 }), ...styles.tnum }} textAlign="right" valueStyle={{ @@ -290,7 +286,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ }, }} onSave={amount => { - onBudgetAction(monthIndex, 'budget-amount', { + onBudgetAction(month, 'budget-amount', { category: category.id, amount, }); @@ -300,7 +296,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ <Field name="spent" width="flex" style={{ textAlign: 'right' }}> <span data-testid="category-month-spent" - onClick={() => onShowActivity(category.id, monthIndex)} + onClick={() => onShowActivity(category.id, month)} > <CellValue binding={rolloverBudget.catSumAmount(category.id)} @@ -331,7 +327,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({ <BalanceTooltip categoryId={category.id} tooltip={balanceTooltip} - monthIndex={monthIndex} + month={month} onBudgetAction={onBudgetAction} /> )} @@ -369,13 +365,13 @@ export function IncomeGroupMonth() { type IncomeCategoryMonthProps = { category: { id: string; name: string }; isLast: boolean; - monthIndex: number; - onShowActivity: (id: string, idx: number) => void; + month: string; + onShowActivity: (id: string, month: string) => void; }; export function IncomeCategoryMonth({ category, isLast, - monthIndex, + month, onShowActivity, }: IncomeCategoryMonthProps) { return ( @@ -389,7 +385,7 @@ export function IncomeCategoryMonth({ ...(isLast && { borderBottomWidth: 0 }), }} > - <span onClick={() => onShowActivity(category.id, monthIndex)}> + <span onClick={() => onShowActivity(category.id, month)}> <CellValue binding={rolloverBudget.catSumAmount(category.id)} type="financial" diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx index bf68c07afaa6cdad2afee0ba0c8a2320a55d6697..81e9f18cbe986cb73bd90890eb55cc7631f0e66e 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx @@ -4,7 +4,7 @@ import * as monthUtils from 'loot-core/src/shared/months'; type RolloverContextDefinition = { summaryCollapsed: boolean; - onBudgetAction: (idx: string, action: string, arg?: unknown) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; onToggleSummaryCollapse: () => void; currentMonth: string; }; diff --git a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx index 75051811aa0ec9729935bfa66519f0ecbc5ec953..5630ad92bfec8c4b9f7778793b43a4427858402d 100644 --- a/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx +++ b/packages/desktop-client/src/components/budget/rollover/budgetsummary/ToBudget.tsx @@ -13,7 +13,7 @@ import { ToBudgetMenu } from './ToBudgetMenu'; type ToBudgetProps = { month: string; - onBudgetAction: (idx: string, action: string, arg?: unknown) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; prevMonthName: string; showTotalsTooltipOnHover?: boolean; style?: CSSProperties; diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 4eb5cad56261600eaf4b083dd73d153776783bf2..c7fcdd68fdbc9298c0e180176ab5ac0b5b62ac1f 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -1,4 +1,4 @@ -import React, { memo, useRef, useState } from 'react'; +import React, { memo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import memoizeOne from 'memoize-one'; @@ -9,10 +9,6 @@ import * as monthUtils from 'loot-core/src/shared/months'; import { useLocalPref } from '../../../hooks/useLocalPref'; import { useNavigate } from '../../../hooks/useNavigate'; -import { - SingleActiveEditFormProvider, - useSingleActiveEditForm, -} from '../../../hooks/useSingleActiveEditForm'; import { SvgLogo } from '../../../icons/logo'; import { SvgAdd, SvgArrowThinLeft, SvgArrowThinRight } from '../../../icons/v1'; import { useResponsive } from '../../../ResponsiveProvider'; @@ -28,7 +24,6 @@ import { Page } from '../../Page'; import { CellValue } from '../../spreadsheet/CellValue'; import { useFormat } from '../../spreadsheet/useFormat'; import { useSheetValue } from '../../spreadsheet/useSheetValue'; -import { AmountInput } from '../../util/AmountInput'; import { MOBILE_NAV_HEIGHT } from '../MobileNavTabs'; import { PullToRefresh } from '../PullToRefresh'; @@ -123,66 +118,67 @@ function BudgetCell({ name, binding, style, - textStyle, categoryId, month, onBudgetAction, - onEdit, - onBlur, - isEditing, }) { - const sheetValue = useSheetValue(binding); + const dispatch = useDispatch(); + const [budgetType = 'rollover'] = useLocalPref('budgetType'); - function updateBudgetAmount(amount) { - onBudgetAction?.(month, 'budget-amount', { - category: categoryId, - amount, - }); - } + const categoryBudgetMenuModal = `${budgetType}-budget-menu`; - function onAmountClick() { - onEdit?.(); - } + const onOpenCategoryBudgetMenu = () => { + dispatch( + pushModal(categoryBudgetMenuModal, { + categoryId, + month, + onUpdateBudget: amount => { + onBudgetAction(month, 'budget-amount', { + category: categoryId, + amount, + }); + }, + onCopyLastMonthAverage: () => { + onBudgetAction(month, 'copy-single-last', { + category: categoryId, + }); + }, + onSetMonthsAverage: numberOfMonths => { + if ( + numberOfMonths !== 3 && + numberOfMonths !== 6 && + numberOfMonths !== 12 + ) { + return; + } + + onBudgetAction(month, `set-single-${numberOfMonths}-avg`, { + category: categoryId, + }); + }, + onApplyBudgetTemplate: () => { + onBudgetAction(month, 'apply-single-category-template', { + category: categoryId, + }); + }, + }), + ); + }; return ( - <View style={style}> - <AmountInput - value={sheetValue} - zeroSign="+" - style={{ - ...(!isEditing && { display: 'none' }), - height: ROW_HEIGHT, - transform: 'translateX(6px)', - }} - focused={isEditing} - textStyle={{ ...styles.smallText, ...textStyle }} - onUpdate={updateBudgetAmount} - onBlur={onBlur} - /> - <View - role="button" - style={{ - ...(isEditing && { display: 'none' }), - justifyContent: 'center', - alignItems: 'flex-end', - height: ROW_HEIGHT, - }} - > - <CellValue - binding={binding} - type="financial" - style={{ - ...styles.smallText, - ...textStyle, - ...styles.underlinedText, - }} - getStyle={makeAmountGrey} - data-testid={name} - onPointerUp={onAmountClick} - onPointerDown={e => e.preventDefault()} - /> - </View> - </View> + <CellValue + binding={binding} + type="financial" + style={{ + textAlign: 'right', + ...styles.smallText, + ...style, + ...styles.underlinedText, + }} + getStyle={makeAmountGrey} + data-testid={name} + onClick={onOpenCategoryBudgetMenu} + /> ); } @@ -247,17 +243,8 @@ const ExpenseCategory = memo(function ExpenseCategory({ const opacity = blank ? 0 : 1; const [budgetType = 'rollover'] = useLocalPref('budgetType'); - const [isEditingBudget, setIsEditingBudget] = useState(false); - const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm(); const dispatch = useDispatch(); - const onEditBudget = () => { - onRequestActiveEdit(`${category.id}-budget`, () => { - setIsEditingBudget(true); - return () => setIsEditingBudget(false); - }); - }; - const onCarryover = carryover => { onBudgetAction(month, 'carryover', { category: category.id, @@ -305,7 +292,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ ); }; - const onOpenBalanceActionMenu = () => { + const onOpenBalanceMenu = () => { dispatch( pushModal(`${budgetType}-balance-menu`, { categoryId: category.id, @@ -367,16 +354,12 @@ const ExpenseCategory = memo(function ExpenseCategory({ name="budgeted" binding={budgeted} style={{ - ...(!show3Cols && !showBudgetedCol && { display: 'none' }), width: 90, + ...(!show3Cols && !showBudgetedCol && { display: 'none' }), }} - textStyle={{ ...styles.smallText, textAlign: 'right' }} categoryId={category.id} month={month} onBudgetAction={onBudgetAction} - isEditing={isEditingBudget} - onEdit={onEditBudget} - onBlur={onClearActiveEdit} /> <View style={{ @@ -409,7 +392,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ height: ROW_HEIGHT, }} > - <span role="button" onClick={() => onOpenBalanceActionMenu?.()}> + <span role="button" onClick={() => onOpenBalanceMenu?.()}> <BalanceWithCarryover carryover={carryover} balance={balance} @@ -693,15 +676,6 @@ const IncomeCategory = memo(function IncomeCategory({ onBudgetAction, }) { const listItemRef = useRef(); - const [isEditingBudget, setIsEditingBudget] = useState(false); - const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm(); - - const onEditBudget = () => { - onRequestActiveEdit(`${category.id}-budget`, () => { - setIsEditingBudget(true); - return () => setIsEditingBudget(false); - }); - }; return ( <ListItem @@ -739,29 +713,14 @@ const IncomeCategory = memo(function IncomeCategory({ </Text> </View> {budgeted && ( - <View - style={{ - justifyContent: 'center', - alignItems: 'flex-end', - width: 90, - height: ROW_HEIGHT, - }} - > - <BudgetCell - name="budgeted" - binding={budgeted} - style={{ - width: 90, - }} - textStyle={{ ...styles.smallText, textAlign: 'right' }} - categoryId={category.id} - month={month} - onBudgetAction={onBudgetAction} - isEditing={isEditingBudget} - onEdit={onEditBudget} - onBlur={onClearActiveEdit} - /> - </View> + <BudgetCell + name="budgeted" + binding={budgeted} + style={{ width: 90 }} + categoryId={category.id} + month={month} + onBudgetAction={onBudgetAction} + /> )} <View style={{ @@ -1061,54 +1020,52 @@ function BudgetGroups({ const { incomeGroup, expenseGroups } = separateGroups(categoryGroups); return ( - <SingleActiveEditFormProvider formName="mobile-budget-table"> - <View - data-testid="budget-groups" - style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }} - > - {expenseGroups - .filter(group => !group.hidden || showHiddenCategories) - .map(group => { - return ( - <ExpenseGroup - key={group.id} - type={type} - group={group} - showBudgetedCol={showBudgetedCol} - gestures={gestures} - month={month} - editMode={editMode} - onEditGroup={onEditGroup} - onEditCategory={onEditCategory} - onSaveCategory={onSaveCategory} - onDeleteCategory={onDeleteCategory} - onAddCategory={onAddCategory} - onReorderCategory={onReorderCategory} - onReorderGroup={onReorderGroup} - onBudgetAction={onBudgetAction} - show3Cols={show3Cols} - showHiddenCategories={showHiddenCategories} - /> - ); - })} + <View + data-testid="budget-groups" + style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }} + > + {expenseGroups + .filter(group => !group.hidden || showHiddenCategories) + .map(group => { + return ( + <ExpenseGroup + key={group.id} + type={type} + group={group} + showBudgetedCol={showBudgetedCol} + gestures={gestures} + month={month} + editMode={editMode} + onEditGroup={onEditGroup} + onEditCategory={onEditCategory} + onSaveCategory={onSaveCategory} + onDeleteCategory={onDeleteCategory} + onAddCategory={onAddCategory} + onReorderCategory={onReorderCategory} + onReorderGroup={onReorderGroup} + onBudgetAction={onBudgetAction} + show3Cols={show3Cols} + showHiddenCategories={showHiddenCategories} + /> + ); + })} - {incomeGroup && ( - <IncomeGroup - type={type} - group={incomeGroup} - month={month} - onAddCategory={onAddCategory} - onSaveCategory={onSaveCategory} - onDeleteCategory={onDeleteCategory} - showHiddenCategories={showHiddenCategories} - editMode={editMode} - onEditGroup={onEditGroup} - onEditCategory={onEditCategory} - onBudgetAction={onBudgetAction} - /> - )} - </View> - </SingleActiveEditFormProvider> + {incomeGroup && ( + <IncomeGroup + type={type} + group={incomeGroup} + month={month} + onAddCategory={onAddCategory} + onSaveCategory={onSaveCategory} + onDeleteCategory={onDeleteCategory} + showHiddenCategories={showHiddenCategories} + editMode={editMode} + onEditGroup={onEditGroup} + onEditCategory={onEditCategory} + onBudgetAction={onBudgetAction} + /> + )} + </View> ); } @@ -1118,7 +1075,6 @@ export function BudgetTable({ month, monthBounds, // editMode, - // refreshControl, onPrevMonth, onNextMonth, onSaveGroup, @@ -1134,7 +1090,7 @@ export function BudgetTable({ onRefresh, onEditGroup, onEditCategory, - onOpenBudgetActionMenu, + onOpenBudgetMonthMenu, }) { const { width } = useResponsive(); const show3Cols = width >= 360; @@ -1185,7 +1141,7 @@ export function BudgetTable({ }} hoveredStyle={noBackgroundColorStyle} activeStyle={noBackgroundColorStyle} - onClick={() => onOpenBudgetActionMenu?.(month)} + onClick={() => onOpenBudgetMonthMenu?.(month)} > <SvgLogo width="20" height="20" /> </Button> diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index d3c2bd2a78806f3edb1ac7a3c6ff1f757912a32a..0e244fb1f7b1a283555d482e676b9b69a0c784ff 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -343,14 +343,13 @@ function BudgetInner(props: BudgetInnerProps) { const onEditCategory = id => { const category = categories.find(c => c.id === id); - const categoryGroup = categoryGroups.find(g => g.id === category.cat_group); dispatch( pushModal('category-menu', { categoryId: category.id, - categoryGroup, onSave: onSaveCategory, onEditNotes: onEditCategoryNotes, onDelete: onDeleteCategory, + onBudgetAction, }), ); }; @@ -360,7 +359,7 @@ function BudgetInner(props: BudgetInnerProps) { pushModal('switch-budget-type', { onSwitch: () => { onSwitchBudgetType?.(); - dispatch(collapseModals('budget-menu')); + dispatch(collapseModals('budget-month-menu')); }, }), ); @@ -372,12 +371,12 @@ function BudgetInner(props: BudgetInnerProps) { const onToggleHiddenCategories = () => { setShowHiddenCategoriesPref(!showHiddenCategories); - dispatch(collapseModals('budget-menu')); + dispatch(collapseModals('budget-month-menu')); }; - const onOpenBudgetActionMenu = month => { + const onOpenBudgetMonthMenu = month => { dispatch( - pushModal('budget-menu', { + pushModal('budget-month-menu', { month, onToggleHiddenCategories, onSwitchBudgetType: _onSwitchBudgetType, @@ -433,7 +432,7 @@ function BudgetInner(props: BudgetInnerProps) { onRefresh={onRefresh} onEditGroup={onEditGroup} onEditCategory={onEditCategory} - onOpenBudgetActionMenu={onOpenBudgetActionMenu} + onOpenBudgetMonthMenu={onOpenBudgetMonthMenu} /> )} </SyncRefresh> diff --git a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx similarity index 67% rename from packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx rename to packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx index 482759d5f76067b87d2266cda5c73be31eabfad3..17b0d0d3281d6efc488ef3b99099d75f2147707c 100644 --- a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx @@ -1,4 +1,12 @@ -import { memo, useEffect, useRef, useState } from 'react'; +import React, { + type Ref, + type ComponentPropsWithRef, + type HTMLProps, + memo, + useEffect, + useRef, + useState, +} from 'react'; import { toRelaxedNumber, @@ -8,23 +16,41 @@ import { import { useLocalPref } from '../../../hooks/useLocalPref'; import { useMergedRefs } from '../../../hooks/useMergedRefs'; -import { theme } from '../../../style'; +import { type CSSProperties, theme } from '../../../style'; import { Button } from '../../common/Button'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; +type AmountInputProps = { + value: number; + focused?: boolean; + style?: CSSProperties; + textStyle?: CSSProperties; + inputRef?: Ref<HTMLInputElement>; + onFocus?: HTMLProps<HTMLInputElement>['onFocus']; + onBlur?: HTMLProps<HTMLInputElement>['onBlur']; + onEnter?: HTMLProps<HTMLInputElement>['onKeyUp']; + onChangeValue?: (value: string) => void; + onUpdate?: (value: string) => void; + onUpdateAmount?: (value: number) => void; +}; + const AmountInput = memo(function AmountInput({ focused, style, textStyle, ...props -}) { +}: AmountInputProps) { const [editing, setEditing] = useState(false); const [text, setText] = useState(''); const [value, setValue] = useState(0); - const inputRef = useRef(); + const inputRef = useRef<HTMLInputElement>(); const [hideFraction = false] = useLocalPref('hideFraction'); - const mergedInputRef = useMergedRefs(props.inputRef, inputRef); + + const mergedInputRef = useMergedRefs<HTMLInputElement>( + props.inputRef, + inputRef, + ); const initialValue = Math.abs(props.value); @@ -44,11 +70,17 @@ const AmountInput = memo(function AmountInput({ return toRelaxedNumber(text.replace(/[,.]/, getNumberFormat().separator)); }; - const onKeyPress = e => { + const onKeyUp: HTMLProps<HTMLInputElement>['onKeyUp'] = e => { if (e.key === 'Backspace' && text === '') { setEditing(true); + } else if (e.key === 'Enter') { + props.onEnter?.(e); + if (!e.defaultPrevented) { + onUpdate(e.currentTarget.value); + } } }; + const applyText = () => { const parsed = parseText(); const newValue = editing ? parsed : value; @@ -60,17 +92,24 @@ const AmountInput = memo(function AmountInput({ return newValue; }; - const onFocus = e => { + const onFocus: HTMLProps<HTMLInputElement>['onFocus'] = e => { props.onFocus?.(e); }; - const onBlur = e => { - const value = applyText(); + const onUpdate = (value: string) => { props.onUpdate?.(value); + const amount = applyText(); + props.onUpdateAmount?.(amount); + }; + + const onBlur: HTMLProps<HTMLInputElement>['onBlur'] = e => { props.onBlur?.(e); + if (!e.defaultPrevented) { + onUpdate(e.target.value); + } }; - const onChangeText = text => { + const onChangeText = (text: string) => { if (text.slice(-1) === '.') { text = text.slice(0, -1); } @@ -83,7 +122,7 @@ const AmountInput = memo(function AmountInput({ setEditing(true); setText(text); - props.onChange?.(text); + props.onChangeValue?.(text); }; const input = ( @@ -96,7 +135,7 @@ const AmountInput = memo(function AmountInput({ onChange={e => onChangeText(e.target.value)} onFocus={onFocus} onBlur={onBlur} - onKeyUp={onKeyPress} + onKeyUp={onKeyUp} data-testid="amount-input" style={{ flex: 1, textAlign: 'center', position: 'absolute' }} /> @@ -111,14 +150,17 @@ const AmountInput = memo(function AmountInput({ borderRadius: 4, padding: 5, backgroundColor: theme.tableBackground, + maxWidth: 'calc(100% - 40px)', ...style, }} > <View style={{ overflowY: 'auto', overflowX: 'hidden' }}>{input}</View> <Text - style={textStyle} + style={{ + pointerEvents: 'none', + ...textStyle, + }} data-testid="amount-fake-input" - pointerEvents="none" > {editing ? amountToCurrency(text) : amountToCurrency(value)} </Text> @@ -126,11 +168,22 @@ const AmountInput = memo(function AmountInput({ ); }); +type FocusableAmountInputProps = Omit<AmountInputProps, 'onFocus'> & { + sign?: '+' | '-'; + zeroSign?: '+' | '-'; + focused?: boolean; + disabled?: boolean; + focusedStyle?: CSSProperties; + buttonProps?: ComponentPropsWithRef<typeof Button>; + onFocus?: () => void; +}; + export const FocusableAmountInput = memo(function FocusableAmountInput({ value, - sign, // + or - - zeroSign, // + or - + sign, + zeroSign, focused, + disabled, textStyle, style, focusedStyle, @@ -138,9 +191,18 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ onFocus, onBlur, ...props -}) { +}: FocusableAmountInputProps) { const [isNegative, setIsNegative] = useState(true); + const maybeApplyNegative = (amount: number, negative: boolean) => { + const absValue = Math.abs(amount); + return negative ? -absValue : absValue; + }; + + const onUpdateAmount = (amount: number, negative: boolean) => { + props.onUpdateAmount?.(maybeApplyNegative(amount, negative)); + }; + useEffect(() => { if (sign) { setIsNegative(sign === '-'); @@ -150,17 +212,12 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ }, [sign, value, zeroSign]); const toggleIsNegative = () => { - setIsNegative(!isNegative); - props.onUpdate?.(maybeApplyNegative(value, !isNegative)); - }; - - const maybeApplyNegative = (val, negative) => { - const absValue = Math.abs(val); - return negative ? -absValue : absValue; - }; + if (disabled) { + return; + } - const onUpdate = val => { - props.onUpdate?.(maybeApplyNegative(val, isNegative)); + onUpdateAmount(value, !isNegative); + setIsNegative(!isNegative); }; return ( @@ -170,8 +227,8 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ value={value} onFocus={onFocus} onBlur={onBlur} - onUpdate={onUpdate} - focused={focused} + onUpdateAmount={amount => onUpdateAmount(amount, isNegative)} + focused={focused && !disabled} style={{ width: 80, justifyContent: 'center', @@ -216,7 +273,6 @@ export const FocusableAmountInput = memo(function FocusableAmountInput({ borderBottomWidth: 1, borderColor: '#e0e0e0', justifyContent: 'center', - transform: [{ translateY: 0.5 }], ...style, }} > diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index b800f9595ccabeb9b1ef33122e1ea515d8bbab62..02864a9b5b870ea14654f47814eb7a945d32d2c8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -720,16 +720,15 @@ const TransactionEditInner = memo(function TransactionEditInner({ zeroSign="-" focused={totalAmountFocused} onFocus={onTotalAmountEdit} - onUpdate={onTotalAmountUpdate} + onUpdateAmount={onTotalAmountUpdate} focusedStyle={{ width: 'auto', padding: '5px', paddingLeft: '20px', paddingRight: '20px', - minWidth: 120, - transform: [{ translateY: -0.5 }], + minWidth: '100%', }} - textStyle={{ fontSize: 30, textAlign: 'center' }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} /> </View> diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx index 33c456f873954c372473c641514afd018f4d0fcb..f798c50327a1114621554fe92a0cec74ee66f74c 100644 --- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { type ComponentProps, useState } from 'react'; import { useLiveQuery } from 'loot-core/src/client/query-hooks'; import { q } from 'loot-core/src/shared/query'; @@ -146,7 +146,8 @@ export function AccountMenuModal({ flexWrap: 'wrap', justifyContent: 'space-between', alignContent: 'space-between', - margin: '10px 0', + paddingTop: 10, + paddingBottom: 10, }} > <Button style={buttonStyle} onClick={_onEditNotes}> @@ -176,6 +177,11 @@ function AdditionalAccountMenu({ height: styles.mobileMinHeight, }; + const getItemStyle: ComponentProps<typeof Menu>['getItemStyle'] = item => ({ + ...itemStyle, + ...(item.name === 'close' && { color: theme.errorTextMenu }), + }); + return ( <View> <Button @@ -199,7 +205,7 @@ function AdditionalAccountMenu({ }} > <Menu - getItemStyle={() => itemStyle} + getItemStyle={getItemStyle} items={[ account.closed ? { diff --git a/packages/desktop-client/src/components/modals/BudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx similarity index 87% rename from packages/desktop-client/src/components/modals/BudgetMenuModal.tsx rename to packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx index 59a7d867c7e3b2fd5a5ab384da04b939ed236813..05c7dd0352f5ac82346ff92fc4540f01a0ea2054 100644 --- a/packages/desktop-client/src/components/modals/BudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetMonthMenuModal.tsx @@ -6,16 +6,18 @@ import { Menu } from '../common/Menu'; import { Modal } from '../common/Modal'; import { type CommonModalProps } from '../Modals'; -type BudgetMenuModalProps = ComponentPropsWithoutRef<typeof BudgetMenu> & { +type BudgetMonthMenuModalProps = ComponentPropsWithoutRef< + typeof BudgetMonthMenu +> & { modalProps: CommonModalProps; }; -export function BudgetMenuModal({ +export function BudgetMonthMenuModal({ modalProps, month, onToggleHiddenCategories, onSwitchBudgetType, -}: BudgetMenuModalProps) { +}: BudgetMonthMenuModalProps) { const defaultMenuItemStyle: CSSProperties = { ...styles.mobileMenuItem, color: theme.menuItemText, @@ -36,9 +38,9 @@ export function BudgetMenuModal({ borderRadius: '6px', }} > - <BudgetMenu - getItemStyle={() => defaultMenuItemStyle} + <BudgetMonthMenu month={month} + getItemStyle={() => defaultMenuItemStyle} onToggleHiddenCategories={onToggleHiddenCategories} onSwitchBudgetType={onSwitchBudgetType} /> @@ -46,7 +48,7 @@ export function BudgetMenuModal({ ); } -type BudgetMenuProps = Omit< +type BudgetMonthMenuProps = Omit< ComponentPropsWithoutRef<typeof Menu>, 'onMenuSelect' | 'items' > & { @@ -55,13 +57,13 @@ type BudgetMenuProps = Omit< onSwitchBudgetType: () => void; }; -function BudgetMenu({ +function BudgetMonthMenu({ // onEditMode, month, onToggleHiddenCategories, onSwitchBudgetType, ...props -}: BudgetMenuProps) { +}: BudgetMonthMenuProps) { const isReportBudgetEnabled = useFeatureFlag('reportBudget'); const onMenuSelect = (name: string) => { @@ -76,7 +78,7 @@ function BudgetMenu({ onSwitchBudgetType?.(); break; default: - throw new Error(`Unrecognized menu option: ${name}`); + throw new Error(`Unrecognized menu item: ${name}`); } }; diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index 3f99d8e2bfd6c24e2f51a641e57f9392fd84310b..e9a2c1ffd1d6292eed1d4200df9abd1e421cd4c7 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -174,6 +174,11 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { height: styles.mobileMinHeight, }; + const getItemStyle = item => ({ + ...itemStyle, + ...(item.name === 'delete' && { color: theme.errorTextMenu }), + }); + return ( <View> {!group.is_income && ( @@ -202,7 +207,7 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { ...styles.mediumText, color: theme.formLabelText, }} - getItemStyle={() => itemStyle} + getItemStyle={getItemStyle} items={ [ { diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index 75ffcaa1ec42a1be87ef2a6f4f32e81a0a665059..686e99dd40f6ec3d1b426ebd65b48ec3a27b8b53 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -4,12 +4,12 @@ import React, { useState } from 'react'; import { useLiveQuery } from 'loot-core/src/client/query-hooks'; import { q } from 'loot-core/src/shared/query'; import { - type CategoryGroupEntity, type CategoryEntity, type NoteEntity, } from 'loot-core/src/types/models'; -import { useCategories } from '../../hooks/useCategories'; +import { useCategory } from '../../hooks/useCategory'; +import { useCategoryGroup } from '../../hooks/useCategoryGroup'; import { SvgDotsHorizontalTriple, SvgTrash } from '../../icons/v1'; import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; @@ -24,7 +24,6 @@ import { Tooltip } from '../tooltips'; type CategoryMenuModalProps = { modalProps: CommonModalProps; categoryId: string; - categoryGroup?: CategoryGroupEntity; onSave: (category: CategoryEntity) => void; onEditNotes: (id: string) => void; onDelete: (categoryId: string) => void; @@ -34,14 +33,13 @@ type CategoryMenuModalProps = { export function CategoryMenuModal({ modalProps, categoryId, - categoryGroup, onSave, onEditNotes, onDelete, onClose, }: CategoryMenuModalProps) { - const { list: categories } = useCategories(); - const category = categories.find(c => c.id === categoryId); + const category = useCategory(categoryId); + const categoryGroup = useCategoryGroup(category?.cat_group); const data = useLiveQuery<NoteEntity[]>( () => q('notes').filter({ id: category.id }).select('*'), [category.id], @@ -143,7 +141,8 @@ export function CategoryMenuModal({ flexWrap: 'wrap', justifyContent: 'space-between', alignContent: 'space-between', - margin: '10px 0', + paddingTop: 10, + paddingBottom: 10, }} > <Button style={buttonStyle} onClick={_onEditNotes}> @@ -168,6 +167,11 @@ function AdditionalCategoryMenu({ height: styles.mobileMinHeight, }; + const getItemStyle = item => ({ + ...itemStyle, + ...(item.name === 'delete' && { color: theme.errorTextMenu }), + }); + return ( <View> <Button @@ -191,7 +195,7 @@ function AdditionalCategoryMenu({ }} > <Menu - getItemStyle={() => itemStyle} + getItemStyle={getItemStyle} items={[ !categoryGroup?.hidden && { name: 'toggleVisibility', diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d994f67c07222b117b31c85970322e0e54e03c5 --- /dev/null +++ b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx @@ -0,0 +1,101 @@ +import React, { + useState, + type ComponentPropsWithoutRef, + useEffect, +} from 'react'; + +import { reportBudget } from 'loot-core/client/queries'; +import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; + +import { useCategory } from '../../hooks/useCategory'; +import { type CSSProperties, theme, styles } from '../../style'; +import { BudgetMenu } from '../budget/report/BudgetMenu'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; +import { type CommonModalProps } from '../Modals'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; + +type ReportBudgetMenuModalProps = ComponentPropsWithoutRef< + typeof BudgetMenu +> & { + modalProps: CommonModalProps; + categoryId: string; + onUpdateBudget: (amount: number) => void; +}; + +export function ReportBudgetMenuModal({ + modalProps, + categoryId, + onUpdateBudget, + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, +}: ReportBudgetMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + const budgeted = useSheetValue(reportBudget.catBudgeted(categoryId)); + const category = useCategory(categoryId); + const [amountFocused, setAmountFocused] = useState(false); + + const _onUpdateBudget = (amount: number) => { + onUpdateBudget?.(amountToInteger(amount)); + }; + + useEffect(() => { + setAmountFocused(true); + }, []); + + return ( + <Modal + title={`Budget: ${category?.name}`} + showHeader + focusAfterClose={false} + {...modalProps} + padding={0} + style={{ + flex: 1, + padding: '0 10px', + paddingBottom: 10, + borderRadius: '6px', + }} + > + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <FocusableAmountInput + value={integerToAmount(budgeted || 0)} + focused={amountFocused} + onFocus={() => setAmountFocused(true)} + onBlur={() => setAmountFocused(false)} + onEnter={() => modalProps.onClose()} + zeroSign="+" + focusedStyle={{ + width: 'auto', + padding: '5px', + paddingLeft: '20px', + paddingRight: '20px', + minWidth: '100%', + }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} + onUpdateAmount={_onUpdateBudget} + /> + </View> + <BudgetMenu + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthAverage={onCopyLastMonthAverage} + onSetMonthsAverage={onSetMonthsAverage} + onApplyBudgetTemplate={onApplyBudgetTemplate} + /> + </Modal> + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c06e91781473e89665a35d16ce1d2be71512d2c7 --- /dev/null +++ b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx @@ -0,0 +1,101 @@ +import React, { + useState, + type ComponentPropsWithoutRef, + useEffect, +} from 'react'; + +import { rolloverBudget } from 'loot-core/client/queries'; +import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; + +import { useCategory } from '../../hooks/useCategory'; +import { type CSSProperties, theme, styles } from '../../style'; +import { BudgetMenu } from '../budget/rollover/BudgetMenu'; +import { Modal } from '../common/Modal'; +import { View } from '../common/View'; +import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; +import { type CommonModalProps } from '../Modals'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; + +type RolloverBudgetMenuModalProps = ComponentPropsWithoutRef< + typeof BudgetMenu +> & { + modalProps: CommonModalProps; + categoryId: string; + onUpdateBudget: (amount: number) => void; +}; + +export function RolloverBudgetMenuModal({ + modalProps, + categoryId, + onUpdateBudget, + onCopyLastMonthAverage, + onSetMonthsAverage, + onApplyBudgetTemplate, +}: RolloverBudgetMenuModalProps) { + const defaultMenuItemStyle: CSSProperties = { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + }; + + const budgeted = useSheetValue(rolloverBudget.catBudgeted(categoryId)); + const category = useCategory(categoryId); + const [amountFocused, setAmountFocused] = useState(false); + + const _onUpdateBudget = (amount: number) => { + onUpdateBudget?.(amountToInteger(amount)); + }; + + useEffect(() => { + setAmountFocused(true); + }, []); + + return ( + <Modal + title={`Budget: ${category?.name}`} + showHeader + focusAfterClose={false} + {...modalProps} + padding={0} + style={{ + flex: 1, + padding: '0 10px', + paddingBottom: 10, + borderRadius: '6px', + }} + > + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <FocusableAmountInput + value={integerToAmount(budgeted || 0)} + focused={amountFocused} + onFocus={() => setAmountFocused(true)} + onBlur={() => setAmountFocused(false)} + onEnter={() => modalProps.onClose()} + zeroSign="+" + focusedStyle={{ + width: 'auto', + padding: '5px', + paddingLeft: '20px', + paddingRight: '20px', + minWidth: '100%', + }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} + onUpdateAmount={_onUpdateBudget} + /> + </View> + <BudgetMenu + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthAverage={onCopyLastMonthAverage} + onSetMonthsAverage={onSetMonthsAverage} + onApplyBudgetTemplate={onApplyBudgetTemplate} + /> + </Modal> + ); +} diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx index 2839c3ee666583842099e18797f7884c18033604..f0d55b7abcbedc96575ea6becf1e8ba6ccbf42f6 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx @@ -15,7 +15,7 @@ import { useSheetValue } from '../spreadsheet/useSheetValue'; type RolloverBudgetSummaryModalProps = { modalProps: CommonModalProps; - onBudgetAction: (idx: string | number, action: string, arg?: unknown) => void; + onBudgetAction: (month: string, action: string, arg?: unknown) => void; month: string; }; @@ -67,7 +67,7 @@ export function RolloverBudgetSummaryModal({ const onClick = () => { dispatch( - pushModal('rollover-to-budget-menu', { + pushModal('rollover-summary-to-budget-menu', { month, onTransfer: openTransferModal, onResetHoldBuffer, diff --git a/packages/desktop-client/src/hooks/useCategory.ts b/packages/desktop-client/src/hooks/useCategory.ts new file mode 100644 index 0000000000000000000000000000000000000000..028d504090615badd857993e347611cd031af3d8 --- /dev/null +++ b/packages/desktop-client/src/hooks/useCategory.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +import { useCategories } from './useCategories'; + +export function useCategory(id: string) { + const { list: categories } = useCategories(); + return useMemo(() => categories.find(c => c.id === id), [id, categories]); +} diff --git a/packages/desktop-client/src/hooks/useCategoryGroup.ts b/packages/desktop-client/src/hooks/useCategoryGroup.ts new file mode 100644 index 0000000000000000000000000000000000000000..53806fe3cff466b9ea56aa9034f76e17c3d46a02 --- /dev/null +++ b/packages/desktop-client/src/hooks/useCategoryGroup.ts @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; + +import { useCategories } from './useCategories'; + +export function useCategoryGroup(id: string) { + const { grouped: categoryGroups } = useCategories(); + return useMemo( + () => categoryGroups.find(g => g.id === id), + [id, categoryGroups], + ); +} diff --git a/packages/desktop-client/src/hooks/useMergedRefs.ts b/packages/desktop-client/src/hooks/useMergedRefs.ts index b30cbef5fa151f5c9179d5decc61e1bd7c645bd1..498b588416b06ec00827fe8d038654d793c21d3d 100644 --- a/packages/desktop-client/src/hooks/useMergedRefs.ts +++ b/packages/desktop-client/src/hooks/useMergedRefs.ts @@ -2,7 +2,13 @@ import { useCallback } from 'react'; import type { MutableRefObject, Ref, RefCallback } from 'react'; export function useMergedRefs<T>( - ...refs: (RefCallback<T> | MutableRefObject<T> | Ref<T> | null | undefined)[] + ...refs: ( + | RefCallback<T | null | undefined> + | MutableRefObject<T | null | undefined> + | Ref<T | null | undefined> + | null + | undefined + )[] ): Ref<T> { return useCallback( (value: T) => { diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index eef5b20f8074e2704ca1fd6753078122a8b4b966..96c3b65cc3ab966a43ee19b096b9e875069ff6f0 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -143,12 +143,28 @@ type FinanceModals = { }; 'category-menu': { categoryId: string; - categoryGroup?: CategoryGroupEntity; onSave: (category: CategoryEntity) => void; onEditNotes: (id: string) => void; onDelete: (categoryId: string) => void; + onBudgetAction: (month: string, action: string, args?: unknown) => void; onClose?: () => void; }; + 'rollover-budget-menu': { + categoryId: string; + month: string; + onUpdateBudget: (amount: number) => void; + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; + }; + 'report-budget-menu': { + categoryId: string; + month: string; + onUpdateBudget: (amount: number) => void; + onCopyLastMonthAverage: () => void; + onSetMonthsAverage: (numberOfMonths: number) => void; + onApplyBudgetTemplate: () => void; + }; 'category-group-menu': { groupId: string; onSave: (group: CategoryGroupEntity) => void; @@ -186,7 +202,7 @@ type FinanceModals = { onTransfer: () => void; onCover: () => void; }; - 'rollover-to-budget-menu': { + 'rollover-summary-to-budget-menu': { month: string; onTransfer: () => void; onHoldBuffer: () => void; @@ -216,7 +232,7 @@ type FinanceModals = { onPost: (transactionId: string) => void; onSkip: (transactionId: string) => void; }; - 'budget-menu': { + 'budget-month-menu': { month: string; onToggleHiddenCategories: () => void; onSwitchBudgetType: () => void; diff --git a/upcoming-release-notes/2501.md b/upcoming-release-notes/2501.md new file mode 100644 index 0000000000000000000000000000000000000000..d667d63f7412e4e4c94180404ffd2d3c4b140492 --- /dev/null +++ b/upcoming-release-notes/2501.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Mobile budget menu modal to set budget amounts.