Compare commits
	
		
			7 Commits
		
	
	
		
			1.0.0
			...
			e3249d19c0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e3249d19c0 | |||
| bed538793d | |||
| 602f20ad18 | |||
| 457c96b37e | |||
| a3f2b77548 | |||
| 3c5c82371a | |||
| fb46626cff | 
							
								
								
									
										661
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										661
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,33 +6,34 @@ edition = "2024"
 | 
			
		||||
[dependencies]
 | 
			
		||||
env_logger = "0.11.8"
 | 
			
		||||
log = "0.4.27"
 | 
			
		||||
diesel = { version = "2.2.0", features = ["postgres", "r2d2"] }
 | 
			
		||||
diesel = { version = "2.2.10", features = ["postgres", "r2d2"] }
 | 
			
		||||
diesel_migrations = "2.1.0"
 | 
			
		||||
clap = { version = "4.5.35", features = ["env", "derive"] }
 | 
			
		||||
actix-web = "4"
 | 
			
		||||
actix-cors = "0.7.0"
 | 
			
		||||
actix-multipart = "0.7.0"
 | 
			
		||||
clap = { version = "4.5.38", features = ["env", "derive"] }
 | 
			
		||||
actix-web = "4.11.0"
 | 
			
		||||
actix-cors = "0.7.1"
 | 
			
		||||
actix-multipart = "0.7.2"
 | 
			
		||||
actix-remote-ip = "0.1.0"
 | 
			
		||||
actix-session = { version = "0.10.0", features = ["redis-session"] }
 | 
			
		||||
actix-session = { version = "0.10.1", features = ["redis-session"] }
 | 
			
		||||
actix-files = "0.6.6"
 | 
			
		||||
lazy_static = "1.5.0"
 | 
			
		||||
anyhow = "1.0.97"
 | 
			
		||||
anyhow = "1.0.98"
 | 
			
		||||
serde = { version = "1.0.219", features = ["derive"] }
 | 
			
		||||
rust-s3 = "0.36.0-beta.2"
 | 
			
		||||
thiserror = "2.0.12"
 | 
			
		||||
tokio = "1.44.1"
 | 
			
		||||
tokio = "1.45.0"
 | 
			
		||||
futures-util = "0.3.31"
 | 
			
		||||
serde_json = "1.0.140"
 | 
			
		||||
light-openid = "1.0.4"
 | 
			
		||||
rand = "0.9.0"
 | 
			
		||||
rand = "0.9.1"
 | 
			
		||||
ipnet = { version = "2.11.0", features = ["serde"] }
 | 
			
		||||
lazy-regex = "3.4.1"
 | 
			
		||||
jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] }
 | 
			
		||||
mime_guess = "2.0.5"
 | 
			
		||||
rust-embed = { version = "8.6.0" }
 | 
			
		||||
sha2 = "0.10.8"
 | 
			
		||||
rust-embed = { version = "8.7.2" }
 | 
			
		||||
sha2 = "0.11.0-pre.5"
 | 
			
		||||
base16ct = "0.2.0"
 | 
			
		||||
httpdate = "1.0.3"
 | 
			
		||||
chrono = "0.4.41"
 | 
			
		||||
tempfile = "3.19.1"
 | 
			
		||||
zip = "2.6.1"
 | 
			
		||||
rust_xlsxwriter = "0.86.1"
 | 
			
		||||
tempfile = "3.20.0"
 | 
			
		||||
zip = "3.0.0"
 | 
			
		||||
rust_xlsxwriter = "0.87.0"
 | 
			
		||||
@@ -78,12 +78,15 @@ pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) -
 | 
			
		||||
 | 
			
		||||
/// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
 | 
			
		||||
pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult {
 | 
			
		||||
    let accounts = accounts_service::get_list_user(auth.user_id()).await?;
 | 
			
		||||
    let mut accounts = accounts_service::get_list_user(auth.user_id()).await?;
 | 
			
		||||
    accounts.sort_by_key(|a| a.id());
 | 
			
		||||
 | 
			
		||||
    let mut out = FinancesManagerFile { accounts: vec![] };
 | 
			
		||||
 | 
			
		||||
    for account in accounts {
 | 
			
		||||
        let movements = movements_service::get_list_account(account.id()).await?;
 | 
			
		||||
        let mut movements = movements_service::get_list_account(account.id()).await?;
 | 
			
		||||
        movements.sort_by(|a, b| b.time.cmp(&a.time));
 | 
			
		||||
 | 
			
		||||
        let mut file_account = FinancesManagerAccount {
 | 
			
		||||
            name: account.name,
 | 
			
		||||
            movements: Vec::with_capacity(movements.len()),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@ pub fn sha512(bytes: &[u8]) -> String {
 | 
			
		||||
    let mut hasher = Sha512::new();
 | 
			
		||||
    hasher.update(bytes);
 | 
			
		||||
    let h = hasher.finalize();
 | 
			
		||||
    format!("{:x}", h)
 | 
			
		||||
    base16ct::lower::encode_string(h.as_slice())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1483
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1483
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -16,34 +16,34 @@
 | 
			
		||||
    "@jsonjoy.com/base64": "^1.1.2",
 | 
			
		||||
    "@mdi/js": "^7.4.47",
 | 
			
		||||
    "@mdi/react": "^1.6.1",
 | 
			
		||||
    "@mui/icons-material": "^7.0.1",
 | 
			
		||||
    "@mui/material": "^7.0.1",
 | 
			
		||||
    "@mui/x-charts": "^8.2.0",
 | 
			
		||||
    "@mui/x-data-grid": "^7.28.3",
 | 
			
		||||
    "@mui/x-date-pickers": "^8.0.0-beta.3",
 | 
			
		||||
    "@mui/icons-material": "^7.1.0",
 | 
			
		||||
    "@mui/material": "^7.1.0",
 | 
			
		||||
    "@mui/x-charts": "^8.3.1",
 | 
			
		||||
    "@mui/x-data-grid": "^8.3.1",
 | 
			
		||||
    "@mui/x-date-pickers": "^8.3.1",
 | 
			
		||||
    "date-and-time": "^3.6.0",
 | 
			
		||||
    "dayjs": "^1.11.13",
 | 
			
		||||
    "filesize": "^10.1.6",
 | 
			
		||||
    "qrcode.react": "^4.2.0",
 | 
			
		||||
    "react": "^19.1.0",
 | 
			
		||||
    "react-dom": "^19.1.0",
 | 
			
		||||
    "react-router": "^7.4.1",
 | 
			
		||||
    "react-router-dom": "^7.4.1",
 | 
			
		||||
    "react-router": "^7.6.0",
 | 
			
		||||
    "react-router-dom": "^7.6.0",
 | 
			
		||||
    "ts-pattern": "^5.7.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.23.0",
 | 
			
		||||
    "@types/react": "^19.1.0",
 | 
			
		||||
    "@types/react-dom": "^19.1.1",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "eslint": "^9.23.0",
 | 
			
		||||
    "eslint-plugin-react-dom": "^1.40.2",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.1.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.19",
 | 
			
		||||
    "eslint-plugin-react-x": "^1.40.2",
 | 
			
		||||
    "globals": "^16.0.0",
 | 
			
		||||
    "typescript": "~5.8.2",
 | 
			
		||||
    "typescript-eslint": "^8.29.0",
 | 
			
		||||
    "vite": "^6.2.5"
 | 
			
		||||
    "@eslint/js": "^9.26.0",
 | 
			
		||||
    "@types/react": "^19.1.4",
 | 
			
		||||
    "@types/react-dom": "^19.1.5",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.4.1",
 | 
			
		||||
    "eslint": "^9.27.0",
 | 
			
		||||
    "eslint-plugin-react-dom": "^1.49.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^00.4.20",
 | 
			
		||||
    "eslint-plugin-react-x": "^1.49.0",
 | 
			
		||||
    "globals": "^16.1.0",
 | 
			
		||||
    "typescript": "~5.8.3",
 | 
			
		||||
    "typescript-eslint": "^8.32.1",
 | 
			
		||||
    "vite": "^6.3.5"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -121,16 +121,18 @@ function MovementsTable(p: {
 | 
			
		||||
 | 
			
		||||
  const chooseAccount = useSelectAccount();
 | 
			
		||||
 | 
			
		||||
  const [labelFilter, setLabelFilter] = React.useState("");
 | 
			
		||||
  const [filter, setFilter] = React.useState("");
 | 
			
		||||
 | 
			
		||||
  const filteredList = React.useMemo(() => {
 | 
			
		||||
    return p.movements.filter((m) =>
 | 
			
		||||
      m.label.toLowerCase().includes(labelFilter.toLowerCase())
 | 
			
		||||
    return p.movements.filter(
 | 
			
		||||
      (m) =>
 | 
			
		||||
        m.label.toLowerCase().includes(filter.toLowerCase()) ||
 | 
			
		||||
        m.amount.toString().includes(filter)
 | 
			
		||||
    );
 | 
			
		||||
  }, [p.movements, labelFilter]);
 | 
			
		||||
  }, [p.movements, filter]);
 | 
			
		||||
 | 
			
		||||
  const [rowSelectionModel, setRowSelectionModel] =
 | 
			
		||||
    React.useState<GridRowSelectionModel>([]);
 | 
			
		||||
    React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
 | 
			
		||||
 | 
			
		||||
  // Set uploaded file
 | 
			
		||||
  const setUploadedFile = async (
 | 
			
		||||
@@ -216,7 +218,7 @@ function MovementsTable(p: {
 | 
			
		||||
  const moveMultiple = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const movements = p.movements.filter((m) =>
 | 
			
		||||
        rowSelectionModel.includes(m.id)
 | 
			
		||||
        rowSelectionModel.ids.has(m.id)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const targetAccount = await chooseAccount(
 | 
			
		||||
@@ -260,7 +262,7 @@ function MovementsTable(p: {
 | 
			
		||||
  const deleteMultiple = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const movements = p.movements.filter((m) =>
 | 
			
		||||
        rowSelectionModel.includes(m.id)
 | 
			
		||||
        rowSelectionModel.ids.has(m.id)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
@@ -382,26 +384,30 @@ function MovementsTable(p: {
 | 
			
		||||
    <>
 | 
			
		||||
      <div style={{ display: "flex" }}>
 | 
			
		||||
        <TextField
 | 
			
		||||
          placeholder="Filter by label"
 | 
			
		||||
          placeholder="Filter by label or amount"
 | 
			
		||||
          variant="standard"
 | 
			
		||||
          size="small"
 | 
			
		||||
          value={labelFilter}
 | 
			
		||||
          value={filter}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            setLabelFilter(e.target.value);
 | 
			
		||||
            setFilter(e.target.value);
 | 
			
		||||
          }}
 | 
			
		||||
          style={{ padding: "0px", flex: 1 }}
 | 
			
		||||
        />
 | 
			
		||||
        <span style={{ flex: 1 }}></span>
 | 
			
		||||
        <Tooltip title="Refresh table">
 | 
			
		||||
          <IconButton onClick={() => { p.needReload(false); }}>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              p.needReload(false);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <RefreshIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
        <Tooltip title="Move all the selected entries to another account">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            disabled={
 | 
			
		||||
              rowSelectionModel.length === 0 ||
 | 
			
		||||
              rowSelectionModel.length === p.movements.length
 | 
			
		||||
              rowSelectionModel.ids.size === 0 ||
 | 
			
		||||
              rowSelectionModel.ids.size === p.movements.length
 | 
			
		||||
            }
 | 
			
		||||
            onClick={moveMultiple}
 | 
			
		||||
          >
 | 
			
		||||
@@ -411,8 +417,8 @@ function MovementsTable(p: {
 | 
			
		||||
        <Tooltip title="Delete all the selected entries">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            disabled={
 | 
			
		||||
              rowSelectionModel.length === 0 ||
 | 
			
		||||
              rowSelectionModel.length === p.movements.length
 | 
			
		||||
              rowSelectionModel.ids.size === 0 ||
 | 
			
		||||
              rowSelectionModel.ids.size === p.movements.length
 | 
			
		||||
            }
 | 
			
		||||
            onClick={deleteMultiple}
 | 
			
		||||
          >
 | 
			
		||||
 
 | 
			
		||||
@@ -158,7 +158,7 @@ function ImportExportModal(p: {
 | 
			
		||||
        </CardContent>{" "}
 | 
			
		||||
        <CardActions>
 | 
			
		||||
          <span style={{ flex: 1 }}>
 | 
			
		||||
            <RouterLink to={p.exportURL}>
 | 
			
		||||
            <RouterLink to={p.exportURL} target="_blank">
 | 
			
		||||
              <Button
 | 
			
		||||
                startIcon={<DownloadIcon />}
 | 
			
		||||
                variant="outlined"
 | 
			
		||||
 
 | 
			
		||||
@@ -133,7 +133,7 @@ function InboxTable(p: {
 | 
			
		||||
  }, [p.entries, labelFilter]);
 | 
			
		||||
 | 
			
		||||
  const [rowSelectionModel, setRowSelectionModel] =
 | 
			
		||||
    React.useState<GridRowSelectionModel>([]);
 | 
			
		||||
    React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
 | 
			
		||||
 | 
			
		||||
  const [attaching, setAttaching] = React.useState<InboxEntry | undefined>();
 | 
			
		||||
 | 
			
		||||
@@ -244,7 +244,7 @@ function InboxTable(p: {
 | 
			
		||||
 | 
			
		||||
      // Find the entry to map
 | 
			
		||||
      const entries = p.entries.filter(
 | 
			
		||||
        (m) => rowSelectionModel.includes(m.id) && !m.movement_id
 | 
			
		||||
        (m) => rowSelectionModel.ids.has(m.id) && !m.movement_id
 | 
			
		||||
      );
 | 
			
		||||
      const movements: Movement[][] = [];
 | 
			
		||||
 | 
			
		||||
@@ -324,7 +324,7 @@ function InboxTable(p: {
 | 
			
		||||
  const deleteMultiple = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const deletedEntries = p.entries.filter((m) =>
 | 
			
		||||
        rowSelectionModel.includes(m.id)
 | 
			
		||||
        rowSelectionModel.ids.has(m.id)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
@@ -437,7 +437,9 @@ function InboxTable(p: {
 | 
			
		||||
              icon={<SearchIcon />}
 | 
			
		||||
              label="Attach entry to movement"
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              onClick={() => { handleAttachClick(params.row); }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                handleAttachClick(params.row);
 | 
			
		||||
              }}
 | 
			
		||||
              disabled={!!params.row.movement_id}
 | 
			
		||||
            />
 | 
			
		||||
          </Tooltip>,
 | 
			
		||||
@@ -496,7 +498,7 @@ function InboxTable(p: {
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
        <Tooltip title="Attach all the selected inbox entries to movements">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            disabled={rowSelectionModel.length === 0}
 | 
			
		||||
            disabled={rowSelectionModel.ids.size === 0}
 | 
			
		||||
            onClick={attachMultiple}
 | 
			
		||||
          >
 | 
			
		||||
            <SearchIcon />
 | 
			
		||||
@@ -505,8 +507,8 @@ function InboxTable(p: {
 | 
			
		||||
        <Tooltip title="Delete all the selected inbox entries">
 | 
			
		||||
          <IconButton
 | 
			
		||||
            disabled={
 | 
			
		||||
              rowSelectionModel.length === 0 ||
 | 
			
		||||
              rowSelectionModel.length === p.entries.length
 | 
			
		||||
              rowSelectionModel.ids.size === 0 ||
 | 
			
		||||
              rowSelectionModel.ids.size === p.entries.length
 | 
			
		||||
            }
 | 
			
		||||
            onClick={deleteMultiple}
 | 
			
		||||
          >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { DatePicker } from "@mui/x-date-pickers";
 | 
			
		||||
import { DateField } from "@mui/x-date-pickers";
 | 
			
		||||
import { dateToTime, timeToDate } from "../../utils/DateUtils";
 | 
			
		||||
import { TextFieldVariants } from "@mui/material";
 | 
			
		||||
 | 
			
		||||
@@ -13,13 +13,16 @@ export function DateInput(p: {
 | 
			
		||||
  variant?: TextFieldVariants;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <DatePicker
 | 
			
		||||
    <DateField
 | 
			
		||||
      autoFocus={p.autoFocus}
 | 
			
		||||
      readOnly={p.editable === false}
 | 
			
		||||
      label={p.label}
 | 
			
		||||
      slotProps={{
 | 
			
		||||
        field: { ref: p.ref },
 | 
			
		||||
        textField: { variant: p.variant ?? "standard", style: p.style },
 | 
			
		||||
        textField: {
 | 
			
		||||
          ref: p.ref,
 | 
			
		||||
          variant: p.variant ?? "standard",
 | 
			
		||||
          style: p.style,
 | 
			
		||||
        },
 | 
			
		||||
      }}
 | 
			
		||||
      value={timeToDate(p.value)}
 | 
			
		||||
      onChange={(v) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user