Skip to content

Commit

Permalink
Calendar Report
Browse files Browse the repository at this point in the history
  • Loading branch information
lelemm committed Nov 12, 2024
1 parent 9e47801 commit ad6ac02
Show file tree
Hide file tree
Showing 12 changed files with 2,026 additions and 17 deletions.
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 @@ -22,6 +22,7 @@ import {
import { useAccounts } from '../../hooks/useAccounts';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
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 @@ -34,6 +35,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 @@ -51,6 +53,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 @@ -396,6 +400,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 @@ -551,6 +559,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,290 @@
import { type Ref, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

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 { 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) => void;
// onFilter: (
// conditionsOrSavedFilter:
// | null
// | {
// conditions: RuleConditionEntity[];
// conditionsOp: 'and' | 'or';
// id: RuleConditionEntity[];
// }
// | RuleConditionEntity,
// ) => void;
};
export function CalendarGraph({
data,
start,
firstDayOfWeekIdx,
//onFilter,
onDayClick,
}: CalendarGraphProps) {
const { t } = useTranslation();
const startingDate = startOfWeek(new Date(), {
weekStartsOn: firstDayOfWeekIdx
? (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,
}}
>
{Array.from({ length: 7 }, (_, index) => (
<View
key={index}
style={{
textAlign: 'center',
fontSize: 14,
fontWeight: 500,
padding: '3px 0',
height: '100%',
width: '100%',
position: 'relative',
}}
>
{format(addDays(startingDate, index), 'EEEEE')}
</View>
))}
</View>
<View
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gridAutoRows: '1fr',
gap: 2,
width: '100%',
height: '100%',
//gridTemplateRows: `repeat(${Math.trunc(data.length) <= data.length / 7 ? Math.trunc(data.length) : Math.trunc(data.length) + 1},1fr)`,
}}
>
{data.map((day, index) =>
!isSameMonth(day.date, startOfMonth(start)) ? (
<View key={index} />
) : (
<Tooltip
key={index}
content={
<View>
<View style={{ marginBottom: 10 }}>
<strong>
{t('Day:') + ' '}
{format(day.date, 'dd')}
</strong>
</View>
<View style={{ lineHeight: 1.5 }}>
<View
style={{
display: 'grid',
gridTemplateColumns: '70px 1fr 60px',
gridAutoRows: '1fr',
}}
>
{day.incomeValue !== 0 && (
<>
<View
style={{
textAlign: 'right',
marginRight: 4,
}}
>
Income:
</View>
<View style={{ color: chartTheme.colors.blue }}>
{day.incomeValue !== 0
? amountToCurrency(day.incomeValue)
: ''}
</View>
<View style={{ marginLeft: 4 }}>
({Math.round(day.incomeSize * 100) / 100 + '%'})
</View>
</>
)}
{day.expenseValue !== 0 && (
<>
<View
style={{
textAlign: 'right',
marginRight: 4,
}}
>
Expenses:
</View>
<View style={{ color: chartTheme.colors.red }}>
{day.expenseValue !== 0
? amountToCurrency(day.expenseValue)
: ''}
</View>
<View style={{ marginLeft: 4 }}>
({Math.round(day.expenseSize * 100) / 100 + '%'})
</View>
</>
)}
</View>
</View>
</View>
}
placement="bottom end"
style={{
...styles.tooltip,
lineHeight: 1.5,
padding: '6px 10px',
}}
>
<DayButton
key={index}
resizeRef={index === 15 ? buttonRef : null}
fontSize={fontSize}
day={day}
onPress={() => onDayClick(day.date)}
/>
</Tooltip>
),
)}
</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]);

return (
<Button
ref={resizeRef}
style={{
borderColor: 'transparent',
backgroundColor: theme.menuAutoCompleteBackground,
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
className="bar positive-bar"
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
className="bar"
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`,
fontWeight: 500,
position: 'relative',
}}
>
{getDate(day.date)}
</span>
</Button>
);
}
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

0 comments on commit ad6ac02

Please sign in to comment.