import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import LinkOffIcon from "@mui/icons-material/LinkOff"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Checkbox, FormControlLabel, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip, } from "@mui/material"; import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import React from "react"; import { InboxApi, InboxEntry } from "../api/InboxApi"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider"; import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; import { useUnmatchedInboxEntriesCount } from "../hooks/UnmatchedInboxEntriesCountProvider"; import { AmountWidget } from "../widgets/AmountWidget"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { DateWidget } from "../widgets/DateWidget"; import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; import { MovementWidget } from "../widgets/MovementWidget"; import { NewMovementWidget } from "../widgets/NewMovementWidget"; import { UploadedFileWidget } from "../widgets/UploadedFileWidget"; export function InboxRoute(): React.ReactElement { const loadingMessage = useLoadingMessage(); const alert = useAlert(); const unmatched = useUnmatchedInboxEntriesCount(); const [entries, setEntries] = React.useState(); const [includeAttached, setIncludeAttached] = React.useState(false); const loadKey = React.useRef(1); const load = async () => { setEntries(await InboxApi.GetList(includeAttached)); }; const reload = async (skipEntries: boolean) => { try { loadingMessage.show("Refreshing the list of inbox entries..."); unmatched.reload(); if (!skipEntries) await load(); } catch (e) { console.error("Failed to load list of inbox entries!", e); alert(`Failed to refresh the list of inbox entries! ${e}`); } finally { loadingMessage.hide(); } }; return ( { setIncludeAttached(value); }} /> } label="Include attached" /> reload(false)}> } >
( { setEntries((entries) => { return entries?.filter((m) => m.id !== del.id); }); }} onReload={reload} showMovements={includeAttached} /> )} /> reload(false)} />
); } function InboxTable(p: { entries: InboxEntry[]; onDeleteEntry: (entry: InboxEntry) => void; onReload: (skipEntries: boolean) => void; showMovements?: boolean; }): React.ReactElement { const alert = useAlert(); const confirm = useConfirm(); const snackbar = useSnackbar(); const loadingMessage = useLoadingMessage(); const [rowSelectionModel, setRowSelectionModel] = React.useState([]); // Delete inbox entry const handleDeleteClick = async (entry: InboxEntry) => { try { if ( !(await confirm( `Do you really want to delete this inbox entry ${ entry.label ?? "" } (${entry.amount ?? 0} €)?` )) ) return; await InboxApi.Delete(entry); p.onDeleteEntry(entry); p.onReload(true); } catch (e) { console.error("Failed to delete entry!", e); alert(`Failed to delete entry! ${e}`); } }; // Detach inbox entry from movement const handleDetachClick = async (entry: InboxEntry) => { try { if ( !(await confirm( `Do you really want to detach this inbox entry ${ entry.label ?? "" } (${entry.amount ?? 0} €) from its movement?`, "Detach inbox entry from movement", "Detach" )) ) return; entry.movement_id = undefined; await InboxApi.Update(entry); p.onReload(true); } catch (e) { console.error("Failed to detach movement from entry!", e); alert(`Failed to detach movement from entry! ${e}`); } }; const columns: GridColDef<(typeof p.entries)[number]>[] = [ { 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: 3, editable: true, type: "string", }, { field: "amount", headerName: "Amount", width: 110, editable: true, type: "number", align: "left", headerAlign: "left", renderCell: (params) => { if (params.row.amount) return ; else return Unspecified; }, }, { field: "file", headerName: "File", editable: false, flex: 3, renderCell: (params) => { return ; }, }, { field: "movement_id", headerName: "Movement", editable: false, flex: 3, renderCell: (params) => { if (params.row.movement_id) return ; }, }, { field: "actions", type: "actions", headerName: "", width: 55, cellClassName: "actions", editable: false, getActions: (params) => { return [ , ]; }, }, ]; return (
columns={columns} rows={p.entries} autoPageSize checkboxSelection initialState={{ sorting: { sortModel: [{ field: "time", sort: "desc" }], }, columns: { columnVisibilityModel: { movement_id: p.showMovements!!, }, }, }} onRowSelectionModelChange={(newRowSelectionModel) => { setRowSelectionModel(newRowSelectionModel); }} rowSelectionModel={rowSelectionModel} processRowUpdate={async (n) => { try { return await InboxApi.Update(n); } catch (e) { console.error("Failed to update movement information!", e); alert(`Failed to update row! ${e}`); throw e; } finally { p.onReload(true); } }} />
); } function InboxEntryActionMenu(p: { entry: InboxEntry; onDelete: (entry: InboxEntry) => void; onDetach: (entry: InboxEntry) => 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 ( <> {/* Unlink entry */} {p.entry.movement_id && ( { handleClose(); p.onDetach(p.entry); }} > Detach from movement )} {/* Delete */} { handleClose(); p.onDelete(p.entry); }} > Delete ); }