Can create movements from UI
This commit is contained in:
		
							
								
								
									
										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/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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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