Fixed cascading history item deletion, added login disclaimer.
This commit is contained in:
parent
71e99d1c94
commit
aba86b6979
|
|
@ -4,6 +4,7 @@ import std.datetime;
|
||||||
|
|
||||||
import d2sqlite3;
|
import d2sqlite3;
|
||||||
import handy_http_primitives : Optional;
|
import handy_http_primitives : Optional;
|
||||||
|
import slf4d;
|
||||||
|
|
||||||
import account.data;
|
import account.data;
|
||||||
import account.model;
|
import account.model;
|
||||||
|
|
@ -84,16 +85,6 @@ SQL",
|
||||||
|
|
||||||
void deleteById(ulong id) {
|
void deleteById(ulong id) {
|
||||||
doTransaction(db, () {
|
doTransaction(db, () {
|
||||||
// Delete associated history.
|
|
||||||
util.sqlite.update(
|
|
||||||
db,
|
|
||||||
"DELETE FROM history_item
|
|
||||||
WHERE id IN (
|
|
||||||
SELECT history_item_id FROM account_history_item
|
|
||||||
WHERE account_id = ?
|
|
||||||
)",
|
|
||||||
id
|
|
||||||
);
|
|
||||||
// Delete all associated transactions.
|
// Delete all associated transactions.
|
||||||
util.sqlite.update(
|
util.sqlite.update(
|
||||||
db,
|
db,
|
||||||
|
|
@ -101,8 +92,19 @@ SQL",
|
||||||
"(SELECT transaction_id FROM account_journal_entry WHERE account_id = ?)",
|
"(SELECT transaction_id FROM account_journal_entry WHERE account_id = ?)",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
// Finally delete the account itself (and all cascaded entities, like journal entries).
|
// Delete the account itself (and all cascaded entities, like journal entries).
|
||||||
util.sqlite.update(db, "DELETE FROM account WHERE id = ?", id);
|
util.sqlite.update(db, "DELETE FROM account WHERE id = ?", id);
|
||||||
|
/* Delete all orphaned history entries for journal entries referencing deleted transactions.
|
||||||
|
This is needed because even though the above `DELETE` cascades to
|
||||||
|
delete all history items for this account, there are some history
|
||||||
|
items linked to other accounts which aren't deleted automatically.
|
||||||
|
*/
|
||||||
|
util.sqlite.update(
|
||||||
|
db,
|
||||||
|
"DELETE FROM account_history_item WHERE type LIKE 'JOURNAL_ENTRY' AND id NOT IN (
|
||||||
|
SELECT item_id FROM history_item_linked_journal_entry
|
||||||
|
)"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,13 +161,12 @@ SQL",
|
||||||
Page!AccountHistoryItemResponse getHistory(ulong accountId, in PageRequest pagination) {
|
Page!AccountHistoryItemResponse getHistory(ulong accountId, in PageRequest pagination) {
|
||||||
ulong count = util.sqlite.count(
|
ulong count = util.sqlite.count(
|
||||||
db,
|
db,
|
||||||
"SELECT COUNT(history_item_id) FROM account_history_item WHERE account_id = ?",
|
"SELECT COUNT(id) FROM account_history_item WHERE account_id = ?",
|
||||||
accountId
|
accountId
|
||||||
);
|
);
|
||||||
const baseQuery = "SELECT hi.id, hi.timestamp, hi.type " ~
|
const baseQuery = "SELECT id, timestamp, type " ~
|
||||||
"FROM account_history_item ahi " ~
|
"FROM account_history_item " ~
|
||||||
"LEFT JOIN history_item hi ON ahi.history_item_id = hi.id " ~
|
"WHERE account_id = ?";
|
||||||
"WHERE ahi.account_id = ?";
|
|
||||||
string query = baseQuery ~ " " ~ pagination.toSql();
|
string query = baseQuery ~ " " ~ pagination.toSql();
|
||||||
// First fetch the basic information about each item.
|
// First fetch the basic information about each item.
|
||||||
BaseHistoryItem[] items = util.sqlite.findAll(
|
BaseHistoryItem[] items = util.sqlite.findAll(
|
||||||
|
|
@ -310,7 +311,7 @@ class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository {
|
||||||
void deleteById(ulong id) {
|
void deleteById(ulong id) {
|
||||||
util.sqlite.update(
|
util.sqlite.update(
|
||||||
db,
|
db,
|
||||||
"DELETE FROM history_item WHERE id IN " ~
|
"DELETE FROM account_history_item WHERE id IN " ~
|
||||||
"(SELECT item_id FROM history_item_linked_journal_entry WHERE journal_entry_id = ?)",
|
"(SELECT item_id FROM history_item_linked_journal_entry WHERE journal_entry_id = ?)",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
@ -320,7 +321,7 @@ class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository {
|
||||||
void deleteByAccountIdAndTransactionId(ulong accountId, ulong transactionId) {
|
void deleteByAccountIdAndTransactionId(ulong accountId, ulong transactionId) {
|
||||||
util.sqlite.update(
|
util.sqlite.update(
|
||||||
db,
|
db,
|
||||||
"DELETE FROM history_item WHERE id IN " ~
|
"DELETE FROM account_history_item WHERE id IN " ~
|
||||||
"(SELECT j.item_id FROM history_item_linked_journal_entry j " ~
|
"(SELECT j.item_id FROM history_item_linked_journal_entry j " ~
|
||||||
" LEFT JOIN account_journal_entry je ON je.id = j.journal_entry_id " ~
|
" LEFT JOIN account_journal_entry je ON je.id = j.journal_entry_id " ~
|
||||||
"WHERE je.account_id = ? AND je.transaction_id = ?)",
|
"WHERE je.account_id = ? AND je.transaction_id = ?)",
|
||||||
|
|
@ -443,7 +444,7 @@ class SqliteAccountValueRecordRepository : AccountValueRecordRepository {
|
||||||
// First delete any associated history items:
|
// First delete any associated history items:
|
||||||
util.sqlite.update(
|
util.sqlite.update(
|
||||||
db,
|
db,
|
||||||
"DELETE FROM history_item WHERE id IN " ~
|
"DELETE FROM account_history_item WHERE id IN " ~
|
||||||
"(SELECT item_id FROM history_item_linked_value_record WHERE value_record_id = ?)",
|
"(SELECT item_id FROM history_item_linked_value_record WHERE value_record_id = ?)",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
@ -505,16 +506,10 @@ private ulong insertNewAccountHistoryItem(
|
||||||
) {
|
) {
|
||||||
util.sqlite.update(
|
util.sqlite.update(
|
||||||
db,
|
db,
|
||||||
"INSERT INTO history_item (timestamp, type) VALUES (?, ?)",
|
"INSERT INTO account_history_item (account_id, timestamp, type) VALUES (?, ?, ?)",
|
||||||
|
accountId,
|
||||||
timestamp.toISOExtString(),
|
timestamp.toISOExtString(),
|
||||||
type
|
type
|
||||||
);
|
);
|
||||||
ulong historyItemId = db.lastInsertRowid();
|
return db.lastInsertRowid();
|
||||||
util.sqlite.update(
|
|
||||||
db,
|
|
||||||
"INSERT INTO account_history_item (account_id, history_item_id) VALUES (?, ?)",
|
|
||||||
accountId,
|
|
||||||
historyItemId
|
|
||||||
);
|
|
||||||
return historyItemId;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,13 @@ class SqliteTransactionRepository : TransactionRepository {
|
||||||
|
|
||||||
void deleteById(ulong id) {
|
void deleteById(ulong id) {
|
||||||
util.sqlite.deleteById(db, TABLE_NAME, id);
|
util.sqlite.deleteById(db, TABLE_NAME, id);
|
||||||
|
// Delete all history items for journal entries that have been removed by the deletion.
|
||||||
|
util.sqlite.update(
|
||||||
|
db,
|
||||||
|
"DELETE FROM account_history_item WHERE type LIKE 'JOURNAL_ENTRY' AND id NOT IN (
|
||||||
|
SELECT item_id FROM history_item_linked_journal_entry
|
||||||
|
)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Transaction parseTransaction(Row row) {
|
static Transaction parseTransaction(Row row) {
|
||||||
|
|
|
||||||
|
|
@ -161,21 +161,13 @@ CREATE TABLE account_value_record_attachment (
|
||||||
|
|
||||||
-- History Entities
|
-- History Entities
|
||||||
|
|
||||||
CREATE TABLE history_item (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
timestamp TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE account_history_item (
|
CREATE TABLE account_history_item (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
history_item_id INTEGER NOT NULL,
|
timestamp TEXT NOT NULL,
|
||||||
PRIMARY KEY (account_id, history_item_id),
|
type TEXT NOT NULL,
|
||||||
CONSTRAINT fk_account_history_item_account
|
CONSTRAINT fk_account_history_item_account
|
||||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
|
||||||
CONSTRAINT fk_account_history_item_history_item
|
|
||||||
FOREIGN KEY (history_item_id) REFERENCES history_item(id)
|
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -184,7 +176,7 @@ CREATE TABLE history_item_text (
|
||||||
item_id INTEGER PRIMARY KEY,
|
item_id INTEGER PRIMARY KEY,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
CONSTRAINT fk_history_item_text_item
|
CONSTRAINT fk_history_item_text_item
|
||||||
FOREIGN KEY (item_id) REFERENCES history_item(id)
|
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -196,7 +188,7 @@ CREATE TABLE history_item_property_change (
|
||||||
new_value TEXT,
|
new_value TEXT,
|
||||||
PRIMARY KEY (item_id, property_name),
|
PRIMARY KEY (item_id, property_name),
|
||||||
CONSTRAINT fk_history_item_property_change_item
|
CONSTRAINT fk_history_item_property_change_item
|
||||||
FOREIGN KEY (item_id) REFERENCES history_item(id)
|
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -206,7 +198,7 @@ CREATE TABLE history_item_linked_value_record (
|
||||||
value_record_id INTEGER NOT NULL,
|
value_record_id INTEGER NOT NULL,
|
||||||
PRIMARY KEY (item_id, value_record_id),
|
PRIMARY KEY (item_id, value_record_id),
|
||||||
CONSTRAINT fk_history_item_linked_value_record_item
|
CONSTRAINT fk_history_item_linked_value_record_item
|
||||||
FOREIGN KEY (item_id) REFERENCES history_item(id)
|
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
CONSTRAINT fk_history_item_linked_value_record_value_record
|
CONSTRAINT fk_history_item_linked_value_record_value_record
|
||||||
FOREIGN KEY (value_record_id) REFERENCES account_value_record(id)
|
FOREIGN KEY (value_record_id) REFERENCES account_value_record(id)
|
||||||
|
|
@ -219,7 +211,7 @@ CREATE TABLE history_item_linked_journal_entry (
|
||||||
journal_entry_id INTEGER NOT NULL,
|
journal_entry_id INTEGER NOT NULL,
|
||||||
PRIMARY KEY (item_id, journal_entry_id),
|
PRIMARY KEY (item_id, journal_entry_id),
|
||||||
CONSTRAINT fk_history_item_linked_journal_entry_item
|
CONSTRAINT fk_history_item_linked_journal_entry_item
|
||||||
FOREIGN KEY (item_id) REFERENCES history_item(id)
|
FOREIGN KEY (item_id) REFERENCES account_history_item(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
CONSTRAINT fk_history_item_linked_journal_entry_journal_entry
|
CONSTRAINT fk_history_item_linked_journal_entry_journal_entry
|
||||||
FOREIGN KEY (journal_entry_id) REFERENCES account_journal_entry(id)
|
FOREIGN KEY (journal_entry_id) REFERENCES account_journal_entry(id)
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import { formatMoney } from '@/api/data';
|
||||||
import AppButton from '../AppButton.vue';
|
import AppButton from '../AppButton.vue';
|
||||||
import { showConfirm } from '@/util/alert';
|
import { showConfirm } from '@/util/alert';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import ButtonBar from '../ButtonBar.vue';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
|
const props = defineProps<{ item: AccountHistoryValueRecordItem, accountId: number }>()
|
||||||
|
|
||||||
async function deleteValueRecord(id: number) {
|
async function deleteValueRecord(id: number) {
|
||||||
const confirm = await showConfirm('Are you sure you want to delete this value record?')
|
const confirm = await showConfirm("Are you sure you want to delete this value record? This may affect how your account's balance is calculated.")
|
||||||
if (!confirm) return
|
if (!confirm) return
|
||||||
const api = new AccountApiClient(route)
|
const api = new AccountApiClient(route)
|
||||||
try {
|
try {
|
||||||
|
|
@ -25,8 +26,10 @@ async function deleteValueRecord(id: number) {
|
||||||
<div>
|
<div>
|
||||||
Value recorded for this account at {{ formatMoney(item.value, item.currency) }}
|
Value recorded for this account at {{ formatMoney(item.value, item.currency) }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<ButtonBar>
|
||||||
<AppButton button-type="button" icon="trash" @click="deleteValueRecord(item.valueRecordId)" />
|
<AppButton type="button" icon="trash" size="sm" @click="deleteValueRecord(item.valueRecordId)">
|
||||||
</div>
|
Delete this record
|
||||||
|
</AppButton>
|
||||||
|
</ButtonBar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ async function addValueRecord() {
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<td>{{ account.description }}</td>
|
<td>{{ account.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="account.currentBalance">
|
<tr v-if="account.currentBalance !== null">
|
||||||
<th>Current Balance</th>
|
<th>Current Balance</th>
|
||||||
<td>{{ formatMoney(account.currentBalance, account.currency) }}</td>
|
<td>{{ formatMoney(account.currentBalance, account.currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -75,24 +75,43 @@ function generateSampleData() {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="app-login-panel">
|
<div class="app-login-panel">
|
||||||
<h1 style="text-align: center;">Login to <span style="font-family: 'PlaywriteNL';">Finnow</span></h1>
|
<h1 style="text-align: center; margin-bottom: 0.5rem; font-family: 'PlaywriteNL';">Finnow</h1>
|
||||||
|
<p style="text-align: center; font-weight: 600; margin-top: 0;">
|
||||||
|
<em>Personal finance for the modern era.</em>
|
||||||
|
</p>
|
||||||
<AppForm @submit="doLogin()">
|
<AppForm @submit="doLogin()">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControl label="Username">
|
<FormControl label="Username" class="login-control">
|
||||||
<input type="text" v-model="username" :disabled="disableForm" />
|
<input class="login-input" type="text" v-model="username" :disabled="disableForm" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Password">
|
<FormControl label="Password" class="login-control">
|
||||||
<input id="password-input" type="password" v-model="password" :disabled="disableForm" />
|
<input class="login-input" id="password-input" type="password" v-model="password" :disabled="disableForm" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<div style="display: flex; margin-left: 1rem; margin-right: 1rem;">
|
<div style="display: flex; margin-left: 1rem; margin-right: 1rem;">
|
||||||
<AppButton type="submit" :disabled="disableForm || !isDataValid()" style="flex-grow: 1;">Login
|
<AppButton type="submit" :disabled="disableForm || !isDataValid()" style="flex-grow: 1;">Login
|
||||||
</AppButton>
|
</AppButton>
|
||||||
<AppButton button-type="button" button-style="secondary" :disabled="disableForm || !isDataValid()"
|
<AppButton type="button" theme="secondary" :disabled="disableForm || !isDataValid()" @click="doRegister()">
|
||||||
@click="doRegister()">Register</AppButton>
|
Register</AppButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isDev">
|
<div v-if="isDev">
|
||||||
<AppButton button-type="button" @click="generateSampleData()">Generate Sample Data</AppButton>
|
<AppButton type="button" @click="generateSampleData()">Generate Sample Data</AppButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Disclaimer note that this project is still a work-in-progress! -->
|
||||||
|
<div style="font-size: smaller;">
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> Finnow is still under development, and may be
|
||||||
|
prone to bugs or unexpected data loss. Proceed to register and use
|
||||||
|
this service at your own risk.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Data is stored securely in protected cloud storage, but may be
|
||||||
|
accessed by a system administrator in case of errors or debugging.
|
||||||
|
Please <strong>DO NOT</strong> store any sensitive financial
|
||||||
|
credentials that you aren't okay with losing or potentially being
|
||||||
|
leaked. Finnow is not responsible for any lost or compromised data.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AppForm>
|
</AppForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,4 +121,14 @@ function generateSampleData() {
|
||||||
max-width: 40ch;
|
max-width: 40ch;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
max-width: 200px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-control {
|
||||||
|
flex-grow: 0;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,13 @@ async function doChangePassword() {
|
||||||
<p>
|
<p>
|
||||||
You are logged in as <code style="font-size: 14px;">{{ authStore.state?.username }}</code>.
|
You are logged in as <code style="font-size: 14px;">{{ authStore.state?.username }}</code>.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
There's not really that much to do with your user account specifically,
|
||||||
|
all important settings are profile-specific.
|
||||||
|
</p>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<AppButton @click="showChangePasswordModal()">Change Password</AppButton>
|
<AppButton @click="showChangePasswordModal()">Change Password</AppButton>
|
||||||
<AppButton @click="doDeleteUser()">Delete My User Account</AppButton>
|
<AppButton icon="trash" theme="secondary" @click="doDeleteUser()">Delete My User</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal for changing the user's password. -->
|
<!-- Modal for changing the user's password. -->
|
||||||
|
|
@ -70,6 +74,9 @@ async function doChangePassword() {
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<AppForm>
|
<AppForm>
|
||||||
<h2>Change Password</h2>
|
<h2>Change Password</h2>
|
||||||
|
<p>
|
||||||
|
Change the password used to log into your user account.
|
||||||
|
</p>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControl label="Current Password">
|
<FormControl label="Current Password">
|
||||||
<input type="password" v-model="currentPassword" minlength="8" />
|
<input type="password" v-model="currentPassword" minlength="8" />
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ onMounted(async () => {
|
||||||
<template>
|
<template>
|
||||||
<HomeModule title="Accounts">
|
<HomeModule title="Accounts">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<table class="app-table">
|
<table class="app-table" v-if="accounts.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
|
@ -47,6 +47,9 @@ onMounted(async () => {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p v-if="accounts.length === 0">
|
||||||
|
You haven't added any accounts. Add one to start tracking your finances.
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions>
|
<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
|
||||||
|
|
|
||||||
|
|
@ -29,45 +29,50 @@ async function fetchPage(pageRequest: PageRequest) {
|
||||||
<template>
|
<template>
|
||||||
<HomeModule title="Transactions">
|
<HomeModule title="Transactions">
|
||||||
<template v-slot:default>
|
<template v-slot:default>
|
||||||
<table class="app-table">
|
<div v-if="transactions.totalElements > 0">
|
||||||
<thead>
|
<table class="app-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Date</th>
|
<tr>
|
||||||
<th>Amount</th>
|
<th>Date</th>
|
||||||
<th>Currency</th>
|
<th>Amount</th>
|
||||||
<th>Description</th>
|
<th>Currency</th>
|
||||||
<th>Credited Account</th>
|
<th>Description</th>
|
||||||
<th>Debited Account</th>
|
<th>Credited Account</th>
|
||||||
<th></th>
|
<th>Debited Account</th>
|
||||||
</tr>
|
<th></th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="tx in transactions.items" :key="tx.id">
|
<tbody>
|
||||||
<td>
|
<tr v-for="tx in transactions.items" :key="tx.id">
|
||||||
{{ new Date(tx.timestamp).toLocaleDateString() }}
|
<td>
|
||||||
</td>
|
{{ new Date(tx.timestamp).toLocaleDateString() }}
|
||||||
<td style="text-align: right;">{{ formatMoney(tx.amount, tx.currency) }}</td>
|
</td>
|
||||||
<td>{{ tx.currency.code }}</td>
|
<td style="text-align: right;">{{ formatMoney(tx.amount, tx.currency) }}</td>
|
||||||
<td>{{ tx.description }}</td>
|
<td>{{ tx.currency.code }}</td>
|
||||||
<td>
|
<td>{{ tx.description }}</td>
|
||||||
<RouterLink v-if="tx.creditedAccount"
|
<td>
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.creditedAccount.id}`">
|
<RouterLink v-if="tx.creditedAccount"
|
||||||
{{ tx.creditedAccount?.name }}
|
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.creditedAccount.id}`">
|
||||||
</RouterLink>
|
{{ tx.creditedAccount?.name }}
|
||||||
</td>
|
</RouterLink>
|
||||||
<td>
|
</td>
|
||||||
<RouterLink v-if="tx.debitedAccount"
|
<td>
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.debitedAccount.id}`">
|
<RouterLink v-if="tx.debitedAccount"
|
||||||
{{ tx.debitedAccount?.name }}
|
:to="`/profiles/${getSelectedProfile(route)}/accounts/${tx.debitedAccount.id}`">
|
||||||
</RouterLink>
|
{{ tx.debitedAccount?.name }}
|
||||||
</td>
|
</RouterLink>
|
||||||
<td>
|
</td>
|
||||||
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/transactions/${tx.id}`">View</RouterLink>
|
<td>
|
||||||
</td>
|
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/transactions/${tx.id}`">View</RouterLink>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
</table>
|
||||||
|
<PaginationControls :page="transactions" @update="pr => fetchPage(pr)"></PaginationControls>
|
||||||
|
</div>
|
||||||
|
<p v-if="transactions.totalElements === 0">
|
||||||
|
You haven't added any transactions.
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions>
|
<template v-slot:actions>
|
||||||
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)">
|
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue