Can delete a movement
This commit is contained in:
parent
68dfbfff2b
commit
bff1c2d171
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user