import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import DriveFileMoveOutlineIcon from "@mui/icons-material/DriveFileMoveOutline"; import LinkOffIcon from "@mui/icons-material/LinkOff"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import RefreshIcon from "@mui/icons-material/Refresh"; import { IconButton, ListItemIcon, ListItemText, TextField, Tooltip, Typography, } from "@mui/material"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import React from "react"; import { useParams } from "react-router-dom"; import { UploadedFile } from "../api/FileApi"; import { Movement, MovementApi } from "../api/MovementsApi"; import { useAccounts } from "../hooks/AccountsListProvider"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useSelectAccount } from "../hooks/context_providers/ChooseAccountDialogProvider"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider"; import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; import { AccountIconWidget } from "../widgets/AccountIconWidget"; import { AmountWidget } from "../widgets/AmountWidget"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { DateWidget } from "../widgets/DateWidget"; import { UploadFileButton } from "../widgets/forms/UploadFileButton"; import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; import { NewMovementWidget } from "../widgets/NewMovementWidget"; import { UploadedFileWidget } from "../widgets/UploadedFileWidget"; import { NotFoundRoute } from "./NotFound"; export function AccountRoute(): React.ReactElement { const loadingMessage = useLoadingMessage(); const { accountId } = useParams(); const loadKey = React.useRef(0); const accounts = useAccounts(); const account = accounts.get(Number(accountId)); const [movements, setMovements] = React.useState(); const load = async () => { setMovements(await MovementApi.GetAccountMovements(account!.id)); }; const reload = async (skipMovements = false) => { try { accounts.reloadBalances(); if (!skipMovements) { loadingMessage.show("Refreshing the list of movements"); await load(); } } catch (e) { console.error("Failed to load list of movements!", e); alert(`Failed to refresh the list of movements! ${e}`); } finally { loadingMessage.hide(); } }; if (account === null) return ; return (   {account.name} } >
( { setMovements((movements) => { return movements?.filter((m) => m.id !== del.id); }); }} /> )} />
); } function MovementsTable(p: { movements: Movement[]; needReload: (skipMovements: boolean) => void; onDeleteMovement: (movement: Movement) => void; }): React.ReactElement { const accounts = useAccounts(); const alert = useAlert(); const confirm = useConfirm(); const snackbar = useSnackbar(); const loadingMessage = useLoadingMessage(); const chooseAccount = useSelectAccount(); const [labelFilter, setLabelFilter] = React.useState(""); const filteredList = React.useMemo(() => { return p.movements.filter((m) => m.label.toLowerCase().includes(labelFilter.toLowerCase()) ); }, [p.movements, labelFilter]); const [rowSelectionModel, setRowSelectionModel] = React.useState([]); // Set uploaded file const setUploadedFile = async ( m: Movement, file: UploadedFile | undefined ) => { try { await MovementApi.Update({ ...m, file_id: file?.id ?? undefined }); p.needReload(false); } catch (e) { console.error("Failed to attach file to movement!", e); alert("Failed to attach uploaded file to movement!"); } }; // Change account of movement const handleMoveClick = async (movement: Movement) => { const targetAccount = await chooseAccount( "Transfer movement", `Please select the target account that will receive the movement: ${movement.label} (${movement.amount} €)`, "Transfer movement", [accounts.get(movement.account_id)!] ); if (!targetAccount) return; try { movement.account_id = targetAccount.id; await MovementApi.Update(movement); snackbar("The movement has been successfully moved!"); p.needReload(false); } catch (e) { console.error("Failed to update movement information!", e); alert(`Failed to change movemlent account! ${e}`); } }; // Detach file from movement const handleDetachFile = async (movement: Movement) => { try { if ( !(await confirm( `Do you really want to detach the file attached to the movement ${movement.label} (${movement.amount} €)? The associated file will be automatically deleted within the day if it is not referenced anywhere else!`, `Detach file from movement`, `Detach file` )) ) return; await setUploadedFile(movement, undefined); } catch (e) { console.error("Failed to detach file from movement!", e); alert(`Failed to detach file from movement! ${e}`); } }; // Delete movement 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); p.onDeleteMovement(movement); p.needReload(true); } catch (e) { console.error("Failed to delete movement!", e); alert(`Failed to delete movement! ${e}`); } }; // Move multiple movements const moveMultiple = async () => { try { const movements = p.movements.filter((m) => rowSelectionModel.includes(m.id) ); const targetAccount = await chooseAccount( "Transfer movements", <> Please select the target account that will receive the selected movements:
    {movements.map((m) => (
  • {m.label} ({m.amount} €)
  • ))}
, "Transfer movement", [accounts.get(movements[0].account_id)!] ); if (!targetAccount) return; for (const [num, m] of movements.entries()) { loadingMessage.show(`Moveing movement ${num}/${movements.length}`); m.account_id = targetAccount.id; await MovementApi.Update(m); } snackbar("The movements have been successfully moved!"); p.needReload(false); } catch (e) { console.error("Failed to delete multiple movements!", e); alert(`Failed to delete multiple movements! ${e}`); } finally { loadingMessage.hide(); } }; // Delete multiple movements const deleteMultiple = async () => { try { const movements = p.movements.filter((m) => rowSelectionModel.includes(m.id) ); if ( !(await confirm( <> Do you really want to delete the following movements:
    {movements.map((m) => (
  • {m.label} ({m.amount} €)
  • ))}
)) ) return; for (const [num, m] of movements.entries()) { loadingMessage.show(`Deleting movement ${num}/${movements.length}`); await MovementApi.Delete(m); } snackbar("The movements have been successfully deleted!"); p.needReload(false); } catch (e) { console.error("Failed to delete multiple movements!", e); alert(`Failed to delete multiple movements! ${e}`); } finally { loadingMessage.hide(); } }; const columns: GridColDef<(typeof p.movements)[number]>[] = [ { field: "checked", headerName: "Checked", width: 50, type: "boolean", editable: true, }, { field: "time", headerName: "Date", width: 98 + 80, editable: true, type: "dateTime", valueGetter(_, m) { return new Date(m.time * 1000); }, valueSetter(v, row) { row.time = Math.floor(v.getTime() / 1000); return row; }, renderCell: (params) => { return ; }, }, { field: "label", headerName: "Label", flex: 6, editable: true, type: "string", }, { field: "amount", headerName: "Amount", width: 110, editable: true, type: "number", align: "left", headerAlign: "left", renderCell: (params) => { return ; }, }, { field: "file", headerName: "File", editable: false, flex: 3, renderCell: (params) => { if (!params.row.file_id) return ( setUploadedFile(params.row, f)} /> ); else return ; }, }, { field: "actions", type: "actions", headerName: "", width: 55, cellClassName: "actions", editable: false, getActions: (params) => { return [ , ]; }, }, ]; return ( <>
{ setLabelFilter(e.target.value); }} style={{ padding: "0px", flex: 1 }} /> p.needReload(false)}>
columns={columns} rows={filteredList} autoPageSize checkboxSelection initialState={{ sorting: { sortModel: [{ field: "time", sort: "desc" }], }, columns: { columnVisibilityModel: { checked: false, }, }, }} onRowSelectionModelChange={(newRowSelectionModel) => { setRowSelectionModel(newRowSelectionModel); }} rowSelectionModel={rowSelectionModel} processRowUpdate={async (n) => { try { return await MovementApi.Update(n); } catch (e) { console.error("Failed to update movement information!", e); alert(`Failed to update row! ${e}`); throw e; } finally { p.needReload(true); } }} />
); } function MovementActionMenu(p: { movement: Movement; onDetachFile: (m: Movement) => void; onMove: (m: Movement) => void; onDelete: (m: Movement) => void; }): React.ReactElement { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return ( <> {/* Detach file */} {p.movement.file_id && ( { handleClose(); p.onDetachFile(p.movement); }} > Detach file )} {/* Move to another account */} { handleClose(); p.onMove(p.movement); }} > Move {/* Delete */} { handleClose(); p.onDelete(p.movement); }} > Delete ); }