Can create movements from UI
This commit is contained in:
@ -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<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/movement`,
|
||||
method: "POST",
|
||||
jsonData: q,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export interface ServerConstraints {
|
||||
token_name: LenConstraint;
|
||||
token_ip_net: LenConstraint;
|
||||
token_max_inactivity: LenConstraint;
|
||||
account_name: LenConstraint;
|
||||
movement_label: LenConstraint;
|
||||
}
|
||||
|
||||
|
@ -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 <NotFoundRoute />;
|
||||
|
||||
return (
|
||||
@ -30,6 +35,7 @@ export function AccountRoute(): React.ReactElement {
|
||||
}
|
||||
>
|
||||
TODO : table
|
||||
<NewMovementWidget account={account} onCreated={reload} />
|
||||
</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;
|
||||
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" }}
|
||||
|
Reference in New Issue
Block a user