Implement editing and deleting balance assertions
This commit is contained in:
parent
e791fb2a8a
commit
c5c3d1133a
117
src/components/BalanceAssertionEditor.vue
Normal file
117
src/components/BalanceAssertionEditor.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<!--
|
||||||
|
DrCr: Web-based double-entry bookkeeping framework
|
||||||
|
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-[max-content_1fr] space-y-2 mb-4 items-baseline">
|
||||||
|
<label for="dt" class="block text-gray-900 pr-4">Date</label>
|
||||||
|
<div>
|
||||||
|
<input type="date" class="bordered-field" id="dt" v-model="assertion.dt">
|
||||||
|
</div>
|
||||||
|
<label for="description" class="block text-gray-900 pr-4">Description</label>
|
||||||
|
<div>
|
||||||
|
<input type="text" class="bordered-field" id="description" v-model="assertion.description" placeholder=" ">
|
||||||
|
</div>
|
||||||
|
<label for="account" class="block text-gray-900 pr-4">Account</label>
|
||||||
|
<div class="relative combobox">
|
||||||
|
<input type="text" class="bordered-field peer" id="account" v-model="assertion.account" placeholder=" " autocomplete="off">
|
||||||
|
<!--{% include 'components/accounts_combobox_inner.html' %}-->
|
||||||
|
</div>
|
||||||
|
<label for="amount" class="block text-gray-900 pr-4">Balance</label>
|
||||||
|
<div class="relative shadow-sm">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- TODO: Display existing credit assertion as credit, not as negative debit -->
|
||||||
|
<input type="number" class="bordered-field pl-7 pr-16" step="0.01" v-model="assertion.amount_abs" placeholder="0.00">
|
||||||
|
<div class="absolute inset-y-0 right-0 flex items-center">
|
||||||
|
<select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" v-model="assertion.sign">
|
||||||
|
<option value="dr">Dr</option>
|
||||||
|
<option value="cr">Cr</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4 space-x-2">
|
||||||
|
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAssertion" v-if="assertion.id !== null">Delete</button>
|
||||||
|
<button class="btn-primary" @click="saveAssertion">Save</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
||||||
|
|
||||||
|
export interface EditingAssertion {
|
||||||
|
id: number | null,
|
||||||
|
dt: string,
|
||||||
|
description: string,
|
||||||
|
account: string,
|
||||||
|
sign: string,
|
||||||
|
amount_abs: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { assertion } = defineProps<{ assertion: EditingAssertion }>();
|
||||||
|
|
||||||
|
async function saveAssertion() {
|
||||||
|
// Save changes to the assertion
|
||||||
|
const amount_abs = deserialiseAmount('' + assertion.amount_abs);
|
||||||
|
const quantity = assertion.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity;
|
||||||
|
|
||||||
|
const session = await db.load();
|
||||||
|
|
||||||
|
if (assertion.id === null) {
|
||||||
|
await session.execute(
|
||||||
|
`INSERT INTO balance_assertions (dt, description, account, quantity, commodity)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await session.execute(
|
||||||
|
`UPDATE balance_assertions
|
||||||
|
SET dt = $1, description = $2, account = $3, quantity = $4, commodity = $5
|
||||||
|
WHERE id = $6`,
|
||||||
|
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity, assertion.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await getCurrentWindow().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAssertion() {
|
||||||
|
// Delete the current assertion
|
||||||
|
if (!await confirm('Are you sure you want to delete this balance assertion? This operation is irreversible.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await db.load();
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
`DELETE FROM balance_assertions
|
||||||
|
WHERE id = $1`,
|
||||||
|
[assertion.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
await getCurrentWindow().close();
|
||||||
|
}
|
||||||
|
</script>
|
@ -31,6 +31,8 @@ async function initApp() {
|
|||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'index', component: () => import('./pages/HomeView.vue') },
|
{ path: '/', name: 'index', component: () => import('./pages/HomeView.vue') },
|
||||||
{ path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') },
|
{ path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') },
|
||||||
|
{ path: '/balance-assertions/edit/:id', name: 'balance-assertions-edit', component: () => import('./pages/EditBalanceAssertionView.vue') },
|
||||||
|
{ path: '/balance-assertions/new', name: 'balance-assertions-new', component: () => import('./pages/NewBalanceAssertionView.vue') },
|
||||||
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
|
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
|
||||||
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
|
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
|
||||||
{ path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') },
|
{ path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') },
|
||||||
|
@ -22,12 +22,10 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="my-4 flex gap-x-2">
|
<div class="my-4 flex gap-x-2">
|
||||||
<!--<a href="{{ url_for('balance_assertions_new') }}" class="btn-primary pl-2">
|
<a href="/balance-assertions/new" class="btn-primary pl-2" onclick="return openLinkInNewWindow(this);">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
<PlusIcon class="w-4 h-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
||||||
</svg>
|
|
||||||
New assertion
|
New assertion
|
||||||
</a>-->
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
@ -54,10 +52,8 @@
|
|||||||
<XMarkIcon class="w-4 h-4 text-red-500" v-if="!assertion.isValid" />
|
<XMarkIcon class="w-4 h-4 text-red-500" v-if="!assertion.isValid" />
|
||||||
</td>
|
</td>
|
||||||
<td class="py-0.5 pl-1 text-gray-900 text-end">
|
<td class="py-0.5 pl-1 text-gray-900 text-end">
|
||||||
<a href="#" class="text-gray-500 hover:text-gray-700">
|
<a :href="'/balance-assertions/edit/' + assertion.id" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 inline align-middle -mt-0.5">
|
<PencilIcon class="w-4 h-4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -72,7 +68,8 @@
|
|||||||
|
|
||||||
import { db, totalBalancesAtDate } from '../db.ts';
|
import { db, totalBalancesAtDate } from '../db.ts';
|
||||||
import { pp } from '../display.ts';
|
import { pp } from '../display.ts';
|
||||||
import { CheckIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
import { CheckIcon, PencilIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
|
|
||||||
const balanceAssertions = ref([] as ValidatedBalanceAssertion[]);
|
const balanceAssertions = ref([] as ValidatedBalanceAssertion[]);
|
||||||
|
|
||||||
|
67
src/pages/EditBalanceAssertionView.vue
Normal file
67
src/pages/EditBalanceAssertionView.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!--
|
||||||
|
DrCr: Web-based double-entry bookkeeping framework
|
||||||
|
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="page-heading mb-4">
|
||||||
|
Edit balance assertion
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<BalanceAssertionEditor :assertion="assertion" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import { db, serialiseAmount } from '../db.ts';
|
||||||
|
import BalanceAssertionEditor, { EditingAssertion } from '../components/BalanceAssertionEditor.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const assertion = ref({
|
||||||
|
id: null,
|
||||||
|
dt: null!,
|
||||||
|
description: null!,
|
||||||
|
account: null!,
|
||||||
|
sign: null!,
|
||||||
|
amount_abs: null!,
|
||||||
|
} as EditingAssertion);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const session = await db.load();
|
||||||
|
|
||||||
|
const rawAssertions: any[] = await session.select(
|
||||||
|
`SELECT *
|
||||||
|
FROM balance_assertions
|
||||||
|
WHERE id = $1`,
|
||||||
|
[route.params.id]
|
||||||
|
);
|
||||||
|
const rawAssertion = rawAssertions[0];
|
||||||
|
|
||||||
|
// Format parameters for display
|
||||||
|
rawAssertion.dt = dayjs(rawAssertion.dt).format('YYYY-MM-DD');
|
||||||
|
rawAssertion.sign = rawAssertion.quantity >= 0 ? 'dr' : 'cr';
|
||||||
|
rawAssertion.amount_abs = serialiseAmount(Math.abs(rawAssertion.quantity), rawAssertion.commodity);
|
||||||
|
|
||||||
|
assertion.value = rawAssertion as EditingAssertion;
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
</script>
|
@ -30,7 +30,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { JoinedTransactionPosting, Posting, db, joinedToTransactions, serialiseAmount } from '../db.ts';
|
import { JoinedTransactionPosting, db, joinedToTransactions, serialiseAmount } from '../db.ts';
|
||||||
import TransactionEditor, { EditingTransaction } from '../components/TransactionEditor.vue';
|
import TransactionEditor, { EditingTransaction } from '../components/TransactionEditor.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -56,16 +56,18 @@
|
|||||||
|
|
||||||
const transactions = joinedToTransactions(joinedTransactionPostings);
|
const transactions = joinedToTransactions(joinedTransactionPostings);
|
||||||
if (transactions.length !== 1) { throw new Error('Unexpected number of transactions returned from SQL'); }
|
if (transactions.length !== 1) { throw new Error('Unexpected number of transactions returned from SQL'); }
|
||||||
transaction.value = transactions[0] as unknown as EditingTransaction;
|
const rawTransaction = transactions[0] as any;
|
||||||
|
|
||||||
// Format dt
|
// Format dt
|
||||||
transaction.value.dt = dayjs(transaction.value.dt).format('YYYY-MM-DD')
|
rawTransaction.dt = dayjs(rawTransaction.dt).format('YYYY-MM-DD');
|
||||||
|
|
||||||
// Initialise sign and amount_abs
|
// Initialise sign and amount_abs
|
||||||
for (const posting of transaction.value.postings) {
|
for (const posting of rawTransaction.postings) {
|
||||||
posting.sign = (posting as unknown as Posting).quantity! >= 0 ? 'dr' : 'cr';
|
posting.sign = posting.quantity >= 0 ? 'dr' : 'cr';
|
||||||
posting.amount_abs = serialiseAmount(Math.abs((posting as unknown as Posting).quantity), (posting as unknown as Posting).commodity);
|
posting.amount_abs = serialiseAmount(Math.abs(posting.quantity), posting.commodity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transaction.value = rawTransaction as EditingTransaction;
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
</script>
|
</script>
|
||||||
|
41
src/pages/NewBalanceAssertionView.vue
Normal file
41
src/pages/NewBalanceAssertionView.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
DrCr: Web-based double-entry bookkeeping framework
|
||||||
|
Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo)
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="page-heading mb-4">
|
||||||
|
New balance assertion
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<BalanceAssertionEditor :assertion="assertion" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import BalanceAssertionEditor, { EditingAssertion } from '../components/BalanceAssertionEditor.vue';
|
||||||
|
|
||||||
|
const assertion = ref({
|
||||||
|
id: null,
|
||||||
|
dt: dayjs().format('YYYY-MM-DD'),
|
||||||
|
description: '',
|
||||||
|
account: '',
|
||||||
|
sign: 'dr',
|
||||||
|
amount_abs: '',
|
||||||
|
} as EditingAssertion);
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user