Add Delete Account button with confirmation modal

Red "Delete Account" button at top of Account Settings. Clicking opens
a second modal with an irreversibility warning and the account name,
requiring a second explicit confirmation before deletion. Deletes the
account row plus all associated checks and deposits. Redirects to the
setup wizard if no accounts remain.
This commit is contained in:
2026-03-18 21:15:05 -06:00
parent 35a5d576ea
commit e4feafa82b
4 changed files with 82 additions and 0 deletions
+3
View File
@@ -174,6 +174,9 @@ button:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-ghost { background: transparent; color: var(--text-muted); } .btn-ghost { background: transparent; color: var(--text-muted); }
.btn-ghost:hover { color: var(--text); background: var(--bg); } .btn-ghost:hover { color: var(--text); background: var(--bg); }
.btn-danger { background: #dc2626; color: #fff; font-weight: 500; }
.btn-danger:not(:disabled):hover { background: #b91c1c; }
.delete-account-row { margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--border); }
.btn-icon { .btn-icon {
background: transparent; background: transparent;
+23
View File
@@ -300,6 +300,10 @@
<div class="modal-body"> <div class="modal-body">
<form id="acct-settings-form" novalidate> <form id="acct-settings-form" novalidate>
<div class="delete-account-row">
<button type="button" id="btn-delete-account" class="btn-danger">Delete Account</button>
</div>
<p class="settings-section-label">Organization</p> <p class="settings-section-label">Organization</p>
<div class="form-group required"> <div class="form-group required">
<label for="as-company1">Name</label> <label for="as-company1">Name</label>
@@ -413,6 +417,25 @@
</div> </div>
</div> </div>
<!-- Delete account confirmation modal -->
<div id="delete-account-overlay" class="modal-overlay"></div>
<div id="delete-account-modal" class="modal" role="dialog" aria-labelledby="delete-account-title">
<div class="modal-header">
<h2 id="delete-account-title">Delete Account</h2>
<button id="btn-close-delete-account" class="btn-icon" title="Close">×</button>
</div>
<div class="modal-body">
<div class="warning-box" style="background:#fef2f2;border-color:#f87171;color:#7f1d1d">
<strong>This cannot be undone.</strong> Deleting this account will permanently remove all checks, deposits, and account settings. There is no recovery.
</div>
<p style="margin-top:14px;font-size:13px">Are you sure you want to delete <strong id="delete-account-name"></strong>?</p>
</div>
<div class="modal-footer">
<button id="btn-confirm-delete-account" class="btn-danger">Yes, Delete Account</button>
<button id="btn-cancel-delete-account" class="btn-ghost">Cancel</button>
</div>
</div>
<!-- QBO Import modal --> <!-- QBO Import modal -->
<div id="qbo-import-overlay" class="modal-overlay"></div> <div id="qbo-import-overlay" class="modal-overlay"></div>
<div id="qbo-import-modal" class="modal modal-wide" role="dialog" aria-labelledby="qbo-import-title"> <div id="qbo-import-modal" class="modal modal-wide" role="dialog" aria-labelledby="qbo-import-title">
+40
View File
@@ -644,6 +644,40 @@ async function saveAccountSettings() {
} }
} }
// ── Delete account ────────────────────────────────────────────────────────────
function openDeleteAccount() {
const name = (state.account && state.account.company1) || 'this account';
document.getElementById('delete-account-name').textContent = name;
document.getElementById('delete-account-overlay').classList.add('open');
document.getElementById('delete-account-modal').classList.add('open');
}
function closeDeleteAccount() {
document.getElementById('delete-account-overlay').classList.remove('open');
document.getElementById('delete-account-modal').classList.remove('open');
}
async function confirmDeleteAccount() {
const btn = document.getElementById('btn-confirm-delete-account');
btn.disabled = true;
btn.textContent = 'Deleting…';
try {
await apiFetch('DELETE', `/api/account/${state.activeAccountId}`);
closeDeleteAccount();
closeAccountSettings();
state.account = null;
state.activeAccountId = null;
state.checks = [];
localStorage.removeItem('activeAccountId');
await loadAccounts(); // will open wizard if no accounts remain
} catch (err) {
alert('Delete failed: ' + err.message);
btn.disabled = false;
btn.textContent = 'Yes, Delete Account';
}
}
// ── QBO Import ──────────────────────────────────────────────────────────────── // ── QBO Import ────────────────────────────────────────────────────────────────
let qboChecksRecords = null; let qboChecksRecords = null;
@@ -1241,6 +1275,12 @@ function init() {
document.getElementById('acct-settings-overlay').addEventListener('click', closeAccountSettings); document.getElementById('acct-settings-overlay').addEventListener('click', closeAccountSettings);
document.getElementById('btn-save-acct-settings').addEventListener('click', saveAccountSettings); document.getElementById('btn-save-acct-settings').addEventListener('click', saveAccountSettings);
document.getElementById('btn-delete-account').addEventListener('click', openDeleteAccount);
document.getElementById('btn-close-delete-account').addEventListener('click', closeDeleteAccount);
document.getElementById('btn-cancel-delete-account').addEventListener('click', closeDeleteAccount);
document.getElementById('delete-account-overlay').addEventListener('click', closeDeleteAccount);
document.getElementById('btn-confirm-delete-account').addEventListener('click', confirmDeleteAccount);
document.getElementById('btn-set-check-no').addEventListener('click', openSetCheckNo); document.getElementById('btn-set-check-no').addEventListener('click', openSetCheckNo);
document.getElementById('btn-close-set-check-no').addEventListener('click', closeSetCheckNo); document.getElementById('btn-close-set-check-no').addEventListener('click', closeSetCheckNo);
document.getElementById('btn-cancel-set-check-no').addEventListener('click', closeSetCheckNo); document.getElementById('btn-cancel-set-check-no').addEventListener('click', closeSetCheckNo);
+16
View File
@@ -104,6 +104,22 @@ app.put('/api/account/:id/check-no', (req, res) => {
res.json({ next_check_no: next }); res.json({ next_check_no: next });
}); });
// DELETE /api/account/:id - delete account and all associated data
app.delete('/api/account/:id', (req, res) => {
const db = require('./db/database');
const account = db.prepare('SELECT id FROM account WHERE id = ?').get(req.params.id);
if (!account) return res.status(404).json({ error: 'Account not found.' });
db.transaction(() => {
// deposit_items deleted via ON DELETE CASCADE from deposits
db.prepare('DELETE FROM deposits WHERE account_id = ?').run(req.params.id);
db.prepare('DELETE FROM checks WHERE account_id = ?').run(req.params.id);
db.prepare('DELETE FROM account WHERE id = ?').run(req.params.id);
})();
res.status(204).end();
});
// POST /api/account/setup - create a new account (wizard) // POST /api/account/setup - create a new account (wizard)
app.post('/api/account/setup', (req, res) => { app.post('/api/account/setup', (req, res) => {
const db = require('./db/database'); const db = require('./db/database');