Can create movements from UI
This commit is contained in:
parent
18bed77c7b
commit
09e44da46e
46
moneymgr_web/package-lock.json
generated
46
moneymgr_web/package-lock.json
generated
@ -17,7 +17,7 @@
|
|||||||
"@mui/icons-material": "^7.0.1",
|
"@mui/icons-material": "^7.0.1",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
"@mui/x-data-grid": "^7.28.3",
|
"@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",
|
"date-and-time": "^3.6.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
@ -275,9 +275,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.26.10",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
@ -1625,15 +1625,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-date-pickers": {
|
"node_modules/@mui/x-date-pickers": {
|
||||||
"version": "7.28.3",
|
"version": "8.0.0-beta.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.0.0-beta.3.tgz",
|
||||||
"integrity": "sha512-5umKB/DIMfDN+FAlzcrocix9PpoJDJ+5hMdlby8spTPObP4wCSN+wkEhk0vFC7qE9FAWXr4wjemaKvsNf41cCw==",
|
"integrity": "sha512-4N2JsUiz69TUfRLqjabW05Anrjz5fbT5jBTE5bO6uHnxei/yC1q4j+6uvbRd72zK/EQSn3iEyx6SsWuhsu37Gw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.25.7",
|
"@babel/runtime": "^7.27.0",
|
||||||
"@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta",
|
"@mui/utils": "^7.0.0",
|
||||||
"@mui/x-internals": "7.28.0",
|
"@mui/x-internals": "8.0.0-beta.3",
|
||||||
"@types/react-transition-group": "^4.4.11",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-transition-group": "^4.4.5"
|
"react-transition-group": "^4.4.5"
|
||||||
@ -1648,8 +1648,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.9.0",
|
"@emotion/react": "^11.9.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@mui/material": "^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 || ^7.0.0-beta",
|
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
||||||
"date-fns": "^2.25.0 || ^3.2.0 || ^4.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",
|
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
|
||||||
"dayjs": "^1.10.7",
|
"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": {
|
"node_modules/@mui/x-internals": {
|
||||||
"version": "7.28.0",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.28.0.tgz",
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"@mui/icons-material": "^7.0.1",
|
"@mui/icons-material": "^7.0.1",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
"@mui/x-data-grid": "^7.28.3",
|
"@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",
|
"date-and-time": "^3.6.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
|
@ -4,6 +4,15 @@ export interface Balances {
|
|||||||
[key: number]: number;
|
[key: number]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MovementUpdate {
|
||||||
|
account_id: number;
|
||||||
|
time: number;
|
||||||
|
label: String;
|
||||||
|
file_id?: number;
|
||||||
|
amount: number;
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class MovementApi {
|
export class MovementApi {
|
||||||
/**
|
/**
|
||||||
* Get all accounts balances
|
* Get all accounts balances
|
||||||
@ -16,4 +25,15 @@ export class MovementApi {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new movement
|
||||||
|
*/
|
||||||
|
static async Create(q: MovementUpdate): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: `/movement`,
|
||||||
|
method: "POST",
|
||||||
|
jsonData: q,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ export interface ServerConstraints {
|
|||||||
token_name: LenConstraint;
|
token_name: LenConstraint;
|
||||||
token_ip_net: LenConstraint;
|
token_ip_net: LenConstraint;
|
||||||
token_max_inactivity: LenConstraint;
|
token_max_inactivity: LenConstraint;
|
||||||
|
account_name: LenConstraint;
|
||||||
movement_label: LenConstraint;
|
movement_label: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { NotFoundRoute } from "./NotFound";
|
|||||||
import { AccountWidget } from "../widgets/AccountWidget";
|
import { AccountWidget } from "../widgets/AccountWidget";
|
||||||
import { AmountWidget } from "../widgets/AmountWidget";
|
import { AmountWidget } from "../widgets/AmountWidget";
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
|
import { NewMovementWidget } from "../widgets/NewMovementWidget";
|
||||||
|
|
||||||
export function AccountRoute(): React.ReactElement {
|
export function AccountRoute(): React.ReactElement {
|
||||||
const { accountId } = useParams();
|
const { accountId } = useParams();
|
||||||
@ -12,6 +13,10 @@ export function AccountRoute(): React.ReactElement {
|
|||||||
const accounts = useAccounts();
|
const accounts = useAccounts();
|
||||||
const account = accounts.get(Number(accountId));
|
const account = accounts.get(Number(accountId));
|
||||||
|
|
||||||
|
const reload = async () => {
|
||||||
|
accounts.reloadBalances();
|
||||||
|
};
|
||||||
|
|
||||||
if (account === null) return <NotFoundRoute />;
|
if (account === null) return <NotFoundRoute />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -30,6 +35,7 @@ export function AccountRoute(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
TODO : table
|
TODO : table
|
||||||
|
<NewMovementWidget account={account} onCreated={reload} />
|
||||||
</MoneyMgrWebRouteContainer>
|
</MoneyMgrWebRouteContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
98
moneymgr_web/src/widgets/NewMovementWidget.tsx
Normal file
98
moneymgr_web/src/widgets/NewMovementWidget.tsx
Normal file
@ -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<number | undefined>(time());
|
||||||
|
const [label, setLabel] = React.useState<string | undefined>("");
|
||||||
|
const [amount, setAmount] = React.useState<number | undefined>(0);
|
||||||
|
|
||||||
|
const submit = async (e: React.SyntheticEvent<any>) => {
|
||||||
|
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 (
|
||||||
|
<form
|
||||||
|
onSubmit={submit}
|
||||||
|
style={{ marginTop: "10px", display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<Typography style={{ marginRight: "10px" }}>New movement</Typography>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
autoFocus
|
||||||
|
editable
|
||||||
|
style={{ flex: 1, maxWidth: "140px" }}
|
||||||
|
value={movTime}
|
||||||
|
onValueChange={setMovTime}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
placeholder="Movement label"
|
||||||
|
value={label}
|
||||||
|
onValueChange={setLabel}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
size={ServerApi.Config.constraints.movement_label}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount"
|
||||||
|
style={{ flex: 1, maxWidth: "110px" }}
|
||||||
|
value={String(amount)}
|
||||||
|
onValueChange={(a) => setAmount(Number(a))}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Add new movement">
|
||||||
|
<IconButton onClick={submit}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<input type="submit" style={{ display: "none" }} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
28
moneymgr_web/src/widgets/forms/DateInput.tsx
Normal file
28
moneymgr_web/src/widgets/forms/DateInput.tsx
Normal file
@ -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 (
|
||||||
|
<DatePicker
|
||||||
|
autoFocus={p.autoFocus}
|
||||||
|
readOnly={p.editable === false}
|
||||||
|
label={p.label}
|
||||||
|
slotProps={{ textField: { variant: "standard", style: p.style } }}
|
||||||
|
value={timeToDate(p.value)}
|
||||||
|
onChange={(v) => {
|
||||||
|
try {
|
||||||
|
p.onValueChange(dateToTime(v ?? undefined));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse date!", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -14,6 +14,7 @@ export function TextInput(p: {
|
|||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
minRows?: number;
|
minRows?: number;
|
||||||
maxRows?: number;
|
maxRows?: number;
|
||||||
|
placeholder?: string;
|
||||||
type?: React.HTMLInputTypeAttribute;
|
type?: React.HTMLInputTypeAttribute;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
@ -48,7 +49,7 @@ export function TextInput(p: {
|
|||||||
readOnly: !p.editable,
|
readOnly: !p.editable,
|
||||||
type: p.type,
|
type: p.type,
|
||||||
},
|
},
|
||||||
htmlInput: { maxLength: p.size?.max },
|
htmlInput: { maxLength: p.size?.max, placeholder: p.placeholder },
|
||||||
}}
|
}}
|
||||||
variant={p.variant ?? "standard"}
|
variant={p.variant ?? "standard"}
|
||||||
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user