Skip to content

Commit

Permalink
fix(web): fix line breaks in currency values
Browse files Browse the repository at this point in the history
  • Loading branch information
mikonse committed Sep 8, 2024
1 parent e12fe45 commit d139a95
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 100 deletions.
8 changes: 7 additions & 1 deletion frontend/apps/web/src/hooks/useFormatCurrency.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as React from "react";
const formatDef = new Intl.NumberFormat("de", {
currency: "EUR",
style: "currency",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});

export const useFormatCurrency = () => {
return React.useCallback((value: number, currencySymbol: string) => {
return `${value.toFixed(2)} ${currencySymbol}`;
return formatDef.format(value).replace("€", currencySymbol);
}, []);
};
105 changes: 105 additions & 0 deletions frontend/apps/web/src/pages/accounts/BalanceBarGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as React from "react";
import { useFormatCurrency } from "@/hooks";
import { useAppSelector } from "@/store";
import { selectAccountBalances, useSortedAccounts } from "@abrechnung/redux";
import { Theme, useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { Bar, BarChart, Cell, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { CategoricalChartFunc } from "recharts/types/chart/generateCategoricalChart";
import { Group } from "@abrechnung/api";

export type BalanceBarGraphProps = {
group: Group;
};

type Data = {
name: string;
id: number;
balance: number;
totalPaid: number;
totalConsumed: number;
};

export const BalanceBarGraph: React.FC<BalanceBarGraphProps> = ({ group }) => {
const formatCurrency = useFormatCurrency();
const theme: Theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
const navigate = useNavigate();

const personalAccounts = useSortedAccounts(group.id, "name", "personal");
const balances = useAppSelector((state) => selectAccountBalances(state, group.id));

const colorGreen = theme.palette.mode === "light" ? theme.palette.success.light : theme.palette.success.dark;
const colorRed = theme.palette.mode === "light" ? theme.palette.error.light : theme.palette.error.dark;

const roundTwoDecimals = (val: number) => +val.toFixed(2);

const chartData: Data[] = personalAccounts.map((account) => {
const balance = balances[account.id];
return {
name: account.name,
balance: roundTwoDecimals(balance?.balance ?? 0),
totalPaid: roundTwoDecimals(balance?.totalPaid ?? 0),
totalConsumed: roundTwoDecimals(balance?.totalConsumed ?? 0),
id: account.id,
};
});

const chartHeight = Object.keys(balances).length * 30 + 100;

// TODO determine the rendered width of the account names and take the maximum
const yaxiswidth = isSmallScreen
? Math.max(Math.max(...personalAccounts.map((account) => account.name.length)), 20)
: Math.max(...personalAccounts.map((account) => account.name.length)) * 7 + 5;

const handleBarClick: CategoricalChartFunc = (data) => {
const id = data.activePayload?.[0].payload.id;
navigate(`/groups/${group.id}/accounts/${id}`);
};

return (
<div className="area-chart-wrapper" style={{ width: "100%", height: `${chartHeight}px` }}>
<ResponsiveContainer>
<BarChart
data={chartData}
margin={{
top: 20,
right: 20,
bottom: 20,
left: 30,
}}
layout="vertical"
onClick={handleBarClick}
>
<XAxis stroke={theme.palette.text.primary} type="number" unit={group.currency_symbol} />
<YAxis dataKey="name" stroke={theme.palette.text.primary} type="category" width={yaxiswidth} />
<Tooltip
formatter={(label) => formatCurrency(parseFloat(String(label)), group.currency_symbol)}
labelStyle={{
color: theme.palette.text.primary,
}}
itemStyle={{
color: theme.palette.text.primary,
}}
contentStyle={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
}}
/>
<Bar dataKey="balance">
{chartData.map((entry, index) => {
return <Cell key={`cell-${index}`} fill={entry["balance"] >= 0 ? colorGreen : colorRed} />;
})}
<LabelList
dataKey={(entry) => formatCurrency((entry as Data).balance, group.currency_symbol)}
position="insideLeft"
fill={theme.palette.text.primary}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
};
102 changes: 3 additions & 99 deletions frontend/apps/web/src/pages/accounts/Balances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,18 @@ import {
import { useTheme } from "@mui/material/styles";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, Link as RouterLink, useNavigate } from "react-router-dom";
import { Bar, BarChart, Cell, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { CategoricalChartFunc } from "recharts/types/chart/generateCategoricalChart";
import { Navigate, Link as RouterLink } from "react-router-dom";
import { BalanceBarGraph } from "./BalanceBarGraph";

interface Props {
groupId: number;
}

type Data = {
name: string;
id: number;
balance: number;
totalPaid: number;
totalConsumed: number;
};

export const Balances: React.FC<Props> = ({ groupId }) => {
const { t } = useTranslation();
const formatCurrency = useFormatCurrency();
const theme: Theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
const navigate = useNavigate();

const group = useGroup(groupId);
const personalAccounts = useSortedAccounts(groupId, "name", "personal");
Expand All @@ -51,8 +41,6 @@ export const Balances: React.FC<Props> = ({ groupId }) => {

const [selectedTab, setSelectedTab] = useState("1");

const colorGreen = theme.palette.mode === "light" ? theme.palette.success.light : theme.palette.success.dark;
const colorRed = theme.palette.mode === "light" ? theme.palette.error.light : theme.palette.error.dark;
const colorGreenInverted = theme.palette.mode === "dark" ? theme.palette.success.light : theme.palette.success.dark;
const colorRedInverted = theme.palette.mode === "dark" ? theme.palette.error.light : theme.palette.error.dark;

Expand All @@ -62,19 +50,6 @@ export const Balances: React.FC<Props> = ({ groupId }) => {
return <Navigate to="/404" />;
}

const roundTwoDecimals = (val: number) => +val.toFixed(2);

const chartData: Data[] = personalAccounts.map((account) => {
const balance = balances[account.id];
return {
name: account.name,
balance: roundTwoDecimals(balance?.balance ?? 0),
totalPaid: roundTwoDecimals(balance?.totalPaid ?? 0),
totalConsumed: roundTwoDecimals(balance?.totalConsumed ?? 0),
id: account.id,
};
});

const unbalancedClearingAccounts = clearingAccounts
.filter((account) => balances[account.id]?.balance !== 0)
.map((account) => {
Expand All @@ -85,18 +60,6 @@ export const Balances: React.FC<Props> = ({ groupId }) => {
};
});

const chartHeight = Object.keys(balances).length * 30 + 100;

// TODO determine the rendered width of the account names and take the maximum
const yaxiswidth = isSmallScreen
? Math.max(Math.max(...personalAccounts.map((account) => account.name.length)), 20)
: Math.max(...personalAccounts.map((account) => account.name.length)) * 7 + 5;

const handleBarClick: CategoricalChartFunc = (data) => {
const id = data.activePayload?.[0].payload.id;
navigate(`/groups/${group.id}/accounts/${id}`);
};

return (
<MobilePaper>
<TabContext value={selectedTab}>
Expand Down Expand Up @@ -151,66 +114,7 @@ export const Balances: React.FC<Props> = ({ groupId }) => {
))}
</List>
) : (
<div className="area-chart-wrapper" style={{ width: "100%", height: `${chartHeight}px` }}>
<ResponsiveContainer>
<BarChart
data={chartData}
margin={{
top: 20,
right: 20,
bottom: 20,
left: 30,
}}
layout="vertical"
onClick={handleBarClick}
>
<XAxis
stroke={theme.palette.text.primary}
type="number"
unit={group.currency_symbol}
/>
<YAxis
dataKey="name"
stroke={theme.palette.text.primary}
type="category"
width={yaxiswidth}
/>
<Tooltip
formatter={(label) =>
formatCurrency(parseFloat(String(label)), group.currency_symbol)
}
labelStyle={{
color: theme.palette.text.primary,
}}
itemStyle={{
color: theme.palette.text.primary,
}}
contentStyle={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
}}
/>
<Bar dataKey="balance">
{chartData.map((entry, index) => {
return (
<Cell
key={`cell-${index}`}
fill={entry["balance"] >= 0 ? colorGreen : colorRed}
/>
);
})}
<LabelList
dataKey={(entry) =>
formatCurrency((entry as Data).balance, group.currency_symbol)
}
position="insideLeft"
fill={theme.palette.text.primary}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<BalanceBarGraph group={group} />
)}
</TabPanel>
<TabPanel value="2" sx={{ padding: { xs: 1, md: 2 } }}>
Expand Down

0 comments on commit d139a95

Please sign in to comment.