diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png index 1685e7e51d7..3fe452daf6a 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png index 4f37ab8c64c..00d51d50a20 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png index 38e8e4570aa..9c938d6086c 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png index c207e1b5faf..8665904546a 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/common/AnchorLink.tsx b/packages/desktop-client/src/components/common/AnchorLink.tsx index c1ea4749385..8b4632ceb46 100644 --- a/packages/desktop-client/src/components/common/AnchorLink.tsx +++ b/packages/desktop-client/src/components/common/AnchorLink.tsx @@ -10,6 +10,7 @@ type AnchorLinkProps = { style?: CSSProperties; activeStyle?: CSSProperties; children?: ReactNode; + report?: []; }; export default function AnchorLink({ @@ -17,12 +18,14 @@ export default function AnchorLink({ style, activeStyle, children, + report, }: AnchorLinkProps) { let match = useMatch({ path: to }); return ( ; + items: Array; onMenuSelect: (itemName: MenuItem['name']) => void; }; @@ -56,7 +56,7 @@ export default function Menu({ let onKeyDown = e => { let filteredItems = items.filter( - item => item && item !== Menu.line && item.type !== Menu.label, + item => item && item.type !== Menu.line && item.type !== Menu.label, ); let currentIndex = filteredItems.indexOf(items[hoveredIndex]); @@ -84,7 +84,7 @@ export default function Menu({ case 'Enter': e.preventDefault(); const item = items[hoveredIndex]; - if (hoveredIndex !== null && item !== Menu.line) { + if (hoveredIndex !== null && item.type !== Menu.line) { onMenuSelect?.(item.name); } break; @@ -107,7 +107,7 @@ export default function Menu({ > {header} {items.map((item, idx) => { - if (item === Menu.line) { + if (item.type === Menu.line) { return ( @@ -142,7 +142,7 @@ export default function Menu({ padding: '9px 10px', marginTop: idx === 0 || - lastItem === Menu.line || + lastItem.type === Menu.line || lastItem.type === Menu.label ? 0 : -3, @@ -182,6 +182,5 @@ export default function Menu({ ); } -const MenuLine: unique symbol = Symbol('menu-line'); -Menu.line = MenuLine; +Menu.line = Symbol('menu-line'); Menu.label = Symbol('menu-label'); diff --git a/packages/desktop-client/src/components/common/MenuButton.tsx b/packages/desktop-client/src/components/common/MenuButton.tsx index 66d4b58ec23..c3496e7ad8a 100644 --- a/packages/desktop-client/src/components/common/MenuButton.tsx +++ b/packages/desktop-client/src/components/common/MenuButton.tsx @@ -4,9 +4,9 @@ import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple'; import Button from './Button'; -export default function MenuButton({ onClick }) { +export default function MenuButton({ onClick, style }) { return ( - + + + )} + {err && ( + + {err} + + )} + + ); +} + +export function SaveReportMenuButton({ + reportId, + start, + end, + mode, + groupBy, + balanceType, + empty, + hidden, + uncat, + graphType, + viewLabels, + viewLegend, + viewSummary, + filters, + conditionsOp, + selectedCategories, + onReportChange, + onResetReports, + data, +}) { + let [nameOpen, setNameOpen] = useState(false); let [menuOpen, setMenuOpen] = useState(false); + let [menuItem, setMenuItem] = useState(null); + let [err, setErr] = useState(null); + let [name, setName] = useState(reportId.name); + let inputRef = createRef(); + let id = reportId.id; + let savedReport; + + const onAddUpdate = async () => { + let savedReport; + let res; + //save existing states + savedReport = { + ...reportId, + mode: mode, + group_by: groupBy, + balance_type: balanceType, + show_empty: Convert(empty), + show_offbudgethidden: Convert(hidden), + show_uncategorized: Convert(uncat), + graph_type: graphType, + selected_categories: selectedCategories, + conditions: filters, + conditions_op: conditionsOp, + name: name, + start_date: start, + end_date: end, + data: data, + status: 'saved', + }; + if (menuItem === 'save-report') { + //create new flow + const { status, ...sendSaved } = savedReport; + res = await sendCatch('report-create', { + state: sendSaved, + }); + savedReport = { + ...savedReport, + id: res.data, + }; + } else { + //rename or update flow + if (menuItem === 'rename-report') { + //rename + savedReport = { + ...reportId, + name: name, + }; + } + //send update and rename to DB + const { status, ...sendSaved } = savedReport; + res = await sendCatch('report-update', { + state: sendSaved, + }); + } + if (res.error) { + setErr(res.error.message); + setNameOpen(true); + } else { + setNameOpen(false); + onReportChange(savedReport, 'add-update'); + } + }; + + const onNameChange = cond => { + setName(cond); + }; + + const onMenuSelect = async item => { + setMenuItem(item); + switch (item) { + case 'rename-report': + setErr(null); + setMenuOpen(false); + setNameOpen(true); + break; + case 'delete-report': + setMenuOpen(false); + await send('report-delete', id); + onResetReports(); + break; + case 'update-report': + setErr(null); + setMenuOpen(false); + onAddUpdate(); + break; + case 'save-report': + setErr(null); + setMenuOpen(false); + setNameOpen(true); + break; + case 'reload-report': + setMenuOpen(false); + savedReport = { + status: 'saved', + }; + onReportChange(savedReport, 'reload'); + break; + case 'reset-report': + setMenuOpen(false); + onResetReports(); + break; + default: + } + }; return ( - {'Unsaved Report'}  + {!reportId.id ? 'Unsaved filter' : reportId.name}  - {menuOpen && } + {menuOpen && ( + setMenuOpen(false)} + reportId={reportId} + onMenuSelect={onMenuSelect} + /> + )} + {nameOpen && ( + setNameOpen(false)} + menuItem={menuItem} + onNameChange={onNameChange} + inputRef={inputRef} + onAddUpdate={onAddUpdate} + err={err} + /> + )} ); } diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx index df19427fb66..ff278a381ac 100644 --- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx @@ -164,9 +164,9 @@ function StackedBarGraph({ formatter={numberFormatterTooltip} isAnimationActive={false} /> - - - + {!compact && } + {!compact && } + {!compact && } {data.groupBy .slice(0) .reverse() diff --git a/packages/desktop-client/src/components/reports/graphs/TableGraph.tsx b/packages/desktop-client/src/components/reports/graphs/TableGraph.tsx new file mode 100644 index 00000000000..6f4ad6fbed2 --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/TableGraph.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import * as monthUtils from 'loot-core/src/shared/months'; + +import { styles } from '../../../style'; +import { type CSSProperties } from '../../../style'; +import View from '../../common/View'; +import { ReportOptions } from '../ReportOptions'; +import ReportTable from '../ReportTable'; +import ReportTableHeader from '../ReportTableHeader'; +import ReportTableList from '../ReportTableList'; +import ReportTableTotals from '../ReportTableTotals'; + +type TableGraphProps = { + mode; + start; + end; + scrollWidth?; + saveScrollWidth?; + balanceType; + style?: CSSProperties; + data; + groupBy; + empty; + compact: boolean; +}; + +function TableGraph({ + mode, + start, + end, + scrollWidth, + saveScrollWidth, + balanceType, + style, + data, + groupBy, + empty, + compact, +}: TableGraphProps) { + const months = monthUtils.rangeInclusive(start, end); + + return ( + + + + + + + + ); +} + +export default TableGraph; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.js b/packages/desktop-client/src/components/reports/reports/CustomReport.js index 8aeb682c95d..7f58e1b02ec 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.js +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import * as d from 'date-fns'; @@ -17,8 +18,9 @@ import View from '../../common/View'; import { AppliedFilters } from '../../filters/FiltersMenu'; import PrivacyFilter from '../../PrivacyFilter'; import { ChooseGraph } from '../ChooseGraph'; +import Convert from '../Convert'; import Header from '../Header'; -import { ReportOptions } from '../ReportOptions'; +import { ReportOptions, defaultState } from '../ReportOptions'; import { ReportSidebar } from '../ReportSidebar'; import { ReportLegend, ReportSummary } from '../ReportSummary'; import { ReportTopbar } from '../ReportTopbar'; @@ -45,26 +47,51 @@ export default function CustomReport() { onCondOpChange, } = useFilters(); - const [selectedCategories, setSelectedCategories] = useState(null); + const location = useLocation(); + const converted = location.state.report && { + ...location.state.report, + empty: Convert(location.state.report.empty), + hidden: Convert(location.state.report.hidden), + uncat: Convert(location.state.report.uncat), + viewLabels: Convert(location.state.report.viewLabels), + viewLegend: Convert(location.state.report.viewLegend), + viewSummary: Convert(location.state.report.viewSummary), + }; + const loadReport = location.state.report ? converted : defaultState(); + const stateDefault = defaultState(); + + useEffect(() => { + if (location.state.report) { + onCondOpChange(location.state.report.conditionsOp); + location.state.report.conditions.forEach(filter => { + onApplyFilter(filter); + }); + } + }, []); + + const [selectedCategories, setSelectedCategories] = useState( + loadReport.selectedCategories, + ); const [allMonths, setAllMonths] = useState(null); const [typeDisabled, setTypeDisabled] = useState(['Net']); - const [start, setStart] = useState( - monthUtils.subMonths(monthUtils.currentMonth(), 5), - ); - const [end, setEnd] = useState(monthUtils.currentMonth()); + const [start, setStart] = useState(loadReport.start); + const [end, setEnd] = useState(loadReport.end); - const [mode, setMode] = useState('total'); - const [groupBy, setGroupBy] = useState('Category'); - const [balanceType, setBalanceType] = useState('Expense'); - const [empty, setEmpty] = useState(false); - const [hidden, setHidden] = useState(false); - const [uncat, setUncat] = useState(false); + const [mode, setMode] = useState(loadReport.mode); + const [reportId, setReportId] = useState( + location.state.report ? location.state.report : [], + ); + const [groupBy, setGroupBy] = useState(loadReport.groupBy); + const [balanceType, setBalanceType] = useState(loadReport.balanceType); + const [empty, setEmpty] = useState(loadReport.empty); + const [hidden, setHidden] = useState(loadReport.hidden); + const [uncat, setUncat] = useState(loadReport.uncat); const [dateRange, setDateRange] = useState('6 months'); - const [graphType, setGraphType] = useState('BarGraph'); - const [viewLegend, setViewLegend] = useState(false); - const [viewSummary, setViewSummary] = useState(false); - const [viewLabels, setViewLabels] = useState(false); + const [graphType, setGraphType] = useState(loadReport.graphType); + const [viewLegend, setViewLegend] = useState(loadReport.viewLegend); + const [viewSummary, setViewSummary] = useState(loadReport.viewSummary); + const [viewLabels, setViewLabels] = useState(loadReport.viewLabels); //const [legend, setLegend] = useState([]); let legend = []; const dateRangeLine = ReportOptions.dateRange.length - 1; @@ -137,16 +164,44 @@ export default function CustomReport() { }, []); let [scrollWidth, setScrollWidth] = useState(0); + const splitData = ['Month', 'Year'].includes(groupBy) ? 'monthData' : 'data'; if (!allMonths || !data) { return null; } - const onChangeDates = (start, end) => { setStart(start); setEnd(end); }; + const onResetReports = () => { + setMode(stateDefault.mode); + setGroupBy(stateDefault.groupBy); + setBalanceType(stateDefault.balanceType); + setEmpty(stateDefault.empty); + setHidden(stateDefault.hidden); + setUncat(stateDefault.uncat); + setGraphType(stateDefault.graphType); + setViewLabels(stateDefault.viewLabels); + setViewLegend(stateDefault.viewLegend); + setViewSummary(stateDefault.viewSummary); + onApplyFilter(null); + onCondOpChange('and'); + setReportId([]); + setStart(stateDefault.start); + setEnd(stateDefault.end); + }; + + const onReportChange = (savedReport, item) => { + if (item === 'reload') { + //need to pull reportsList with state change PR + } else { + if (savedReport.status) { + } + } + setReportId({ ...reportId, ...savedReport }); + }; + return (