Skip to content

Commit

Permalink
Merge pull request #60 from assetgrid/develop
Browse files Browse the repository at this point in the history
Merge develop into main
  • Loading branch information
alex6480 authored Oct 20, 2022
2 parents 2b3b540 + 2a434c8 commit 8d72b06
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 42 deletions.
2 changes: 1 addition & 1 deletion backend/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{
public static class Config
{
public const string Version = "0.2.1";
public const string Version = "0.2.2-dev";
}
}
19 changes: 8 additions & 11 deletions backend/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,9 @@ public async Task<IActionResult> 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)
{
Expand All @@ -247,12 +248,6 @@ public async Task<IActionResult> 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)
Expand All @@ -262,10 +257,12 @@ public async Task<IActionResult> 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
{
Expand Down
40 changes: 30 additions & 10 deletions backend/Data/Search/TransactionSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ public static class TransactionSearch
{
private static Dictionary<string, Type> columnTypes = new Dictionary<string, Type> {
{ "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<Transaction> ApplySearch(this IQueryable<Transaction> items, ViewSearch query, bool applyOrder)
Expand Down Expand Up @@ -53,15 +56,15 @@ public static IQueryable<Transaction> ApplySearch(this IQueryable<Transaction> 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));
Expand Down Expand Up @@ -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
}
}
4 changes: 2 additions & 2 deletions frontend/src/components/account/AccountBalanceChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetMovementResponse | "fetching">("fetching");
const [resolution, setResolution] = React.useState<"month" | "day" | "week" | "year">("day");
const [displayingPeriod, setDisplayingPeriod] = React.useState(props.period);
Expand All @@ -51,7 +52,6 @@ export default function AccountBalanceChart (props: Props): React.ReactElement {
if (movements === "fetching") {
return <>Please wait&hellip;</>;
}
console.log(movements);

let balances: number[] = [];
let revenues: number[] = movements.items.map(point => point.revenue.toNumber());
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/account/AccountCategoryChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataType | "fetching">("fetching");
const api = useApi();

Expand Down
12 changes: 9 additions & 3 deletions frontend/src/components/account/AccountLink.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
? <FontAwesomeIcon icon={faUser} />
: <FontAwesomeIcon icon={faMoneyCheck} />;

if (props.disabled === true) {
return <span className="transaction-link">
<span>#{props.account.id}</span> {props.account.name}
<span>{icon}</span> {props.account.name}
</span>;
}

return <Link className="account-link" to={routes.account(props.account.id.toString())} state={{ page: 1 }} target={props.targetBlank === true ? "_blank" : "_self"}>
<span>#{props.account.id}</span> {props.account.name}
<span>{icon}</span> {props.account.name}
</Link>;
}
2 changes: 1 addition & 1 deletion frontend/src/components/account/AccountList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function AccountList (props: Props): React.ReactElement {
<td>{account.description}</td>
<td>{account.identifiers.join(", ")}</td>
<td>
<YesNoDisplay value={account.includeInNetWorth} />
<YesNoDisplay value={account.favorite} />
</td>
</tr>}
/>;
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/common/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ export default function Table<T> (props: Props<T>): React.ReactElement {
const to = page * props.pageSize;

if (props.type === "sync") {
if (props.items.length !== totalItems && props.page !== 1) {
props.goToPage(1);
}
setItems(props.items.slice(from, to));
setTotalItems(props.items.length);
if (props.afterDraw != null) {
Expand All @@ -235,6 +238,9 @@ export default function Table<T> (props: Props<T>): React.ReactElement {
} else {
const result = await props.fetchItems(api, from, to, draw ?? 0);
if (result.draw === (props.draw ?? 0)) {
if (props.type !== "async-increment" && result.totalItems !== totalItems && props.page !== 1) {
props.goToPage(1);
}
setItems(result.items);
setTotalItems(result.totalItems);
if (props.afterDraw !== undefined) {
Expand Down
50 changes: 44 additions & 6 deletions frontend/src/components/input/InputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div className="field">
{props.label !== undefined && <label className="label">{props.label}</label>}
Expand All @@ -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 && <p className="help has-text-danger">
{props.errors![0]}
{isError && typeof props.errors === "object" && <p className="help has-text-danger">
{props.errors[0]}
</p>}
</div>
</div>
</div>;

function onBeforeInput (event: React.FormEvent<HTMLInputElement>): boolean {
const change = (event as any).data;
if (!/^[-\d,.]*$/.test(change)) {
event.preventDefault();
return false;
}
return true;
}

function onChange (event: React.ChangeEvent<HTMLInputElement>): 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);
}
}
9 changes: 5 additions & 4 deletions frontend/src/components/pages/account/PageAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,21 @@ export default function PageAccount (): React.ReactElement {
}, "");
}

async function goToPage (page: number | "increment" | "decrement"): Promise<void> {
async function goToPage (newPage: number | "increment" | "decrement"): Promise<void> {
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);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/transaction/import/Import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ function SaveProfileModal (props: SaveProfileModalProps): React.ReactElement {

return <Modal
active={props.active}
title={"Merge transactions"}
title={"Save import profile"}
close={() => props.close()}
footer={<>
{<InputButton onClick={forget(saveProfile)} disabled={isCreating || api === null} className="is-primary">Save profile</InputButton>}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/transaction/table/TransactionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && <div>
Actions
Expand Down

0 comments on commit 8d72b06

Please sign in to comment.