From 04c22c798f3ddbd8f223f7421de755e1c1f9709f Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 18 Nov 2024 20:43:37 +1100 Subject: [PATCH] Implement reconciling statement lines --- src/pages/StatementLinesView.vue | 96 ++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/src/pages/StatementLinesView.vue b/src/pages/StatementLinesView.vue index b672521..d98a468 100644 --- a/src/pages/StatementLinesView.vue +++ b/src/pages/StatementLinesView.vue @@ -64,13 +64,13 @@ import dayjs from 'dayjs'; - import { PencilIcon } from '@heroicons/vue/24/outline'; + import { CheckIcon, PencilIcon } from '@heroicons/vue/24/outline'; import { onUnmounted, ref, watch } from 'vue'; import { db } from '../db.ts'; import { renderComponent } from '../webutil.ts'; -import { ppWithCommodity } from '../display.ts'; + import { ppWithCommodity } from '../display.ts'; interface StatementLine { id: number, @@ -125,6 +125,91 @@ import { ppWithCommodity } from '../display.ts'; statementLines.value = newStatementLines; } + // TODO: Could probably avoid polluting global scope by using clusterize clusterChanged callback + (window as any).showClassifyLinePanel = function(el: HTMLAnchorElement) { + const CheckIconHTML = renderComponent(CheckIcon, { 'class': 'w-5 h-5' }); + + const td = el.closest('td')!; + td.className = 'relative'; // CSS trickery so as to not expand the height of the tr + td.innerHTML = + `
+ + +
`; + + td.querySelector('input')!.addEventListener('keydown', async function(event: KeyboardEvent) { + if (event.key === 'Enter') { + await onLineClassified(event); + } + }) + td.querySelector('button')!.addEventListener('click', onLineClassified); + + td.querySelector('input')!.focus(); + + return false; + }; + + async function onLineClassified(event: Event) { + // Callback when clicking OK or pressing enter to classify a statement line + if ((event.target as HTMLInputElement).disabled) { + return; + } + + const td = (event.target as Element).closest('td')!; + const tr = td.closest('tr')!; + const lineId = parseInt(tr.dataset.lineId!); + const chargeAccount = (td.querySelector('input')! as HTMLInputElement).value; + + if (!chargeAccount) { + return; + } + + // Disable further submissions + td.querySelector('input')!.disabled = true; + td.querySelector('button')!.disabled = true; + + const statementLine = statementLines.value.find((l) => l.id === lineId)!; + + // Insert transaction and statement line reconciliation atomically + const session = await db.load(); + const dbTransaction = await session.begin(); + + // Insert transaction + const transactionResult = await dbTransaction.execute( + `INSERT INTO transactions (dt, description) + VALUES ($1, $2)`, + [statementLine.dt, statementLine.description] + ); + const transactionId = transactionResult.lastInsertId; + + // Insert posting for this account + const accountPostingResult = await dbTransaction.execute( + `INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance) + VALUES ($1, NULL, $2, $3, $4, NULL)`, + [transactionId, statementLine.source_account, statementLine.quantity, statementLine.commodity] + ); + const accountPostingId = accountPostingResult.lastInsertId; + + // Insert posting for the charge account - no need to remember this ID + await dbTransaction.execute( + `INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance) + VALUES ($1, NULL, $2, $3, $4, NULL)`, + [transactionId, chargeAccount, -statementLine.quantity, statementLine.commodity] + ); + + // Insert statement line reconciliation + await dbTransaction.execute( + `INSERT INTO statement_line_reconciliations (statement_line_id, posting_id) + VALUES ($1, $2)`, + [statementLine.id, accountPostingId] + ); + + dbTransaction.commit(); + + // Reload transactions and re-render the table + await load(); + } + function renderTable() { const PencilIconHTML = renderComponent(PencilIcon, { 'class': 'w-4 h-4 inline align-middle -mt-0.5' }); // Pre-render the pencil icon const rows = []; @@ -134,13 +219,12 @@ import { ppWithCommodity } from '../display.ts'; if (line.posting_accounts.length === 0) { // Unreconciled reconciliationCell = - `Unclassified - ${ PencilIconHTML }`; + `Unclassified`; } else if (line.posting_accounts.length === 2) { // Simple reconciliation const otherAccount = line.posting_accounts.find((a) => a !== line.source_account); reconciliationCell = - `${ otherAccount } + `${ otherAccount } ${ PencilIconHTML }`; } else { // Complex reconciliation @@ -150,7 +234,7 @@ import { ppWithCommodity } from '../display.ts'; } rows.push( - ` + ` ${ line.source_account } ${ dayjs(line.dt).format('YYYY-MM-DD') }