diff --git a/frontend/apps/web/src/app/Router.tsx b/frontend/apps/web/src/app/Router.tsx
index 87d8a02b..419893da 100644
--- a/frontend/apps/web/src/app/Router.tsx
+++ b/frontend/apps/web/src/app/Router.tsx
@@ -91,6 +91,10 @@ const router = createBrowserRouter([
},
],
},
+ {
+ path: "404",
+ element: ,
+ },
{
path: "*",
element: ,
diff --git a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx
index 95c8e30d..ffcab152 100644
--- a/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx
+++ b/frontend/apps/web/src/app/authenticated-layout/AuthenticatedLayout.tsx
@@ -56,7 +56,7 @@ export const AuthenticatedLayout: React.FC = () => {
const location = useLocation();
const params = useParams();
const groupId = params["groupId"] ? Number(params["groupId"]) : undefined;
- const [anchorEl, setAnchorEl] = React.useState(null);
+ const [anchorEl, setAnchorEl] = React.useState(null);
const theme: Theme = useTheme();
const dotsMenuOpen = Boolean(anchorEl);
const cfg = useConfig();
@@ -67,11 +67,11 @@ export const AuthenticatedLayout: React.FC = () => {
return ;
}
- const handleProfileMenuOpen = (event) => {
+ const handleProfileMenuOpen = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
};
- const handleDotsMenuClose = (event) => {
+ const handleDotsMenuClose = () => {
setAnchorEl(null);
};
diff --git a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx
index ad92d0ab..87be76b8 100644
--- a/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx
+++ b/frontend/apps/web/src/app/authenticated-layout/SidebarGroupList.tsx
@@ -21,7 +21,7 @@ export const SidebarGroupList: React.FC = ({ activeGroupId }) => {
setShowGroupCreationModal(true);
};
- const closeGroupCreateModal = (evt, reason) => {
+ const closeGroupCreateModal = (reason: string) => {
if (reason !== "backdropClick") {
setShowGroupCreationModal(false);
}
@@ -38,7 +38,7 @@ export const SidebarGroupList: React.FC = ({ activeGroupId }) => {
diff --git a/frontend/apps/web/src/components/AccountSelect.tsx b/frontend/apps/web/src/components/AccountSelect.tsx
index eea1845f..e419ef35 100644
--- a/frontend/apps/web/src/components/AccountSelect.tsx
+++ b/frontend/apps/web/src/components/AccountSelect.tsx
@@ -46,7 +46,7 @@ export const AccountSelect: React.FC = ({
options={filteredAccounts}
getOptionLabel={(acc: Account) => acc.name}
multiple={false}
- value={value !== undefined ? (accounts.find((acc) => acc.id === value) ?? null) : null}
+ value={value !== undefined ? (accounts.find((acc) => acc.id === value) ?? undefined) : undefined}
disabled={disabled}
openOnFocus
fullWidth
diff --git a/frontend/apps/web/src/components/AddNewTagDialog.tsx b/frontend/apps/web/src/components/AddNewTagDialog.tsx
index 10cc1f0b..2878fabe 100644
--- a/frontend/apps/web/src/components/AddNewTagDialog.tsx
+++ b/frontend/apps/web/src/components/AddNewTagDialog.tsx
@@ -29,13 +29,13 @@ export const AddNewTagDialog: React.FC = ({ open, onCreate, onClose }) =>
setError(false);
};
- const onKeyUp = (key) => {
+ const onKeyUp = (key: React.KeyboardEvent) => {
if (key.keyCode === 13) {
handleSave();
}
};
- const handleChange = (evt) => {
+ const handleChange = (evt: React.ChangeEvent) => {
const newValue = evt.target.value;
if (newValue !== null && newValue !== "") {
setError(false);
diff --git a/frontend/apps/web/src/components/DateInput.tsx b/frontend/apps/web/src/components/DateInput.tsx
index a1571395..b7f3a4e3 100644
--- a/frontend/apps/web/src/components/DateInput.tsx
+++ b/frontend/apps/web/src/components/DateInput.tsx
@@ -14,9 +14,10 @@ interface Props {
export const DateInput: React.FC = ({ value, onChange, helperText, error, disabled = false }) => {
const { t } = useTranslation();
- const handleChange = (value: DateTime) => {
- if (value.toISODate()) {
- onChange(value.toISODate());
+ const handleChange = (value: DateTime | null) => {
+ const stringified = value?.toISODate();
+ if (stringified) {
+ onChange(stringified);
}
};
diff --git a/frontend/apps/web/src/components/NumericInput.tsx b/frontend/apps/web/src/components/NumericInput.tsx
index c55dbc46..5befe0a3 100644
--- a/frontend/apps/web/src/components/NumericInput.tsx
+++ b/frontend/apps/web/src/components/NumericInput.tsx
@@ -13,10 +13,10 @@ export const NumericInput: React.FC = ({ value, isCurrency, o
const [internalValue, setInternalValue] = React.useState("");
React.useEffect(() => {
- setInternalValue(isCurrency ? value.toFixed(2) : String(value));
+ setInternalValue(isCurrency ? (value?.toFixed(2) ?? "") : String(value));
}, [value, setInternalValue, isCurrency]);
- const onInternalChange = (event) => {
+ const onInternalChange = (event: React.ChangeEvent) => {
setInternalValue(event.target.value);
// TODO: validate input
};
diff --git a/frontend/apps/web/src/components/ShareSelect.tsx b/frontend/apps/web/src/components/ShareSelect.tsx
index b9e3835e..ec8addb8 100644
--- a/frontend/apps/web/src/components/ShareSelect.tsx
+++ b/frontend/apps/web/src/components/ShareSelect.tsx
@@ -53,7 +53,7 @@ const ShareSelectRow: React.FC = ({
onChange(account.id, newValue);
};
- const handleToggleShare = (event) => {
+ const handleToggleShare = (event: React.ChangeEvent) => {
if (event.target.checked) {
onChange(account.id, 1);
} else {
@@ -234,19 +234,27 @@ export const ShareSelect: React.FC = ({
{editable && (
}
- checked={showEvents}
- onChange={(event: React.ChangeEvent) =>
- setShowEvents(event.target.checked)
+ control={
+ ) =>
+ setShowEvents(event.target.checked)
+ }
+ />
}
+ checked={showEvents}
label={t("shareSelect.showEvents")}
/>
}
- checked={showAdvanced}
- onChange={(event: React.ChangeEvent) =>
- setShowAdvanced(event.target.checked)
+ control={
+ ) =>
+ setShowAdvanced(event.target.checked)
+ }
+ />
}
+ checked={showAdvanced}
label={t("common.advanced")}
/>
diff --git a/frontend/apps/web/src/components/TagSelector.tsx b/frontend/apps/web/src/components/TagSelector.tsx
index ebe2b5a5..f6012191 100644
--- a/frontend/apps/web/src/components/TagSelector.tsx
+++ b/frontend/apps/web/src/components/TagSelector.tsx
@@ -32,11 +32,11 @@ export const TagSelector: React.FC = ({
const possibleTags = useAppSelector((state) => selectTagsInGroup({ state, groupId }));
- const handleChange = (event) => {
+ const handleChange = (event: React.ChangeEvent) => {
if (!editable) {
return;
}
- const newTags = event.target.value;
+ const newTags = event.target.value as unknown as string[];
if (newTags.indexOf(CREATE_TAG) > -1) {
openAddTagDialog();
return;
@@ -64,9 +64,9 @@ export const TagSelector: React.FC = ({
value={value}
SelectProps={{
multiple: true,
- renderValue: (selected: string[]) => (
+ renderValue: (selected: unknown) => (
- {selected.map((value) => (
+ {(selected as string[]).map((value) => (
))}
diff --git a/frontend/apps/web/src/components/TextInput.tsx b/frontend/apps/web/src/components/TextInput.tsx
index 680ba405..7326ba75 100644
--- a/frontend/apps/web/src/components/TextInput.tsx
+++ b/frontend/apps/web/src/components/TextInput.tsx
@@ -14,7 +14,7 @@ export const TextInput: React.FC = ({ value, onChange, ...props
setInternalValue(String(value));
}, [value, setInternalValue]);
- const onInternalChange = (event) => {
+ const onInternalChange = (event: React.ChangeEvent) => {
setInternalValue(event.target.value);
};
diff --git a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx
index 4024a604..93df5479 100644
--- a/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx
+++ b/frontend/apps/web/src/components/accounts/AccountClearingListEntry.tsx
@@ -1,33 +1,30 @@
-import { selectAccountBalances, selectAccountById, selectGroupCurrencySymbol } from "@abrechnung/redux";
+import { selectAccountBalances, selectGroupCurrencySymbol } from "@abrechnung/redux";
import { Box, ListItemAvatar, ListItemText, Tooltip, Typography } from "@mui/material";
import { DateTime } from "luxon";
import React from "react";
import { balanceColor } from "@/core/utils";
-import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store";
+import { selectGroupSlice, useAppSelector } from "@/store";
import { getAccountLink } from "@/utils";
import { ClearingAccountIcon } from "../style/AbrechnungIcons";
import ListItemLink from "../style/ListItemLink";
import { useTranslation } from "react-i18next";
import { useFormatCurrency } from "@/hooks";
+import { ClearingAccount } from "@abrechnung/types";
interface Props {
groupId: number;
accountId: number;
- clearingAccountId: number;
+ clearingAccount: ClearingAccount;
}
-export const AccountClearingListEntry: React.FC = ({ groupId, accountId, clearingAccountId }) => {
+export const AccountClearingListEntry: React.FC = ({ groupId, accountId, clearingAccount }) => {
const { t } = useTranslation();
const formatCurrency = useFormatCurrency();
const balances = useAppSelector((state) => selectAccountBalances({ state, groupId }));
const currency_symbol = useAppSelector((state) =>
selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId })
);
- const clearingAccount = useAppSelector((state) =>
- selectAccountById({ state: selectAccountSlice(state), groupId, accountId: clearingAccountId })
- );
- if (clearingAccount.type !== "clearing") {
- console.error("expected a clearing account but received a personal account");
+ if (!currency_symbol) {
return null;
}
diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx
index e0031b47..7d0a4305 100644
--- a/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx
+++ b/frontend/apps/web/src/components/accounts/AccountTransactionList.tsx
@@ -83,7 +83,7 @@ export const AccountTransactionList: React.FC = ({ groupId, account }) =>
key={`clearing-${entry.id}`}
accountId={account.id}
groupId={groupId}
- clearingAccountId={entry.id}
+ clearingAccount={entry}
/>
);
}
@@ -98,7 +98,7 @@ export const AccountTransactionList: React.FC = ({ groupId, account }) =>
key={`transaction-${entry.id}`}
accountId={account.id}
groupId={groupId}
- transactionId={entry.id}
+ transaction={entry}
/>
);
})}
diff --git a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx
index ff9eb146..a4815810 100644
--- a/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx
+++ b/frontend/apps/web/src/components/accounts/AccountTransactionListEntry.tsx
@@ -1,4 +1,4 @@
-import { selectGroupCurrencySymbol, selectTransactionBalanceEffect, selectTransactionById } from "@abrechnung/redux";
+import { selectGroupCurrencySymbol, selectTransactionBalanceEffect } from "@abrechnung/redux";
import { HelpOutline } from "@mui/icons-material";
import { Chip, ListItemAvatar, ListItemText, Tooltip, Typography } from "@mui/material";
import { DateTime } from "luxon";
@@ -8,20 +8,18 @@ import { selectGroupSlice, selectTransactionSlice, useAppSelector } from "@/stor
import { PurchaseIcon, TransferIcon } from "../style/AbrechnungIcons";
import ListItemLink from "../style/ListItemLink";
import { useTranslation } from "react-i18next";
+import { Transaction } from "@abrechnung/types";
interface Props {
groupId: number;
- transactionId: number;
+ transaction: Transaction;
accountId: number;
}
-export const AccountTransactionListEntry: React.FC = ({ groupId, transactionId, accountId }) => {
+export const AccountTransactionListEntry: React.FC = ({ groupId, transaction, accountId }) => {
const { t } = useTranslation();
const balanceEffect = useAppSelector((state) =>
- selectTransactionBalanceEffect({ state: selectTransactionSlice(state), groupId, transactionId })
- );
- const transaction = useAppSelector((state) =>
- selectTransactionById({ state: selectTransactionSlice(state), groupId, transactionId })
+ selectTransactionBalanceEffect({ state: selectTransactionSlice(state), groupId, transactionId: transaction.id })
);
const currency_symbol = useAppSelector((state) =>
selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId })
diff --git a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx
index 3be2e342..6311c40f 100644
--- a/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx
+++ b/frontend/apps/web/src/components/accounts/BalanceHistoryGraph.tsx
@@ -63,7 +63,10 @@ export const BalanceHistoryGraph: React.FC = ({ groupId, accountId }) =>
const graphData: Serie[] = [];
let lastPoint = balanceHistory[0];
- const makeSerie = () => {
+ const makeSerie = (): {
+ id: string;
+ data: Array<{ x: Date; y: number; changeOrigin: BalanceChangeOrigin }>;
+ } => {
return {
id: `serie-${graphData.length}`,
data: [],
diff --git a/frontend/apps/web/src/components/accounts/BalanceTable.tsx b/frontend/apps/web/src/components/accounts/BalanceTable.tsx
index 3ccfadbf..bcb19497 100644
--- a/frontend/apps/web/src/components/accounts/BalanceTable.tsx
+++ b/frontend/apps/web/src/components/accounts/BalanceTable.tsx
@@ -4,6 +4,7 @@ import { DataGrid, GridColDef, GridToolbar } from "@mui/x-data-grid";
import React from "react";
import { renderCurrency } from "../style/datagrid/renderCurrency";
import { useTranslation } from "react-i18next";
+import { Navigate } from "react-router-dom";
interface Props {
groupId: number;
@@ -17,6 +18,10 @@ export const BalanceTable: React.FC = ({ groupId }) => {
const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId }));
const balances = useAppSelector((state) => selectAccountBalances({ state, groupId }));
+ if (!group) {
+ return ;
+ }
+
const tableData = personalAccounts.map((acc) => {
return {
id: acc.id,
diff --git a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx
index 06c0d833..b725be03 100644
--- a/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx
+++ b/frontend/apps/web/src/components/accounts/ClearingAccountDetail.tsx
@@ -1,26 +1,27 @@
-import { selectAccountBalances, selectAccountById, selectGroupCurrencySymbol } from "@abrechnung/redux";
+import { selectAccountBalances, selectGroupCurrencySymbol } from "@abrechnung/redux";
import { TableCell, Typography } from "@mui/material";
import React from "react";
-import { selectAccountSlice, selectGroupSlice, useAppSelector } from "@/store";
+import { selectGroupSlice, useAppSelector } from "@/store";
import { ShareSelect } from "../ShareSelect";
import { useTranslation } from "react-i18next";
import { useFormatCurrency } from "@/hooks";
+import { Account } from "@abrechnung/types";
interface Props {
groupId: number;
- accountId: number;
+ account: Account;
}
-export const ClearingAccountDetail: React.FC = ({ groupId, accountId }) => {
+export const ClearingAccountDetail: React.FC = ({ groupId, account }) => {
const { t } = useTranslation();
const formatCurrency = useFormatCurrency();
- const account = useAppSelector((state) =>
- selectAccountById({ state: selectAccountSlice(state), groupId, accountId })
- );
const currency_symbol = useAppSelector((state) =>
selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId })
);
const balances = useAppSelector((state) => selectAccountBalances({ state, groupId }));
+ if (!currency_symbol) {
+ return null;
+ }
if (account.type !== "clearing") {
throw new Error("expected a clearing account to render ClearingAccountDetail, but got a personal account");
}
diff --git a/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx b/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx
index 43c8f0b7..f1f5a2c7 100644
--- a/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx
+++ b/frontend/apps/web/src/components/accounts/DeleteAccountModal.tsx
@@ -1,24 +1,22 @@
import React from "react";
import { api } from "@/core/api";
import { Dialog, DialogTitle, DialogActions, DialogContent, Button, LinearProgress } from "@mui/material";
-import { selectAccountSlice, useAppDispatch, useAppSelector } from "@/store";
-import { deleteAccount, selectAccountById } from "@abrechnung/redux";
+import { useAppDispatch } from "@/store";
+import { deleteAccount } from "@abrechnung/redux";
import { toast } from "react-toastify";
import { useTranslation } from "react-i18next";
+import { Account } from "@abrechnung/types";
interface Props {
show: boolean;
onClose: () => void;
onAccountDeleted?: () => void;
groupId: number;
- accountId: number | null;
+ account: Account | null;
}
-export const DeleteAccountModal: React.FC = ({ show, onClose, groupId, accountId, onAccountDeleted }) => {
+export const DeleteAccountModal: React.FC = ({ show, onClose, groupId, account, onAccountDeleted }) => {
const { t } = useTranslation();
- const account = useAppSelector((state) =>
- selectAccountById({ state: selectAccountSlice(state), groupId, accountId })
- );
const dispatch = useAppDispatch();
const [showProgress, setShowProgress] = React.useState(false);
@@ -30,7 +28,7 @@ export const DeleteAccountModal: React.FC = ({ show, onClose, groupId, ac
return;
}
setShowProgress(true);
- dispatch(deleteAccount({ groupId, accountId, api }))
+ dispatch(deleteAccount({ groupId, accountId: account.id, api }))
.unwrap()
.then(() => {
if (onAccountDeleted) {
diff --git a/frontend/apps/web/src/components/groups/GroupCreateModal.tsx b/frontend/apps/web/src/components/groups/GroupCreateModal.tsx
index a3e7d2bf..600e1663 100644
--- a/frontend/apps/web/src/components/groups/GroupCreateModal.tsx
+++ b/frontend/apps/web/src/components/groups/GroupCreateModal.tsx
@@ -26,12 +26,15 @@ const validationSchema = z.object({
type FormValues = z.infer;
+const initialValues: FormValues = {
+ name: "",
+ description: "",
+ addUserAccountOnJoin: false,
+};
+
interface Props {
show: boolean;
- onClose: (
- event: Record,
- reason: "escapeKeyDown" | "backdropClick" | "completed" | "closeButton"
- ) => void;
+ onClose: (reason: "escapeKeyDown" | "backdropClick" | "completed" | "closeButton") => void;
}
export const GroupCreateModal: React.FC = ({ show, onClose }) => {
@@ -53,7 +56,7 @@ export const GroupCreateModal: React.FC = ({ show, onClose }) => {
.unwrap()
.then(() => {
setSubmitting(false);
- onClose({}, "completed");
+ onClose("completed");
})
.catch((err) => {
toast.error(err);
@@ -62,15 +65,11 @@ export const GroupCreateModal: React.FC = ({ show, onClose }) => {
};
return (
-