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";
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/accounts")
|
||||
@GetMapping(PROFILE_PATH ~ "/accounts")
|
||||
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
|
|
@ -34,7 +34,7 @@ void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse res
|
|||
writeJsonBody(response, accounts);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH)
|
||||
@GetMapping(ACCOUNT_PATH)
|
||||
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/accounts")
|
||||
@PostMapping(PROFILE_PATH ~ "/accounts")
|
||||
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto ds = getProfileDataSource(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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.PUT, ACCOUNT_PATH)
|
||||
@PutMapping(ACCOUNT_PATH)
|
||||
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH)
|
||||
@DeleteMapping(ACCOUNT_PATH)
|
||||
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
auto ds = getProfileDataSource(request);
|
||||
ds.getAccountRepository().deleteById(accountId);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/balance")
|
||||
@GetMapping(ACCOUNT_PATH ~ "/balance")
|
||||
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
auto ds = getProfileDataSource(request);
|
||||
|
|
@ -149,6 +149,56 @@ void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpRespons
|
|||
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:
|
||||
|
||||
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
module profile.api;
|
||||
|
||||
import std.json;
|
||||
import asdf;
|
||||
import handy_http_primitives;
|
||||
import handy_http_data.json;
|
||||
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 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> {
|
||||
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))
|
||||
})
|
||||
|
||||
function exportAccounts() {
|
||||
const api = new AccountApiClient(route)
|
||||
api.downloadAccountsExport()
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<HomeModule title="Accounts">
|
||||
<template v-slot:default>
|
||||
<AccountCard
|
||||
v-for="a in accounts"
|
||||
:account="a"
|
||||
:key="a.id"
|
||||
/>
|
||||
<AccountCard v-for="a in accounts" :account="a" :key="a.id" />
|
||||
<p v-if="accounts.length === 0">
|
||||
You haven't added any accounts. Add one to start tracking your finances.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<AppBadge
|
||||
v-for="bal in totalBalances"
|
||||
:key="bal.currency.code"
|
||||
>
|
||||
<AppBadge v-for="bal in totalBalances" :key="bal.currency.code">
|
||||
{{ bal.currency.code }} Total:
|
||||
<span class="font-mono">{{ formatMoney(bal.balance, bal.currency) }}</span>
|
||||
</AppBadge>
|
||||
<AppBadge
|
||||
v-for="debt in totalOwed"
|
||||
:key="debt.currency.code"
|
||||
>
|
||||
<AppBadge v-for="debt in totalOwed" :key="debt.currency.code">
|
||||
{{ debt.currency.code }} Debt:
|
||||
<span
|
||||
class="font-mono"
|
||||
:class="{ 'text-negative': debt.balance > 0 }"
|
||||
>{{ formatMoney(debt.balance, debt.currency) }}</span
|
||||
>
|
||||
<span class="font-mono" :class="{ 'text-negative': debt.balance > 0 }">{{ formatMoney(debt.balance,
|
||||
debt.currency) }}</span>
|
||||
</AppBadge>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<AppButton
|
||||
icon="plus"
|
||||
@click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)"
|
||||
>Add Account
|
||||
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-account`)">Add Account
|
||||
</AppButton>
|
||||
<AppButton icon="download" size="sm" @click="exportAccounts()">
|
||||
Export
|
||||
</AppButton>
|
||||
</template>
|
||||
</HomeModule>
|
||||
|
|
|
|||
Loading…
Reference in New Issue