Added transactions CSV / JSON file export.
This commit is contained in:
parent
1ff3ac5058
commit
9cb2d562d8
|
|
@ -22,7 +22,7 @@ import util.data;
|
||||||
|
|
||||||
immutable DEFAULT_TRANSACTION_PAGE = PageRequest(1, 10, [Sort("txn.timestamp", SortDir.DESC)]);
|
immutable DEFAULT_TRANSACTION_PAGE = PageRequest(1, 10, [Sort("txn.timestamp", SortDir.DESC)]);
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions")
|
@GetMapping(PROFILE_PATH ~ "/transactions")
|
||||||
void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||||
|
|
@ -30,7 +30,7 @@ void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, responsePage);
|
writeJsonBody(response, responsePage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, 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) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||||
|
|
@ -38,7 +38,13 @@ void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpRespo
|
||||||
writeJsonBody(response, page);
|
writeJsonBody(response, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
@GetMapping(PROFILE_PATH ~ "/transactions/export")
|
||||||
|
void handleExportTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
exportTransactionsToFile(ds, request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request));
|
TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request));
|
||||||
|
|
@ -47,7 +53,7 @@ void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/transactions")
|
@PostMapping(PROFILE_PATH ~ "/transactions")
|
||||||
void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import asdf : serializeToJson;
|
import asdf : serializeToJson;
|
||||||
auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request);
|
auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request);
|
||||||
|
|
@ -57,7 +63,7 @@ void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
@PutMapping(PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import asdf : serializeToJson;
|
import asdf : serializeToJson;
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -68,14 +74,14 @@ void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpRespon
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
@DeleteMapping(PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
ulong txnId = getTransactionIdOrThrow(request);
|
ulong txnId = getTransactionIdOrThrow(request);
|
||||||
deleteTransaction(ds, txnId);
|
deleteTransaction(ds, txnId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags")
|
@GetMapping(PROFILE_PATH ~ "/transaction-tags")
|
||||||
void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
string[] tags = ds.getTransactionTagRepository().findAll();
|
string[] tags = ds.getTransactionTagRepository().findAll();
|
||||||
|
|
@ -88,14 +94,14 @@ private ulong getTransactionIdOrThrow(in ServerHttpRequest request) {
|
||||||
|
|
||||||
// Vendors API
|
// Vendors API
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors")
|
@GetMapping(PROFILE_PATH ~ "/vendors")
|
||||||
void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionVendor[] vendors = getAllVendors(ds);
|
TransactionVendor[] vendors = getAllVendors(ds);
|
||||||
writeJsonBody(response, vendors);
|
writeJsonBody(response, vendors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
@GetMapping(PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionVendor vendor = getVendor(ds, getVendorId(request));
|
TransactionVendor vendor = getVendor(ds, getVendorId(request));
|
||||||
|
|
@ -107,7 +113,7 @@ struct VendorPayload {
|
||||||
string description;
|
string description;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/vendors")
|
@PostMapping(PROFILE_PATH ~ "/vendors")
|
||||||
void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -115,7 +121,7 @@ void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
||||||
writeJsonBody(response, vendor);
|
writeJsonBody(response, vendor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
@PutMapping(PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -123,7 +129,7 @@ void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
||||||
writeJsonBody(response, updated);
|
writeJsonBody(response, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
@DeleteMapping(PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
deleteVendor(ds, getVendorId(request));
|
deleteVendor(ds, getVendorId(request));
|
||||||
|
|
@ -172,7 +178,7 @@ struct CategoryPayload {
|
||||||
Nullable!ulong parentId;
|
Nullable!ulong parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/categories")
|
@PostMapping(PROFILE_PATH ~ "/categories")
|
||||||
void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -180,7 +186,7 @@ void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, category);
|
writeJsonBody(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
@PutMapping(PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||||
void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -189,7 +195,7 @@ void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, category);
|
writeJsonBody(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
@DeleteMapping(PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||||
void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
ulong categoryId = getCategoryId(request);
|
ulong categoryId = getCategoryId(request);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
module transaction.service;
|
module transaction.service;
|
||||||
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
|
import streams : isByteOutputStream;
|
||||||
import std.datetime;
|
import std.datetime;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
|
|
@ -271,6 +272,93 @@ void updateAttachments(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports transaction data to a file for download.
|
||||||
|
* Params:
|
||||||
|
* ds = The profile datasource to use.
|
||||||
|
* request = The request to read filter parameters from.
|
||||||
|
* fileFormat = The file format to write.
|
||||||
|
*/
|
||||||
|
void exportTransactionsToFile(
|
||||||
|
ProfileDataSource ds,
|
||||||
|
in ServerHttpRequest request,
|
||||||
|
ref ServerHttpResponse response
|
||||||
|
) {
|
||||||
|
Page!TransactionsListItem data = ds.getTransactionRepository()
|
||||||
|
.search(PageRequest.unpaged(), request);
|
||||||
|
ExportFileFormat fileFormat = getPreferredExportFileFormat(request);
|
||||||
|
if (fileFormat == ExportFileFormat.JSON) {
|
||||||
|
import handy_http_data : writeJsonBody;
|
||||||
|
addFileExportHeaders(response, "transactions.json", ContentTypes.APPLICATION_JSON);
|
||||||
|
writeJsonBody(response, data.items);
|
||||||
|
} else if (fileFormat == ExportFileFormat.CSV) {
|
||||||
|
addFileExportHeaders(response, "transactions.csv", ContentTypes.TEXT_CSV);
|
||||||
|
writeTransactionsCsvExport(&response.outputStream, data.items);
|
||||||
|
} else {
|
||||||
|
throw new HttpStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Invalid file export format requested. JSON or CSV are permitted."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeTransactionsCsvExport(S)(
|
||||||
|
S outputStream,
|
||||||
|
in TransactionsListItem[] items
|
||||||
|
) if (isByteOutputStream!S) {
|
||||||
|
import util.csv;
|
||||||
|
import std.format : format;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.string : join;
|
||||||
|
CsvStreamWriter!S csv = CsvStreamWriter!(S)(outputStream);
|
||||||
|
csv
|
||||||
|
.append("ID")
|
||||||
|
.append("Timestamp")
|
||||||
|
.append("Added to Finnow")
|
||||||
|
.append("Amount")
|
||||||
|
.append("Currency")
|
||||||
|
.append("Description")
|
||||||
|
.append("Internal Transfer")
|
||||||
|
.append("Vendor")
|
||||||
|
.append("Category")
|
||||||
|
.append("Credited Account")
|
||||||
|
.append("Credited Account Type")
|
||||||
|
.append("Credited Account Number")
|
||||||
|
.append("Debited Account")
|
||||||
|
.append("Debited Account Type")
|
||||||
|
.append("Debited Account Number")
|
||||||
|
.append("tags")
|
||||||
|
.newLine();
|
||||||
|
|
||||||
|
foreach (item; items) {
|
||||||
|
string tagsStr = join(item.tags, ",");
|
||||||
|
if (tagsStr.length > 0) {
|
||||||
|
tagsStr = "\"" ~ tagsStr ~ "\"";
|
||||||
|
}
|
||||||
|
csv
|
||||||
|
.append(item.id)
|
||||||
|
.append(item.timestamp)
|
||||||
|
.append(item.addedAt)
|
||||||
|
.append(format(
|
||||||
|
"%." ~ item.currency.fractionalDigits.to!string ~ "f",
|
||||||
|
MoneyValue(item.currency, item.amount).toFloatingPoint()
|
||||||
|
))
|
||||||
|
.append(item.currency.code)
|
||||||
|
.append(item.description)
|
||||||
|
.append(item.internalTransfer)
|
||||||
|
.append(item.vendor.mapIfPresent!(v => v.name).orElse(null))
|
||||||
|
.append(item.category.mapIfPresent!(c => c.name).orElse(null))
|
||||||
|
.append(item.creditedAccount.mapIfPresent!(a => a.name).orElse(null))
|
||||||
|
.append(item.creditedAccount.mapIfPresent!(a => a.type).orElse(null))
|
||||||
|
.append(item.creditedAccount.mapIfPresent!(a => "#" ~ a.numberSuffix).orElse(null))
|
||||||
|
.append(item.debitedAccount.mapIfPresent!(a => a.name).orElse(null))
|
||||||
|
.append(item.debitedAccount.mapIfPresent!(a => a.type).orElse(null))
|
||||||
|
.append(item.debitedAccount.mapIfPresent!(a => "#" ~ a.numberSuffix).orElse(null))
|
||||||
|
.append(tagsStr)
|
||||||
|
.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vendors Services
|
// Vendors Services
|
||||||
|
|
||||||
TransactionVendor[] getAllVendors(ProfileDataSource ds) {
|
TransactionVendor[] getAllVendors(ProfileDataSource ds) {
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,47 @@ struct TimeRange {
|
||||||
Optional!SysTime fromTime;
|
Optional!SysTime fromTime;
|
||||||
Optional!SysTime toTime;
|
Optional!SysTime toTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExportFileFormat {
|
||||||
|
JSON,
|
||||||
|
CSV
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportFileFormat getPreferredExportFileFormat(in ServerHttpRequest request) {
|
||||||
|
if ("Accept" in request.headers) {
|
||||||
|
string acceptStr = request.getHeaderAs!string("Accept");
|
||||||
|
if (acceptStr !is null && acceptStr == ContentTypes.TEXT_CSV) {
|
||||||
|
return ExportFileFormat.CSV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string fileFormatStr = request.getParamAs!string("file-format");
|
||||||
|
if (fileFormatStr !is null && fileFormatStr == ContentTypes.TEXT_CSV) {
|
||||||
|
return ExportFileFormat.CSV;
|
||||||
|
}
|
||||||
|
return ExportFileFormat.JSON; // JSON is the fallback if CSV is not requested.
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFileExportHeaders(
|
||||||
|
ref ServerHttpResponse response,
|
||||||
|
string filename,
|
||||||
|
string contentType,
|
||||||
|
bool includeTimestampSuffix = true
|
||||||
|
) {
|
||||||
|
response.headers.add("Content-Type", contentType);
|
||||||
|
if (includeTimestampSuffix) {
|
||||||
|
import std.format : format;
|
||||||
|
import std.string : lastIndexOf;
|
||||||
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
string timeStr = format!"_%04d-%02d-%02d_%02d-%02d-%02d"(
|
||||||
|
now.year, now.month, now.day,
|
||||||
|
now.hour, now.minute, now.second
|
||||||
|
);
|
||||||
|
ptrdiff_t dotIdx = lastIndexOf(filename, '.');
|
||||||
|
if (dotIdx == -1) {
|
||||||
|
filename = filename ~ timeStr;
|
||||||
|
} else {
|
||||||
|
filename = filename[0..dotIdx] ~ timeStr ~ filename[dotIdx..$];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.headers.add("Content-Disposition", "attachment; filename=" ~ filename);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ struct Page(T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ulong getTotalPageCount(ulong totalElements, ulong pageSize) {
|
private ulong getTotalPageCount(ulong totalElements, ulong pageSize) {
|
||||||
|
if (pageSize == 0) return totalElements > 0 ? 1 : 0;
|
||||||
return totalElements / pageSize + (totalElements % pageSize > 0 ? 1 : 0);
|
return totalElements / pageSize + (totalElements % pageSize > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,4 +164,6 @@ unittest {
|
||||||
assert(getTotalPageCount(5, 6) == 1);
|
assert(getTotalPageCount(5, 6) == 1);
|
||||||
assert(getTotalPageCount(5, 123) == 1);
|
assert(getTotalPageCount(5, 123) == 1);
|
||||||
assert(getTotalPageCount(250, 100) == 3);
|
assert(getTotalPageCount(250, 100) == 3);
|
||||||
|
assert(getTotalPageCount(25, 0) == 1);
|
||||||
|
assert(getTotalPageCount(0, 0) == 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,10 @@ export class TransactionApiClient extends ApiClient {
|
||||||
return super.getJson(this.path + '/transactions/search?' + params.toString())
|
return super.getJson(this.path + '/transactions/search?' + params.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportTransactions(params: URLSearchParams): Promise<void> {
|
||||||
|
return super.getFile(this.path + '/transactions/export?' + params.toString())
|
||||||
|
}
|
||||||
|
|
||||||
getTransaction(id: number): Promise<TransactionDetail> {
|
getTransaction(id: number): Promise<TransactionDetail> {
|
||||||
return super.getJson(this.path + '/transactions/' + id)
|
return super.getJson(this.path + '/transactions/' + id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,13 @@ function clearFilters() {
|
||||||
maxAmountFilter.value = undefined
|
maxAmountFilter.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportToFile() {
|
||||||
|
const params = buildFiltersQuery()
|
||||||
|
params.append('file-format', 'text/csv')
|
||||||
|
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||||
|
await api.exportTransactions(params)
|
||||||
|
}
|
||||||
|
|
||||||
function goToHome() {
|
function goToHome() {
|
||||||
router.push(`/profiles/${getSelectedProfile(route)}`)
|
router.push(`/profiles/${getSelectedProfile(route)}`)
|
||||||
}
|
}
|
||||||
|
|
@ -226,83 +233,42 @@ function loadAllParamValues(key: string): string[] {
|
||||||
<AppPage title="Transactions">
|
<AppPage title="Transactions">
|
||||||
<AppForm>
|
<AppForm>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControl
|
<FormControl label="Search" hint="Free-form text search against description, tags, vendor, category, account.">
|
||||||
label="Search"
|
<input v-model="searchQuery" type="text" placeholder="Search for transactions..." />
|
||||||
hint="Free-form text search against description, tags, vendor, category, account."
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search for transactions..."
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<div class="vueselect-control">
|
<div class="vueselect-control">
|
||||||
<h5>Tag</h5>
|
<h5>Tag</h5>
|
||||||
<VueSelect
|
<VueSelect v-model="tagFilters" :options="tagOptions" placeholder="Select tags" is-multi />
|
||||||
v-model="tagFilters"
|
|
||||||
:options="tagOptions"
|
|
||||||
placeholder="Select tags"
|
|
||||||
is-multi
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="vueselect-control">
|
<div class="vueselect-control">
|
||||||
<h5>Vendor</h5>
|
<h5>Vendor</h5>
|
||||||
<VueSelect
|
<VueSelect v-model="vendorFilters" :options="vendorOptions" placeholder="Select vendors" is-multi />
|
||||||
v-model="vendorFilters"
|
|
||||||
:options="vendorOptions"
|
|
||||||
placeholder="Select vendors"
|
|
||||||
is-multi
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="vueselect-control">
|
<div class="vueselect-control">
|
||||||
<h5>Category</h5>
|
<h5>Category</h5>
|
||||||
<VueSelect
|
<VueSelect v-model="categoryFilters" :options="categoryOptions" placeholder="Select categories" is-multi />
|
||||||
v-model="categoryFilters"
|
|
||||||
:options="categoryOptions"
|
|
||||||
placeholder="Select categories"
|
|
||||||
is-multi
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="vueselect-control">
|
<div class="vueselect-control">
|
||||||
<h5>Account</h5>
|
<h5>Account</h5>
|
||||||
<VueSelect
|
<VueSelect v-model="accountFilters" :options="accountOptions" placeholder="Select accounts" is-multi />
|
||||||
v-model="accountFilters"
|
|
||||||
:options="accountOptions"
|
|
||||||
placeholder="Select accounts"
|
|
||||||
is-multi
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControl label="Max Amount">
|
<FormControl label="Max Amount">
|
||||||
<input
|
<input v-model="maxAmountFilter" type="number" min="0" step="1" />
|
||||||
v-model="maxAmountFilter"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Min Amount">
|
<FormControl label="Min Amount">
|
||||||
<input
|
<input v-model="minAmountFilter" type="number" min="0" step="1" />
|
||||||
v-model="minAmountFilter"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControl label="Sort By">
|
<FormControl label="Sort By">
|
||||||
<select v-model="selectedSort">
|
<select v-model="selectedSort">
|
||||||
<option
|
<option v-for="sortOpt in SORT_PROPERTIES" :key="sortOpt.property" :value="sortOpt.property">
|
||||||
v-for="sortOpt in SORT_PROPERTIES"
|
|
||||||
:key="sortOpt.property"
|
|
||||||
:value="sortOpt.property"
|
|
||||||
>
|
|
||||||
{{ sortOpt.label }}
|
{{ sortOpt.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -315,36 +281,19 @@ function loadAllParamValues(key: string): string[] {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<ButtonBar>
|
<ButtonBar>
|
||||||
<AppButton
|
<AppButton size="sm" icon="home" @click="goToHome()">Back to Homepage</AppButton>
|
||||||
size="sm"
|
<AppButton size="sm" icon="trash" @click="clearFilters()">Clear Filters</AppButton>
|
||||||
icon="home"
|
<AppButton size="sm" icon="file-export" @click="exportToFile()">Export to CSV</AppButton>
|
||||||
@click="goToHome()"
|
|
||||||
>Back to Homepage</AppButton
|
|
||||||
>
|
|
||||||
<AppButton
|
|
||||||
size="sm"
|
|
||||||
icon="trash"
|
|
||||||
@click="clearFilters()"
|
|
||||||
>Clear Filters</AppButton
|
|
||||||
>
|
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
</AppForm>
|
</AppForm>
|
||||||
|
|
||||||
<PaginationControls
|
<PaginationControls :page="page" @update="(pr) => fetchPage(pr.page, pr.size)" class="align-right" />
|
||||||
:page="page"
|
|
||||||
@update="(pr) => fetchPage(pr.page, pr.size)"
|
|
||||||
class="align-right"
|
|
||||||
/>
|
|
||||||
<AppBadge size="sm">
|
<AppBadge size="sm">
|
||||||
{{ page.totalElements }} search
|
{{ page.totalElements }} search
|
||||||
{{ page.totalElements == 1 ? 'result' : 'results' }}
|
{{ page.totalElements == 1 ? 'result' : 'results' }}
|
||||||
in {{ lastFetchTime }} milliseconds
|
in {{ lastFetchTime }} milliseconds
|
||||||
</AppBadge>
|
</AppBadge>
|
||||||
<TransactionCard
|
<TransactionCard v-for="txn in page.items" :key="txn.id" :tx="txn" />
|
||||||
v-for="txn in page.items"
|
|
||||||
:key="txn.id"
|
|
||||||
:tx="txn"
|
|
||||||
/>
|
|
||||||
</AppPage>
|
</AppPage>
|
||||||
</template>
|
</template>
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|
@ -353,7 +302,7 @@ function loadAllParamValues(key: string): string[] {
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vueselect-control > h5 {
|
.vueselect-control>h5 {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue