From 1e47012ef2d4e52cd89ca4060b5545aab50e506c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 16:20:55 +0200 Subject: [PATCH 01/11] Fix favorite --- backend/Config.cs | 2 +- frontend/src/components/account/AccountList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/Config.cs b/backend/Config.cs index ce863ec..c11504c 100644 --- a/backend/Config.cs +++ b/backend/Config.cs @@ -2,6 +2,6 @@ { public static class Config { - public const string Version = "0.2.1"; + public const string Version = "0.2.2-dev"; } } diff --git a/frontend/src/components/account/AccountList.tsx b/frontend/src/components/account/AccountList.tsx index 2c07f06..c55edf9 100644 --- a/frontend/src/components/account/AccountList.tsx +++ b/frontend/src/components/account/AccountList.tsx @@ -34,7 +34,7 @@ export default function AccountList (props: Props): React.ReactElement { {account.description} {account.identifiers.join(", ")} - + } />; From f5f68f1682616730a4cfb127c5d7825b9e8a083e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 17:46:27 +0200 Subject: [PATCH 02/11] Remove debug code --- frontend/src/components/account/AccountBalanceChart.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/account/AccountBalanceChart.tsx b/frontend/src/components/account/AccountBalanceChart.tsx index 42ccd52..b44c454 100644 --- a/frontend/src/components/account/AccountBalanceChart.tsx +++ b/frontend/src/components/account/AccountBalanceChart.tsx @@ -51,7 +51,6 @@ export default function AccountBalanceChart (props: Props): React.ReactElement { if (movements === "fetching") { return <>Please wait…; } - console.log(movements); let balances: number[] = []; let revenues: number[] = movements.items.map(point => point.revenue.toNumber()); From 47acce12dd898e6dbdf5ad02d11d44b1f9cffbbf Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 18:00:41 +0200 Subject: [PATCH 03/11] Fix sorting issues --- backend/Data/Search/TransactionSearch.cs | 40 ++++++++++++++----- .../transaction/table/TransactionList.tsx | 4 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/backend/Data/Search/TransactionSearch.cs b/backend/Data/Search/TransactionSearch.cs index 5c1a09e..49b899f 100644 --- a/backend/Data/Search/TransactionSearch.cs +++ b/backend/Data/Search/TransactionSearch.cs @@ -17,11 +17,14 @@ public static class TransactionSearch { private static Dictionary columnTypes = new Dictionary { { "Id", typeof(int) }, - { "SourceAccountId", typeof(int?) }, - { "DestinationAccountId", typeof(int?) }, + { "SourceAccount.Id", typeof(int?) }, + { "SourceAccount.Name", typeof(string) }, + { "DestinationAccount.Id", typeof(int?) }, + { "DestinationAccount.Name", typeof(string) }, { "Description", typeof(string) }, { "DateTime", typeof(DateTime) }, { "Total", typeof(long) }, + { "Category", typeof(string) }, }; public static IQueryable ApplySearch(this IQueryable items, ViewSearch query, bool applyOrder) @@ -53,15 +56,15 @@ public static IQueryable ApplySearch(this IQueryable i var orderColumn = query.OrderByColumn; var orderColumnType = columnTypes[orderColumn]; string command = (query.Descending ?? false) ? "OrderByDescending" : "OrderBy"; - var property = Expression.Property(parameter, orderColumn); - if (query.OrderByColumn == "Category") + Expression property = query.OrderByColumn switch { - property = Expression.Property(property, "Name"); - } - if (query.OrderByColumn == "SourceAccountId" || query.OrderByColumn == "DestinationAccountId") - { - property = Expression.Property(Expression.Property(parameter, query.OrderByColumn.Substring(0, query.OrderByColumn.Length - 2)), "Id"); - } + "Category" => CategoryExpression(parameter), + "SourceAccount.Id" => Expression.Property(parameter, "SourceAccountId"), + "SourceAccount.Name" => Expression.Property(Expression.Property(parameter, "SourceAccount"), "Name"), + "DestinationAccount.Id" => Expression.Property(parameter, "DestinationAccountId"), + "DestinationAccount.Name" => Expression.Property(Expression.Property(parameter, "DestinationAccount"), "Name"), + _ => Expression.Property(parameter, orderColumn) + }; var orderByExpression = Expression.Lambda(property, parameter); var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { typeof(Transaction), orderColumnType }, items.Expression, Expression.Quote(orderByExpression)); @@ -145,6 +148,23 @@ public static Expression TransactionLinesAny(Expression transactionParameter, Fu return expression; } + public static Expression CategoryExpression(Expression parameter) + { + var method = typeof(Enumerable) + .GetMethods() + .Single(method => method.Name == "First" && + method.GetParameters().Length == 1) + .MakeGenericMethod(typeof(TransactionLine)); + + var firstLine = Expression.Call( + // Call method Any on the transaction's lines with the child lambda as the parameter + null, + method, + Expression.Property(parameter, "TransactionLines") + ); + return Expression.Property(firstLine, "Category"); + } + #endregion } } diff --git a/frontend/src/components/transaction/table/TransactionList.tsx b/frontend/src/components/transaction/table/TransactionList.tsx index 970f21e..86ab9c4 100644 --- a/frontend/src/components/transaction/table/TransactionList.tsx +++ b/frontend/src/components/transaction/table/TransactionList.tsx @@ -107,8 +107,8 @@ function TransactionList (props: Props): React.ReactElement { {renderColumnHeader("Timestamp", "DateTime", "numeric")} {renderColumnHeader("Description", "Description", "string")} {renderColumnHeader("Amount", "Total", "numeric", true)} - {renderColumnHeader("Source", "SourceAccountId", "numeric")} - {renderColumnHeader("Destination", "DestinationAccountId", "numeric")} + {renderColumnHeader("Source", "SourceAccount.Name", "string")} + {renderColumnHeader("Destination", "DestinationAccount.Name", "string")} {renderColumnHeader("Category", "Category", "string")} {props.allowEditing === true &&
Actions From 107f2ea68d585540a72228977563dc5a70ae1a01 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 18:04:53 +0200 Subject: [PATCH 04/11] Prevent navigating to non-existant pages --- frontend/src/components/common/Table.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/components/common/Table.tsx b/frontend/src/components/common/Table.tsx index cfa146f..698d8d4 100644 --- a/frontend/src/components/common/Table.tsx +++ b/frontend/src/components/common/Table.tsx @@ -227,6 +227,9 @@ export default function Table (props: Props): React.ReactElement { const to = page * props.pageSize; if (props.type === "sync") { + if (props.items.length !== totalItems) { + props.goToPage(1); + } setItems(props.items.slice(from, to)); setTotalItems(props.items.length); if (props.afterDraw != null) { @@ -235,6 +238,9 @@ export default function Table (props: Props): React.ReactElement { } else { const result = await props.fetchItems(api, from, to, draw ?? 0); if (result.draw === (props.draw ?? 0)) { + if (result.totalItems !== totalItems) { + props.goToPage(1); + } setItems(result.items); setTotalItems(result.totalItems); if (props.afterDraw !== undefined) { From 0882849e8ce4728e6ba35f2fdf62b140ca4de0a8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 19:07:19 +0200 Subject: [PATCH 05/11] Improve number input --- frontend/src/components/input/InputNumber.tsx | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/input/InputNumber.tsx b/frontend/src/components/input/InputNumber.tsx index d0cb92e..5aef463 100644 --- a/frontend/src/components/input/InputNumber.tsx +++ b/frontend/src/components/input/InputNumber.tsx @@ -18,7 +18,20 @@ type Props = { }); export default function InputNumber (props: Props): React.ReactElement { - const isError = props.errors !== undefined && props.errors.length > 0; + const [isInvalidValue, setIsInvalidValue] = React.useState(false); + const isError = isInvalidValue || (props.errors !== undefined && props.errors.length > 0); + const [value, setValue] = React.useState(props.value?.toString() ?? ""); + const [numericValue, setNumericValue] = React.useState(props.value); + + React.useEffect(() => { + if (props.value?.toString() !== numericValue?.toString()) { + if (typeof props.value === "number" && !isNaN(props.value)) { + setValue(props.value); + } else if (typeof props.value === "object" && props.value?.isNaN() !== true) { + setValue(props.value?.toString() ?? ""); + } + } + }, [props.value]); return
{props.label !== undefined && } @@ -28,21 +41,46 @@ export default function InputNumber (props: Props): React.ReactElement { className={"input" + (isError ? " is-danger" : "") + (props.isSmall === true ? " is-small" : "")} type="number" placeholder={props.label} - value={props.value?.toString() ?? ""} + value={value} disabled={props.disabled} onChange={onChange} + onBeforeInput={onBeforeInput} /> - {isError &&

- {props.errors![0]} + {isError && typeof props.errors === "object" &&

+ {props.errors[0]}

}
; + function onBeforeInput (event: React.FormEvent): boolean { + const change = (event as any).data; + if (!/^[-\d,.]*$/.test(change)) { + event.preventDefault(); + return false; + } + return true; + } + function onChange (event: React.ChangeEvent): void { let value: Decimal | null = new Decimal(event.target.valueAsNumber); - if (value.isNaN()) value = props.allowNull ? null : new Decimal(0); + if (value.isNaN()) { + if (props.allowNull && event.target.value.trim() === "") { + value = null; + setValue(""); + setIsInvalidValue(false); + setNumericValue(null); + props.onChange(value); + } else { + setValue(event.target.value); + setIsInvalidValue(true); + } + return; + } - props.onChange(value!); + setIsInvalidValue(false); + setValue(event.target.value); + setNumericValue(value); + props.onChange(value); } } From ce010966cfb45322b372ee03fb83d3834e140125 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 19:22:10 +0200 Subject: [PATCH 06/11] Trigger build --- frontend/src/components/input/InputNumber.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/input/InputNumber.tsx b/frontend/src/components/input/InputNumber.tsx index 5aef463..bb624f6 100644 --- a/frontend/src/components/input/InputNumber.tsx +++ b/frontend/src/components/input/InputNumber.tsx @@ -22,7 +22,7 @@ export default function InputNumber (props: Props): React.ReactElement { const isError = isInvalidValue || (props.errors !== undefined && props.errors.length > 0); const [value, setValue] = React.useState(props.value?.toString() ?? ""); const [numericValue, setNumericValue] = React.useState(props.value); - + React.useEffect(() => { if (props.value?.toString() !== numericValue?.toString()) { if (typeof props.value === "number" && !isNaN(props.value)) { From e2b783fb5a3347216eaf8876a67a501dee017a7d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 20:31:03 +0200 Subject: [PATCH 07/11] Improve performance --- frontend/src/components/account/AccountCategoryChart.tsx | 4 +++- frontend/src/components/input/InputNumber.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/account/AccountCategoryChart.tsx b/frontend/src/components/account/AccountCategoryChart.tsx index ec067b5..09f7961 100644 --- a/frontend/src/components/account/AccountCategoryChart.tsx +++ b/frontend/src/components/account/AccountCategoryChart.tsx @@ -36,7 +36,9 @@ interface Props { type DataType = Array<{ category: string, transfer: boolean, revenue: number, expenses: number }>; interface ChartDataType { category: string, revenue: number, expenses: number, transferRevenue: number, transferExpenses: number }; -export default function AccountCategoryChart (props: Props): React.ReactElement { +export default React.memo(AccountCategoryChart, (a, b) => a.id === b.id && a.period === b.period && a.type === b.type); + +function AccountCategoryChart (props: Props): React.ReactElement { const [data, setData] = React.useState("fetching"); const api = useApi(); diff --git a/frontend/src/components/input/InputNumber.tsx b/frontend/src/components/input/InputNumber.tsx index bb624f6..5aef463 100644 --- a/frontend/src/components/input/InputNumber.tsx +++ b/frontend/src/components/input/InputNumber.tsx @@ -22,7 +22,7 @@ export default function InputNumber (props: Props): React.ReactElement { const isError = isInvalidValue || (props.errors !== undefined && props.errors.length > 0); const [value, setValue] = React.useState(props.value?.toString() ?? ""); const [numericValue, setNumericValue] = React.useState(props.value); - + React.useEffect(() => { if (props.value?.toString() !== numericValue?.toString()) { if (typeof props.value === "number" && !isNaN(props.value)) { From 2d630bbba95deafb0cf8ef8429aa1ceb6d50d21a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 19 Oct 2022 21:03:18 +0200 Subject: [PATCH 08/11] Improve performance on account page --- frontend/src/components/account/AccountBalanceChart.tsx | 3 ++- frontend/src/components/common/Table.tsx | 4 ++-- frontend/src/components/pages/account/PageAccount.tsx | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/account/AccountBalanceChart.tsx b/frontend/src/components/account/AccountBalanceChart.tsx index b44c454..08e1448 100644 --- a/frontend/src/components/account/AccountBalanceChart.tsx +++ b/frontend/src/components/account/AccountBalanceChart.tsx @@ -36,7 +36,8 @@ interface Props { period: Period } -export default function AccountBalanceChart (props: Props): React.ReactElement { +export default React.memo(AccountBalanceChart); +function AccountBalanceChart (props: Props): React.ReactElement { const [movements, setMovements] = React.useState("fetching"); const [resolution, setResolution] = React.useState<"month" | "day" | "week" | "year">("day"); const [displayingPeriod, setDisplayingPeriod] = React.useState(props.period); diff --git a/frontend/src/components/common/Table.tsx b/frontend/src/components/common/Table.tsx index 698d8d4..e94256d 100644 --- a/frontend/src/components/common/Table.tsx +++ b/frontend/src/components/common/Table.tsx @@ -227,7 +227,7 @@ export default function Table (props: Props): React.ReactElement { const to = page * props.pageSize; if (props.type === "sync") { - if (props.items.length !== totalItems) { + if (props.items.length !== totalItems && props.page !== 1) { props.goToPage(1); } setItems(props.items.slice(from, to)); @@ -238,7 +238,7 @@ export default function Table (props: Props): React.ReactElement { } else { const result = await props.fetchItems(api, from, to, draw ?? 0); if (result.draw === (props.draw ?? 0)) { - if (result.totalItems !== totalItems) { + if (props.type !== "async-increment" && result.totalItems !== totalItems && props.page !== 1) { props.goToPage(1); } setItems(result.items); diff --git a/frontend/src/components/pages/account/PageAccount.tsx b/frontend/src/components/pages/account/PageAccount.tsx index 68529a5..8c33afb 100644 --- a/frontend/src/components/pages/account/PageAccount.tsx +++ b/frontend/src/components/pages/account/PageAccount.tsx @@ -150,20 +150,21 @@ export default function PageAccount (): React.ReactElement { }, ""); } - async function goToPage (page: number | "increment" | "decrement"): Promise { + async function goToPage (newPage: number | "increment" | "decrement"): Promise { if (api === null) return; + if (newPage === page) return; - if (page === "increment") { + if (newPage === "increment") { const nextPeriod = PeriodFunctions.increment(period); const transactionCount = await countTransactions(api, nextPeriod); const lastPage = Math.max(1, Math.ceil(transactionCount / pageSize)); setPeriod(nextPeriod); setPage(lastPage); - } else if (page === "decrement") { + } else if (newPage === "decrement") { setPeriod(PeriodFunctions.decrement(period)); setPage(1); } else { - setPage(page); + setPage(newPage); } setDraw(draw => draw + 1); } From 4d3c5facbf3be03257f521b80f08cee08eb88eba Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Oct 2022 11:54:29 +0200 Subject: [PATCH 09/11] Fix import profile modal name --- backend/Controllers/AccountController.cs | 6 ------ frontend/src/components/transaction/import/Import.tsx | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/Controllers/AccountController.cs b/backend/Controllers/AccountController.cs index a9607ba..4ba52e5 100644 --- a/backend/Controllers/AccountController.cs +++ b/backend/Controllers/AccountController.cs @@ -247,12 +247,6 @@ public async Task Transactions(int id, ViewTransactionListRequest .ThenBy(transaction => transaction.Id); } - var test = query - .Skip(request.From) - .Take(request.To - request.From) - .SelectView(user.Id) - .ToList(); - var result = await query .Skip(request.From) .Take(request.To - request.From) diff --git a/frontend/src/components/transaction/import/Import.tsx b/frontend/src/components/transaction/import/Import.tsx index 82e67f5..d6e5df8 100644 --- a/frontend/src/components/transaction/import/Import.tsx +++ b/frontend/src/components/transaction/import/Import.tsx @@ -186,7 +186,7 @@ function SaveProfileModal (props: SaveProfileModalProps): React.ReactElement { return props.close()} footer={<> {Save profile} From 97543272c40ba7284c69f2a207468ae6473be088 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Oct 2022 12:04:22 +0200 Subject: [PATCH 10/11] Fix incorrect balance on account page --- backend/Controllers/AccountController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/Controllers/AccountController.cs b/backend/Controllers/AccountController.cs index 4ba52e5..62f9edd 100644 --- a/backend/Controllers/AccountController.cs +++ b/backend/Controllers/AccountController.cs @@ -228,8 +228,9 @@ public async Task Transactions(int id, ViewTransactionListRequest return NotFound(); } - var query = _context.Transactions + var accountTransactions = _context.Transactions .Where(transaction => transaction.SourceAccountId == id || transaction.DestinationAccountId == id); + var query = accountTransactions; if (request.Query != null) { @@ -256,10 +257,12 @@ public async Task Transactions(int id, ViewTransactionListRequest .OrderBy(transaction => transaction.DateTime) .ThenBy(transaction => transaction.Id) .FirstOrDefault(); - var total = firstTransaction == null ? 0 : await query - .Where(transaction => transaction.DateTime < firstTransaction.DateTime || (transaction.DateTime == firstTransaction.DateTime && transaction.Id < firstTransaction.Id)) - .Select(transaction => transaction.Total * (transaction.DestinationAccountId == id ? 1 : -1)) - .SumAsync(); + var total = firstTransaction == null + ? 0 + : await accountTransactions + .Where(transaction => transaction.DateTime < firstTransaction.DateTime || (transaction.DateTime == firstTransaction.DateTime && transaction.Id < firstTransaction.Id)) + .Select(transaction => transaction.Total * (transaction.DestinationAccountId == id ? 1 : -1)) + .SumAsync(); return Ok(new ViewTransactionList { From 2a434c800c6ac7c54f1ca2587d19db9203b9242a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Oct 2022 20:02:46 +0200 Subject: [PATCH 11/11] Display owned and not-owned accounts differently --- frontend/src/components/account/AccountLink.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/account/AccountLink.tsx b/frontend/src/components/account/AccountLink.tsx index 54c0d2f..4096607 100644 --- a/frontend/src/components/account/AccountLink.tsx +++ b/frontend/src/components/account/AccountLink.tsx @@ -1,3 +1,5 @@ +import { faMoneyCheck, faUser } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as React from "react"; import { Link } from "react-router-dom"; import { routes } from "../../lib/routes"; @@ -9,14 +11,18 @@ export interface Props { disabled?: boolean } -export default function AccountLink (props: Props): React.ReactElement { +export default function AccountLink(props: Props): React.ReactElement { + const icon = props.account.includeInNetWorth + ? + : ; + if (props.disabled === true) { return - #{props.account.id} {props.account.name} + {icon} {props.account.name} ; } return - #{props.account.id} {props.account.name} + {icon} {props.account.name} ; }