Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar Report #3828

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3bf2ac9
Calendar Report
lelemm Oct 29, 2024
a2cebb2
Code review suggestions from Code Rabbit AI
lelemm Nov 12, 2024
c887f22
changes suggested in discord
lelemm Nov 13, 2024
b58a546
Apply suggestions from code review
lelemm Nov 13, 2024
db47cb5
code generated by code rabbit was wrong
lelemm Nov 13, 2024
1062dc7
fixes and code review suggestions
lelemm Nov 13, 2024
a395a80
reverting change suggested by code rabbit because it fells slow on th…
lelemm Nov 13, 2024
0c928cf
Added privacyfilter
lelemm Nov 13, 2024
13afc8c
Update packages/desktop-client/src/components/reports/reports/Calenda…
lelemm Nov 13, 2024
92315bb
Merge branch 'master' into calendar_report
lelemm Nov 13, 2024
050391c
Merge branch 'master' into calendar_report
lelemm Nov 13, 2024
66e228e
removed suggestion from code rabbit
lelemm Nov 14, 2024
30dcceb
Merge branch 'calendar_report' of https://github.com/lelemm/actual in…
lelemm Nov 14, 2024
2663461
Merge branch 'master' into calendar_report
lelemm Nov 14, 2024
a8a0bdb
changed md type to Features
lelemm Nov 14, 2024
de725e3
fixed negative collapsable transactions
lelemm Nov 18, 2024
7cdbbe8
changes from review
lelemm Nov 18, 2024
e798eea
limiting cards to show maximum of 4 calendars before adding a scrollbar
lelemm Nov 18, 2024
9eb38ac
linter, code cleanup
lelemm Nov 18, 2024
79da7a9
Merge branch 'master' into calendar_report
lelemm Nov 18, 2024
e235664
merge fix
lelemm Nov 18, 2024
c286f7f
Apply suggestions from code review
lelemm Nov 21, 2024
1f74430
code review
lelemm Nov 21, 2024
e24ac74
Merge branch 'calendar_report' of https://github.com/lelemm/actual in…
lelemm Nov 21, 2024
07960ca
Update packages/desktop-client/src/components/reports/reports/Calenda…
lelemm Nov 21, 2024
3bc7c1c
code review
lelemm Nov 21, 2024
89da079
linter
lelemm Nov 21, 2024
e3d8a52
fixes
lelemm Nov 21, 2024
8cc40cb
code review
lelemm Nov 21, 2024
d86855c
Merge remote-tracking branch 'org/master' into calendar_report
lelemm Nov 21, 2024
662d547
multiple fixes
lelemm Nov 21, 2024
2954139
Apply suggestions from code review
lelemm Nov 21, 2024
7c97f71
fixes, linter and typecheck
lelemm Nov 21, 2024
b960fb1
minHeight on header
lelemm Nov 21, 2024
8c06842
Merge remote-tracking branch 'org/master' into calendar_report
lelemm Nov 21, 2024
190e157
removed highlight background suggested in review
lelemm Nov 22, 2024
0279686
Merge branch 'master' into calendar_report
lelemm Nov 22, 2024
342c764
added widget to export control
lelemm Nov 25, 2024
12570a0
Merge branch 'master' into calendar_report
lelemm Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/desktop-client/src/components/reports/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {

import { useAccounts } from '../../hooks/useAccounts';
import { useNavigate } from '../../hooks/useNavigate';
import { useSyncedPref } from '../../hooks/useSyncedPref';
import { breakpoints } from '../../tokens';
import { Button } from '../common/Button2';
import { Menu } from '../common/Menu';
Expand All @@ -33,6 +34,7 @@ import { useResponsive } from '../responsive/ResponsiveProvider';

import { NON_DRAGGABLE_AREA_CLASS_NAME } from './constants';
import { LoadingIndicator } from './LoadingIndicator';
import { CalendarCard } from './reports/CalendarCard';
import { CashFlowCard } from './reports/CashFlowCard';
import { CustomReportListCards } from './reports/CustomReportListCards';
import { MarkdownCard } from './reports/MarkdownCard';
Expand All @@ -50,6 +52,8 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget {
export function Overview() {
const { t } = useTranslation();
const dispatch = useDispatch();
const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';

const triggerRef = useRef(null);
const extraMenuTriggerRef = useRef(null);
Expand Down Expand Up @@ -381,6 +385,10 @@ export function Overview() {
name: 'markdown-card' as const,
text: t('Text widget'),
},
{
name: 'calendar-card' as const,
text: t('Calendar card'),
},
{
name: 'custom-report' as const,
text: t('New custom report'),
Expand Down Expand Up @@ -522,6 +530,15 @@ export function Overview() {
report={customReportMap.get(item.meta.id)}
onRemove={() => onRemoveWidget(item.i)}
/>
) : item.type === 'calendar-card' ? (
<CalendarCard
widgetId={item.i}
isEditing={isEditing}
meta={item.meta}
firstDayOfWeekIdx={firstDayOfWeekIdx}
onMetaChange={newMeta => onMetaChange(item, newMeta)}
onRemove={() => onRemoveWidget(item.i)}
/>
) : null}
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';

import { Overview } from './Overview';
import { Calendar } from './reports/Calendar';
import { CashFlow } from './reports/CashFlow';
import { CustomReport } from './reports/CustomReport';
import { NetWorth } from './reports/NetWorth';
Expand All @@ -19,6 +20,8 @@ export function ReportRouter() {
<Route path="/custom/:id" element={<CustomReport />} />
<Route path="/spending" element={<Spending />} />
<Route path="/spending/:id" element={<Spending />} />
<Route path="/calendar" element={<Calendar />} />
<Route path="/calendar/:id" element={<Calendar />} />
</Routes>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
import { type Ref, useEffect, useState } from 'react';

import {
addDays,
format,
getDate,
isSameMonth,
startOfMonth,
startOfWeek,
} from 'date-fns';

import { amountToCurrency } from 'loot-core/shared/util';
import { type SyncedPrefs } from 'loot-core/types/prefs';

import { useResizeObserver } from '../../../hooks/useResizeObserver';
import { styles, theme } from '../../../style';
import { Button } from '../../common/Button2';
import { Tooltip } from '../../common/Tooltip';
import { View } from '../../common/View';
import { PrivacyFilter } from '../../PrivacyFilter';
import { chartTheme } from '../chart-theme';

type CalendarGraphProps = {
data: {
date: Date;
incomeValue: number;
expenseValue: number;
incomeSize: number;
expenseSize: number;
}[];
start: Date;
firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
onDayClick: (date: Date | null) => void;
};
export function CalendarGraph({
data,
start,
firstDayOfWeekIdx,
onDayClick,
}: CalendarGraphProps) {
const startingDate = startOfWeek(new Date(), {
weekStartsOn:
firstDayOfWeekIdx !== undefined &&
!Number.isNaN(parseInt(firstDayOfWeekIdx)) &&
parseInt(firstDayOfWeekIdx) >= 0 &&
parseInt(firstDayOfWeekIdx) <= 6
? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: 0,
});
const [fontSize, setFontSize] = useState(14);

const buttonRef = useResizeObserver(rect => {
const newValue = Math.floor(rect.height / 2);
if (newValue > 14) {
setFontSize(14);
} else {
setFontSize(newValue);
}
});

return (
<>
<View
style={{
color: theme.pageTextSubdued,
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gridAutoRows: '1fr',
gap: 2,
}}
onClick={() => onDayClick(null)}
>
{Array.from({ length: 7 }, (_, index) => (
<View
key={index}
style={{
textAlign: 'center',
fontSize: 14,
fontWeight: 500,
padding: '3px 0',
height: '100%',
width: '100%',
position: 'relative',
marginBottom: 4,
}}
>
{format(addDays(startingDate, index), 'EEEEE')}
</View>
))}
</View>
<View
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gridAutoRows: '1fr',
gap: 2,
width: '100%',
height: '100%',
}}
>
{data.map((day, index) =>
!isSameMonth(day.date, startOfMonth(start)) ? (
<View
key={`empty-${day.date.getTime()}`}
onClick={() => onDayClick(null)}
/>
) : day.incomeValue !== 0 || day.expenseValue !== 0 ? (
<Tooltip
key={day.date.getTime()}
content={
<View>
<View style={{ marginBottom: 10 }}>
<strong>{format(day.date, 'MMM dd')}</strong>
</View>
<View style={{ lineHeight: 1.5 }}>
<View
style={{
display: 'grid',
gridTemplateColumns: '70px 1fr 60px',
gridAutoRows: '1fr',
}}
>
<View
style={{
textAlign: 'right',
marginRight: 4,
}}
>
Income:
</View>
<View
style={{
color: chartTheme.colors.blue,
flexDirection: 'row',
}}
>
{day.incomeValue !== 0 ? (
<PrivacyFilter>
{amountToCurrency(day.incomeValue)}
</PrivacyFilter>
) : (
''
)}
</View>
<View style={{ marginLeft: 4, flexDirection: 'row' }}>
(
<PrivacyFilter>
{Math.round(day.incomeSize * 100) / 100 + '%'}
</PrivacyFilter>
)
</View>
<View
style={{
textAlign: 'right',
marginRight: 4,
}}
>
Expenses:
</View>
<View
style={{
color: chartTheme.colors.red,
flexDirection: 'row',
}}
>
{day.expenseValue !== 0 ? (
<PrivacyFilter>
{amountToCurrency(day.expenseValue)}
</PrivacyFilter>
) : (
''
)}
</View>
<View style={{ marginLeft: 4, flexDirection: 'row' }}>
(
<PrivacyFilter>
{Math.round(day.expenseSize * 100) / 100 + '%'}
</PrivacyFilter>
)
</View>
</View>
</View>
</View>
}
placement="bottom end"
style={{
...styles.tooltip,
lineHeight: 1.5,
padding: '6px 10px',
}}
>
<DayButton
key={day.date.getTime()}
resizeRef={el => {
if (index === 15 && el) {
buttonRef(el);
}
}}
fontSize={fontSize}
day={day}
onPress={() => onDayClick(day.date)}
/>
</Tooltip>
) : (
<DayButton
key={day.date.getTime()}
resizeRef={el => {
if (index === 15 && el) {
buttonRef(el);
}
}}
fontSize={fontSize}
day={day}
onPress={() => onDayClick(day.date)}
/>
),
)}
</View>
</>
);
}

type DayButtonProps = {
fontSize: number;
resizeRef: Ref<HTMLButtonElement>;
day: {
date: Date;
incomeSize: number;
expenseSize: number;
};
onPress: () => void;
};
function DayButton({ day, onPress, fontSize, resizeRef }: DayButtonProps) {
const [currentFontSize, setCurrentFontSize] = useState(fontSize);

useEffect(() => {
setCurrentFontSize(fontSize);
}, [fontSize]);

lelemm marked this conversation as resolved.
Show resolved Hide resolved
return (
<Button
ref={resizeRef}
aria-label={format(day.date, 'MMMM d, yyyy')}
style={{
borderColor: 'transparent',
backgroundColor: theme.calendarCellBackground,
position: 'relative',
padding: 'unset',
height: '100%',
minWidth: 0,
minHeight: 0,
margin: 0,
}}
onPress={() => onPress()}
>
{day.expenseSize !== 0 && (
<View
style={{
position: 'absolute',
width: '50%',
height: '100%',
background: chartTheme.colors.red,
opacity: 0.2,
right: 0,
}}
/>
)}
{day.incomeSize !== 0 && (
<View
style={{
position: 'absolute',
width: '50%',
height: '100%',
background: chartTheme.colors.blue,
opacity: 0.2,
left: 0,
}}
/>
)}
<View
style={{
position: 'absolute',
left: 0,
bottom: 0,
opacity: 0.9,
height: `${Math.ceil(day.incomeSize)}%`,
backgroundColor: chartTheme.colors.blue,
width: '50%',
transition: 'height 0.5s ease-out',
}}
/>

<View
style={{
position: 'absolute',
right: 0,
bottom: 0,
opacity: 0.9,
height: `${Math.ceil(day.expenseSize)}%`,
backgroundColor: chartTheme.colors.red,
width: '50%',
transition: 'height 0.5s ease-out',
}}
/>
<span
style={{
fontSize: `${currentFontSize}px`,
lelemm marked this conversation as resolved.
Show resolved Hide resolved
fontWeight: 500,
position: 'relative',
}}
>
{getDate(day.date)}
</span>
</Button>
);
}
lelemm marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,10 @@ export function getFullRange(start: string) {

export function getLatestRange(offset: number) {
const end = monthUtils.currentMonth();
const start = monthUtils.subMonths(end, offset);
let start = end;
if (offset !== 1) {
start = monthUtils.subMonths(end, offset);
}
return [start, end, 'sliding-window'] as const;
}

Expand Down
Loading