Added internal_transfer attribute to transactions, migration of schemas automatically.
This commit is contained in:
parent
83db4baa5b
commit
cb690702cc
|
|
@ -39,10 +39,12 @@ class FileSystemProfileRepository : ProfileRepository {
|
|||
if (!exists(getProfilesDir())) mkdir(getProfilesDir());
|
||||
ProfileDataSource ds = new SqliteProfileDataSource(path);
|
||||
import std.datetime;
|
||||
import std.conv;
|
||||
auto propsRepo = ds.getPropertiesRepository();
|
||||
propsRepo.setProperty("name", name);
|
||||
propsRepo.setProperty("createdAt", Clock.currTime(UTC()).toISOExtString());
|
||||
propsRepo.setProperty("user", username);
|
||||
propsRepo.setProperty("database-schema-version", SCHEMA_VERSION.to!string());
|
||||
return new Profile(name);
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +135,9 @@ class SqlitePropertiesRepository : PropertiesRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private const SCHEMA = import("sql/schema.sql");
|
||||
private const uint SCHEMA_VERSION = 1;
|
||||
|
||||
/**
|
||||
* An SQLite implementation of the ProfileDataSource that uses a single
|
||||
* database connection to initialize various entity data access objects lazily.
|
||||
|
|
@ -145,7 +150,6 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
import attachment.data;
|
||||
import attachment.data_impl_sqlite;
|
||||
|
||||
const SCHEMA = import("sql/schema.sql");
|
||||
private const string dbPath;
|
||||
Database db;
|
||||
|
||||
|
|
@ -159,6 +163,7 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
infoF!"Initializing database: %s"(dbPath);
|
||||
db.run(SCHEMA);
|
||||
}
|
||||
migrateSchema();
|
||||
}
|
||||
|
||||
PropertiesRepository getPropertiesRepository() return scope {
|
||||
|
|
@ -200,4 +205,32 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
void doTransaction(void delegate () dg) {
|
||||
util.sqlite.doTransaction(db, dg);
|
||||
}
|
||||
|
||||
private void migrateSchema() {
|
||||
import std.conv;
|
||||
PropertiesRepository propsRepo = new SqlitePropertiesRepository(this.db);
|
||||
uint currentVersion;
|
||||
try {
|
||||
currentVersion = propsRepo.findProperty("database-schema-version")
|
||||
.mapIfPresent!(s => s.to!uint).orElse(0);
|
||||
} catch (ConvException e) {
|
||||
warn("Failed to parse database-schema-version property.", e);
|
||||
currentVersion = 0;
|
||||
}
|
||||
if (currentVersion == SCHEMA_VERSION) return;
|
||||
|
||||
static const migrations = [
|
||||
import("sql/migrations/1.sql")
|
||||
];
|
||||
static if (migrations.length != SCHEMA_VERSION) {
|
||||
static assert(false, "Schema version doesn't match the list of defined migrations.");
|
||||
}
|
||||
|
||||
while (currentVersion < SCHEMA_VERSION) {
|
||||
infoF!"Migrating schema from version %d to %d."(currentVersion, currentVersion + 1);
|
||||
db.run(migrations[currentVersion]);
|
||||
currentVersion++;
|
||||
propsRepo.setProperty("database-schema-version", currentVersion.to!string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,48 +264,49 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
item.amount = row.peek!ulong(3);
|
||||
item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4));
|
||||
item.description = row.peek!string(5);
|
||||
item.internalTransfer = row.peek!bool(6);
|
||||
|
||||
Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6);
|
||||
Nullable!ulong vendorId = row.peek!(Nullable!ulong)(7);
|
||||
if (!vendorId.isNull) {
|
||||
item.vendor = Optional!(TransactionDetail.Vendor).of(
|
||||
TransactionDetail.Vendor(
|
||||
vendorId.get,
|
||||
row.peek!string(7),
|
||||
row.peek!string(8)
|
||||
row.peek!string(8),
|
||||
row.peek!string(9)
|
||||
)).toNullable;
|
||||
}
|
||||
Nullable!ulong categoryId = row.peek!(Nullable!ulong)(9);
|
||||
Nullable!ulong categoryId = row.peek!(Nullable!ulong)(10);
|
||||
if (!categoryId.isNull) {
|
||||
item.category = Optional!(TransactionDetail.Category).of(
|
||||
TransactionDetail.Category(
|
||||
categoryId.get,
|
||||
row.peek!(Nullable!ulong)(10),
|
||||
row.peek!string(11),
|
||||
row.peek!(Nullable!ulong)(11),
|
||||
row.peek!string(12),
|
||||
row.peek!string(13)
|
||||
row.peek!string(13),
|
||||
row.peek!string(14)
|
||||
)).toNullable;
|
||||
}
|
||||
Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(14);
|
||||
Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(15);
|
||||
if (!creditedAccountId.isNull) {
|
||||
item.creditedAccount = Optional!(TransactionDetail.Account).of(
|
||||
TransactionDetail.Account(
|
||||
creditedAccountId.get,
|
||||
row.peek!string(15),
|
||||
row.peek!string(16),
|
||||
row.peek!string(17)
|
||||
row.peek!string(17),
|
||||
row.peek!string(18)
|
||||
)).toNullable;
|
||||
}
|
||||
Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(18);
|
||||
Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(19);
|
||||
if (!debitedAccountId.isNull) {
|
||||
item.debitedAccount = Optional!(TransactionDetail.Account).of(
|
||||
TransactionDetail.Account(
|
||||
debitedAccountId.get,
|
||||
row.peek!string(19),
|
||||
row.peek!string(20),
|
||||
row.peek!string(21)
|
||||
row.peek!string(21),
|
||||
row.peek!string(22)
|
||||
)).toNullable;
|
||||
}
|
||||
string tagsStr = row.peek!string(22);
|
||||
string tagsStr = row.peek!string(23);
|
||||
if (tagsStr !is null && tagsStr.length > 0) {
|
||||
import std.string : split;
|
||||
item.tags = tagsStr.split(",");
|
||||
|
|
@ -353,6 +354,7 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
data.amount,
|
||||
data.currencyCode,
|
||||
data.description,
|
||||
data.internalTransfer,
|
||||
data.vendorId,
|
||||
data.categoryId
|
||||
);
|
||||
|
|
@ -378,6 +380,7 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
data.amount,
|
||||
data.currencyCode,
|
||||
data.description,
|
||||
data.internalTransfer,
|
||||
data.vendorId,
|
||||
data.categoryId,
|
||||
transactionId
|
||||
|
|
@ -403,20 +406,6 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
);
|
||||
}
|
||||
|
||||
static Transaction parseTransaction(Row row) {
|
||||
import std.typecons : Nullable;
|
||||
return Transaction(
|
||||
row.peek!ulong(0),
|
||||
SysTime.fromISOExtString(row.peek!string(1)),
|
||||
SysTime.fromISOExtString(row.peek!string(2)),
|
||||
row.peek!ulong(3),
|
||||
Currency.ofCode(row.peek!(string, PeekMode.slice)(4)),
|
||||
row.peek!string(5),
|
||||
toOptional(row.peek!(Nullable!ulong)(6)),
|
||||
toOptional(row.peek!(Nullable!ulong)(7))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to parse a list of transaction list items as obtained from the
|
||||
* `get_transactions.sql` query. Because there are possibly multiple rows
|
||||
|
|
@ -459,45 +448,46 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
item.amount = row.peek!ulong(3);
|
||||
item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4));
|
||||
item.description = row.peek!string(5);
|
||||
item.internalTransfer = row.peek!bool(6);
|
||||
// Read the nullable Vendor information.
|
||||
Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6);
|
||||
Nullable!ulong vendorId = row.peek!(Nullable!ulong)(7);
|
||||
if (!vendorId.isNull) {
|
||||
string vendorName = row.peek!string(7);
|
||||
string vendorName = row.peek!string(8);
|
||||
item.vendor = Optional!(TransactionsListItem.Vendor).of(
|
||||
TransactionsListItem.Vendor(vendorId.get, vendorName));
|
||||
}
|
||||
// Read the nullable Category information.
|
||||
Nullable!ulong categoryId = row.peek!(Nullable!ulong)(8);
|
||||
Nullable!ulong categoryId = row.peek!(Nullable!ulong)(9);
|
||||
if (!categoryId.isNull) {
|
||||
string categoryName = row.peek!string(9);
|
||||
string categoryColor = row.peek!string(10);
|
||||
string categoryName = row.peek!string(10);
|
||||
string categoryColor = row.peek!string(11);
|
||||
item.category = Optional!(TransactionsListItem.Category).of(
|
||||
TransactionsListItem.Category(categoryId.get, categoryName, categoryColor));
|
||||
}
|
||||
// Read the nullable creditedAccount.
|
||||
Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(11);
|
||||
Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(12);
|
||||
if (!creditedAccountId.isNull) {
|
||||
ulong id = creditedAccountId.get;
|
||||
string name = row.peek!string(12);
|
||||
string type = row.peek!string(13);
|
||||
string suffix = row.peek!string(14);
|
||||
string name = row.peek!string(13);
|
||||
string type = row.peek!string(14);
|
||||
string suffix = row.peek!string(15);
|
||||
item.creditedAccount = Optional!(TransactionsListItem.Account).of(
|
||||
TransactionsListItem.Account(id, name, type, suffix));
|
||||
}
|
||||
// Read the nullable debitedAccount.
|
||||
Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(15);
|
||||
Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(16);
|
||||
if (!debitedAccountId.isNull) {
|
||||
ulong id = debitedAccountId.get;
|
||||
string name = row.peek!string(16);
|
||||
string type = row.peek!string(17);
|
||||
string suffix = row.peek!string(18);
|
||||
string name = row.peek!string(17);
|
||||
string type = row.peek!string(18);
|
||||
string suffix = row.peek!string(19);
|
||||
item.debitedAccount = Optional!(TransactionsListItem.Account).of(
|
||||
TransactionsListItem.Account(id, name, type, suffix));
|
||||
}
|
||||
}
|
||||
|
||||
// Read multi-row properties, like tags, to the current item.
|
||||
string tag = row.peek!string(19);
|
||||
string tag = row.peek!string(20);
|
||||
if (tag !is null) {
|
||||
item.tags ~= tag;
|
||||
}
|
||||
|
|
@ -509,56 +499,6 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
return app[];
|
||||
}
|
||||
|
||||
static TransactionsListItem parseTransactionsListItem(Row row) {
|
||||
TransactionsListItem item;
|
||||
item.id = row.peek!ulong(0);
|
||||
item.timestamp = row.peek!string(1);
|
||||
item.addedAt = row.peek!string(2);
|
||||
item.amount = row.peek!ulong(3);
|
||||
item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4));
|
||||
item.description = row.peek!string(5);
|
||||
|
||||
Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6);
|
||||
if (!vendorId.isNull) {
|
||||
string vendorName = row.peek!string(7);
|
||||
item.vendor = Optional!(TransactionsListItem.Vendor).of(
|
||||
TransactionsListItem.Vendor(vendorId.get, vendorName));
|
||||
}
|
||||
Nullable!ulong categoryId = row.peek!(Nullable!ulong)(8);
|
||||
if (!categoryId.isNull) {
|
||||
string categoryName = row.peek!string(9);
|
||||
string categoryColor = row.peek!string(10);
|
||||
item.category = Optional!(TransactionsListItem.Category).of(
|
||||
TransactionsListItem.Category(categoryId.get, categoryName, categoryColor));
|
||||
}
|
||||
Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(11);
|
||||
if (!creditedAccountId.isNull) {
|
||||
ulong id = creditedAccountId.get;
|
||||
string name = row.peek!string(12);
|
||||
string type = row.peek!string(13);
|
||||
string suffix = row.peek!string(14);
|
||||
item.creditedAccount = Optional!(TransactionsListItem.Account).of(
|
||||
TransactionsListItem.Account(id, name, type, suffix));
|
||||
}
|
||||
Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(15);
|
||||
if (!debitedAccountId.isNull) {
|
||||
ulong id = debitedAccountId.get;
|
||||
string name = row.peek!string(16);
|
||||
string type = row.peek!string(17);
|
||||
string suffix = row.peek!string(18);
|
||||
item.debitedAccount = Optional!(TransactionsListItem.Account).of(
|
||||
TransactionsListItem.Account(id, name, type, suffix));
|
||||
}
|
||||
string tagsStr = row.peek!string(19);
|
||||
if (tagsStr !is null && tagsStr.length > 0) {
|
||||
import std.string : split;
|
||||
item.tags = tagsStr.split(",");
|
||||
} else {
|
||||
item.tags = [];
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private void insertLineItems(ulong transactionId, in AddTransactionPayload data) {
|
||||
foreach (size_t idx, lineItem; data.lineItems) {
|
||||
util.sqlite.update(
|
||||
|
|
@ -595,6 +535,7 @@ class SqliteTransactionRepository : TransactionRepository {
|
|||
.select("txn.amount")
|
||||
.select("txn.currency")
|
||||
.select("txn.description")
|
||||
.select("txn.internal_transfer")
|
||||
.select("txn.vendor_id")
|
||||
.select("vendor.name")
|
||||
.select("txn.category_id")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct TransactionsListItem {
|
|||
ulong amount;
|
||||
Currency currency;
|
||||
string description;
|
||||
bool internalTransfer;
|
||||
@serdeTransformOut!serializeOptional
|
||||
Optional!Vendor vendor;
|
||||
@serdeTransformOut!serializeOptional
|
||||
|
|
@ -54,6 +55,7 @@ struct TransactionDetail {
|
|||
ulong amount;
|
||||
Currency currency;
|
||||
string description;
|
||||
bool internalTransfer;
|
||||
Nullable!Vendor vendor;
|
||||
Nullable!Category category;
|
||||
Nullable!Account creditedAccount;
|
||||
|
|
@ -98,6 +100,7 @@ struct AddTransactionPayload {
|
|||
ulong amount;
|
||||
string currencyCode;
|
||||
string description;
|
||||
bool internalTransfer;
|
||||
Nullable!ulong vendorId;
|
||||
Nullable!ulong categoryId;
|
||||
Nullable!ulong creditedAccountId;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,25 @@ void applyFilters(ref QueryBuilder qb, in ServerHttpRequest request) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean filter for internal transfer.
|
||||
if (request.hasParam("internal-transfer")) {
|
||||
string value = request.getParamValues("internal-transfer")[0]
|
||||
.strip()
|
||||
.toUpper();
|
||||
Optional!bool internalTransferFilter = Optional!bool.empty();
|
||||
if (value == "Y" || value == "YES" || value == "TRUE" || value == "1") {
|
||||
internalTransferFilter = Optional!bool.of(true);
|
||||
} else if (value == "N" || value == "NO" || value == "FALSE" || value == "0") {
|
||||
internalTransferFilter = Optional!bool.of(false);
|
||||
}
|
||||
if (!internalTransferFilter.isNull) {
|
||||
qb.where("txn.internal_transfer = ?");
|
||||
qb.withArgBinding((ref stmt, ref idx) {
|
||||
stmt.bind(idx++, internalTransferFilter.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Textual search query:
|
||||
if (request.hasParam("q")) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ txn.added_at AS added_at,
|
|||
txn.amount AS amount,
|
||||
txn.currency AS currency,
|
||||
txn.description AS description,
|
||||
txn.internal_transfer AS internal_transfer,
|
||||
|
||||
txn.vendor_id AS vendor_id,
|
||||
vendor.name AS vendor_name,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ INSERT INTO "transaction" (
|
|||
amount,
|
||||
currency,
|
||||
description,
|
||||
internal_transfer,
|
||||
vendor_id,
|
||||
category_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "transaction"
|
||||
ADD COLUMN internal_transfer BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
|
@ -63,6 +63,7 @@ CREATE TABLE "transaction" (
|
|||
amount INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
description TEXT,
|
||||
internal_transfer BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
vendor_id INTEGER,
|
||||
category_id INTEGER,
|
||||
CONSTRAINT fk_transaction_vendor
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ SET
|
|||
amount = ?,
|
||||
currency = ?,
|
||||
description = ?,
|
||||
internal_transfer = ?,
|
||||
vendor_id = ?,
|
||||
category_id = ?
|
||||
WHERE id = ?
|
||||
|
|
@ -32,17 +32,6 @@ export interface TransactionCategoryTree {
|
|||
depth: number
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: number
|
||||
timestamp: string
|
||||
addedAt: string
|
||||
amount: number
|
||||
currency: string
|
||||
description: string
|
||||
vendorId: number | null
|
||||
categoryId: number | null
|
||||
}
|
||||
|
||||
export interface TransactionsListItem {
|
||||
id: number
|
||||
timestamp: string
|
||||
|
|
@ -50,6 +39,7 @@ export interface TransactionsListItem {
|
|||
amount: number
|
||||
currency: Currency
|
||||
description: string
|
||||
internalTransfer: boolean
|
||||
vendor: TransactionsListItemVendor | null
|
||||
category: TransactionsListItemCategory | null
|
||||
creditedAccount: TransactionsListItemAccount | null
|
||||
|
|
@ -82,6 +72,7 @@ export interface TransactionDetail {
|
|||
amount: number
|
||||
currency: Currency
|
||||
description: string
|
||||
internalTransfer: boolean
|
||||
vendor: TransactionVendor | null
|
||||
category: TransactionCategory | null
|
||||
creditedAccount: TransactionDetailAccount | null
|
||||
|
|
@ -111,6 +102,7 @@ export interface AddTransactionPayload {
|
|||
amount: number
|
||||
currencyCode: string
|
||||
description: string
|
||||
internalTransfer: boolean
|
||||
vendorId: number | null
|
||||
categoryId: number | null
|
||||
creditedAccountId: number | null
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { useTemplateRef, ref, computed } from 'vue'
|
|||
import ModalWrapper from './common/ModalWrapper.vue'
|
||||
import AppButton from './common/AppButton.vue'
|
||||
|
||||
const timeoutModal = useTemplateRef("timeoutModal")
|
||||
const timeoutModal = useTemplateRef('timeoutModal')
|
||||
const secondsUntilLogout = ref(30)
|
||||
const timeoutTimerId = ref<number | undefined>()
|
||||
const timePhrase = computed(() => {
|
||||
if (secondsUntilLogout.value !== 1) {
|
||||
return secondsUntilLogout.value + " seconds"
|
||||
return secondsUntilLogout.value + ' seconds'
|
||||
}
|
||||
return secondsUntilLogout.value + " second"
|
||||
return secondsUntilLogout.value + ' second'
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -40,8 +40,8 @@ defineExpose({ start })
|
|||
<ModalWrapper ref="timeoutModal">
|
||||
<template v-slot:default>
|
||||
<p>
|
||||
You've been inactive for a while, so to ensure the safety of your
|
||||
personal information, you will be logged out in
|
||||
You've been inactive for a while, so to ensure the safety of your personal information, you
|
||||
will be logged out in
|
||||
<strong>{{ timePhrase }}</strong>
|
||||
unless you click below to remain logged in.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -31,12 +31,20 @@ defineExpose({ show, close, isOpen })
|
|||
</script>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<dialog ref="dialog" class="app-modal-dialog" :id="id">
|
||||
<dialog
|
||||
ref="dialog"
|
||||
class="app-modal-dialog"
|
||||
:id="id"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
<div class="app-modal-dialog-actions">
|
||||
<slot name="buttons">
|
||||
<AppButton button-style="secondary" @click="close()">Close</AppButton>
|
||||
<AppButton
|
||||
button-style="secondary"
|
||||
@click="close()"
|
||||
>Close</AppButton
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
|
|||
|
|
@ -51,4 +51,8 @@ defineProps<{
|
|||
font-family: 'OpenSans', sans-serif;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.app-form-control > label input[type='checkbox'] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ const router = useRouter()
|
|||
const authStore = useAuthStore()
|
||||
|
||||
const IDLE_TIMEOUT_SECONDS = 300
|
||||
const idleTimeoutModal = useTemplateRef("idleTimeoutModal")
|
||||
const idleTimeoutModal = useTemplateRef('idleTimeoutModal')
|
||||
useIdleObserver({
|
||||
timeout: IDLE_TIMEOUT_SECONDS * 1000,
|
||||
onIdle: () => idleTimeoutModal.value?.start()
|
||||
onIdle: () => idleTimeoutModal.value?.start(),
|
||||
})
|
||||
|
||||
const authCheckTimer: Ref<number | undefined> = ref(undefined)
|
||||
|
|
@ -70,16 +70,25 @@ async function checkAuth() {
|
|||
<div>
|
||||
<header class="app-header-bar">
|
||||
<div>
|
||||
<h1 class="app-header-text" @click="onHeaderClicked()">
|
||||
<h1
|
||||
class="app-header-text"
|
||||
@click="onHeaderClicked()"
|
||||
>
|
||||
Finnow
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<span class="app-user-widget" @click="router.push('/me')">
|
||||
<span
|
||||
class="app-user-widget"
|
||||
@click="router.push('/me')"
|
||||
>
|
||||
<font-awesome-icon icon="fa-user"></font-awesome-icon>
|
||||
</span>
|
||||
|
||||
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
||||
<span
|
||||
class="app-logout-button"
|
||||
@click="authStore.onUserLoggedOut()"
|
||||
>
|
||||
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const unsavedEdits = computed(() => {
|
|||
amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== tx.amount
|
||||
const currencyChanged = currency.value?.code !== tx.currency.code
|
||||
const descriptionChanged = description.value !== tx.description
|
||||
const internalTransferChanged = internalTransfer.value !== tx.internalTransfer
|
||||
const vendorChanged = vendor.value?.id !== tx.vendor?.id
|
||||
const categoryChanged = categoryId.value !== (tx.category?.id ?? null)
|
||||
const creditedAccountChanged = creditedAccountId.value !== (tx.creditedAccount?.id ?? null)
|
||||
|
|
@ -94,6 +95,7 @@ const unsavedEdits = computed(() => {
|
|||
Amount changed: ${amountChanged}
|
||||
Currency changed: ${currencyChanged}
|
||||
Description changed: ${descriptionChanged}
|
||||
Internal Transfer changed: ${internalTransferChanged}
|
||||
Vendor changed: ${vendorChanged}
|
||||
Category changed: ${categoryChanged}
|
||||
Credited account changed: ${creditedAccountChanged}
|
||||
|
|
@ -108,6 +110,7 @@ const unsavedEdits = computed(() => {
|
|||
amountChanged ||
|
||||
currencyChanged ||
|
||||
descriptionChanged ||
|
||||
internalTransferChanged ||
|
||||
vendorChanged ||
|
||||
categoryChanged ||
|
||||
creditedAccountChanged ||
|
||||
|
|
@ -136,6 +139,7 @@ const timestamp = ref('')
|
|||
const amount = ref(0)
|
||||
const currency: Ref<Currency | null> = ref(null)
|
||||
const description = ref('')
|
||||
const internalTransfer = ref(false)
|
||||
const vendor: Ref<TransactionVendor | null> = ref(null)
|
||||
const categoryId: Ref<number | null> = ref(null)
|
||||
const creditedAccountId: Ref<number | null> = ref(null)
|
||||
|
|
@ -208,6 +212,7 @@ async function doSubmit() {
|
|||
amount: floatMoneyToInteger(amount.value, currency.value),
|
||||
currencyCode: currency.value?.code ?? '',
|
||||
description: description.value,
|
||||
internalTransfer: internalTransfer.value,
|
||||
vendorId: vendorId,
|
||||
categoryId: categoryId.value,
|
||||
creditedAccountId: creditedAccountId.value,
|
||||
|
|
@ -260,6 +265,7 @@ function loadValuesFromExistingTransaction(t: TransactionDetail) {
|
|||
amount.value = t.amount / Math.pow(10, t.currency.fractionalDigits)
|
||||
currency.value = t.currency
|
||||
description.value = t.description
|
||||
internalTransfer.value = t.internalTransfer
|
||||
vendor.value = t.vendor ?? null
|
||||
categoryId.value = t.category?.id ?? null
|
||||
creditedAccountId.value = t.creditedAccount?.id ?? null
|
||||
|
|
@ -391,6 +397,20 @@ function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<!-- One last group for less-often used fields: -->
|
||||
<FormGroup>
|
||||
<FormControl
|
||||
label="Internal Transfer"
|
||||
hint="Mark this transaction as an internal transfer to ignore it in analytics. Useful for things like credit card payments."
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="internalTransfer"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions
|
||||
@cancel="doCancel()"
|
||||
:disabled="loading || !formValid || !unsavedEdits"
|
||||
|
|
|
|||
Loading…
Reference in New Issue