Added account export
This commit is contained in:
parent
78ebbac9ca
commit
d7630f3c15
|
|
@ -24,7 +24,7 @@ import attachment.dto;
|
||||||
|
|
||||||
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/accounts")
|
@GetMapping(PROFILE_PATH ~ "/accounts")
|
||||||
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import std.algorithm;
|
import std.algorithm;
|
||||||
import std.array;
|
import std.array;
|
||||||
|
|
@ -34,7 +34,7 @@ void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse res
|
||||||
writeJsonBody(response, accounts);
|
writeJsonBody(response, accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH)
|
@GetMapping(ACCOUNT_PATH)
|
||||||
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
|
|
@ -43,7 +43,7 @@ void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse resp
|
||||||
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/accounts")
|
@PostMapping(PROFILE_PATH ~ "/accounts")
|
||||||
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
||||||
|
|
@ -60,7 +60,7 @@ void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.PUT, ACCOUNT_PATH)
|
@PutMapping(ACCOUNT_PATH)
|
||||||
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
||||||
|
|
@ -80,14 +80,14 @@ void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id)));
|
writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH)
|
@DeleteMapping(ACCOUNT_PATH)
|
||||||
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
ds.getAccountRepository().deleteById(accountId);
|
ds.getAccountRepository().deleteById(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/balance")
|
@GetMapping(ACCOUNT_PATH ~ "/balance")
|
||||||
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
|
|
@ -149,6 +149,56 @@ void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpRespons
|
||||||
writeJsonBody(response, balances);
|
writeJsonBody(response, balances);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(PROFILE_PATH ~ "/accounts/export")
|
||||||
|
void handleGetAccountsExport(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
import std.algorithm;
|
||||||
|
import std.array;
|
||||||
|
import std.conv : to;
|
||||||
|
import util.csv;
|
||||||
|
import streams;
|
||||||
|
auto pc = getProfileContextOrThrow(request);
|
||||||
|
auto ds = getProfileDataSource(pc);
|
||||||
|
AccountResponse[] accounts = ds.getAccountRepository().findAll()
|
||||||
|
.map!(a => AccountResponse.of(a, getBalance(ds, a.id))).array;
|
||||||
|
|
||||||
|
response.headers.add("Content-Type", "text/csv");
|
||||||
|
response.headers.add("Content-Disposition", "attachment; filename=accounts.csv");
|
||||||
|
CsvStreamWriter!(OutputStream!(ubyte)*) csv = CsvStreamWriter!(OutputStream!(ubyte)*)(&response.outputStream);
|
||||||
|
csv
|
||||||
|
.append("ID")
|
||||||
|
.append("Name")
|
||||||
|
.append("Number Suffix")
|
||||||
|
.append("Currency")
|
||||||
|
.append("Type")
|
||||||
|
.append("Balance")
|
||||||
|
.append("Created At")
|
||||||
|
.append("Archived")
|
||||||
|
.append("Description")
|
||||||
|
.newLine();
|
||||||
|
|
||||||
|
foreach (account; accounts) {
|
||||||
|
csv
|
||||||
|
.append(account.id)
|
||||||
|
.append(account.name)
|
||||||
|
.append(account.numberSuffix)
|
||||||
|
.append(account.currency.code)
|
||||||
|
.append(account.type)
|
||||||
|
.append(account.currentBalance
|
||||||
|
.mapIfPresent!((b) {
|
||||||
|
auto money = MoneyValue(account.currency, b);
|
||||||
|
auto n = money.toFloatingPoint();
|
||||||
|
import std.format;
|
||||||
|
return format("%." ~ account.currency.fractionalDigits.to!string ~ "f", n);
|
||||||
|
})
|
||||||
|
.orElse("")
|
||||||
|
)
|
||||||
|
.append(account.createdAt)
|
||||||
|
.append(account.archived)
|
||||||
|
.append(account.description)
|
||||||
|
.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Value records:
|
// Value records:
|
||||||
|
|
||||||
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
module profile.api;
|
module profile.api;
|
||||||
|
|
||||||
import std.json;
|
import std.json;
|
||||||
import asdf;
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import handy_http_data.json;
|
import handy_http_data.json;
|
||||||
import handy_http_handlers.path_handler : getPathParamAs, PathMapping;
|
import handy_http_handlers.path_handler : getPathParamAs, PathMapping;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
module util.csv;
|
||||||
|
|
||||||
|
import streams;
|
||||||
|
import handy_http_primitives : Optional;
|
||||||
|
import std.conv : to;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility that allows for convenient writing of data to an output
|
||||||
|
* stream in CSV format.
|
||||||
|
*/
|
||||||
|
struct CsvStreamWriter(S) if (isByteOutputStream!S) {
|
||||||
|
S stream;
|
||||||
|
private bool atStartOfRow = true;
|
||||||
|
private StreamResult result;
|
||||||
|
|
||||||
|
this(S outputStream) {
|
||||||
|
this.stream = outputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a value to the current row.
|
||||||
|
* Params:
|
||||||
|
* value = The value to append.
|
||||||
|
*/
|
||||||
|
ref append(T)(T value) {
|
||||||
|
if (!atStartOfRow) {
|
||||||
|
result = stream.writeToStream([',']);
|
||||||
|
throwIfError(result);
|
||||||
|
}
|
||||||
|
string s = value.to!string();
|
||||||
|
result = stream.writeToStream(cast(ubyte[]) s);
|
||||||
|
throwIfError(result);
|
||||||
|
atStartOfRow = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a new line, moving to the next row.
|
||||||
|
*/
|
||||||
|
ref newLine() {
|
||||||
|
result = stream.writeToStream(['\n']);
|
||||||
|
throwIfError(result);
|
||||||
|
static if (isFlushableStream!S) {
|
||||||
|
auto optionalError = stream.flushStream();
|
||||||
|
throwIfError(optionalError);
|
||||||
|
}
|
||||||
|
atStartOfRow = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwIfError(StreamResult result) {
|
||||||
|
if (result.hasError) {
|
||||||
|
throw new Exception("Stream error: " ~ result.error.message.to!string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwIfError(Optional!StreamError result) {
|
||||||
|
if (!result.isNull) {
|
||||||
|
throw new Exception("Stream error: " ~ result.value.message.to!string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -149,4 +149,12 @@ struct MoneyValue {
|
||||||
static if (op == "-") return MoneyValue(currency, -this.value);
|
static if (op == "-") return MoneyValue(currency, -this.value);
|
||||||
static assert(false, "Operator " ~ op ~ " is not supported.");
|
static assert(false, "Operator " ~ op ~ " is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double toFloatingPoint() const {
|
||||||
|
double factor = 1;
|
||||||
|
for (int i = 0; i < currency.fractionalDigits; i++) {
|
||||||
|
factor *= 10;
|
||||||
|
}
|
||||||
|
return value / factor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,4 +213,8 @@ export class AccountApiClient extends ApiClient {
|
||||||
deleteValueRecord(accountId: number, valueRecordId: number): Promise<void> {
|
deleteValueRecord(accountId: number, valueRecordId: number): Promise<void> {
|
||||||
return super.delete(this.path + '/' + accountId + '/value-records/' + valueRecordId)
|
return super.delete(this.path + '/' + accountId + '/value-records/' + valueRecordId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadAccountsExport(): Promise<void> {
|
||||||
|
return super.getFile(this.path + '/export')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,45 +47,37 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function exportAccounts() {
|
||||||
|
const api = new AccountApiClient(route)
|
||||||
|
api.downloadAccountsExport()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<HomeModule title="Accounts">
|
<HomeModule title="Accounts">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<AccountCard
|
<AccountCard v-for="a in accounts" :account="a" :key="a.id" />
|
||||||
v-for="a in accounts"
|
|
||||||
:account="a"
|
|
||||||
:key="a.id"
|
|
||||||
/>
|
|
||||||
<p v-if="accounts.length === 0">
|
<p v-if="accounts.length === 0">
|
||||||
You haven't added any accounts. Add one to start tracking your finances.
|
You haven't added any accounts. Add one to start tracking your finances.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<AppBadge
|
<AppBadge v-for="bal in totalBalances" :key="bal.currency.code">
|
||||||
v-for="bal in totalBalances"
|
|
||||||
:key="bal.currency.code"
|
|
||||||
>
|
|
||||||
{{ bal.currency.code }} Total:
|
{{ bal.currency.code }} Total:
|
||||||
<span class="font-mono">{{ formatMoney(bal.balance, bal.currency) }}</span>
|
<span class="font-mono">{{ formatMoney(bal.balance, bal.currency) }}</span>
|
||||||
</AppBadge>
|
</AppBadge>
|
||||||
<AppBadge
|
<AppBadge v-for="debt in totalOwed" :key="debt.currency.code">
|
||||||
v-for="debt in totalOwed"
|
|
||||||
:key="debt.currency.code"
|
|
||||||
>
|
|
||||||
{{ debt.currency.code }} Debt:
|
{{ debt.currency.code }} Debt:
|
||||||
<span
|
<span class="font-mono" :class="{ 'text-negative': debt.balance > 0 }">{{ formatMoney(debt.balance,
|
||||||
class="font-mono"
|
debt.currency) }}</span>
|
||||||
:class="{ 'text-negative': debt.balance > 0 }"
|
|
||||||
>{{ formatMoney(debt.balance, debt.currency) }}</span
|
|
||||||
>
|
|
||||||
</AppBadge>
|
</AppBadge>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions>
|
<template v-slot:actions>
|
||||||
<AppButton
|
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)">Add Account
|
||||||
icon="plus"
|
</AppButton>
|
||||||
@click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)"
|
<AppButton icon="download" size="sm" @click="exportAccounts()">
|
||||||
>Add Account
|
Export
|
||||||
</AppButton>
|
</AppButton>
|
||||||
</template>
|
</template>
|
||||||
</HomeModule>
|
</HomeModule>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue