Can delete a movement

This commit is contained in:
Pierre HUBERT 2025-04-22 12:14:50 +02:00
parent 68dfbfff2b
commit bff1c2d171
4 changed files with 83 additions and 22 deletions

View File

@ -1,13 +1,11 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export interface Balances { type Balances = Record<number, number>;
[key: number]: number;
}
export interface MovementUpdate { export interface MovementUpdate {
account_id: number; account_id: number;
time: number; time: number;
label: String; label: string;
file_id?: number; file_id?: number;
amount: number; amount: number;
checked: boolean; checked: boolean;
@ -73,4 +71,14 @@ export class MovementApi {
}) })
).data; ).data;
} }
/**
* Delete a movement
*/
static async Delete(movement: Movement): Promise<void> {
await APIClient.exec({
uri: `/movement/${movement.id}`,
method: "DELETE",
});
}
} }

View File

@ -1,17 +1,19 @@
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import { Tooltip, Typography } from "@mui/material";
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; import { Movement, MovementApi } from "../api/MovementsApi";
import { useAccounts } from "../hooks/AccountsListProvider"; import { useAccounts } from "../hooks/AccountsListProvider";
import { NotFoundRoute } from "./NotFound"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { AccountWidget } from "../widgets/AccountWidget"; import { AccountWidget } from "../widgets/AccountWidget";
import { AmountWidget } from "../widgets/AmountWidget"; import { AmountWidget } from "../widgets/AmountWidget";
import { Typography } from "@mui/material";
import { NewMovementWidget } from "../widgets/NewMovementWidget";
import { Movement, MovementApi } from "../api/MovementsApi";
import React from "react";
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { DateWidget } from "../widgets/DateWidget"; import { DateWidget } from "../widgets/DateWidget";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
import { NewMovementWidget } from "../widgets/NewMovementWidget";
import { NotFoundRoute } from "./NotFound";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
export function AccountRoute(): React.ReactElement { export function AccountRoute(): React.ReactElement {
const { accountId } = useParams(); const { accountId } = useParams();
@ -28,10 +30,12 @@ export function AccountRoute(): React.ReactElement {
setMovements(await MovementApi.GetAccountMovements(account!.id)); setMovements(await MovementApi.GetAccountMovements(account!.id));
}; };
const reload = async () => { const reload = (skipMovements = false) => {
accounts.reloadBalances(); accounts.reloadBalances();
if (!skipMovements) {
setMovements(undefined); setMovements(undefined);
loadKey.current += 1; loadKey.current += 1;
}
}; };
if (account === null) return <NotFoundRoute />; if (account === null) return <NotFoundRoute />;
@ -58,7 +62,9 @@ export function AccountRoute(): React.ReactElement {
load={load} load={load}
ready={movements !== null} ready={movements !== null}
errMsg="Failed to load the list of movements!" errMsg="Failed to load the list of movements!"
build={() => <MovementsTable movements={movements!} />} build={() => (
<MovementsTable needReload={reload} movements={movements!} />
)}
/> />
</div> </div>
<NewMovementWidget account={account} onCreated={reload} /> <NewMovementWidget account={account} onCreated={reload} />
@ -69,9 +75,31 @@ export function AccountRoute(): React.ReactElement {
function MovementsTable(p: { function MovementsTable(p: {
movements: Movement[]; movements: Movement[];
needReload: () => {}; needReload: (skipMovements: boolean) => void;
}): React.ReactElement { }): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
const confirm = useConfirm();
const handleDeleteClick = async (movement: Movement) => {
try {
if (
!(await confirm(
`Do you really want to delete the movement ${movement.label} (${movement.amount}€)?`
))
)
return;
await MovementApi.Delete(movement);
const id = p.movements.findIndex((m) => movement.id === m.id);
p.movements.slice(id, id);
p.needReload(false);
} catch (e) {
console.error("Failed to delete movement!", e);
alert(`Failed to delete movement! ${e}`);
}
};
const columns: GridColDef<(typeof p.movements)[number]>[] = [ const columns: GridColDef<(typeof p.movements)[number]>[] = [
{ {
@ -122,6 +150,26 @@ function MovementsTable(p: {
headerName: "File", headerName: "File",
// TODO // TODO
}, },
{
field: "actions",
type: "actions",
headerName: "Actions",
width: 80,
cellClassName: "actions",
editable: false,
getActions: (params) => {
return [
<Tooltip title="Delete the movement">
<GridActionsCellItem
icon={<DeleteIcon color="error" />}
label="Delete"
onClick={() => handleDeleteClick(params.row)}
color="inherit"
/>
</Tooltip>,
];
},
},
]; ];
return ( return (
<DataGrid<Movement> <DataGrid<Movement>
@ -146,6 +194,8 @@ function MovementsTable(p: {
console.error("Failed to update movement information!", e); console.error("Failed to update movement information!", e);
alert(`Failed to update row! ${e}`); alert(`Failed to update row! ${e}`);
throw e; throw e;
} finally {
p.needReload(true);
} }
}} }}
/> />

View File

@ -13,7 +13,7 @@ import { AmountInput } from "./forms/AmountInput";
export function NewMovementWidget(p: { export function NewMovementWidget(p: {
account: Account; account: Account;
onCreated: () => {}; onCreated: () => void;
}): React.ReactElement { }): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const alert = useAlert(); const alert = useAlert();
@ -91,7 +91,7 @@ export function NewMovementWidget(p: {
type="text" type="text"
placeholder="Amount" placeholder="Amount"
style={{ flex: 1, maxWidth: "110px" }} style={{ flex: 1, maxWidth: "110px" }}
value={amount} value={amount ?? 0}
onValueChange={setAmount} onValueChange={setAmount}
/> />
<Tooltip title="Add new movement"> <Tooltip title="Add new movement">

View File

@ -13,7 +13,7 @@ export function AmountInput(p: {
placeholder: string; placeholder: string;
style: React.CSSProperties; style: React.CSSProperties;
value: number; value: number;
onValueChange: (val: number) => {}; onValueChange: (val: number) => void;
}): React.ReactElement { }): React.ReactElement {
const [state, setState] = React.useState(InputState.Normal); const [state, setState] = React.useState(InputState.Normal);
@ -32,7 +32,10 @@ export function AmountInput(p: {
{...p} {...p}
value={value} value={value}
onValueChange={(a) => { onValueChange={(a) => {
if (a === "-") return setState(InputState.StartNeg); if (a === "-") {
setState(InputState.StartNeg);
return;
}
if (a?.endsWith(".")) { if (a?.endsWith(".")) {
setState(InputState.StartDecimal); setState(InputState.StartDecimal);
@ -44,7 +47,7 @@ export function AmountInput(p: {
// Empty field // Empty field
if (a?.length === 0) p.onValueChange(NaN); if (a?.length === 0) p.onValueChange(NaN);
// Input number // Input number
else if ((a?.length ?? 0 > 0) && !Number.isNaN(parsed)) else if ((a?.length ?? 0) > 0 && !Number.isNaN(parsed))
p.onValueChange(parsed); p.onValueChange(parsed);
}} }}
/> />