diff --git a/moneymgr_web/package-lock.json b/moneymgr_web/package-lock.json index 79943ac..ed73a2e 100644 --- a/moneymgr_web/package-lock.json +++ b/moneymgr_web/package-lock.json @@ -17,7 +17,7 @@ "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", "@mui/x-data-grid": "^7.28.3", - "@mui/x-date-pickers": "^7.28.3", + "@mui/x-date-pickers": "^8.0.0-beta.3", "date-and-time": "^3.6.0", "dayjs": "^1.11.13", "qrcode.react": "^4.2.0", @@ -275,9 +275,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1625,15 +1625,15 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.28.3.tgz", - "integrity": "sha512-5umKB/DIMfDN+FAlzcrocix9PpoJDJ+5hMdlby8spTPObP4wCSN+wkEhk0vFC7qE9FAWXr4wjemaKvsNf41cCw==", + "version": "8.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.0.0-beta.3.tgz", + "integrity": "sha512-4N2JsUiz69TUfRLqjabW05Anrjz5fbT5jBTE5bO6uHnxei/yC1q4j+6uvbRd72zK/EQSn3iEyx6SsWuhsu37Gw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", - "@mui/x-internals": "7.28.0", - "@types/react-transition-group": "^4.4.11", + "@babel/runtime": "^7.27.0", + "@mui/utils": "^7.0.0", + "@mui/x-internals": "8.0.0-beta.3", + "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" @@ -1648,8 +1648,8 @@ "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", "dayjs": "^1.10.7", @@ -1690,6 +1690,26 @@ } } }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/x-internals": { + "version": "8.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.0.0-beta.3.tgz", + "integrity": "sha512-crbtLMWhI0sFXaZLknXPEGEaPLxpdIe8XAkJIr0HXD563TagGeyVk8lbNLoa5H3mVHWxmzNYiGUA4ns5Q6urQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@mui/utils": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-internals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.28.0.tgz", diff --git a/moneymgr_web/package.json b/moneymgr_web/package.json index 250cfb5..7aff60d 100644 --- a/moneymgr_web/package.json +++ b/moneymgr_web/package.json @@ -19,7 +19,7 @@ "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", "@mui/x-data-grid": "^7.28.3", - "@mui/x-date-pickers": "^7.28.3", + "@mui/x-date-pickers": "^8.0.0-beta.3", "date-and-time": "^3.6.0", "dayjs": "^1.11.13", "qrcode.react": "^4.2.0", diff --git a/moneymgr_web/src/api/MovementsApi.ts b/moneymgr_web/src/api/MovementsApi.ts index 96b66ce..ebb2dca 100644 --- a/moneymgr_web/src/api/MovementsApi.ts +++ b/moneymgr_web/src/api/MovementsApi.ts @@ -4,6 +4,15 @@ export interface Balances { [key: number]: number; } +export interface MovementUpdate { + account_id: number; + time: number; + label: String; + file_id?: number; + amount: number; + checked: boolean; +} + export class MovementApi { /** * Get all accounts balances @@ -16,4 +25,15 @@ export class MovementApi { }) ).data; } + + /** + * Create a new movement + */ + static async Create(q: MovementUpdate): Promise { + await APIClient.exec({ + uri: `/movement`, + method: "POST", + jsonData: q, + }); + } } diff --git a/moneymgr_web/src/api/ServerApi.ts b/moneymgr_web/src/api/ServerApi.ts index 6e74c22..8fc6647 100644 --- a/moneymgr_web/src/api/ServerApi.ts +++ b/moneymgr_web/src/api/ServerApi.ts @@ -17,6 +17,7 @@ export interface ServerConstraints { token_name: LenConstraint; token_ip_net: LenConstraint; token_max_inactivity: LenConstraint; + account_name: LenConstraint; movement_label: LenConstraint; } diff --git a/moneymgr_web/src/routes/AccountRoute.tsx b/moneymgr_web/src/routes/AccountRoute.tsx index 15dd9f1..1d68ede 100644 --- a/moneymgr_web/src/routes/AccountRoute.tsx +++ b/moneymgr_web/src/routes/AccountRoute.tsx @@ -5,6 +5,7 @@ import { NotFoundRoute } from "./NotFound"; import { AccountWidget } from "../widgets/AccountWidget"; import { AmountWidget } from "../widgets/AmountWidget"; import { Typography } from "@mui/material"; +import { NewMovementWidget } from "../widgets/NewMovementWidget"; export function AccountRoute(): React.ReactElement { const { accountId } = useParams(); @@ -12,6 +13,10 @@ export function AccountRoute(): React.ReactElement { const accounts = useAccounts(); const account = accounts.get(Number(accountId)); + const reload = async () => { + accounts.reloadBalances(); + }; + if (account === null) return ; return ( @@ -30,6 +35,7 @@ export function AccountRoute(): React.ReactElement { } > TODO : table + ); } diff --git a/moneymgr_web/src/widgets/NewMovementWidget.tsx b/moneymgr_web/src/widgets/NewMovementWidget.tsx new file mode 100644 index 0000000..27e2a6b --- /dev/null +++ b/moneymgr_web/src/widgets/NewMovementWidget.tsx @@ -0,0 +1,98 @@ +import { IconButton, Tooltip, Typography } from "@mui/material"; +import { Account } from "../api/AccountApi"; +import { DateInput } from "./forms/DateInput"; +import { time } from "../utils/DateUtils"; +import React from "react"; +import { TextInput } from "./forms/TextInput"; +import { ServerApi } from "../api/ServerApi"; +import AddIcon from "@mui/icons-material/Add"; +import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; +import { MovementApi } from "../api/MovementsApi"; + +export function NewMovementWidget(p: { + account: Account; + onCreated: () => {}; +}): React.ReactElement { + const snackbar = useSnackbar(); + const alert = useAlert(); + + const [movTime, setMovTime] = React.useState(time()); + const [label, setLabel] = React.useState(""); + const [amount, setAmount] = React.useState(0); + + const submit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + + if ((label?.length ?? 0) === 0) { + alert("Please specify movement label!"); + return; + } + + if (!movTime) { + alert("Please specify movement date!"); + return; + } + + try { + await MovementApi.Create({ + account_id: p.account.id, + checked: false, + amount: amount!, + label: label!, + time: movTime, + }); + + snackbar("The movement was successfully created!"); + + p.onCreated(); + + setLabel(""); + setAmount(0); + } catch (e) { + console.error(`Failed to create movement!`, e); + alert(`Failed to create movement! ${e}`); + } + }; + + return ( +
+ New movement +   + +   + +   + setAmount(Number(a))} + /> + + + + + + + + ); +} diff --git a/moneymgr_web/src/widgets/forms/DateInput.tsx b/moneymgr_web/src/widgets/forms/DateInput.tsx new file mode 100644 index 0000000..afded24 --- /dev/null +++ b/moneymgr_web/src/widgets/forms/DateInput.tsx @@ -0,0 +1,28 @@ +import { DatePicker } from "@mui/x-date-pickers"; +import { dateToTime, timeToDate } from "../../utils/DateUtils"; + +export function DateInput(p: { + editable?: boolean; + autoFocus?: boolean; + label?: string; + style?: React.CSSProperties; + value: number | undefined; + onValueChange: (v: number | undefined) => void; +}): React.ReactElement { + return ( + { + try { + p.onValueChange(dateToTime(v ?? undefined)); + } catch (e) { + console.warn("Failed to parse date!", e); + } + }} + /> + ); +} diff --git a/moneymgr_web/src/widgets/forms/TextInput.tsx b/moneymgr_web/src/widgets/forms/TextInput.tsx index 5913537..5303d48 100644 --- a/moneymgr_web/src/widgets/forms/TextInput.tsx +++ b/moneymgr_web/src/widgets/forms/TextInput.tsx @@ -14,6 +14,7 @@ export function TextInput(p: { multiline?: boolean; minRows?: number; maxRows?: number; + placeholder?: string; type?: React.HTMLInputTypeAttribute; style?: React.CSSProperties; helperText?: string; @@ -48,7 +49,7 @@ export function TextInput(p: { readOnly: !p.editable, type: p.type, }, - htmlInput: { maxLength: p.size?.max }, + htmlInput: { maxLength: p.size?.max, placeholder: p.placeholder }, }} variant={p.variant ?? "standard"} style={p.style ?? { width: "100%", marginBottom: "15px" }}