From 0b586039c347d78ea4e6bdabd1a91966bf2c9243 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Sat, 10 May 2025 18:37:51 +0200 Subject: [PATCH] Can create inbox entries --- moneymgr_web/src/App.tsx | 2 + moneymgr_web/src/api/InboxApi.ts | 46 +++++++ moneymgr_web/src/api/ServerApi.ts | 1 + moneymgr_web/src/routes/InboxRoute.tsx | 75 ++++++++++ moneymgr_web/src/utils/StringUtils.tsx | 7 + moneymgr_web/src/widgets/MoneyNavList.tsx | 8 +- .../src/widgets/NewMovementWidget.tsx | 129 ++++++++++++++---- .../src/widgets/forms/UploadFileButton.tsx | 4 +- 8 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 moneymgr_web/src/api/InboxApi.ts create mode 100644 moneymgr_web/src/routes/InboxRoute.tsx create mode 100644 moneymgr_web/src/utils/StringUtils.tsx diff --git a/moneymgr_web/src/App.tsx b/moneymgr_web/src/App.tsx index 4649677..6fc7b3e 100644 --- a/moneymgr_web/src/App.tsx +++ b/moneymgr_web/src/App.tsx @@ -11,6 +11,7 @@ import { AccountRoute } from "./routes/AccountRoute"; import { AccountsRoute } from "./routes/AccountsRoute"; import { BackupRoute } from "./routes/BackupRoute"; import { HomeRoute } from "./routes/HomeRoute"; +import { InboxRoute } from "./routes/InboxRoute"; import { NotFoundRoute } from "./routes/NotFound"; import { TokensRoute } from "./routes/TokensRoute"; import { LoginRoute } from "./routes/auth/LoginRoute"; @@ -45,6 +46,7 @@ export function App() { } /> } /> } /> + } /> } /> diff --git a/moneymgr_web/src/api/InboxApi.ts b/moneymgr_web/src/api/InboxApi.ts new file mode 100644 index 0000000..55a3748 --- /dev/null +++ b/moneymgr_web/src/api/InboxApi.ts @@ -0,0 +1,46 @@ +import { APIClient } from "./ApiClient"; + +export interface InboxEntry { + id: number; + file_id: number; + user_id: number; + movement_id?: number; + time: number; + label?: string; + amount?: number; + time_create: number; + time_update: number; +} + +export interface InboxEntryUpdate { + file_id: number; + movement_id?: number; + time: number; + label?: string; + amount?: number; +} + +export class InboxApi { + /** + * Create a new inbox entry + */ + static async Create(entry: InboxEntryUpdate): Promise { + await APIClient.exec({ + uri: `/inbox`, + method: "POST", + jsonData: entry, + }); + } + + /** + * Get the list of inbox entries + */ + static async GetList(includeAttached: boolean): Promise { + return ( + await APIClient.exec({ + uri: `/inbox?include_attached=${includeAttached ? "true" : "false"}`, + method: "GET", + }) + ).data; + } +} diff --git a/moneymgr_web/src/api/ServerApi.ts b/moneymgr_web/src/api/ServerApi.ts index fafceee..4abf9f4 100644 --- a/moneymgr_web/src/api/ServerApi.ts +++ b/moneymgr_web/src/api/ServerApi.ts @@ -19,6 +19,7 @@ export interface ServerConstraints { token_max_inactivity: LenConstraint; account_name: LenConstraint; movement_label: LenConstraint; + inbox_entry_label: LenConstraint; file_allowed_types: string[]; } diff --git a/moneymgr_web/src/routes/InboxRoute.tsx b/moneymgr_web/src/routes/InboxRoute.tsx new file mode 100644 index 0000000..7d30d2f --- /dev/null +++ b/moneymgr_web/src/routes/InboxRoute.tsx @@ -0,0 +1,75 @@ +import RefreshIcon from "@mui/icons-material/Refresh"; +import { Checkbox, FormControlLabel, IconButton, Tooltip } from "@mui/material"; +import React from "react"; +import { InboxApi, InboxEntry } from "../api/InboxApi"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; +import { NewMovementWidget } from "../widgets/NewMovementWidget"; + +export function InboxRoute(): React.ReactElement { + const loadingMessage = useLoadingMessage(); + const alert = useAlert(); + + 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..."); + // TODO : trigger reload number of inbox entries + 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(e.target.checked); + reload(false); + }} + /> + } + label="Include attached" + /> + + reload(false)}> + + + + + } + > +
+
+ <>todo table} + /> + reload(false)} /> +
+
+
+ ); +} diff --git a/moneymgr_web/src/utils/StringUtils.tsx b/moneymgr_web/src/utils/StringUtils.tsx new file mode 100644 index 0000000..fc925b2 --- /dev/null +++ b/moneymgr_web/src/utils/StringUtils.tsx @@ -0,0 +1,7 @@ +/** + * Make the first letter of a word uppercase + */ +export function firstLetterUppercase(s: string): string { + if (s.length === 0) return s; + return s.charAt(0).toUpperCase() + s.slice(1); +} diff --git a/moneymgr_web/src/widgets/MoneyNavList.tsx b/moneymgr_web/src/widgets/MoneyNavList.tsx index 4f2c4ea..d60777f 100644 --- a/moneymgr_web/src/widgets/MoneyNavList.tsx +++ b/moneymgr_web/src/widgets/MoneyNavList.tsx @@ -1,4 +1,4 @@ -import { mdiCashMultiple, mdiHome } from "@mdi/js"; +import { mdiCashMultiple, mdiHome, mdiInbox } from "@mdi/js"; import Icon from "@mdi/react"; import { Divider, @@ -35,6 +35,12 @@ export function MoneyNavList(): React.ReactElement { uri="/accounts" icon={} /> + {/* TODO : show number of unmatched */} + } + /> {accounts.list.isEmpty && ( void; -}): React.ReactElement { +export function NewMovementWidget( + p: { + onCreated: () => void; + } & ({ isInbox: true } | { isInbox?: undefined; account: Account }) +): React.ReactElement { const snackbar = useSnackbar(); const alert = useAlert(); const dateInputRef = useRef(null); + const [file, setFile] = React.useState(); const [movTime, setMovTime] = React.useState(time()); const [label, setLabel] = React.useState(""); const [amount, setAmount] = React.useState(0); + const entity = p.isInbox ? "inbox entry" : "movement"; + const submit = async (e: React.SyntheticEvent) => { e.preventDefault(); - if ((label?.length ?? 0) === 0) { - alert("Please specify movement label!"); + if (!p.isInbox && (label?.length ?? 0) === 0) { + alert(`Please specify ${entity} label!`); + return; + } + + if (p.isInbox && !file) { + alert(`Please specify ${entity} file!`); return; } if (!movTime) { - alert("Please specify movement date!"); + alert(`Please specify ${entity} date!`); return; } try { - await MovementApi.Create({ - account_id: p.account.id, - checked: false, - amount: amount!, - label: label!, - time: movTime, - }); + if (!p.isInbox) { + await MovementApi.Create({ + account_id: p.account.id, + checked: false, + amount: amount!, + label: label!, + time: movTime, + }); + } else { + await InboxApi.Create({ + file_id: file!.id, + amount: amount, + label: label, + time: movTime, + }); + } - snackbar("The movement was successfully created!"); + snackbar(`The ${entity} was successfully created!`); p.onCreated(); + setFile(undefined); setLabel(""); setAmount(0); // Give back focus to date input dateInputRef.current?.querySelector("input")?.focus(); } catch (e) { - console.error(`Failed to create movement!`, e); - alert(`Failed to create movement! ${e}`); + console.error(`Failed to create ${entity}!`, e); + alert(`Failed to create ${entity}! ${e}`); } }; @@ -66,8 +91,45 @@ export function NewMovementWidget(p: { onSubmit={submit} style={{ marginTop: "10px", display: "flex", alignItems: "center" }} > - New movement + {/* Label */} + {/* Add label only when creating movement */} + {!p.isInbox ? ( + New {entity} + ) : ( + <> + )} + {/* File input */} + {/* Add file only when creating inbox entries */} + {p.isInbox ? ( + file ? ( + <> + + + + setFile(undefined)}> + + + + ) : ( + + ) + ) : ( + <> + )}   + {/* Date input */}   + {/* Label input */}   + {/* Amount input */} - + {/* Submit button */} + diff --git a/moneymgr_web/src/widgets/forms/UploadFileButton.tsx b/moneymgr_web/src/widgets/forms/UploadFileButton.tsx index 4a2b48b..9ef832c 100644 --- a/moneymgr_web/src/widgets/forms/UploadFileButton.tsx +++ b/moneymgr_web/src/widgets/forms/UploadFileButton.tsx @@ -13,6 +13,7 @@ export function UploadFileButton(p: { onUploaded: (file: UploadedFile) => void; label: string; tooltip: string; + disableSuccessSnackBar?: boolean; }): React.ReactElement { const alert = useAlert(); const loadingMessage = useLoadingMessage(); @@ -39,7 +40,8 @@ export function UploadFileButton(p: { const result = await FileApi.UploadFile(file[0]); - snackbar("The file was successfully uploaded!"); + if (!p.disableSuccessSnackBar) + snackbar("The file was successfully uploaded!"); p.onUploaded(result); } catch (e) {