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";
export interface Balances {
[key: number]: number;
}
type Balances = Record<number, number>;
export interface MovementUpdate {
account_id: number;
time: number;
label: String;
label: string;
file_id?: number;
amount: number;
checked: boolean;
@ -73,4 +71,14 @@ export class MovementApi {
})
).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 { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
import { Movement, MovementApi } from "../api/MovementsApi";
import { useAccounts } from "../hooks/AccountsListProvider";
import { NotFoundRoute } from "./NotFound";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { AccountWidget } from "../widgets/AccountWidget";
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 { DataGrid, GridColDef } from "@mui/x-data-grid";
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 {
const { accountId } = useParams();
@ -28,10 +30,12 @@ export function AccountRoute(): React.ReactElement {
setMovements(await MovementApi.GetAccountMovements(account!.id));
};
const reload = async () => {
const reload = (skipMovements = false) => {
accounts.reloadBalances();
if (!skipMovements) {
setMovements(undefined);
loadKey.current += 1;
}
};
if (account === null) return <NotFoundRoute />;
@ -58,7 +62,9 @@ export function AccountRoute(): React.ReactElement {
load={load}
ready={movements !== null}
errMsg="Failed to load the list of movements!"
build={() => <MovementsTable movements={movements!} />}
build={() => (
<MovementsTable needReload={reload} movements={movements!} />
)}
/>
</div>
<NewMovementWidget account={account} onCreated={reload} />
@ -69,9 +75,31 @@ export function AccountRoute(): React.ReactElement {
function MovementsTable(p: {
movements: Movement[];
needReload: () => {};
needReload: (skipMovements: boolean) => void;
}): React.ReactElement {
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]>[] = [
{
@ -122,6 +150,26 @@ function MovementsTable(p: {
headerName: "File",
// 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 (
<DataGrid<Movement>
@ -146,6 +194,8 @@ function MovementsTable(p: {
console.error("Failed to update movement information!", e);
alert(`Failed to update row! ${e}`);
throw e;
} finally {
p.needReload(true);
}
}}
/>

View File

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

View File

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