diff --git a/moneymgr_backend/src/controllers/movement_controller.rs b/moneymgr_backend/src/controllers/movement_controller.rs index 63f2cd8..3f30794 100644 --- a/moneymgr_backend/src/controllers/movement_controller.rs +++ b/moneymgr_backend/src/controllers/movement_controller.rs @@ -22,10 +22,55 @@ pub async fn get_accounts_balances(auth: AuthExtractor) -> HttpResult { Ok(HttpResponse::Ok().json(movements_service::get_balances(auth.user_id()).await?)) } +/// Select movements filter +#[derive(serde::Deserialize)] +pub struct SelectMovementFilters { + amount_min: Option, + amount_max: Option, + time_min: Option, + time_max: Option, + label: Option, + limit: Option, +} + /// Get the list of movements of an account -pub async fn get_list_of_account(account_id: AccountInPath) -> HttpResult { - Ok(HttpResponse::Ok() - .json(movements_service::get_list_account(account_id.as_ref().id()).await?)) +pub async fn get_list_of_account( + account_id: AccountInPath, + query: web::Query, +) -> HttpResult { + let mut list = movements_service::get_list_account(account_id.as_ref().id()).await?; + + if let Some(amount_min) = query.amount_min { + list.retain(|l| l.amount >= amount_min); + } + + if let Some(amount_max) = query.amount_max { + list.retain(|l| l.amount <= amount_max); + } + + if let Some(time_min) = query.time_min { + list.retain(|l| l.time >= time_min); + } + + if let Some(time_max) = query.time_max { + list.retain(|l| l.time <= time_max); + } + + if let Some(label) = &query.label { + list.retain(|l| { + l.label + .to_lowercase() + .contains(label.to_lowercase().as_str()) + }); + } + + if let Some(limit) = query.limit { + if list.len() > limit { + list = list[..limit].to_vec(); + } + } + + Ok(HttpResponse::Ok().json(list)) } /// Get a single movement information diff --git a/moneymgr_web/src/api/MovementsApi.ts b/moneymgr_web/src/api/MovementsApi.ts index 06fb2e7..e1e245f 100644 --- a/moneymgr_web/src/api/MovementsApi.ts +++ b/moneymgr_web/src/api/MovementsApi.ts @@ -50,10 +50,34 @@ export class MovementApi { /** * Get all the movements of an account */ - static async GetAccountMovements(account_id: number): Promise { + static async GetAccountMovements( + account_id: number, + filters?: { + amount_min?: number; + amount_max?: number; + time_min?: number; + time_max?: number; + label?: string; + limit?: number; + } + ): Promise { + let filtersS = new URLSearchParams(); + if (filters) { + if (filters.amount_min) + filtersS.append("amount_min", filters.amount_min.toString()); + if (filters.amount_max) + filtersS.append("amount_max", filters.amount_max.toString()); + if (filters.time_min) + filtersS.append("time_min", filters.time_min.toString()); + if (filters.time_max) + filtersS.append("time_max", filters.time_max.toString()); + if (filters.label) filtersS.append("label", filters.label); + if (filters.limit) filtersS.append("limit", filters.limit); + } + return ( await APIClient.exec({ - uri: `/account/${account_id}/movements`, + uri: `/account/${account_id}/movements?${filtersS}`, method: "GET", }) ).data; diff --git a/moneymgr_web/src/routes/InboxRoute.tsx b/moneymgr_web/src/routes/InboxRoute.tsx index f539f55..d4eb184 100644 --- a/moneymgr_web/src/routes/InboxRoute.tsx +++ b/moneymgr_web/src/routes/InboxRoute.tsx @@ -25,7 +25,7 @@ import { AmountWidget } from "../widgets/AmountWidget"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { DateWidget } from "../widgets/DateWidget"; import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer"; -import { MovementWidget } from "../widgets/MovementWidget"; +import { AsyncMovementWidget } from "../widgets/MovementWidget"; import { NewMovementWidget } from "../widgets/NewMovementWidget"; import { UploadedFileWidget } from "../widgets/UploadedFileWidget"; @@ -271,7 +271,7 @@ function InboxTable(p: { flex: 3, renderCell: (params) => { if (params.row.movement_id) - return ; + return ; }, }, { diff --git a/moneymgr_web/src/widgets/MovementWidget.tsx b/moneymgr_web/src/widgets/MovementWidget.tsx index fafbd59..6be2a47 100644 --- a/moneymgr_web/src/widgets/MovementWidget.tsx +++ b/moneymgr_web/src/widgets/MovementWidget.tsx @@ -3,13 +3,12 @@ import CallReceivedIcon from "@mui/icons-material/CallReceived"; import React from "react"; import { Movement, MovementApi } from "../api/MovementsApi"; import { useAccounts } from "../hooks/AccountsListProvider"; +import { fmtDateFromTime } from "../utils/DateUtils"; import { AccountIconWidget } from "./AccountIconWidget"; import { AmountWidget } from "./AmountWidget"; import { AsyncWidget } from "./AsyncWidget"; -export function MovementWidget(p: { id: number }): React.ReactElement { - const accounts = useAccounts(); - +export function AsyncMovementWidget(p: { id: number }): React.ReactElement { const [movement, setMovement] = React.useState(); const load = async () => { @@ -21,46 +20,51 @@ export function MovementWidget(p: { id: number }): React.ReactElement { loadKey={p.id} load={load} errMsg="Failed to load movement!" - build={() => ( - - {movement!.amount > 0 ? ( - - ) : ( - - )} - - - {movement?.label} - - - - - • - - - {accounts.get(movement!.account_id)?.name} - - - - )} + build={() => } /> ); } + +export function MovementWidget(p: { movement: Movement }): React.ReactElement { + const accounts = useAccounts(); + + return ( + + {p.movement!.amount > 0 ? ( + + ) : ( + + )} + + + {p.movement?.label} + + + + + • + + + {accounts.get(p.movement!.account_id)?.name} + + • + {fmtDateFromTime(p.movement.time)} + + + + ); +} diff --git a/moneymgr_web/src/widgets/NewMovementWidget.tsx b/moneymgr_web/src/widgets/NewMovementWidget.tsx index bce0e41..f37c8a8 100644 --- a/moneymgr_web/src/widgets/NewMovementWidget.tsx +++ b/moneymgr_web/src/widgets/NewMovementWidget.tsx @@ -113,7 +113,11 @@ export function NewMovementWidget( > - { setFile(undefined); }}> + { + setFile(undefined); + }} + > @@ -156,7 +160,6 @@ export function NewMovementWidget( {/* Amount input */} void; + initialValues?: { + amount?: number; + accountId?: number; + date?: number; + label?: string; + }; +}): React.ReactElement { + const [amount, setAmount] = React.useState( + p.initialValues?.amount + ); + const [accountId, setAccountId] = React.useState( + p.initialValues?.accountId + ); + const [date, setDate] = React.useState( + p.initialValues?.date + ); + const [label, setLabel] = React.useState( + p.initialValues?.label + ); + + const filters = { + label: label, + amount_min: amount ? amount - 0.5 : undefined, + amount_max: amount ? amount + 0.5 : undefined, + time_min: date ? date - 3600 * 24 : undefined, + time_max: date ? date + 3600 * 24 : undefined, + limit: 10, + }; + + const [list, setList] = React.useState(); + + const load = async () => { + if (accountId) + setList(await MovementApi.GetAccountMovements(accountId, filters)); + else setList(undefined); + }; + + return ( + +
+ + + + + + + +
+ + { + if (list === null) + return ( + + Select an account to begin research. + + ); + if (list?.length === 0) + return ( + + No result. + + ); + + return ( + <> + {list?.map((entry) => ( + + p.onChange(entry.id)} + > + + + + ))} + + ); + }} + /> +
+ ); +} diff --git a/moneymgr_web/src/widgets/forms/AccountInput.tsx b/moneymgr_web/src/widgets/forms/AccountInput.tsx index 8b53f33..50707c1 100644 --- a/moneymgr_web/src/widgets/forms/AccountInput.tsx +++ b/moneymgr_web/src/widgets/forms/AccountInput.tsx @@ -4,21 +4,25 @@ import { useAccounts } from "../../hooks/AccountsListProvider"; import { AmountWidget } from "../AmountWidget"; export function AccountInput(p: { - value: number; + value?: number; onChange: (value: number) => void; label?: string; + style?: React.CSSProperties; }): React.ReactElement { const accounts = useAccounts(); let current = p.value; - if (!current && accounts.list.list.length > 0) + if (!current && accounts.list.list.length > 0) { current = (accounts.list.default ?? accounts.list.list[0]).id; + p.onChange(current); + } return (