diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx index ad8f0c3a857ccb3bbbacb3d789185737886cf95a..dce8850f8fa77517e06f826de4a23932c546515c 100644 --- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx +++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx @@ -176,4 +176,5 @@ export function ChooseGraph({ </View> ); } + return null; } diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.jsx index 539aeda890fa57448a3ef9ed7516e4d0f092cd48..ce0c49b44990a1c6ea1309315e2a32c2d7182155 100644 --- a/packages/desktop-client/src/components/reports/Overview.jsx +++ b/packages/desktop-client/src/components/reports/Overview.jsx @@ -4,11 +4,12 @@ import { useReports } from 'loot-core/src/client/data-hooks/reports'; import { useAccounts } from '../../hooks/useAccounts'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; -import { styles } from '../../style'; +import { theme, styles } from '../../style'; import { View } from '../common/View'; import { CashFlowCard } from './reports/CashFlowCard'; import { CustomReportCard } from './reports/CustomReportCard'; +import { CustomReportListCards } from './reports/CustomReportListCards'; import { NetWorthCard } from './reports/NetWorthCard'; import { SankeyCard } from './reports/SankeyCard'; @@ -18,6 +19,8 @@ export function Overview() { const customReportsFeatureFlag = useFeatureFlag('customReports'); + const featureCount = + 3 - (sankeyFeatureFlag ? 1 : 0) - (customReportsFeatureFlag ? 1 : 0); const accounts = useAccounts(); return ( <View @@ -42,13 +45,25 @@ export function Overview() { }} > {sankeyFeatureFlag && <SankeyCard />} - {customReportsFeatureFlag ? ( - <CustomReportCard reports={customReports} /> - ) : ( - <div style={{ flex: 1 }} /> - )} - {!sankeyFeatureFlag && <div style={{ flex: 1 }} />} + {customReportsFeatureFlag && <CustomReportCard />} + {featureCount !== 3 && + [...Array(featureCount)].map((e, i) => ( + <View key={i} style={{ padding: 15, flex: 1 }} /> + ))} </View> + {customReportsFeatureFlag && ( + <> + <View + style={{ + height: 1, + backgroundColor: theme.pillBorderDark, + marginTop: 10, + flexShrink: 0, + }} + /> + <CustomReportListCards reports={customReports} /> + </> + )} </View> ); } diff --git a/packages/desktop-client/src/components/reports/SaveReport.tsx b/packages/desktop-client/src/components/reports/SaveReport.tsx index 37ca8e4e9c5fe7fead14cda2be83fd4c18a2d440..4200d781fcf47db63e90b7b57eaddd62045a22c2 100644 --- a/packages/desktop-client/src/components/reports/SaveReport.tsx +++ b/packages/desktop-client/src/components/reports/SaveReport.tsx @@ -1,8 +1,6 @@ import React, { createRef, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -//import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; +import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; import { type CustomReportEntity } from 'loot-core/src/types/models'; import { SvgExpandArrow } from '../../icons/v0'; @@ -38,59 +36,54 @@ export function SaveReport({ const [menuOpen, setMenuOpen] = useState(false); const [menuItem, setMenuItem] = useState(''); const [err, setErr] = useState(''); - const [res, setRes] = useState(''); const [name, setName] = useState(report.name ?? ''); const inputRef = createRef<HTMLInputElement>(); const onAddUpdate = async (menuChoice: string) => { - let savedReport: CustomReportEntity; - //save existing states - savedReport = { - ...report, - ...customReportItems, - }; - if (menuChoice === 'save-report') { - setRes(''); - //create new flow - /* - res = await sendCatch('report/create', { - ...savedReport, - }); - */ - savedReport = { - ...savedReport, - id: uuidv4(), + const newSavedReport = { + ...report, + ...customReportItems, name, }; - } - if (menuChoice === 'rename-report') { - //rename - savedReport = { - ...savedReport, - name, - }; - } + const response = await sendCatch('report/create', newSavedReport); + + if (response.error) { + setErr(response.error.message); + setNameMenuOpen(true); + return; + } - if (menuChoice === 'update-report') { - //send update and rename to DB - /* - res = await sendCatch('report/update', { - ...savedReport, - }); - */ - } - if (res !== '') { - setErr(res); - setNameMenuOpen(true); - } else { setNameMenuOpen(false); onReportChange({ - savedReport, - type: menuChoice === 'rename-report' ? 'rename' : 'add-update', + savedReport: { + ...newSavedReport, + id: response.data, + }, + type: 'add-update', }); + return; } + + const updatedReport = { + ...report, + ...(menuChoice === 'rename-report' ? { name } : customReportItems), + }; + + const response = await sendCatch('report/update', updatedReport); + + if (response.error) { + setErr(response.error.message); + setNameMenuOpen(true); + return; + } + + setNameMenuOpen(false); + onReportChange({ + savedReport: updatedReport, + type: menuChoice === 'rename-report' ? 'rename' : 'add-update', + }); }; const onMenuSelect = async (item: string) => { @@ -103,7 +96,8 @@ export function SaveReport({ break; case 'delete-report': setMenuOpen(false); - //await send('report/delete', id); + setName(''); + await send('report/delete', report.id); onResetReports(); break; case 'update-report': @@ -122,6 +116,7 @@ export function SaveReport({ break; case 'reset-report': setMenuOpen(false); + setName(''); onResetReports(); break; default: diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx index 54b1420301fe7a17b177765ac5b52b563f6c5f6e..276c15a401b39d1d916116d3cf37405be030a00a 100644 --- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx @@ -23,7 +23,6 @@ import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; -import { PrivacyFilter } from '../../PrivacyFilter'; import { Container } from '../Container'; import { numberFormatterTooltip } from '../numberFormatter'; @@ -68,30 +67,28 @@ const CustomTooltip = ({ <strong>{payload[0].payload.date}</strong> </div> <div style={{ lineHeight: 1.5 }}> - <PrivacyFilter> - {['totalAssets', 'totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Assets:" - right={amountToCurrency(payload[0].payload.totalAssets)} - /> - )} - {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Debt:" - right={amountToCurrency(payload[0].payload.totalDebts)} - /> - )} - {['totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Net:" - right={ - <strong> - {amountToCurrency(payload[0].payload.totalTotals)} - </strong> - } - /> - )} - </PrivacyFilter> + {['totalAssets', 'totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Assets:" + right={amountToCurrency(payload[0].payload.totalAssets)} + /> + )} + {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Debt:" + right={amountToCurrency(payload[0].payload.totalDebts)} + /> + )} + {['totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Net:" + right={ + <strong> + {amountToCurrency(payload[0].payload.totalTotals)} + </strong> + } + /> + )} </div> </div> </div> diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx index 6263c66513c0a9c5b7c1c03e1d221fdbf06ab240..8489e92dddf27e500390636f841bd744f2061993 100644 --- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx @@ -25,7 +25,6 @@ import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; -import { PrivacyFilter } from '../../PrivacyFilter'; import { Container } from '../Container'; import { getCustomTick } from '../getCustomTick'; import { numberFormatterTooltip } from '../numberFormatter'; @@ -83,30 +82,28 @@ const CustomTooltip = ({ <strong>{payload[0].payload[yAxis]}</strong> </div> <div style={{ lineHeight: 1.5 }}> - <PrivacyFilter> - {['totalAssets', 'totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Assets:" - right={amountToCurrency(payload[0].payload.totalAssets)} - /> - )} - {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Debt:" - right={amountToCurrency(payload[0].payload.totalDebts)} - /> - )} - {['totalTotals'].includes(balanceTypeOp) && ( - <AlignedText - left="Net:" - right={ - <strong> - {amountToCurrency(payload[0].payload.totalTotals)} - </strong> - } - /> - )} - </PrivacyFilter> + {['totalAssets', 'totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Assets:" + right={amountToCurrency(payload[0].payload.totalAssets)} + /> + )} + {['totalDebts', 'totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Debt:" + right={amountToCurrency(payload[0].payload.totalDebts)} + /> + )} + {['totalTotals'].includes(balanceTypeOp) && ( + <AlignedText + left="Net:" + right={ + <strong> + {amountToCurrency(payload[0].payload.totalTotals)} + </strong> + } + /> + )} </div> </div> </div> diff --git a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx index e60bfa1e07e39f34f48893f9cf4e0423350c4259..8714290b0b8c16a15dfcd4265a5d2751a2f43199 100644 --- a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx @@ -32,15 +32,17 @@ const ActiveShapeMobile = props => { return ( <g> - <text x={cx} y={cy + 65} dy={-8} textAnchor="middle" fill={fill}> + <text x={cx} y={cy + 70} dy={-8} textAnchor="middle" fill={fill}> {`${yAxis}`} </text> - <text x={cx - 30} y={cy + 40} dy={0} textAnchor="end" fill={fill}> - {`${amountToCurrency(value)}`} - </text> - <text x={cx + 30} y={cy + 40} dy={0} textAnchor="start" fill="#999"> - {`${(percent * 100).toFixed(2)}%`} - </text> + <PrivacyFilter> + <text x={cx - 40} y={cy + 40} dy={0} textAnchor="end" fill={fill}> + {`${amountToCurrency(value)}`} + </text> + <text x={cx + 45} y={cy + 40} dy={0} textAnchor="start" fill="#999"> + {`${(percent * 100).toFixed(2)}%`} + </text> + </PrivacyFilter> <Sector cx={cx} cy={cy} diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx index 777f4be4c7cfb762ee1c553e7a92f502b194f0c0..3f1d5849b497908512479978eca8888849604b47 100644 --- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx @@ -23,7 +23,6 @@ import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; -import { PrivacyFilter } from '../../PrivacyFilter'; import { Container } from '../Container'; import { getCustomTick } from '../getCustomTick'; import { numberFormatterTooltip } from '../numberFormatter'; @@ -41,12 +40,18 @@ type PayloadItem = { }; type CustomTooltipProps = { + compact: boolean; active?: boolean; payload?: PayloadItem[]; label?: string; }; -const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { +const CustomTooltip = ({ + compact, + active, + payload, + label, +}: CustomTooltipProps) => { if (active && payload && payload.length) { let sumTotals = 0; return ( @@ -65,32 +70,32 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { <div style={{ marginBottom: 10 }}> <strong>{label}</strong> </div> - <div style={{ lineHeight: 1.5 }}> - <PrivacyFilter> - {payload - .slice(0) - .reverse() - .map(pay => { - sumTotals += pay.value; - return ( - pay.value !== 0 && ( - <AlignedText - key={pay.name} - left={pay.name} - right={amountToCurrency(pay.value)} - style={{ color: pay.color }} - /> - ) - ); - })} - <AlignedText - left="Total" - right={amountToCurrency(sumTotals)} - style={{ - fontWeight: 600, - }} - /> - </PrivacyFilter> + <div style={{ lineHeight: 1.4 }}> + {payload + .slice(0) + .reverse() + .map((pay, i) => { + sumTotals += pay.value; + return ( + pay.value !== 0 && + (compact ? i < 5 : true) && ( + <AlignedText + key={pay.name} + left={pay.name} + right={amountToCurrency(pay.value)} + style={{ color: pay.color }} + /> + ) + ); + })} + {payload.length > 5 && compact && '...'} + <AlignedText + left="Total" + right={amountToCurrency(sumTotals)} + style={{ + fontWeight: 600, + }} + /> </div> </div> </div> @@ -161,7 +166,7 @@ export function StackedBarGraph({ margin={{ top: 0, right: 0, left: leftMargin, bottom: 0 }} > <Tooltip - content={<CustomTooltip />} + content={<CustomTooltip compact={compact} />} formatter={numberFormatterTooltip} isAnimationActive={false} cursor={{ fill: 'transparent' }} diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index 139f4716f95070e22427b4a0378337327bbd62de..32b8b7fc278d7a013a77e4156530812885641ae8 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -96,6 +96,7 @@ export function CustomReport() { useEffect(() => { async function run() { + report.conditions.forEach(condition => onApplyFilter(condition)); const trans = await send('get-earliest-transaction'); const currentMonth = monthUtils.currentMonth(); let earliestMonth = trans diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx new file mode 100644 index 0000000000000000000000000000000000000000..40d7742a73cef56e7a6db0188cd33402b5e76d64 --- /dev/null +++ b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx @@ -0,0 +1,209 @@ +import React, { useMemo, useState } from 'react'; + +import { send } from 'loot-core/src/platform/client/fetch'; +import { type CustomReportEntity } from 'loot-core/types/models/reports'; + +import { styles } from '../../../style'; +import { Block } from '../../common/Block'; +import { Menu } from '../../common/Menu'; +import { MenuButton } from '../../common/MenuButton'; +import { MenuTooltip } from '../../common/MenuTooltip'; +import { View } from '../../common/View'; +import { ChooseGraph } from '../ChooseGraph'; +import { DateRange } from '../DateRange'; +import { LoadingIndicator } from '../LoadingIndicator'; +import { ReportCard } from '../ReportCard'; + +type CardMenuProps = { + onClose: () => void; + onMenuSelect: (item: string, reportId: string) => void; + reportId: string; +}; + +function CardMenu({ onClose, onMenuSelect, reportId }: CardMenuProps) { + return ( + <MenuTooltip onClose={onClose} width={120}> + <Menu + onMenuSelect={item => { + onMenuSelect(item, reportId); + }} + items={[ + { + name: 'rename', + text: 'Rename report', + disabled: true, + }, + { + name: 'delete', + text: 'Delete report', + }, + ]} + /> + </MenuTooltip> + ); +} + +function index(data: CustomReportEntity[]): { [key: string]: boolean }[] { + return data.reduce((carry, report) => { + const reportId: string = report.id === undefined ? '' : report.id; + + return { + ...carry, + [reportId]: false, + }; + }, []); +} + +export function CustomReportListCards({ + reports, +}: { + reports: CustomReportEntity[]; +}) { + const result: { [key: string]: boolean }[] = index(reports); + const [reportMenu, setReportMenu] = useState(result); + + const [isCardHovered, setIsCardHovered] = useState(''); + + const onMenuSelect = async (item: string, reportId: string) => { + if (item === 'delete') { + onMenuOpen(reportId, false); + await send('report/delete', reportId); + } + }; + + const onMenuOpen = (item: string, state: boolean) => { + setReportMenu({ ...reportMenu, [item]: state }); + }; + + const chunkSize = 3; + + const groups = useMemo(() => { + return reports + .map((report: CustomReportEntity, i: number) => { + return i % chunkSize === 0 ? reports.slice(i, i + chunkSize) : null; + }) + .filter(e => { + return e; + }); + }, [reports]); + + const remainder = 3 - (reports.length % 3); + + if (reports.length === 0) return null; + return ( + <View> + {groups.map((group, i) => ( + <View + key={i} + style={{ + flex: '0 0 auto', + flexDirection: 'row', + }} + > + {group && + group.map((report, id) => ( + <View key={id} style={{ position: 'relative', flex: '1' }}> + <View style={{ width: '100%', height: '100%' }}> + <ReportCard to="/reports/custom" report={report}> + <View + style={{ flex: 1, padding: 10 }} + onMouseEnter={() => + setIsCardHovered( + report.id === undefined ? '' : report.id, + ) + } + onMouseLeave={() => { + setIsCardHovered(''); + onMenuOpen( + report.id === undefined ? '' : report.id, + false, + ); + }} + > + <View + style={{ + flexDirection: 'row', + flexShrink: 0, + paddingBottom: 5, + }} + > + <View style={{ flex: 1 }}> + <Block + style={{ + ...styles.mediumText, + fontWeight: 500, + marginBottom: 5, + }} + role="heading" + > + {report.name} + </Block> + <DateRange + start={report.startDate} + end={report.endDate} + /> + </View> + </View> + + {report.data ? ( + <ChooseGraph + startDate={report.startDate} + endDate={report.endDate} + data={report.data} + mode={report.mode} + graphType={report.graphType} + balanceType={report.balanceType} + groupBy={report.groupBy} + compact={true} + style={{ height: 'auto', flex: 1 }} + /> + ) : ( + <LoadingIndicator /> + )} + </View> + </ReportCard> + </View> + <View + style={{ + textAlign: 'right', + position: 'absolute', + right: 25, + top: 25, + }} + > + <MenuButton + onClick={() => + onMenuOpen(report.id === undefined ? '' : report.id, true) + } + style={{ + color: + isCardHovered === report.id ? 'inherit' : 'transparent', + }} + /> + {report.id === undefined + ? null + : reportMenu[report.id as keyof typeof reportMenu] && ( + <CardMenu + onMenuSelect={onMenuSelect} + onClose={() => + onMenuOpen( + report.id === undefined ? '' : report.id, + false, + ) + } + reportId={report.id} + /> + )} + </View> + </View> + ))} + {remainder !== 3 && + i + 1 === groups.length && + [...Array(remainder)].map((e, i) => ( + <View key={i} style={{ flex: 1 }} /> + ))} + </View> + ))} + </View> + ); +} diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts index f3e76a95824762e88cd6fec18de116e2b439ebf9..538f5c599d1e6891b535d7480e224c2f9190beeb 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts @@ -27,11 +27,10 @@ export function filterHiddenItems( f.transferAccount !== null : showUncategorized ? //false, true - f.accountOffBudget === false && f.transferAccount === null - : //false false - f.category !== null && f.accountOffBudget === false && - f.transferAccount === null, + (f.transferAccount === null || f.category !== null) + : //false false + f.category !== null && f.accountOffBudget === false, ); return showHide.filter(query => { diff --git a/packages/loot-core/migrations/1707267033000_reports.sql b/packages/loot-core/migrations/1707267033000_reports.sql new file mode 100644 index 0000000000000000000000000000000000000000..2f6705895f90b5932fc7195816722d2ba1b98ff1 --- /dev/null +++ b/packages/loot-core/migrations/1707267033000_reports.sql @@ -0,0 +1,28 @@ +BEGIN TRANSACTION; + +CREATE TABLE custom_reports + ( + id TEXT PRIMARY KEY, + name TEXT, + start_date TEXT, + end_date TEXT, + date_static INTEGER DEFAULT 0, + date_range TEXT, + mode TEXT DEFAULT 'total', + group_by TEXT DEFAULT 'Category', + balance_type TEXT DEFAULT 'Expense', + show_empty INTEGER DEFAULT 0, + show_offbudget INTEGER DEFAULT 0, + show_hidden INTEGER DEFAULT 0, + show_uncategorized INTEGER DEFAULT 0, + selected_categories TEXT, + graph_type TEXT DEFAULT 'BarGraph', + conditions TEXT, + conditions_op TEXT DEFAULT 'and', + metadata TEXT, + interval TEXT DEFAULT 'Monthly', + color_scheme TEXT, + tombstone INTEGER DEFAULT 0 + ); + +COMMIT; \ No newline at end of file diff --git a/packages/loot-core/src/client/data-hooks/reports.ts b/packages/loot-core/src/client/data-hooks/reports.ts index 95b023ff34ee1520e5e6f76364b9e4ce63b3c39b..78246e4779b810c324350e9e9c3a788a5fa6b689 100644 --- a/packages/loot-core/src/client/data-hooks/reports.ts +++ b/packages/loot-core/src/client/data-hooks/reports.ts @@ -34,17 +34,12 @@ function toJS(rows: CustomReportData[]) { return reports; } -/* -leaving as a placeholder for saved reports implementation return an -empty array because "reports" db table doesn't exist yet -*/ export function useReports(): CustomReportEntity[] { const reports: CustomReportEntity[] = toJS( - //useLiveQuery(() => q('reports').select('*'), []) || [], - useLiveQuery(() => q('transaction_filters').select('*'), []) || [], + useLiveQuery(() => q('custom_reports').select('*'), []) || [], ); - /** Sort reports by alphabetical order */ + // Sort reports by alphabetical order function sort(reports: CustomReportEntity[]) { return reports.sort((a, b) => a.name && b.name @@ -55,11 +50,5 @@ export function useReports(): CustomReportEntity[] { ); } - //return useMemo(() => sort(reports), [reports]); - - //everything below this line will be removed once db table is created - const order: CustomReportEntity[] = useMemo(() => sort(reports), [reports]); - const flag = true; - const emptyReports: CustomReportEntity[] = flag ? [] : order; - return emptyReports; + return useMemo(() => sort(reports), [reports]); } diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts index c129710188dc4f22ff9f5aa7b2c8b959cc01bc78..cb466d6f8e1295fc887efc6e4ef2292e3753285a 100644 --- a/packages/loot-core/src/server/aql/schema/index.ts +++ b/packages/loot-core/src/server/aql/schema/index.ts @@ -131,18 +131,21 @@ export const schema = { name: f('string'), start_date: f('string', { default: '2023-06' }), end_date: f('string', { default: '2023-09' }), + date_static: f('integer', { default: 0 }), + date_range: f('string'), mode: f('string', { default: 'total' }), group_by: f('string', { default: 'Category' }), balance_type: f('string', { default: 'Expense' }), - interval: f('string', { default: 'Monthly' }), show_empty: f('integer', { default: 0 }), - show_offbudgethidden: f('integer', { default: 0 }), + show_offbudget: f('integer', { default: 0 }), + show_hidden: f('integer', { default: 0 }), show_uncategorized: f('integer', { default: 0 }), selected_categories: f('json'), graph_type: f('string', { default: 'BarGraph' }), conditions: f('json'), conditions_op: f('string'), metadata: f('json'), + interval: f('string'), color_scheme: f('json'), tombstone: f('boolean'), }, diff --git a/upcoming-release-notes/2335.md b/upcoming-release-notes/2335.md new file mode 100644 index 0000000000000000000000000000000000000000..cdc4f48b3c5dd287dcc907047fe75ecf7bee84ec --- /dev/null +++ b/upcoming-release-notes/2335.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Allows for saving custom reports. Also changes reports dashboard to display saved reports.