Added aggregate data for search page.
This commit is contained in:
parent
bac074599a
commit
6cc29589ba
|
|
@ -17,6 +17,7 @@ import account.api;
|
||||||
import util.money;
|
import util.money;
|
||||||
import util.pagination;
|
import util.pagination;
|
||||||
import util.data;
|
import util.data;
|
||||||
|
import transaction.search_filters;
|
||||||
|
|
||||||
// Transactions API
|
// Transactions API
|
||||||
|
|
||||||
|
|
@ -32,12 +33,21 @@ void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
|
|
||||||
@GetMapping(PROFILE_PATH ~ "/transactions/search")
|
@GetMapping(PROFILE_PATH ~ "/transactions/search")
|
||||||
void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
import transaction.search_filters : extractSearchParams;
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||||
auto page = ds.getTransactionRepository().search(pr, request);
|
auto page = ds.getTransactionRepository().search(pr, extractSearchParams(request));
|
||||||
writeJsonBody(response, page);
|
writeJsonBody(response, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(PROFILE_PATH ~ "/transactions/aggregate-data")
|
||||||
|
void handleGetTransactionAggregateData(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
import transaction.search_filters : extractSearchParams;
|
||||||
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
auto aggregateData = ds.getTransactionRepository().getAggregateData(extractSearchParams(request));
|
||||||
|
writeJsonBody(response, aggregateData);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(PROFILE_PATH ~ "/transactions/export")
|
@GetMapping(PROFILE_PATH ~ "/transactions/export")
|
||||||
void handleExportTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleExportTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
module transaction.data;
|
module transaction.data;
|
||||||
|
|
||||||
import handy_http_primitives : Optional, ServerHttpRequest;
|
import handy_http_primitives : Optional, StringMultiValueMap;
|
||||||
import std.datetime;
|
import std.datetime;
|
||||||
|
|
||||||
import transaction.model;
|
import transaction.model;
|
||||||
|
|
@ -44,7 +44,8 @@ interface TransactionTagRepository {
|
||||||
|
|
||||||
interface TransactionRepository {
|
interface TransactionRepository {
|
||||||
Page!TransactionsListItem findAll(in PageRequest pr);
|
Page!TransactionsListItem findAll(in PageRequest pr);
|
||||||
Page!TransactionsListItem search(in PageRequest pr, in ServerHttpRequest request);
|
Page!TransactionsListItem search(in PageRequest pr, in StringMultiValueMap searchParams);
|
||||||
|
AggregateTransactionData getAggregateData(in StringMultiValueMap searchParams);
|
||||||
Optional!TransactionDetail findById(ulong id);
|
Optional!TransactionDetail findById(ulong id);
|
||||||
TransactionDetail insert(in AddTransactionPayload data);
|
TransactionDetail insert(in AddTransactionPayload data);
|
||||||
void linkAttachment(ulong transactionId, ulong attachmentId);
|
void linkAttachment(ulong transactionId, ulong attachmentId);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
module transaction.data_impl_sqlite;
|
module transaction.data_impl_sqlite;
|
||||||
|
|
||||||
import handy_http_primitives : Optional, ServerHttpRequest;
|
import handy_http_primitives : Optional, StringMultiValueMap;
|
||||||
import std.datetime;
|
import std.datetime;
|
||||||
import std.typecons;
|
import std.typecons;
|
||||||
import d2sqlite3;
|
import d2sqlite3;
|
||||||
|
|
@ -293,7 +293,7 @@ class SqliteTransactionRepository : TransactionRepository {
|
||||||
return Page!(TransactionsListItem).of(results, pr, totalCount);
|
return Page!(TransactionsListItem).of(results, pr, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
Page!TransactionsListItem search(in PageRequest pr, in ServerHttpRequest request) {
|
Page!TransactionsListItem search(in PageRequest pr, in StringMultiValueMap searchParams) {
|
||||||
import std.algorithm;
|
import std.algorithm;
|
||||||
import std.conv;
|
import std.conv;
|
||||||
import std.string : join;
|
import std.string : join;
|
||||||
|
|
@ -303,7 +303,7 @@ class SqliteTransactionRepository : TransactionRepository {
|
||||||
|
|
||||||
// 1. Get the total count of transactions that match the search filters.
|
// 1. Get the total count of transactions that match the search filters.
|
||||||
qb.select("COUNT (DISTINCT txn.id)");
|
qb.select("COUNT (DISTINCT txn.id)");
|
||||||
applyFilters(qb, request, new SqliteTransactionCategoryRepository(db));
|
applyFilters(qb, searchParams, new SqliteTransactionCategoryRepository(db));
|
||||||
string countQuery = qb.build();
|
string countQuery = qb.build();
|
||||||
Statement countStmt = db.prepare(countQuery);
|
Statement countStmt = db.prepare(countQuery);
|
||||||
qb.applyArgBindings(countStmt);
|
qb.applyArgBindings(countStmt);
|
||||||
|
|
@ -335,6 +335,38 @@ class SqliteTransactionRepository : TransactionRepository {
|
||||||
return Page!TransactionsListItem.of(results, pr, count);
|
return Page!TransactionsListItem.of(results, pr, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AggregateTransactionData getAggregateData(in StringMultiValueMap searchParams) {
|
||||||
|
import transaction.search_filters;
|
||||||
|
// Start by using the standard transactions-list query builder to apply filters.
|
||||||
|
QueryBuilder qb = getBuilderForTransactionsList();
|
||||||
|
qb.select("txn.currency AS currency, j_credit.amount AS credits, j_debit.amount AS debits");
|
||||||
|
applyFilters(qb, searchParams, new SqliteTransactionCategoryRepository(db));
|
||||||
|
string baseQuery = qb.build() ~ "\nGROUP BY txn.id";
|
||||||
|
// Now wrap that in a separate query that aggregates credits & debits for each transaction.
|
||||||
|
string aggregateQuery = QueryBuilder("(" ~ baseQuery ~ ") AS base")
|
||||||
|
.select("base.currency")
|
||||||
|
.select("SUM(base.credits)")
|
||||||
|
.select("SUM(base.debits)")
|
||||||
|
.build() ~ " GROUP BY base.currency";
|
||||||
|
Statement stmt = db.prepare(aggregateQuery);
|
||||||
|
// Use the base query's argument bindings to apply to the DB statement.
|
||||||
|
qb.applyArgBindings(stmt);
|
||||||
|
ResultRange result = stmt.execute();
|
||||||
|
|
||||||
|
// Collect the results for each currency into
|
||||||
|
AggregateTransactionData aggregateData;
|
||||||
|
while (!result.empty()) {
|
||||||
|
AggregateTransactionData.CurrencyData currencyData;
|
||||||
|
currencyData.credits = result.front.peek!ulong(1);
|
||||||
|
currencyData.debits = result.front.peek!ulong(2);
|
||||||
|
currencyData.balance = currencyData.debits - currencyData.credits;
|
||||||
|
currencyData.currency = Currency.ofCode(result.front.peek!(string, PeekMode.slice)(0));
|
||||||
|
aggregateData.currencies ~= currencyData;
|
||||||
|
result.popFront();
|
||||||
|
}
|
||||||
|
return aggregateData;
|
||||||
|
}
|
||||||
|
|
||||||
Optional!TransactionDetail findById(ulong id) {
|
Optional!TransactionDetail findById(ulong id) {
|
||||||
Optional!TransactionDetail item = util.sqlite.findOne(
|
Optional!TransactionDetail item = util.sqlite.findOne(
|
||||||
db,
|
db,
|
||||||
|
|
|
||||||
|
|
@ -155,3 +155,18 @@ struct TransactionCategoryBalance {
|
||||||
long balance;
|
long balance;
|
||||||
Currency currency;
|
Currency currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of aggregate data computed from a set of transactions obtained in a
|
||||||
|
* search. Data is grouped by currency.
|
||||||
|
*/
|
||||||
|
struct AggregateTransactionData {
|
||||||
|
/// Aggregate data about all transactions with a common currency.
|
||||||
|
static struct CurrencyData {
|
||||||
|
ulong credits;
|
||||||
|
ulong debits;
|
||||||
|
long balance;
|
||||||
|
Currency currency;
|
||||||
|
}
|
||||||
|
CurrencyData[] currencies;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,24 +20,24 @@ import transaction.data;
|
||||||
* Applies a set of filters to a query builder for searching over transactions.
|
* Applies a set of filters to a query builder for searching over transactions.
|
||||||
* Params:
|
* Params:
|
||||||
* qb = The query builder to add WHERE clauses and argument bindings to.
|
* qb = The query builder to add WHERE clauses and argument bindings to.
|
||||||
* request = The request to get filter options from.
|
* searchParams = The set of search parameters provided by the user.
|
||||||
* categoryRepo = Repository for fetching category data, which might be
|
* categoryRepo = Repository for fetching category data, which might be
|
||||||
* needed if the user is filtering by a parent category.
|
* needed if the user is filtering by a parent category.
|
||||||
*/
|
*/
|
||||||
void applyFilters(
|
void applyFilters(
|
||||||
ref QueryBuilder qb,
|
ref QueryBuilder qb,
|
||||||
in ServerHttpRequest request,
|
in StringMultiValueMap searchParams,
|
||||||
TransactionCategoryRepository categoryRepo
|
TransactionCategoryRepository categoryRepo
|
||||||
) {
|
) {
|
||||||
applyPropertyInFilter!string(qb, request, "tags.tag", "tag");
|
applyPropertyInFilter!string(qb, searchParams, "tags.tag", "tag");
|
||||||
applyPropertyInFilter!ulong(qb, request, "vendor.id", "vendor");
|
applyPropertyInFilter!ulong(qb, searchParams, "vendor.id", "vendor");
|
||||||
applyPropertyInFilter!string(qb, request, "txn.currency", "currency");
|
applyPropertyInFilter!string(qb, searchParams, "txn.currency", "currency");
|
||||||
applyPropertyInFilter!ulong(qb, request, "account_credit.id", "credited-account");
|
applyPropertyInFilter!ulong(qb, searchParams, "account_credit.id", "credited-account");
|
||||||
applyPropertyInFilter!ulong(qb, request, "account_debit.id", "debited-account");
|
applyPropertyInFilter!ulong(qb, searchParams, "account_debit.id", "debited-account");
|
||||||
|
|
||||||
// Separate filter that combines both credit and debit accounts.
|
// Separate filter that combines both credit and debit accounts.
|
||||||
if (request.hasParam("account")) {
|
if (searchParams.contains("account")) {
|
||||||
ulong[] accountIds = request.getParamValues!ulong("account");
|
ulong[] accountIds = searchParams.getAllValuesAs!ulong("account");
|
||||||
string inStr = "(" ~ "?".repeat(accountIds.length).join(",") ~ ")";
|
string inStr = "(" ~ "?".repeat(accountIds.length).join(",") ~ ")";
|
||||||
qb.where("(account_credit.id IN " ~ inStr ~ " OR account_debit.id IN " ~ inStr ~ ")");
|
qb.where("(account_credit.id IN " ~ inStr ~ " OR account_debit.id IN " ~ inStr ~ ")");
|
||||||
qb.withArgBinding((ref stmt, ref idx) {
|
qb.withArgBinding((ref stmt, ref idx) {
|
||||||
|
|
@ -53,8 +53,8 @@ void applyFilters(
|
||||||
|
|
||||||
// Separate filter that handles the hierarchical category relationship, so
|
// Separate filter that handles the hierarchical category relationship, so
|
||||||
// if a parent category is filtered, all children are also included.
|
// if a parent category is filtered, all children are also included.
|
||||||
if (request.hasParam("category")) {
|
if (searchParams.contains("category")) {
|
||||||
ulong[] categoryIds = request.getParamValues!ulong("category");
|
ulong[] categoryIds = searchParams.getAllValuesAs!ulong("category");
|
||||||
auto app = appender!(ulong[]);
|
auto app = appender!(ulong[]);
|
||||||
foreach (id; categoryIds) {
|
foreach (id; categoryIds) {
|
||||||
app ~= getAllPossibleCategoryIds(id, categoryRepo);
|
app ~= getAllPossibleCategoryIds(id, categoryRepo);
|
||||||
|
|
@ -70,8 +70,8 @@ void applyFilters(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.hasParam("min-amount")) {
|
if (searchParams.contains("min-amount")) {
|
||||||
ulong[] values = request.getParamValues!ulong("min-amount");
|
ulong[] values = searchParams.getAllValuesAs!ulong("min-amount");
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
ulong minAmount = values[0];
|
ulong minAmount = values[0];
|
||||||
qb.where("txn.amount >= ?");
|
qb.where("txn.amount >= ?");
|
||||||
|
|
@ -81,8 +81,8 @@ void applyFilters(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.hasParam("max-amount")) {
|
if (searchParams.contains("max-amount")) {
|
||||||
ulong[] values = request.getParamValues!ulong("max-amount");
|
ulong[] values = searchParams.getAllValuesAs!ulong("max-amount");
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
ulong minAmount = values[0];
|
ulong minAmount = values[0];
|
||||||
qb.where("txn.amount <= ?");
|
qb.where("txn.amount <= ?");
|
||||||
|
|
@ -93,10 +93,9 @@ void applyFilters(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boolean filter for internal transfer.
|
// Boolean filter for internal transfer.
|
||||||
if (request.hasParam("internal-transfer")) {
|
if (searchParams.contains("internal-transfer")) {
|
||||||
string value = request.getParamValues("internal-transfer")[0]
|
string value = searchParams.getFirst("internal-transfer").orElseThrow()
|
||||||
.strip()
|
.strip().toUpper();
|
||||||
.toUpper();
|
|
||||||
Optional!bool internalTransferFilter = Optional!bool.empty();
|
Optional!bool internalTransferFilter = Optional!bool.empty();
|
||||||
if (value == "Y" || value == "YES" || value == "TRUE" || value == "1") {
|
if (value == "Y" || value == "YES" || value == "TRUE" || value == "1") {
|
||||||
internalTransferFilter = Optional!bool.of(true);
|
internalTransferFilter = Optional!bool.of(true);
|
||||||
|
|
@ -112,8 +111,8 @@ void applyFilters(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textual search query:
|
// Textual search query:
|
||||||
if (request.hasParam("q")) {
|
if (searchParams.contains("q")) {
|
||||||
string searchQuery = request.getParamValues!string("q")[0];
|
string searchQuery = searchParams.getFirst("q").orElseThrow();
|
||||||
string likeStr = "%" ~ toUpper(strip(searchQuery)) ~ "%";
|
string likeStr = "%" ~ toUpper(strip(searchQuery)) ~ "%";
|
||||||
const string[] conditions = [
|
const string[] conditions = [
|
||||||
"UPPER(txn.description) LIKE ?",
|
"UPPER(txn.description) LIKE ?",
|
||||||
|
|
@ -132,14 +131,36 @@ void applyFilters(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to extract query parameters into a multi-value map to be
|
||||||
|
* used more generally for other functions that require this format for search
|
||||||
|
* parameters.
|
||||||
|
* Params:
|
||||||
|
* request = The HTTP request to extract parameters from.
|
||||||
|
* Returns: A string-string multi-value map.
|
||||||
|
*/
|
||||||
|
StringMultiValueMap extractSearchParams(in ServerHttpRequest request) {
|
||||||
|
auto searchParamsBuilder = StringMultiValueMap.Builder();
|
||||||
|
foreach (queryParam; request.queryParams) {
|
||||||
|
foreach (value; queryParam.values) {
|
||||||
|
searchParamsBuilder.add(queryParam.key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return searchParamsBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private T[] getAllValuesAs(T)(in StringMultiValueMap m, string key) {
|
||||||
|
return m.getAll(key).map!(v => v.to!T).array;
|
||||||
|
}
|
||||||
|
|
||||||
private void applyPropertyInFilter(T)(
|
private void applyPropertyInFilter(T)(
|
||||||
ref QueryBuilder qb,
|
ref QueryBuilder qb,
|
||||||
in ServerHttpRequest request,
|
in StringMultiValueMap searchParams,
|
||||||
string property,
|
string property,
|
||||||
string key
|
string key
|
||||||
) {
|
) {
|
||||||
if (request.hasParam(key)) {
|
if (searchParams.contains(key)) {
|
||||||
T[] values = request.getParamValues!T(key);
|
T[] values = searchParams.getAllValuesAs!T(key);
|
||||||
qb.where(property ~ " IN (" ~ "?".repeat(values.length).join(",") ~ ")");
|
qb.where(property ~ " IN (" ~ "?".repeat(values.length).join(",") ~ ")");
|
||||||
qb.withArgBinding((ref stmt, ref idx) {
|
qb.withArgBinding((ref stmt, ref idx) {
|
||||||
foreach (value; values) {
|
foreach (value; values) {
|
||||||
|
|
@ -150,22 +171,6 @@ private void applyPropertyInFilter(T)(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool hasParam(in ServerHttpRequest request, string key) {
|
|
||||||
foreach (param; request.queryParams) {
|
|
||||||
if (param.key == key && param.values.length > 0) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private T[] getParamValues(T = string)(in ServerHttpRequest request, string key) {
|
|
||||||
foreach (param; request.queryParams) {
|
|
||||||
if (param.key == key) {
|
|
||||||
return param.values.map!(s => s.to!T).array;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private ulong[] getAllPossibleCategoryIds(ulong parentId, TransactionCategoryRepository categoryRepo) {
|
private ulong[] getAllPossibleCategoryIds(ulong parentId, TransactionCategoryRepository categoryRepo) {
|
||||||
auto app = appender!(ulong[]);
|
auto app = appender!(ulong[]);
|
||||||
app ~= parentId;
|
app ~= parentId;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import transaction.api;
|
||||||
import transaction.model;
|
import transaction.model;
|
||||||
import transaction.data;
|
import transaction.data;
|
||||||
import transaction.dto;
|
import transaction.dto;
|
||||||
|
import transaction.search_filters : extractSearchParams;
|
||||||
import profile.data;
|
import profile.data;
|
||||||
import account.model;
|
import account.model;
|
||||||
import account.data;
|
import account.data;
|
||||||
|
|
@ -285,7 +286,7 @@ void exportTransactionsToFile(
|
||||||
ref ServerHttpResponse response
|
ref ServerHttpResponse response
|
||||||
) {
|
) {
|
||||||
Page!TransactionsListItem data = ds.getTransactionRepository()
|
Page!TransactionsListItem data = ds.getTransactionRepository()
|
||||||
.search(PageRequest.unpaged(), request);
|
.search(PageRequest.unpaged(), extractSearchParams(request));
|
||||||
ExportFileFormat fileFormat = getPreferredExportFileFormat(request);
|
ExportFileFormat fileFormat = getPreferredExportFileFormat(request);
|
||||||
if (fileFormat == ExportFileFormat.JSON) {
|
if (fileFormat == ExportFileFormat.JSON) {
|
||||||
import handy_http_data : writeJsonBody;
|
import handy_http_data : writeJsonBody;
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,17 @@ export interface CreateCategoryPayload {
|
||||||
parentId: number | null
|
parentId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AggregateTransactionCurrencyData {
|
||||||
|
credits: number
|
||||||
|
debits: number
|
||||||
|
balance: number
|
||||||
|
currency: Currency
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AggregateTransactionData {
|
||||||
|
currencies: AggregateTransactionCurrencyData[]
|
||||||
|
}
|
||||||
|
|
||||||
export class TransactionApiClient extends ApiClient {
|
export class TransactionApiClient extends ApiClient {
|
||||||
readonly path: string
|
readonly path: string
|
||||||
|
|
||||||
|
|
@ -225,6 +236,10 @@ export class TransactionApiClient extends ApiClient {
|
||||||
return super.getJson(this.path + '/transactions/search?' + params.toString())
|
return super.getJson(this.path + '/transactions/search?' + params.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAggregateTransactionData(params: URLSearchParams): Promise<AggregateTransactionData> {
|
||||||
|
return super.getJson(this.path + '/transactions/aggregate-data?' + params.toString())
|
||||||
|
}
|
||||||
|
|
||||||
exportTransactions(params: URLSearchParams): Promise<void> {
|
exportTransactions(params: URLSearchParams): Promise<void> {
|
||||||
return super.getFile(this.path + '/transactions/export?' + params.toString())
|
return super.getFile(this.path + '/transactions/export?' + params.toString())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-0 {
|
.mb-0 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Account, AccountApiClient } from '@/api/account'
|
import { type Account, AccountApiClient } from '@/api/account'
|
||||||
|
import { formatMoney } from '@/api/data'
|
||||||
import { defaultPage, type Page, type PageRequest, type SortDir } from '@/api/pagination'
|
import { defaultPage, type Page, type PageRequest, type SortDir } from '@/api/pagination'
|
||||||
import { getSelectedProfile } from '@/api/profile'
|
import { getSelectedProfile } from '@/api/profile'
|
||||||
import {
|
import {
|
||||||
TransactionApiClient,
|
TransactionApiClient,
|
||||||
|
type AggregateTransactionData,
|
||||||
type TransactionCategory,
|
type TransactionCategory,
|
||||||
type TransactionVendor,
|
type TransactionVendor,
|
||||||
type TransactionsListItem,
|
type TransactionsListItem,
|
||||||
|
|
@ -36,6 +38,7 @@ const FETCH_DEBOUNCE_DELAY = 300
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const page: Ref<Page<TransactionsListItem>> = ref(defaultPage())
|
const page: Ref<Page<TransactionsListItem>> = ref(defaultPage())
|
||||||
|
const aggregateData: Ref<AggregateTransactionData | undefined> = ref()
|
||||||
const lastFetchTime = ref(0)
|
const lastFetchTime = ref(0)
|
||||||
const fetchTimeoutId = ref<number | undefined>(undefined)
|
const fetchTimeoutId = ref<number | undefined>(undefined)
|
||||||
|
|
||||||
|
|
@ -134,6 +137,10 @@ async function fetchPage(pg: number, size: number) {
|
||||||
const urlWithParams = params.size == 0 ? route.path : route.path + '?' + params.toString()
|
const urlWithParams = params.size == 0 ? route.path : route.path + '?' + params.toString()
|
||||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||||
try {
|
try {
|
||||||
|
// Update aggregate data (asynchronously from main fetching).
|
||||||
|
aggregateData.value = undefined
|
||||||
|
api.getAggregateTransactionData(params).then((data) => (aggregateData.value = data))
|
||||||
|
// Fetch the main page of transactions.
|
||||||
const startTime = performance.now()
|
const startTime = performance.now()
|
||||||
page.value = await api.searchTransactions(params, pageRequest)
|
page.value = await api.searchTransactions(params, pageRequest)
|
||||||
const endTime = performance.now()
|
const endTime = performance.now()
|
||||||
|
|
@ -348,11 +355,41 @@ function loadAllParamValues(key: string): string[] {
|
||||||
@update="(pr) => fetchPage(pr.page, pr.size)"
|
@update="(pr) => fetchPage(pr.page, pr.size)"
|
||||||
class="align-right"
|
class="align-right"
|
||||||
/>
|
/>
|
||||||
<AppBadge size="sm">
|
<div>
|
||||||
{{ page.totalElements }} search
|
<AppBadge size="sm">
|
||||||
{{ page.totalElements == 1 ? 'result' : 'results' }}
|
{{ page.totalElements }} search
|
||||||
in {{ lastFetchTime }} milliseconds
|
{{ page.totalElements == 1 ? 'result' : 'results' }}
|
||||||
</AppBadge>
|
in {{ lastFetchTime }} milliseconds
|
||||||
|
</AppBadge>
|
||||||
|
</div>
|
||||||
|
<div v-if="aggregateData">
|
||||||
|
<div
|
||||||
|
v-for="data in aggregateData.currencies"
|
||||||
|
:key="data.currency.code"
|
||||||
|
class="mt-1 mb-1"
|
||||||
|
>
|
||||||
|
<AppBadge size="sm">
|
||||||
|
{{ data.currency.code }} Credits:
|
||||||
|
<span class="font-mono text-muted">{{ formatMoney(data.credits, data.currency) }}</span>
|
||||||
|
</AppBadge>
|
||||||
|
<AppBadge size="sm">
|
||||||
|
{{ data.currency.code }} Debits:
|
||||||
|
<span class="font-mono text-muted">{{ formatMoney(data.debits, data.currency) }}</span>
|
||||||
|
</AppBadge>
|
||||||
|
<AppBadge size="sm">
|
||||||
|
{{ data.currency.code }} Total:
|
||||||
|
<span
|
||||||
|
class="font-mono"
|
||||||
|
:class="{
|
||||||
|
'text-positive': data.balance > 0,
|
||||||
|
'text-negative': data.balance < 0,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ formatMoney(data.balance, data.currency) }}
|
||||||
|
</span>
|
||||||
|
</AppBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<TransactionCard
|
<TransactionCard
|
||||||
v-for="txn in page.items"
|
v-for="txn in page.items"
|
||||||
:key="txn.id"
|
:key="txn.id"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue