diff --git a/public/css/style.css b/public/css/style.css
index d9434d4..1ca847c 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -411,3 +411,77 @@ input[type="file"] {
padding: 12px 16px;
border-top: 1px solid var(--border);
}
+
+/* ── Setup wizard ── */
+.wizard-modal { width: 520px; }
+
+.wizard-steps-indicator {
+ display: flex;
+ align-items: center;
+ padding: 16px 24px 0;
+ background: var(--surface);
+}
+.wizard-step-dot {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+.wizard-step-dot span {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: var(--border);
+ color: var(--text-muted);
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.15s, color 0.15s;
+}
+.wizard-step-dot.active span,
+.wizard-step-dot.done span {
+ background: var(--primary);
+ color: #fff;
+}
+.wizard-step-dot label {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--text-muted);
+ white-space: nowrap;
+}
+.wizard-step-dot.active label { color: var(--primary); }
+.wizard-step-line {
+ flex: 1;
+ height: 2px;
+ background: var(--border);
+ margin: 0 8px;
+ margin-bottom: 18px;
+ transition: background 0.15s;
+}
+.wizard-step-line.done { background: var(--primary); }
+
+.wizard-body { min-height: 240px; }
+
+.wizard-error {
+ margin-top: 4px;
+ padding: 8px 10px;
+ background: #fee2e2;
+ color: var(--danger);
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.wizard-footer { justify-content: flex-end; }
+.wizard-footer .btn-ghost { margin-right: auto; font-size: 12px; }
+
+.form-row-3 { grid-template-columns: 1fr 80px 100px; }
+.field-hint {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
diff --git a/public/index.html b/public/index.html
index 1e78e53..78188f9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -118,6 +118,103 @@
+
+
+
+
+
+
1
+
+
2
+
+
3
+
+
+
+
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
index 7eb2182..d97b703 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -28,8 +28,8 @@ async function loadAccount() {
try {
state.account = await apiFetch('GET', '/api/account');
renderHeader();
- } catch {
- // account not configured yet — silently skip
+ } catch (err) {
+ if (err.message && err.message.includes('No account')) openWizard();
}
}
@@ -337,6 +337,112 @@ async function reprintCheck(id) {
}
}
+// ── Setup wizard ─────────────────────────────────────────────────────────────
+
+const wizard = { step: 1, logoData: null };
+
+function openWizard() {
+ wizard.step = 1;
+ wizard.logoData = null;
+ document.getElementById('w-logo').value = '';
+ document.getElementById('wizard-error').hidden = true;
+ goToWizardStep(1);
+ document.getElementById('wizard-overlay').classList.add('open');
+ document.getElementById('wizard-modal').classList.add('open');
+ document.getElementById('w-company1').focus();
+}
+
+function closeWizard() {
+ document.getElementById('wizard-overlay').classList.remove('open');
+ document.getElementById('wizard-modal').classList.remove('open');
+}
+
+function goToWizardStep(n) {
+ wizard.step = n;
+ [1, 2, 3].forEach(i => {
+ document.getElementById(`wizard-step-${i}`).hidden = i !== n;
+ const dot = document.querySelector(`.wizard-step-dot[data-step="${i}"]`);
+ dot.classList.toggle('active', i === n);
+ dot.classList.toggle('done', i < n);
+ });
+ document.querySelectorAll('.wizard-step-line').forEach((line, idx) => {
+ line.classList.toggle('done', idx < n - 1);
+ });
+ document.getElementById('btn-wizard-prev').hidden = n === 1;
+ document.getElementById('btn-wizard-next').hidden = n === 3;
+ document.getElementById('btn-wizard-finish').hidden = n !== 3;
+ document.getElementById('wizard-error').hidden = true;
+}
+
+function validateWizardStep() {
+ const err = document.getElementById('wizard-error');
+ if (wizard.step === 1) {
+ if (!document.getElementById('w-company1').value.trim()) {
+ err.textContent = 'Organization name is required.';
+ err.hidden = false;
+ document.getElementById('w-company1').focus();
+ return false;
+ }
+ }
+ if (wizard.step === 2) {
+ if (!document.getElementById('w-bank-name').value.trim()) {
+ err.textContent = 'Bank name is required.';
+ err.hidden = false;
+ document.getElementById('w-bank-name').focus();
+ return false;
+ }
+ }
+ if (wizard.step === 3) {
+ const routing = document.getElementById('w-routing').value.trim();
+ const account = document.getElementById('w-account').value.trim();
+ const startNo = document.getElementById('w-start-check').value.trim();
+ if (!routing) { err.textContent = 'Routing number is required.'; err.hidden = false; return false; }
+ if (!account) { err.textContent = 'Account number is required.'; err.hidden = false; return false; }
+ if (!startNo || parseInt(startNo, 10) < 1) { err.textContent = 'Starting check number is required.'; err.hidden = false; return false; }
+ }
+ return true;
+}
+
+async function finishWizard() {
+ if (!validateWizardStep()) return;
+
+ const city = document.getElementById('w-city').value.trim();
+ const state_ = document.getElementById('w-state').value.trim().toUpperCase();
+ const zip = document.getElementById('w-zip').value.trim();
+ const cityLine = [city, state_ ? (zip ? `${state_} ${zip}` : state_) : zip].filter(Boolean).join(', ');
+
+ const payload = {
+ company1: document.getElementById('w-company1').value.trim(),
+ company2: document.getElementById('w-addr1').value.trim() || null,
+ company3: cityLine || null,
+ company4: document.getElementById('w-contact').value.trim() || null,
+ bank_name: document.getElementById('w-bank-name').value.trim(),
+ bank_info1: document.getElementById('w-bank-addr').value.trim() || null,
+ bank_info2: document.getElementById('w-bank-contact').value.trim() || null,
+ transit_code: document.getElementById('w-transit').value.trim() || null,
+ routing_number: document.getElementById('w-routing').value.trim(),
+ account_number: document.getElementById('w-account').value.trim(),
+ start_check_no: parseInt(document.getElementById('w-start-check').value, 10),
+ logo_data: wizard.logoData || null,
+ };
+
+ const btn = document.getElementById('btn-wizard-finish');
+ btn.disabled = true;
+ btn.textContent = 'Saving…';
+
+ try {
+ await apiFetch('POST', '/api/account/setup', payload);
+ closeWizard();
+ await Promise.all([loadAccount(), loadChecks()]);
+ } catch (err) {
+ const errEl = document.getElementById('wizard-error');
+ errEl.textContent = err.message;
+ errEl.hidden = false;
+ btn.disabled = false;
+ btn.textContent = 'Save & Start';
+ }
+}
+
// ── Import modal ─────────────────────────────────────────────────────────────
function openImportModal() {
@@ -442,6 +548,24 @@ function init() {
// Generate PDF
document.getElementById('btn-generate-pdf').addEventListener('click', generatePdf);
+ // Wizard
+ document.getElementById('btn-wizard-next').addEventListener('click', () => {
+ if (validateWizardStep()) goToWizardStep(wizard.step + 1);
+ });
+ document.getElementById('btn-wizard-prev').addEventListener('click', () => goToWizardStep(wizard.step - 1));
+ document.getElementById('btn-wizard-finish').addEventListener('click', finishWizard);
+ document.getElementById('btn-wizard-skip').addEventListener('click', () => {
+ closeWizard();
+ openImportModal();
+ });
+ document.getElementById('w-logo').addEventListener('change', e => {
+ const file = e.target.files[0];
+ if (!file) { wizard.logoData = null; return; }
+ const reader = new FileReader();
+ reader.onload = ev => { wizard.logoData = ev.target.result; };
+ reader.readAsDataURL(file);
+ });
+
// Import modal
document.getElementById('btn-import').addEventListener('click', openImportModal);
document.getElementById('btn-close-import').addEventListener('click', closeImportModal);
diff --git a/src/app.js b/src/app.js
index 09e1539..7ba6627 100644
--- a/src/app.js
+++ b/src/app.js
@@ -38,6 +38,55 @@ app.post('/api/import', upload.single('mdbfile'), (req, res) => {
}
});
+// Account setup endpoint (first-run wizard)
+app.post('/api/account/setup', (req, res) => {
+ const db = require('./db/database');
+ const existing = db.prepare('SELECT id FROM account WHERE id = 1').get();
+ if (existing) return res.status(409).json({ error: 'Account already configured.' });
+
+ const {
+ company1, company2, company3, company4,
+ bank_name, bank_info1, bank_info2, transit_code,
+ routing_number, account_number, start_check_no, logo_data,
+ } = req.body;
+
+ if (!company1 || !routing_number || !account_number || !start_check_no) {
+ return res.status(400).json({ error: 'Organization name, routing number, account number, and starting check number are required.' });
+ }
+ const checkNo = parseInt(start_check_no, 10);
+ if (isNaN(checkNo) || checkNo < 1) {
+ return res.status(400).json({ error: 'Starting check number must be a positive integer.' });
+ }
+
+ db.prepare(`
+ INSERT INTO account (
+ bank_name, bank_info1, bank_info2, transit_code,
+ routing_number, account_number, start_check_no, current_check_no,
+ company1, company2, company3, company4, logo_data
+ ) VALUES (
+ @bank_name, @bank_info1, @bank_info2, @transit_code,
+ @routing_number, @account_number, @start_check_no, @current_check_no,
+ @company1, @company2, @company3, @company4, @logo_data
+ )
+ `).run({
+ bank_name: bank_name || '',
+ bank_info1: bank_info1 || null,
+ bank_info2: bank_info2 || null,
+ transit_code: transit_code || null,
+ routing_number,
+ account_number,
+ start_check_no: checkNo,
+ current_check_no: checkNo,
+ company1: company1 || null,
+ company2: company2 || null,
+ company3: company3 || null,
+ company4: company4 || null,
+ logo_data: logo_data || null,
+ });
+
+ res.status(201).json({ success: true });
+});
+
// Account info endpoint (read-only for Phase 1)
app.get('/api/account', (req, res) => {
const db = require('./db/database');