diff --git a/src/pages/StatementLinesView.vue b/src/pages/StatementLinesView.vue index ff14630..22ac35f 100644 --- a/src/pages/StatementLinesView.vue +++ b/src/pages/StatementLinesView.vue @@ -23,9 +23,9 @@
- + Import statement @@ -172,6 +172,11 @@ const statementLine = statementLines.value.find((l) => l.id === lineId)!; + if (statementLine.posting_accounts.length !== 0) { + await alert('Cannot reconcile already reconciled statement line'); + return; + } + // Insert transaction and statement line reconciliation atomically const session = await db.load(); const dbTransaction = await session.begin(); @@ -226,22 +231,109 @@ await load(); } + async function reconcileAsTransfer() { + const selectedCheckboxes = document.querySelectorAll('.statement-line-checkbox:checked'); + + if (selectedCheckboxes.length !== 2) { + await alert('Must select exactly 2 statement lines'); + return; + } + + const selectedLineIds = [...selectedCheckboxes].map((el) => parseInt(el.closest('tr')?.dataset.lineId!)); + + const line1 = statementLines.value.find((l) => l.id === selectedLineIds[0])!; + const line2 = statementLines.value.find((l) => l.id === selectedLineIds[1])!; + + // Sanity checks + if (line1.quantity + line2.quantity !== 0 || line1.commodity !== line2.commodity) { + await alert('Selected statement line debits/credits must equal'); + return; + } + if (line1.posting_accounts.length !== 0 || line2.posting_accounts.length !== 0) { + await alert('Cannot reconcile already reconciled statement lines'); + return; + } + + // 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)`, + [line1.dt, line1.description] + ); + const transactionId = transactionResult.lastInsertId; + + // Insert posting for line1 + const postingResult1 = await dbTransaction.execute( + `INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance) + VALUES ($1, $2, $3, $4, $5, NULL)`, + [transactionId, line1.description, line1.source_account, line1.quantity, line1.commodity] + ); + const postingId1 = postingResult1.lastInsertId; + + // Insert statement line reconciliation + await dbTransaction.execute( + `INSERT INTO statement_line_reconciliations (statement_line_id, posting_id) + VALUES ($1, $2)`, + [line1.id, postingId1] + ); + + // Insert posting for line2 + const postingResult2 = await dbTransaction.execute( + `INSERT INTO postings (transaction_id, description, account, quantity, commodity, running_balance) + VALUES ($1, $2, $3, $4, $5, NULL)`, + [transactionId, line2.description, line2.source_account, line2.quantity, line2.commodity] + ); + const postingId2 = postingResult2.lastInsertId; + + // Insert statement line reconciliation + await dbTransaction.execute( + `INSERT INTO statement_line_reconciliations (statement_line_id, posting_id) + VALUES ($1, $2)`, + [line2.id, postingId2] + ); + + // Invalidate running balances + await dbTransaction.execute( + `UPDATE postings + SET running_balance = NULL + FROM ( + SELECT postings.id + FROM transactions + JOIN postings ON transactions.id = postings.transaction_id + WHERE DATE(dt) >= DATE($1) AND account IN ($2, $3) + ) p + WHERE postings.id = p.id`, + [line1.dt, line1.source_account, line2.source_account] + ); + + 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 = []; for (const line of statementLines.value) { - let reconciliationCell; + let reconciliationCell, checkboxCell; if (line.posting_accounts.length === 0) { // Unreconciled reconciliationCell = `Unclassified`; + checkboxCell = ``; // Only show checkbox for unreconciled lines } else if (line.posting_accounts.length === 2) { // Simple reconciliation const otherAccount = line.posting_accounts.find((a) => a !== line.source_account); reconciliationCell = `${ otherAccount } ${ PencilIconHTML }`; + checkboxCell = ''; if (showOnlyUnclassified.value) { continue; } } else { @@ -249,13 +341,14 @@ reconciliationCell = `(Complex) ${ PencilIconHTML }`; + checkboxCell = ''; if (showOnlyUnclassified.value) { continue; } } rows.push( ` - + ${ checkboxCell } ${ line.source_account } ${ dayjs(line.dt).format('YYYY-MM-DD') } ${ line.description }