-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45 from dsaltares/feat/40
feat: 40 - categorised income over time report
- Loading branch information
Showing
6 changed files
with
204 additions
and
133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
src/components/Reports/CategorizedIncomeOverTimeReport.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import type { TimeGranularity } from '@server/reports/types'; | ||
import getDateFilter from '@lib/getDateFilter'; | ||
import FullScreenSpinner from '@components/Layout/FullScreenSpinner'; | ||
import useFiltersFromUrl from '@lib/useFiltersFromUrl'; | ||
import client from '@lib/client'; | ||
import NoTransactionsFound from './NoTransactionsFound'; | ||
import CategoryOverTimeReport from './CategoryOverTimeReport'; | ||
|
||
export default function CategorizedIncomeOverTimeReport() { | ||
const { filtersByField } = useFiltersFromUrl(); | ||
const { data, isLoading: isLoadingReport } = | ||
client.getBucketedCategoryReport.useQuery({ | ||
type: 'Income', | ||
date: getDateFilter(filtersByField), | ||
accounts: filtersByField.accounts?.split(',').map(Number), | ||
categories: filtersByField.categories?.split(',').map(Number), | ||
currency: filtersByField.currency, | ||
granularity: filtersByField.timeGranularity as TimeGranularity, | ||
}); | ||
const { data: categories, isLoading: isLoadingCategories } = | ||
client.getCategories.useQuery(); | ||
const currency = filtersByField.currency || 'EUR'; | ||
|
||
if (isLoadingReport || isLoadingCategories) { | ||
return <FullScreenSpinner />; | ||
} else if (!data || data.length === 0) { | ||
return <NoTransactionsFound />; | ||
} | ||
|
||
return ( | ||
<CategoryOverTimeReport | ||
data={data} | ||
currency={currency} | ||
categories={categories} | ||
numberType="positive" | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import Stack from '@mui/material/Stack'; | ||
import { | ||
Bar, | ||
BarChart, | ||
CartesianGrid, | ||
Legend, | ||
Tooltip, | ||
XAxis, | ||
YAxis, | ||
} from 'recharts'; | ||
import stringToColor from 'string-to-color'; | ||
import Paper from '@mui/material/Paper'; | ||
import TableContainer from '@mui/material/TableContainer'; | ||
import Table from '@mui/material/Table'; | ||
import TableHead from '@mui/material/TableHead'; | ||
import TableRow from '@mui/material/TableRow'; | ||
import TableCell from '@mui/material/TableCell'; | ||
import TableBody from '@mui/material/TableBody'; | ||
import Typography from '@mui/material/Typography'; | ||
import { useMemo } from 'react'; | ||
import { formatAmount } from '@lib/format'; | ||
import CategoryChip from '@components/CategoryChip'; | ||
import type { CategoryBucket } from '@server/reports/types'; | ||
import type { Category } from '@server/category/types'; | ||
import useFiltersFromUrl from '@lib/useFiltersFromUrl'; | ||
import ChartContainer from './ChartContainer'; | ||
import { numberTypeToColor, type NumberType } from './utils'; | ||
|
||
type Props = { | ||
data: CategoryBucket[]; | ||
currency: string; | ||
categories?: Category[]; | ||
numberType?: NumberType; | ||
}; | ||
|
||
export default function CategoryOverTimeReport({ | ||
data, | ||
currency, | ||
categories, | ||
numberType, | ||
}: Props) { | ||
const { filtersByField } = useFiltersFromUrl(); | ||
const filteredCategoryIds = useMemo( | ||
() => new Set(filtersByField.categories?.split(',').map(Number)), | ||
[filtersByField.categories], | ||
); | ||
const categoriesInReport = useMemo( | ||
() => new Set(data?.map((datum) => Object.keys(datum.categories)).flat()), | ||
[data], | ||
); | ||
const selectedCategories = useMemo( | ||
() => | ||
categories?.filter( | ||
(category) => | ||
categoriesInReport.has(category.name) && | ||
(filteredCategoryIds.size === 0 || | ||
filteredCategoryIds.has(category.id)), | ||
), | ||
[categories, filteredCategoryIds, categoriesInReport], | ||
); | ||
|
||
return ( | ||
<Stack gap={2} justifyContent="center"> | ||
<ChartContainer> | ||
<BarChart data={data}> | ||
<CartesianGrid strokeDasharray="3 3" /> | ||
<XAxis dataKey="bucket" /> | ||
<YAxis /> | ||
<Legend | ||
formatter={(_value, _entry, index) => | ||
selectedCategories?.[index]?.name | ||
} | ||
/> | ||
<Tooltip | ||
formatter={(value, _name, _props, index) => [ | ||
formatAmount(value as number, currency), | ||
selectedCategories?.[index]?.name, | ||
]} | ||
wrapperStyle={{ zIndex: 1 }} | ||
/> | ||
{selectedCategories?.map((category) => ( | ||
<Bar | ||
key={category.name} | ||
name={category.name} | ||
dataKey={(datum) => datum.categories[category.name] || 0} | ||
stackId="a" | ||
fill={stringToColor(category.name)} | ||
/> | ||
))} | ||
</BarChart> | ||
</ChartContainer> | ||
<Stack> | ||
<Paper> | ||
<TableContainer> | ||
<Table size="small"> | ||
<TableHead> | ||
<TableRow> | ||
<TableCell>Date</TableCell> | ||
{selectedCategories?.map((category) => ( | ||
<TableCell key={category.id}> | ||
<CategoryChip id={category.id} name={category.name} /> | ||
</TableCell> | ||
))} | ||
<TableCell>Total</TableCell> | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{data.map((datum) => ( | ||
<TableRow key={datum.bucket}> | ||
<TableCell>{datum.bucket}</TableCell> | ||
{selectedCategories?.map((category) => ( | ||
<TableCell key={category.id}> | ||
<Typography | ||
color={numberTypeToColor(numberType)} | ||
variant="inherit" | ||
> | ||
{formatAmount( | ||
datum.categories[category.name], | ||
currency, | ||
)} | ||
</Typography> | ||
</TableCell> | ||
))} | ||
<TableCell> | ||
<Typography | ||
color={numberTypeToColor(numberType)} | ||
variant="inherit" | ||
> | ||
{formatAmount(datum.total, currency)} | ||
</Typography> | ||
</TableCell> | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
</Paper> | ||
</Stack> | ||
</Stack> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export type NumberType = 'positive' | 'negative' | 'neutral'; | ||
|
||
export function numberTypeToColor(numberType: NumberType = 'neutral') { | ||
switch (numberType) { | ||
case 'positive': | ||
return 'success.main'; | ||
case 'negative': | ||
return 'error'; | ||
default: | ||
return 'inherit'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters