From a89db179cdd23817df4a8a97b763da5146ff559a Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Thu, 12 Mar 2026 22:58:56 -0600 Subject: [PATCH] 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) --- fonts/GnuMICR.otf | Bin 0 -> 4424 bytes public/css/style.css | 38 +++++++++++++++ public/index.html | 96 ++++++++++++++++++++++++++++++++++++ public/js/app.js | 114 +++++++++++++++++++++++++++++++++++++++++++ src/app.js | 44 +++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 fonts/GnuMICR.otf diff --git a/fonts/GnuMICR.otf b/fonts/GnuMICR.otf new file mode 100644 index 0000000000000000000000000000000000000000..8fd182331be7a55a5ba09b3804ef8f2bb3e1e5c5 GIT binary patch literal 4424 zcmd5=Z){sv6+iEfpA*-0(=0X+Qu`?zsDsA5BovLQD}^-cI(2PHOH1|v;w7(fs@RDf z8*kYb>P;0AuTX8;Yt=537sezET0g81A)!p-Q$Yl2L4fcVSUw;SUxqfe_o8$Pzx&6I zn`9IamAEhW-E;q)^E7c~{~Z&B|_XY!Bt{2{cT zsO2s6AIhX2pQP5~5Xnb1vZ;)HBe^FJT|-3LcP1yM@@r-4A=rz%IsNN@Kl??TOlr`#94@p_anzK?4 zP>Y(AvVrkuflSex0gmAmvcM^1fm6r=r;r6sAq$*B7C8H8f+pz+%F!5&QkwGMWj77e zy=Z&L;!l^9dT9?v`}x{oNGB*oSsEc5PZs@sG5p^u|$BSZ718iM>L94sssoY~I??mBAVl*v~Y~9OBkYBu{}JgC=Ixq$9j#2$@#q zL6JX6i7p8(Imkp}|08sm`hbP#D4)py&p3L=VJijiN1%_f)D6@Y+WS#&-6_VpG24Yz z-FOQod$+K03Yc<0HU?XaL&12Gdbwn7-qANP`9yAPG@Unh58rF{SeDg=SMMIPKQ}gP zo=9a!Y&&c2n-W6TXtt0U8_pe&{bbh8?@yZMfIV)frtA^3fb}^upSI1soy$y_6K8nm zP_|$mFFZC5l}E;g?d+7j$1E0$NvKOsB9qsa*M771tF=+N7gMY#20PHAC`_nz z(xE1HcF?CgLUt?eE$q?|hKqEGUZPj}24o=rFY;3QCL5WYU{s-<_&m9b+9*mf`UriL zK1S`-Nq5sd^a=W*@-1R{^smaMPn%kPu#X8=kRNv}cEW;q?`-V+59L-7Se;IRkB=)C zmA|N8QGc&Jr2SFv*MA=PY~U56t*N6a{(dClRQ>s?@=n!nuWH`qm^WiAFD)%6{Py5- z|I*(0>Ws0szkhGyW_xh&QvY(ist={F&9mwGYuD!ESU!I(J;bJm(&-_#JmQr7UwX}o z-&@uEFjF`;J3E)~W`c9mvxWErMq!$D7uUPfg~Uj(FgrarH#=R3ubvJTrl$+6LLDEE z1En{^{uJ-eRnMwwZEvlsW@;0wmegwnnYp-UNwd-pfN`y`lHhYNx@vJtalV)J7lYrJ zoqjof;oE_AgMP`gV9t9Z<}FH}oFx?a zDiX$stmv?P8HQone>5ht?@G{l@r5f_pKJ1HK5+H97p^!jHhCAMiJE)J2cCrtP(vCq zPn(0zlg~W;)cK}W`dZ+r^Upl(JlRyEV?9~kLFi^gzI4X?wDftae5sjywISIdb9OE1 zVm%ZrmmBG}3~x~v{MDkhc?r&;_fv6Z5D129J*8H3;rY$>4+*S1cYeFh`QMlq4PYV; zbmLK&@9W2O;oc<6O* zeP|N3iv&myMny){qQb73I`iJy8uZ5din&|6iM)52L6%(bwQZ7n%N0src^cw6l%W*q zxAmMivrdA%9qT02&7GG1F&Bls)3<@>&D@p*c_@TYd2j^c$buWKxJB1>OK#cC3>w#C zu4|y zQa>Daw*BtXufyA&a0Fi_?_ytFlz-rtIsOZPI;y^{d|6fv{hvT?Y%$Nq*t%P4i0R ezcheck + Next check: @@ -238,6 +239,101 @@ + + + + diff --git a/public/js/app.js b/public/js/app.js index 2ba1ab7..7487990 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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 = `Logo preview`; + preview.hidden = false; + }; + reader.readAsDataURL(file); + }); + // Initial data load loadAccounts(); } diff --git a/src/app.js b/src/app.js index 46f6e4c..708c91c 100644 --- a/src/app.js +++ b/src/app.js @@ -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');