diff --git a/moneymgr_web/src/api/MovementsApi.ts b/moneymgr_web/src/api/MovementsApi.ts index 341a031..ab5acf9 100644 --- a/moneymgr_web/src/api/MovementsApi.ts +++ b/moneymgr_web/src/api/MovementsApi.ts @@ -1,13 +1,11 @@ import { APIClient } from "./ApiClient"; -export interface Balances { - [key: number]: number; -} +type Balances = Record; 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 { + await APIClient.exec({ + uri: `/movement/${movement.id}`, + method: "DELETE", + }); + } } diff --git a/moneymgr_web/src/routes/AccountRoute.tsx b/moneymgr_web/src/routes/AccountRoute.tsx index 86f92a0..24e1808 100644 --- a/moneymgr_web/src/routes/AccountRoute.tsx +++ b/moneymgr_web/src/routes/AccountRoute.tsx @@ -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(); - setMovements(undefined); - loadKey.current += 1; + if (!skipMovements) { + setMovements(undefined); + loadKey.current += 1; + } }; if (account === null) return ; @@ -58,7 +62,9 @@ export function AccountRoute(): React.ReactElement { load={load} ready={movements !== null} errMsg="Failed to load the list of movements!" - build={() => } + build={() => ( + + )} /> @@ -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 [ + + } + label="Delete" + onClick={() => handleDeleteClick(params.row)} + color="inherit" + /> + , + ]; + }, + }, ]; return ( @@ -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); } }} /> diff --git a/moneymgr_web/src/widgets/NewMovementWidget.tsx b/moneymgr_web/src/widgets/NewMovementWidget.tsx index a10c6fc..b58bb3b 100644 --- a/moneymgr_web/src/widgets/NewMovementWidget.tsx +++ b/moneymgr_web/src/widgets/NewMovementWidget.tsx @@ -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} /> diff --git a/moneymgr_web/src/widgets/forms/AmountInput.tsx b/moneymgr_web/src/widgets/forms/AmountInput.tsx index 0c813cf..ac9bbb5 100644 --- a/moneymgr_web/src/widgets/forms/AmountInput.tsx +++ b/moneymgr_web/src/widgets/forms/AmountInput.tsx @@ -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); }} />