Phase C: Add Manual Journal Entries (double-entry GL)
- JournalEntry + JournalEntryLine entities with Draft/Posted/Reversed lifecycle - JournalEntryStatus enum (Draft, Posted, Reversed) - Migration AddJournalEntries: two new tables with self-referencing reversal FK - IUnitOfWork/UnitOfWork wired with JournalEntries + JournalEntryLines repos - ApplicationDbContext: DbSets, tenant query filters, reversal FK config - LedgerService: JE lines added as 10th source in GetAccountLedgerAsync and ComputePriorBalanceAsync - JournalEntriesController: Index (All/Draft/Posted tabs), Create, Details, Post, Reverse, Delete - Views: Index, Create (dynamic balanced line grid with running debit/credit totals), Details - journal-entry-create.js: dynamic line management with balance indicator - Nav: Journal Entries added to Finance section in _Layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
const JournalEntry = (() => {
|
||||
let accounts = [];
|
||||
let lineIndex = 0;
|
||||
|
||||
function init(accountList) {
|
||||
accounts = accountList;
|
||||
document.getElementById('addLineBtn').addEventListener('click', addLine);
|
||||
addLine();
|
||||
addLine();
|
||||
}
|
||||
|
||||
function buildAccountOptions(selectedId) {
|
||||
return accounts.map(a =>
|
||||
`<option value="${a.value}" ${a.value == selectedId ? 'selected' : ''}>${escHtml(a.text)}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function addLine(accountId, debit, credit, desc) {
|
||||
const tbody = document.getElementById('linesBody');
|
||||
const idx = lineIndex++;
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.idx = idx;
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<select name="lineAccountIds" class="form-select form-select-sm line-account" required>
|
||||
<option value="">— select account —</option>
|
||||
${buildAccountOptions(accountId)}
|
||||
</select>
|
||||
</td>
|
||||
<td><input name="lineDescriptions" type="text" class="form-control form-control-sm" placeholder="optional" value="${escHtml(desc || '')}" /></td>
|
||||
<td>
|
||||
<input name="lineDebits" type="number" step="0.01" min="0" class="form-control form-control-sm text-end line-debit" placeholder="0.00" value="${debit || ''}" />
|
||||
</td>
|
||||
<td>
|
||||
<input name="lineCreditAmounts" type="number" step="0.01" min="0" class="form-control form-control-sm text-end line-credit" placeholder="0.00" value="${credit || ''}" />
|
||||
</td>
|
||||
<td>
|
||||
<input name="lineOrders" type="hidden" value="${idx}" />
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 remove-line-btn" title="Remove line">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</td>`;
|
||||
tr.querySelector('.remove-line-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
updateTotals();
|
||||
});
|
||||
tr.querySelector('.line-debit').addEventListener('input', updateTotals);
|
||||
tr.querySelector('.line-credit').addEventListener('input', updateTotals);
|
||||
tbody.appendChild(tr);
|
||||
updateTotals();
|
||||
}
|
||||
|
||||
function updateTotals() {
|
||||
let debits = 0, credits = 0;
|
||||
document.querySelectorAll('.line-debit').forEach(el => {
|
||||
debits += parseFloat(el.value) || 0;
|
||||
});
|
||||
document.querySelectorAll('.line-credit').forEach(el => {
|
||||
credits += parseFloat(el.value) || 0;
|
||||
});
|
||||
document.getElementById('totalDebits').textContent = fmtCurrency(debits);
|
||||
document.getElementById('totalCredits').textContent = fmtCurrency(credits);
|
||||
const badge = document.getElementById('balanceStatus');
|
||||
const balanced = Math.abs(debits - credits) < 0.001 && debits > 0;
|
||||
badge.textContent = balanced ? 'Balanced' : 'Unbalanced';
|
||||
badge.className = 'badge ' + (balanced ? 'bg-success' : 'bg-secondary');
|
||||
}
|
||||
|
||||
function fmtCurrency(n) {
|
||||
return n.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
return { init };
|
||||
})();
|
||||
Reference in New Issue
Block a user