Add account settings edit UI and GnuMICR.otf font

- Add gear button in header to open account settings modal
- Modal covers Organization, Bank, Account, Logo upload, and Printer Offset fields
- PUT /api/account/:id backend endpoint with full field validation
- Logo file reader with inline preview; only updates logo if a new file is chosen
- CSS for btn-header-icon, settings-section-label, logo-preview, form-row-4
- Add GnuMICR.otf (tracked alongside existing GnuMICR.ttf)
This commit is contained in:
2026-03-12 22:58:56 -06:00
parent 4ec51c09fd
commit a89db179cd
5 changed files with 292 additions and 0 deletions
BIN
View File
Binary file not shown.
+38
View File
@@ -53,6 +53,17 @@ header {
cursor: pointer;
}
.account-switcher option { background: var(--header-bg); color: #fff; }
.btn-header-icon {
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.3);
color: #fff;
border-radius: 4px;
padding: 2px 7px;
font-size: 14px;
cursor: pointer;
line-height: 1.4;
}
.btn-header-icon:hover { background: rgba(255,255,255,0.28); }
/* ── Toolbar ── */
.toolbar {
@@ -492,8 +503,35 @@ input[type="file"] {
.wizard-footer .btn-ghost { margin-right: auto; font-size: 12px; }
.form-row-3 { grid-template-columns: 1fr 80px 100px; }
.form-row-4 { grid-template-columns: repeat(4, 1fr); }
.field-hint {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* ── Account settings modal ── */
.settings-section-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
padding-bottom: 4px;
border-bottom: 1px solid var(--border);
margin-top: 4px;
}
.logo-preview {
margin-top: 6px;
}
.logo-preview img {
max-height: 60px;
max-width: 200px;
border: 1px solid var(--border);
border-radius: 3px;
padding: 3px;
}
#acct-settings-modal .modal-body {
max-height: calc(100vh - 160px);
overflow-y: auto;
}
+96
View File
@@ -11,6 +11,7 @@
<div class="header-left">
<span class="header-brand" id="company-name">ezcheck</span>
<select id="account-switcher" class="account-switcher" title="Switch account"></select>
<button id="btn-account-settings" class="btn-header-icon" title="Account settings"></button>
</div>
<span class="header-info">Next check: <strong id="current-check-no"></strong></span>
</header>
@@ -238,6 +239,101 @@
</div>
</div>
<!-- Account settings modal -->
<div id="acct-settings-overlay" class="modal-overlay"></div>
<div id="acct-settings-modal" class="modal" role="dialog" aria-labelledby="acct-settings-title">
<div class="modal-header">
<h2 id="acct-settings-title">Account Settings</h2>
<button id="btn-close-acct-settings" class="btn-icon" title="Close">×</button>
</div>
<div class="modal-body">
<form id="acct-settings-form" novalidate>
<p class="settings-section-label">Organization</p>
<div class="form-group required">
<label for="as-company1">Name</label>
<input type="text" id="as-company1" name="company1">
</div>
<div class="form-group">
<label for="as-company2">Address</label>
<input type="text" id="as-company2" name="company2">
</div>
<div class="form-group">
<label for="as-company3">City / State / ZIP</label>
<input type="text" id="as-company3" name="company3">
</div>
<div class="form-group">
<label for="as-company4">Phone / Web / Email</label>
<input type="text" id="as-company4" name="company4">
</div>
<p class="settings-section-label">Bank</p>
<div class="form-group">
<label for="as-bank-name">Bank Name</label>
<input type="text" id="as-bank-name" name="bank_name">
</div>
<div class="form-group">
<label for="as-bank-info1">Bank Address</label>
<input type="text" id="as-bank-info1" name="bank_info1">
</div>
<div class="form-group">
<label for="as-bank-info2">Bank Phone / Web</label>
<input type="text" id="as-bank-info2" name="bank_info2">
</div>
<div class="form-group">
<label for="as-transit">Transit Code</label>
<input type="text" id="as-transit" name="transit_code">
</div>
<p class="settings-section-label">Account</p>
<div class="form-row">
<div class="form-group required">
<label for="as-routing">Routing Number</label>
<input type="text" id="as-routing" name="routing_number" inputmode="numeric" maxlength="9">
</div>
<div class="form-group required">
<label for="as-account">Account Number</label>
<input type="text" id="as-account" name="account_number" inputmode="numeric">
</div>
</div>
<p class="settings-section-label">Logo</p>
<div class="form-group">
<label for="as-logo">Upload new logo</label>
<input type="file" id="as-logo" accept="image/*">
<span class="field-hint">Replaces existing logo. PNG or GIF recommended.</span>
</div>
<div id="as-logo-preview" class="logo-preview" hidden></div>
<p class="settings-section-label">Printer Offset <span class="field-hint">(inches — adjust if checks print misaligned)</span></p>
<div class="form-row form-row-4">
<div class="form-group">
<label for="as-off-left">Left</label>
<input type="number" id="as-off-left" name="offset_left" step="0.01" placeholder="0">
</div>
<div class="form-group">
<label for="as-off-right">Right</label>
<input type="number" id="as-off-right" name="offset_right" step="0.01" placeholder="0">
</div>
<div class="form-group">
<label for="as-off-up">Up</label>
<input type="number" id="as-off-up" name="offset_up" step="0.01" placeholder="0">
</div>
<div class="form-group">
<label for="as-off-down">Down</label>
<input type="number" id="as-off-down" name="offset_down" step="0.01" placeholder="0">
</div>
</div>
<div id="acct-settings-error" class="wizard-error" hidden></div>
</form>
</div>
<div class="modal-footer">
<button id="btn-save-acct-settings" class="btn-primary">Save Changes</button>
<button id="btn-cancel-acct-settings" class="btn-ghost">Cancel</button>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>
+114
View File
@@ -512,6 +512,101 @@ async function runImport() {
}
}
// ── Account settings modal ───────────────────────────────────────────────────
const acctSettings = { logoData: null };
function openAccountSettings() {
const a = state.account;
if (!a) return;
acctSettings.logoData = null;
const f = document.getElementById('acct-settings-form');
f.elements.company1.value = a.company1 || '';
f.elements.company2.value = a.company2 || '';
f.elements.company3.value = a.company3 || '';
f.elements.company4.value = a.company4 || '';
f.elements.bank_name.value = a.bank_name || '';
f.elements.bank_info1.value = a.bank_info1 || '';
f.elements.bank_info2.value = a.bank_info2 || '';
f.elements.transit_code.value = a.transit_code || '';
f.elements.routing_number.value = a.routing_number || '';
f.elements.account_number.value = a.account_number || '';
f.elements.offset_left.value = a.offset_left || 0;
f.elements.offset_right.value = a.offset_right || 0;
f.elements.offset_up.value = a.offset_up || 0;
f.elements.offset_down.value = a.offset_down || 0;
document.getElementById('as-logo').value = '';
document.getElementById('as-logo-preview').hidden = true;
document.getElementById('acct-settings-error').hidden = true;
document.getElementById('btn-save-acct-settings').disabled = false;
document.getElementById('btn-save-acct-settings').textContent = 'Save Changes';
document.getElementById('acct-settings-overlay').classList.add('open');
document.getElementById('acct-settings-modal').classList.add('open');
f.elements.company1.focus();
}
function closeAccountSettings() {
document.getElementById('acct-settings-overlay').classList.remove('open');
document.getElementById('acct-settings-modal').classList.remove('open');
}
async function saveAccountSettings() {
const f = document.getElementById('acct-settings-form');
const errEl = document.getElementById('acct-settings-error');
errEl.hidden = true;
const payload = {
company1: f.elements.company1.value.trim(),
company2: f.elements.company2.value.trim() || null,
company3: f.elements.company3.value.trim() || null,
company4: f.elements.company4.value.trim() || null,
bank_name: f.elements.bank_name.value.trim(),
bank_info1: f.elements.bank_info1.value.trim() || null,
bank_info2: f.elements.bank_info2.value.trim() || null,
transit_code: f.elements.transit_code.value.trim() || null,
routing_number: f.elements.routing_number.value.trim(),
account_number: f.elements.account_number.value.trim(),
offset_left: parseFloat(f.elements.offset_left.value) || 0,
offset_right: parseFloat(f.elements.offset_right.value) || 0,
offset_up: parseFloat(f.elements.offset_up.value) || 0,
offset_down: parseFloat(f.elements.offset_down.value) || 0,
logo_data: acctSettings.logoData || null,
};
if (!payload.company1) {
errEl.textContent = 'Organization name is required.';
errEl.hidden = false;
f.elements.company1.focus();
return;
}
if (!payload.routing_number || !payload.account_number) {
errEl.textContent = 'Routing number and account number are required.';
errEl.hidden = false;
return;
}
const btn = document.getElementById('btn-save-acct-settings');
btn.disabled = true;
btn.textContent = 'Saving…';
try {
state.account = await apiFetch('PUT', `/api/account/${state.activeAccountId}`, payload);
// Refresh account in the accounts list (for the switcher label)
await loadAccounts();
renderHeader();
closeAccountSettings();
} catch (err) {
errEl.textContent = err.message;
errEl.hidden = false;
btn.disabled = false;
btn.textContent = 'Save Changes';
}
}
// ── Utilities ────────────────────────────────────────────────────────────────
function escHtml(str) {
@@ -600,6 +695,25 @@ function init() {
switchAccount(parseInt(e.target.value, 10));
});
// Account settings modal
document.getElementById('btn-account-settings').addEventListener('click', openAccountSettings);
document.getElementById('btn-close-acct-settings').addEventListener('click', closeAccountSettings);
document.getElementById('btn-cancel-acct-settings').addEventListener('click', closeAccountSettings);
document.getElementById('acct-settings-overlay').addEventListener('click', closeAccountSettings);
document.getElementById('btn-save-acct-settings').addEventListener('click', saveAccountSettings);
document.getElementById('as-logo').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) { acctSettings.logoData = null; return; }
const reader = new FileReader();
reader.onload = ev => {
acctSettings.logoData = ev.target.result;
const preview = document.getElementById('as-logo-preview');
preview.innerHTML = `<img src="${ev.target.result}" alt="Logo preview">`;
preview.hidden = false;
};
reader.readAsDataURL(file);
});
// Initial data load
loadAccounts();
}
+44
View File
@@ -26,6 +26,50 @@ app.get('/api/accounts', (req, res) => {
res.json(accounts);
});
// PUT /api/account/:id - update account settings
app.put('/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.' });
const {
company1, company2, company3, company4,
bank_name, bank_info1, bank_info2, bank_info3, transit_code,
routing_number, account_number,
offset_left, offset_right, offset_up, offset_down,
logo_data,
} = req.body;
if (!company1 || !routing_number || !account_number) {
return res.status(400).json({ error: 'Organization name, routing number, and account number are required.' });
}
db.prepare(`
UPDATE account SET
company1 = ?, company2 = ?, company3 = ?, company4 = ?,
bank_name = ?, bank_info1 = ?, bank_info2 = ?, bank_info3 = ?, transit_code = ?,
routing_number = ?, account_number = ?,
offset_left = ?, offset_right = ?, offset_up = ?, offset_down = ?,
logo_data = CASE WHEN ? IS NOT NULL THEN ? ELSE logo_data END,
updated_at = datetime('now')
WHERE id = ?
`).run(
company1 || null, company2 || null, company3 || null, company4 || null,
bank_name || '', bank_info1 || null, bank_info2 || null, bank_info3 || null, transit_code || null,
routing_number, account_number,
parseFloat(offset_left) || 0, parseFloat(offset_right) || 0,
parseFloat(offset_up) || 0, parseFloat(offset_down) || 0,
logo_data || null, logo_data || null,
req.params.id
);
res.json(db.prepare(
'SELECT id, bank_name, bank_info1, bank_info2, bank_info3, transit_code, ' +
'routing_number, account_number, current_check_no, ' +
'company1, company2, company3, company4, check_position FROM account WHERE id = ?'
).get(req.params.id));
});
// GET /api/account/:id - get full account by id
app.get('/api/account/:id', (req, res) => {
const db = require('./db/database');