Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee95dbb09 | |||
| a2de7e2d9d | |||
| c4e4a8c246 | |||
| b692791436 | |||
| 37d70b4d82 | |||
| 7d854d4e01 | |||
| f9f6a4cd9a | |||
| fd36c25636 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.0",
|
"version": "0.4.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.0",
|
"version": "0.4.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezcheck",
|
"name": "ezcheck",
|
||||||
"version": "0.4.0",
|
"version": "0.4.5",
|
||||||
"description": "Self-hosted check printing web app",
|
"description": "Self-hosted check printing web app",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+157
-7
@@ -22,11 +22,17 @@ body {
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#main-app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#main-app[hidden] { display: none; }
|
||||||
|
|
||||||
/* ── Header ── */
|
/* ── Header ── */
|
||||||
header {
|
header {
|
||||||
background: var(--header-bg);
|
background: var(--header-bg);
|
||||||
@@ -861,21 +867,165 @@ input[type="file"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── User management ── */
|
/* ── User management ── */
|
||||||
.account-checkboxes { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
.account-checkboxes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
.account-checkbox-label {
|
.account-checkbox-label {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 20px 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 3px 8px;
|
padding: 6px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 64px;
|
||||||
}
|
}
|
||||||
.account-checkbox-label:hover { border-color: var(--primary); }
|
.account-checkbox-label:hover { border-color: var(--primary); }
|
||||||
|
.account-checkbox-label input[type="checkbox"] {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
.account-checkbox-label span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.account-checkbox-label select {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uf-oidc-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.uf-oidc-section h4 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide layout editor button on portrait/mobile — canvas needs landscape space */
|
/* Hide layout editor button on portrait/mobile — canvas needs landscape space */
|
||||||
@media (max-width: 768px), (orientation: portrait) {
|
@media (max-width: 768px), (orientation: portrait) {
|
||||||
#btn-layout-editor { display: none !important; }
|
#btn-layout-editor { display: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Settings Page ── */
|
||||||
|
.settings-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.settings-page[hidden] { display: none; }
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
background: var(--header-bg);
|
||||||
|
color: var(--header-fg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
height: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.settings-header-left { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.settings-header-right { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.settings-back-link {
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.settings-back-link:hover { color: #fff; }
|
||||||
|
|
||||||
|
.settings-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.settings-sidebar-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 20px 16px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.settings-nav-group { margin-bottom: 8px; }
|
||||||
|
.settings-nav-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 12px 20px 6px;
|
||||||
|
}
|
||||||
|
.settings-nav-item {
|
||||||
|
display: block;
|
||||||
|
padding: 7px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 1px 8px;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
.settings-nav-item:hover { background: var(--bg); }
|
||||||
|
.settings-nav-item.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 32px 48px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 780px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.settings-panel h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.settings-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.settings-section {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.settings-section .form-row { gap: 16px; }
|
||||||
|
.settings-section .form-group { margin-bottom: 4px; }
|
||||||
|
|||||||
+167
-121
@@ -87,6 +87,172 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings page (full-page, hidden by default) -->
|
||||||
|
<div id="settings-page" class="settings-page" hidden>
|
||||||
|
<header class="settings-header">
|
||||||
|
<div class="settings-header-left">
|
||||||
|
<a href="#" id="settings-back-link" class="settings-back-link">← Back</a>
|
||||||
|
</div>
|
||||||
|
<div class="settings-header-right">
|
||||||
|
<span id="settings-username" class="header-username"></span>
|
||||||
|
<button id="btn-settings-logout" class="btn-header-icon" title="Sign out">↩</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="settings-layout">
|
||||||
|
<nav class="settings-sidebar">
|
||||||
|
<div class="settings-sidebar-title">Settings</div>
|
||||||
|
<div class="settings-nav-group" data-admin-only>
|
||||||
|
<div class="settings-nav-label">Administration</div>
|
||||||
|
<a href="#settings/users" class="settings-nav-item" data-settings-tab="users">Users</a>
|
||||||
|
<a href="#settings/smtp" class="settings-nav-item" data-settings-tab="smtp">Email (SMTP)</a>
|
||||||
|
</div>
|
||||||
|
<div class="settings-nav-group">
|
||||||
|
<div class="settings-nav-label">Account</div>
|
||||||
|
<a href="#settings/password" class="settings-nav-item" data-settings-tab="password">Password</a>
|
||||||
|
<a href="#settings/sso" class="settings-nav-item" data-settings-tab="sso">Single Sign-On</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="settings-content">
|
||||||
|
|
||||||
|
<!-- Users panel (admin only) -->
|
||||||
|
<div id="settings-panel-users" class="settings-panel" hidden>
|
||||||
|
<h2>Users</h2>
|
||||||
|
<p class="settings-desc">Manage user accounts and access permissions.</p>
|
||||||
|
<div class="settings-section">
|
||||||
|
<div id="users-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section" id="user-form-section">
|
||||||
|
<h3 id="user-form-title">Add User</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group required">
|
||||||
|
<label for="uf-username">Username</label>
|
||||||
|
<input type="text" id="uf-username" autocapitalize="none">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uf-email">Email <span class="field-hint">(for password reset)</span></label>
|
||||||
|
<input type="email" id="uf-email" autocomplete="email">
|
||||||
|
</div>
|
||||||
|
<div class="form-group required">
|
||||||
|
<label for="uf-password">Password <span class="field-hint" id="uf-password-hint">(min 10 chars, include a digit or symbol)</span></label>
|
||||||
|
<input type="password" id="uf-password" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group required">
|
||||||
|
<label for="uf-role">Role</label>
|
||||||
|
<select id="uf-role">
|
||||||
|
<option value="viewer">Viewer</option>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="uf-accounts-group">
|
||||||
|
<label>Account Access <span class="field-hint">(admins see all — no selection needed)</span></label>
|
||||||
|
<div id="uf-accounts-checkboxes" class="account-checkboxes"></div>
|
||||||
|
</div>
|
||||||
|
<div id="uf-oidc-group" class="uf-oidc-section" hidden>
|
||||||
|
<h4>Single Sign-On Identity</h4>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uf-oidc-sub">OIDC Subject <span class="field-hint">(sub claim from provider)</span></label>
|
||||||
|
<input type="text" id="uf-oidc-sub" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uf-oidc-issuer">OIDC Issuer <span class="field-hint">(provider URL)</span></label>
|
||||||
|
<input type="text" id="uf-oidc-issuer" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="user-form-error" class="wizard-error" hidden></div>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:12px">
|
||||||
|
<button id="btn-save-user" class="btn-primary">Add User</button>
|
||||||
|
<button id="btn-cancel-user-edit" class="btn-ghost" hidden>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SMTP panel (admin only) -->
|
||||||
|
<div id="settings-panel-smtp" class="settings-panel" hidden>
|
||||||
|
<h2>Email Settings</h2>
|
||||||
|
<p class="settings-desc">Configure SMTP for password reset emails.</p>
|
||||||
|
<div class="settings-section" id="smtp-settings-section">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtp-host">SMTP Host</label>
|
||||||
|
<input type="text" id="smtp-host" placeholder="smtp.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="max-width:100px">
|
||||||
|
<label for="smtp-port">Port</label>
|
||||||
|
<input type="number" id="smtp-port" value="587" min="1" max="65535">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="max-width:160px">
|
||||||
|
<label for="smtp-secure">Encryption</label>
|
||||||
|
<select id="smtp-secure">
|
||||||
|
<option value="0">STARTTLS (587)</option>
|
||||||
|
<option value="1">SSL/TLS (465)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtp-user">Username</label>
|
||||||
|
<input type="text" id="smtp-user" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtp-pass">Password <span class="field-hint" id="smtp-pass-hint"></span></label>
|
||||||
|
<input type="password" id="smtp-pass" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="smtp-from">From Address</label>
|
||||||
|
<input type="email" id="smtp-from" placeholder="ezcheck@example.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="smtp-error" class="wizard-error" hidden></div>
|
||||||
|
<div id="smtp-success" class="import-result" hidden></div>
|
||||||
|
<button id="btn-save-smtp" class="btn-secondary" style="margin-top:12px">Save Email Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password panel (all users) -->
|
||||||
|
<div id="settings-panel-password" class="settings-panel" hidden>
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
<p class="settings-desc">Update your login password.</p>
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cp-current">Current Password</label>
|
||||||
|
<input type="password" id="cp-current" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cp-new">New Password <span class="field-hint">(min 10 chars, include a digit or symbol)</span></label>
|
||||||
|
<input type="password" id="cp-new" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cp-confirm">Confirm New</label>
|
||||||
|
<input type="password" id="cp-confirm" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="cp-error" class="wizard-error" hidden></div>
|
||||||
|
<div id="cp-success" class="import-result" hidden>Password changed.</div>
|
||||||
|
<button id="btn-change-password" class="btn-secondary" style="margin-top:12px">Change Password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SSO panel (all users, shown when OIDC enabled) -->
|
||||||
|
<div id="settings-panel-sso" class="settings-panel" hidden>
|
||||||
|
<h2>Single Sign-On</h2>
|
||||||
|
<p class="settings-desc">Link your account to an external identity provider.</p>
|
||||||
|
<div class="settings-section" id="oidc-link-section">
|
||||||
|
<p id="oidc-link-status" class="settings-desc" style="margin-bottom:12px"></p>
|
||||||
|
<a id="btn-oidc-link" href="/api/auth/oidc/link" class="btn-secondary" style="display:inline-block;text-decoration:none">Link My Account</a>
|
||||||
|
<button id="btn-oidc-unlink" class="btn-ghost" style="margin-left:8px" hidden>Unlink</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-app">
|
||||||
<header>
|
<header>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="header-brand" id="company-name">ezcheck</span>
|
<span class="header-brand" id="company-name">ezcheck</span>
|
||||||
@@ -657,128 +823,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
</div><!-- /main-app -->
|
||||||
|
|
||||||
<!-- User management modal (admin only) -->
|
|
||||||
<div id="users-overlay" class="modal-overlay"></div>
|
|
||||||
<div id="users-modal" class="modal modal-wide" role="dialog" aria-labelledby="users-title">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="users-title">Manage Users</h2>
|
|
||||||
<button id="btn-close-users" class="btn-icon" title="Close">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div id="users-list"></div>
|
|
||||||
<div id="user-form-section" style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px">
|
|
||||||
<h3 style="font-size:13px;font-weight:600;margin-bottom:10px" id="user-form-title">Add User</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group required">
|
|
||||||
<label for="uf-username">Username</label>
|
|
||||||
<input type="text" id="uf-username" autocapitalize="none">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="uf-email">Email <span class="field-hint">(for password reset)</span></label>
|
|
||||||
<input type="email" id="uf-email" autocomplete="email">
|
|
||||||
</div>
|
|
||||||
<div class="form-group required">
|
|
||||||
<label for="uf-password">Password <span class="field-hint" id="uf-password-hint">(min 10 chars, include a digit or symbol)</span></label>
|
|
||||||
<input type="password" id="uf-password" autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
<div class="form-group required">
|
|
||||||
<label for="uf-role">Role</label>
|
|
||||||
<select id="uf-role">
|
|
||||||
<option value="viewer">Viewer</option>
|
|
||||||
<option value="editor">Editor</option>
|
|
||||||
<option value="admin">Admin</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" id="uf-accounts-group">
|
|
||||||
<label>Account Access <span class="field-hint">(admins see all — no selection needed)</span></label>
|
|
||||||
<div id="uf-accounts-checkboxes" class="account-checkboxes"></div>
|
|
||||||
</div>
|
|
||||||
<div id="uf-oidc-group" class="form-row" hidden>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="uf-oidc-sub">OIDC Subject <span class="field-hint">(sub claim from provider)</span></label>
|
|
||||||
<input type="text" id="uf-oidc-sub" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="uf-oidc-issuer">OIDC Issuer <span class="field-hint">(provider URL)</span></label>
|
|
||||||
<input type="text" id="uf-oidc-issuer" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="user-form-error" class="wizard-error" hidden></div>
|
|
||||||
<div style="display:flex;gap:8px;margin-top:8px">
|
|
||||||
<button id="btn-save-user" class="btn-primary">Add User</button>
|
|
||||||
<button id="btn-cancel-user-edit" class="btn-ghost" hidden>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- SMTP settings (admin only) -->
|
|
||||||
<div id="smtp-settings-section" style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px">
|
|
||||||
<h3 style="font-size:13px;font-weight:600;margin-bottom:10px">Email Settings (SMTP)</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtp-host">SMTP Host</label>
|
|
||||||
<input type="text" id="smtp-host" placeholder="smtp.example.com">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="max-width:90px">
|
|
||||||
<label for="smtp-port">Port</label>
|
|
||||||
<input type="number" id="smtp-port" value="587" min="1" max="65535">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="max-width:140px">
|
|
||||||
<label for="smtp-secure">Encryption</label>
|
|
||||||
<select id="smtp-secure">
|
|
||||||
<option value="0">STARTTLS (587)</option>
|
|
||||||
<option value="1">SSL/TLS (465)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtp-user">Username</label>
|
|
||||||
<input type="text" id="smtp-user" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtp-pass">Password <span class="field-hint" id="smtp-pass-hint"></span></label>
|
|
||||||
<input type="password" id="smtp-pass" autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="smtp-from">From Address</label>
|
|
||||||
<input type="email" id="smtp-from" placeholder="ezcheck@example.com">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="smtp-error" class="wizard-error" hidden></div>
|
|
||||||
<div id="smtp-success" class="import-result" hidden></div>
|
|
||||||
<button id="btn-save-smtp" class="btn-secondary" style="margin-top:8px">Save Email Settings</button>
|
|
||||||
</div>
|
|
||||||
<!-- Change own password -->
|
|
||||||
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px">
|
|
||||||
<h3 style="font-size:13px;font-weight:600;margin-bottom:10px">Change My Password</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cp-current">Current Password</label>
|
|
||||||
<input type="password" id="cp-current" autocomplete="current-password">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cp-new">New Password <span class="field-hint">(min 10 chars, include a digit or symbol)</span></label>
|
|
||||||
<input type="password" id="cp-new" autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cp-confirm">Confirm New</label>
|
|
||||||
<input type="password" id="cp-confirm" autocomplete="new-password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="cp-error" class="wizard-error" hidden></div>
|
|
||||||
<div id="cp-success" class="import-result" hidden>Password changed.</div>
|
|
||||||
<button id="btn-change-password" class="btn-secondary" style="margin-top:8px">Change Password</button>
|
|
||||||
</div>
|
|
||||||
<!-- Link my OIDC identity (self-service, shown when OIDC is enabled) -->
|
|
||||||
<div id="oidc-link-section" style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px" hidden>
|
|
||||||
<h3 style="font-size:13px;font-weight:600;margin-bottom:10px">Single Sign-On</h3>
|
|
||||||
<p id="oidc-link-status" style="font-size:12px;color:var(--text-muted);margin-bottom:8px"></p>
|
|
||||||
<a id="btn-oidc-link" href="/api/auth/oidc/link" class="btn-secondary" style="display:inline-block;text-decoration:none">Link My Account</a>
|
|
||||||
<button id="btn-oidc-unlink" class="btn-ghost" style="margin-left:8px" hidden>Unlink</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Layout Editor Modal -->
|
<!-- Layout Editor Modal -->
|
||||||
<div id="layout-editor-overlay" class="modal-overlay"></div>
|
<div id="layout-editor-overlay" class="modal-overlay"></div>
|
||||||
|
|||||||
+133
-37
@@ -65,7 +65,8 @@ async function checkAuth() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (location.hash === '#oidc-linked') {
|
if (location.hash === '#oidc-linked') {
|
||||||
history.replaceState(null, '', location.pathname);
|
// After OIDC link callback, navigate to SSO settings panel
|
||||||
|
location.hash = '#settings/sso';
|
||||||
// Fall through to normal auth check — user is still logged in
|
// Fall through to normal auth check — user is still logged in
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ async function checkAuth() {
|
|||||||
state.user = await res.json();
|
state.user = await res.json();
|
||||||
hideLoginOverlay();
|
hideLoginOverlay();
|
||||||
applyRoleUI();
|
applyRoleUI();
|
||||||
|
// Route to settings page if hash says so
|
||||||
|
if (location.hash.startsWith('#settings')) handleHashRoute();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// No session — check if this is first-run (no users at all)
|
// No session — check if this is first-run (no users at all)
|
||||||
@@ -165,6 +168,9 @@ async function logout() {
|
|||||||
document.getElementById('login-error').hidden = true;
|
document.getElementById('login-error').hidden = true;
|
||||||
document.getElementById('login-setup-section').hidden = true;
|
document.getElementById('login-setup-section').hidden = true;
|
||||||
document.getElementById('login-form-section').hidden = false;
|
document.getElementById('login-form-section').hidden = false;
|
||||||
|
// Ensure settings page is hidden and main app restored
|
||||||
|
document.getElementById('settings-page').hidden = true;
|
||||||
|
document.getElementById('main-app').hidden = false;
|
||||||
showLoginOverlay();
|
showLoginOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +182,9 @@ function applyRoleUI() {
|
|||||||
const isEditor = state.accountRole === 'editor' || (!state.accountRole && (role === 'admin' || role === 'editor'));
|
const isEditor = state.accountRole === 'editor' || (!state.accountRole && (role === 'admin' || role === 'editor'));
|
||||||
|
|
||||||
document.getElementById('header-username').textContent = state.user ? state.user.username : '';
|
document.getElementById('header-username').textContent = state.user ? state.user.username : '';
|
||||||
|
document.getElementById('settings-username').textContent = state.user ? state.user.username : '';
|
||||||
|
|
||||||
// Admin-only elements
|
// Admin-only elements (main app + settings sidebar)
|
||||||
document.querySelectorAll('[data-admin-only]').forEach(el => { el.hidden = !isAdmin; });
|
document.querySelectorAll('[data-admin-only]').forEach(el => { el.hidden = !isAdmin; });
|
||||||
|
|
||||||
// Editor+ elements (hide for viewers)
|
// Editor+ elements (hide for viewers)
|
||||||
@@ -191,28 +198,62 @@ function applyRoleUI() {
|
|||||||
|
|
||||||
let usersState = { users: [], editingId: null };
|
let usersState = { users: [], editingId: null };
|
||||||
|
|
||||||
function openUsersModal() {
|
// ── Settings page navigation ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function navigateToSettings(tab) {
|
||||||
const isAdmin = state.user && state.user.role === 'admin';
|
const isAdmin = state.user && state.user.role === 'admin';
|
||||||
document.getElementById('user-form-error').hidden = true;
|
const defaultTab = isAdmin ? 'users' : 'password';
|
||||||
document.getElementById('users-title').textContent = isAdmin ? 'Manage Users' : 'My Account';
|
const resolved = tab || defaultTab;
|
||||||
document.getElementById('users-overlay').classList.add('open');
|
|
||||||
document.getElementById('users-modal').classList.add('open');
|
// Guard non-admin from admin tabs
|
||||||
// Admin-only sections
|
if (!isAdmin && (resolved === 'users' || resolved === 'smtp')) {
|
||||||
document.getElementById('users-list').hidden = !isAdmin;
|
location.hash = '#settings/password';
|
||||||
document.getElementById('user-form-section').hidden = !isAdmin;
|
return;
|
||||||
document.getElementById('smtp-settings-section').hidden = !isAdmin;
|
|
||||||
if (isAdmin) {
|
|
||||||
loadUsers();
|
|
||||||
renderUfAccountCheckboxes();
|
|
||||||
loadSmtpSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('main-app').hidden = true;
|
||||||
|
const sp = document.getElementById('settings-page');
|
||||||
|
sp.hidden = false;
|
||||||
|
document.getElementById('settings-username').textContent = state.user ? state.user.username : '';
|
||||||
|
|
||||||
|
// Check OIDC status to show/hide SSO tab
|
||||||
loadOidcLinkStatus();
|
loadOidcLinkStatus();
|
||||||
|
|
||||||
|
activateSettingsTab(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeUsersModal() {
|
function activateSettingsTab(tab) {
|
||||||
document.getElementById('users-overlay').classList.remove('open');
|
// Hide all panels, show the target
|
||||||
document.getElementById('users-modal').classList.remove('open');
|
document.querySelectorAll('.settings-panel').forEach(p => { p.hidden = true; });
|
||||||
cancelUserEdit();
|
const panel = document.getElementById('settings-panel-' + tab);
|
||||||
|
if (panel) panel.hidden = false;
|
||||||
|
|
||||||
|
// Update sidebar active state
|
||||||
|
document.querySelectorAll('.settings-nav-item').forEach(a => {
|
||||||
|
a.classList.toggle('active', a.dataset.settingsTab === tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load data for the activated tab
|
||||||
|
if (tab === 'users') { loadUsers(); renderUfAccountCheckboxes(); }
|
||||||
|
if (tab === 'smtp') { loadSmtpSettings(); }
|
||||||
|
if (tab === 'sso') { loadOidcLinkStatus(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainApp() {
|
||||||
|
document.getElementById('settings-page').hidden = true;
|
||||||
|
document.getElementById('main-app').hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHashRoute() {
|
||||||
|
const hash = location.hash;
|
||||||
|
if (hash.startsWith('#settings/')) {
|
||||||
|
const tab = hash.split('/')[1];
|
||||||
|
navigateToSettings(tab);
|
||||||
|
} else if (hash.startsWith('#settings')) {
|
||||||
|
navigateToSettings();
|
||||||
|
} else {
|
||||||
|
showMainApp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
@@ -275,8 +316,8 @@ function renderUfAccountCheckboxes() {
|
|||||||
const acctRole = assignment ? assignment.role : 'viewer';
|
const acctRole = assignment ? assignment.role : 'viewer';
|
||||||
return `<label class="account-checkbox-label">
|
return `<label class="account-checkbox-label">
|
||||||
<input type="checkbox" name="uf-account" value="${a.id}"${checked ? ' checked' : ''}>
|
<input type="checkbox" name="uf-account" value="${a.id}"${checked ? ' checked' : ''}>
|
||||||
${escHtml(a.company1 || a.bank_name || `Account ${a.id}`)}
|
<span>${escHtml(a.company1 || a.bank_name || `Account ${a.id}`)}</span>
|
||||||
<select name="uf-account-role" data-account-id="${a.id}" style="margin-left:6px;font-size:12px">
|
<select name="uf-account-role" data-account-id="${a.id}" style="font-size:12px">
|
||||||
<option value="editor"${acctRole === 'editor' ? ' selected' : ''}>Editor</option>
|
<option value="editor"${acctRole === 'editor' ? ' selected' : ''}>Editor</option>
|
||||||
<option value="viewer"${acctRole === 'viewer' ? ' selected' : ''}>Viewer</option>
|
<option value="viewer"${acctRole === 'viewer' ? ' selected' : ''}>Viewer</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -1664,9 +1705,12 @@ async function saveSmtpSettings() {
|
|||||||
async function loadOidcLinkStatus() {
|
async function loadOidcLinkStatus() {
|
||||||
try {
|
try {
|
||||||
const cfg = await fetch('/api/auth/oidc/config').then(r => r.json());
|
const cfg = await fetch('/api/auth/oidc/config').then(r => r.json());
|
||||||
const section = document.getElementById('oidc-link-section');
|
const ssoNavItem = document.querySelector('[data-settings-tab="sso"]');
|
||||||
if (!cfg.enabled) { section.hidden = true; return; }
|
if (!cfg.enabled) {
|
||||||
section.hidden = false;
|
if (ssoNavItem) ssoNavItem.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ssoNavItem) ssoNavItem.hidden = false;
|
||||||
|
|
||||||
const me = await apiFetch('GET', '/api/auth/me');
|
const me = await apiFetch('GET', '/api/auth/me');
|
||||||
const statusEl = document.getElementById('oidc-link-status');
|
const statusEl = document.getElementById('oidc-link-status');
|
||||||
@@ -1886,11 +1930,27 @@ async function init() {
|
|||||||
document.getElementById('btn-reset-submit').addEventListener('click', submitResetPassword);
|
document.getElementById('btn-reset-submit').addEventListener('click', submitResetPassword);
|
||||||
document.getElementById('reset-password2').addEventListener('keydown', e => { if (e.key === 'Enter') submitResetPassword(); });
|
document.getElementById('reset-password2').addEventListener('keydown', e => { if (e.key === 'Enter') submitResetPassword(); });
|
||||||
|
|
||||||
// User management
|
// User management / settings page
|
||||||
document.getElementById('btn-users').addEventListener('click', openUsersModal);
|
document.getElementById('btn-users').addEventListener('click', () => { location.hash = '#settings/users'; });
|
||||||
document.getElementById('header-username').addEventListener('click', openUsersModal);
|
document.getElementById('header-username').addEventListener('click', () => {
|
||||||
document.getElementById('btn-close-users').addEventListener('click', closeUsersModal);
|
const isAdmin = state.user && state.user.role === 'admin';
|
||||||
document.getElementById('users-overlay').addEventListener('click', closeUsersModal);
|
location.hash = isAdmin ? '#settings/users' : '#settings/password';
|
||||||
|
});
|
||||||
|
document.getElementById('settings-back-link').addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
location.hash = '';
|
||||||
|
});
|
||||||
|
document.getElementById('btn-settings-logout').addEventListener('click', () => {
|
||||||
|
location.hash = '';
|
||||||
|
logout();
|
||||||
|
});
|
||||||
|
// Sidebar tab navigation
|
||||||
|
document.querySelectorAll('.settings-nav-item').forEach(a => {
|
||||||
|
a.addEventListener('click', e => { e.preventDefault(); location.hash = a.getAttribute('href'); });
|
||||||
|
});
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
if (state.user) handleHashRoute();
|
||||||
|
});
|
||||||
document.getElementById('users-list').addEventListener('click', e => {
|
document.getElementById('users-list').addEventListener('click', e => {
|
||||||
const editBtn = e.target.closest('.user-btn-edit');
|
const editBtn = e.target.closest('.user-btn-edit');
|
||||||
const deleteBtn = e.target.closest('.user-btn-delete');
|
const deleteBtn = e.target.closest('.user-btn-delete');
|
||||||
@@ -2022,6 +2082,12 @@ function populateLayoutDropdown() {
|
|||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printing safe zone for user-adjustable fields (inches). MICR is exempt.
|
||||||
|
const SAFE_LEFT = 11 / 64;
|
||||||
|
const SAFE_RIGHT = 8.5 - 11 / 64;
|
||||||
|
const SAFE_TOP = 13 / 64;
|
||||||
|
const SAFE_BOTTOM = 3.5 - 0.5;
|
||||||
|
|
||||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
function svgEl(tag, attrs, text) {
|
function svgEl(tag, attrs, text) {
|
||||||
const el = document.createElementNS(SVG_NS, tag);
|
const el = document.createElementNS(SVG_NS, tag);
|
||||||
@@ -2044,11 +2110,41 @@ function renderLayoutCanvas() {
|
|||||||
// White check background
|
// White check background
|
||||||
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
svg.appendChild(svgEl('rect', { x:0, y:0, width:W, height:H, fill:'#fff', stroke:'#bbb', 'stroke-width':1 }));
|
||||||
|
|
||||||
|
// Grid at 1/8" increments (darker every 1/4", darkest on whole inches)
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(8.5 * 8); n8++) {
|
||||||
|
const x = (n8 / 8) * SCALE;
|
||||||
|
if (x >= W) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:x, y1:0, x2:x, y2:H, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
for (let n8 = 1; n8 < Math.ceil(3.5 * 8); n8++) {
|
||||||
|
const y = (n8 / 8) * SCALE;
|
||||||
|
if (y >= H) break;
|
||||||
|
const isInch = n8 % 8 === 0;
|
||||||
|
const isQtr = n8 % 2 === 0;
|
||||||
|
const stroke = isInch ? '#d0d7de' : isQtr ? '#e4e8ed' : '#f0f2f5';
|
||||||
|
svg.appendChild(svgEl('line', { x1:0, y1:y, x2:W, y2:y, stroke, 'stroke-width':1 }));
|
||||||
|
}
|
||||||
|
|
||||||
// MICR reference line
|
// MICR reference line
|
||||||
const micrY = (3.5 - 0.267) * SCALE;
|
const micrY = (3.5 - 0.267) * SCALE;
|
||||||
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
svg.appendChild(svgEl('line', { x1:0, y1:micrY, x2:W, y2:micrY, stroke:'#ccc', 'stroke-width':1, 'stroke-dasharray':'4,4' }));
|
||||||
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
svg.appendChild(svgEl('text', { x:4, y:micrY - 3, 'font-size':9, fill:'#bbb', 'font-family':'sans-serif' }, 'MICR'));
|
||||||
|
|
||||||
|
// Safe zone outline for user-adjustable fields
|
||||||
|
svg.appendChild(svgEl('rect', {
|
||||||
|
x: SAFE_LEFT * SCALE,
|
||||||
|
y: SAFE_TOP * SCALE,
|
||||||
|
width: (SAFE_RIGHT - SAFE_LEFT) * SCALE,
|
||||||
|
height: (SAFE_BOTTOM - SAFE_TOP) * SCALE,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#60a5fa',
|
||||||
|
'stroke-width': 1,
|
||||||
|
'stroke-dasharray': '3,3',
|
||||||
|
}));
|
||||||
|
|
||||||
for (const f of layoutState.fields) {
|
for (const f of layoutState.fields) {
|
||||||
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
||||||
svg.appendChild(g);
|
svg.appendChild(g);
|
||||||
@@ -2273,11 +2369,11 @@ function onLayoutDragMove(e) {
|
|||||||
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
||||||
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
f.x_pos = clampIn(round16(layoutDrag.origX + dx), 0, 8.5);
|
f.x_pos = clampIn(round16(layoutDrag.origX + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(layoutDrag.origY + dy), 0, 3.5);
|
f.y_pos = clampIn(round16(layoutDrag.origY + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (layoutDrag.moveEnd) {
|
if (layoutDrag.moveEnd) {
|
||||||
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), 0, 8.5);
|
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), 0, 3.5);
|
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
// Update just the dragged element for smooth performance
|
// Update just the dragged element for smooth performance
|
||||||
const svg = document.querySelector('#layout-canvas-container svg');
|
const svg = document.querySelector('#layout-canvas-container svg');
|
||||||
@@ -2304,11 +2400,11 @@ function nudgeLayoutField(dx, dy) {
|
|||||||
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const S = 1 / 16;
|
const S = 1 / 16;
|
||||||
f.x_pos = clampIn(round16(f.x_pos + dx * S), 0, 8.5);
|
f.x_pos = clampIn(round16(f.x_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_pos = clampIn(round16(f.y_pos + dy * S), 0, 3.5);
|
f.y_pos = clampIn(round16(f.y_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
if (f.field_type === 'Line' || f.field_type === 'Graph') {
|
||||||
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), 0, 8.5);
|
f.x_end_pos = clampIn(round16(f.x_end_pos + dx * S), SAFE_LEFT, SAFE_RIGHT);
|
||||||
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), 0, 3.5);
|
f.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), SAFE_TOP, SAFE_BOTTOM);
|
||||||
}
|
}
|
||||||
updateLayoutSidebar(f);
|
updateLayoutSidebar(f);
|
||||||
renderLayoutCanvas();
|
renderLayoutCanvas();
|
||||||
|
|||||||
@@ -235,17 +235,27 @@ function generateCheckPdf(account, checks, fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- MICR line ---
|
// --- MICR line ---
|
||||||
|
// Anchor the second transit symbol (the 'A' after routing) at 2 59/64" from left.
|
||||||
|
// Routing extends left from that anchor; account + check extend right.
|
||||||
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no);
|
||||||
const micrPos = pt(0.3, MICR_Y_IN);
|
const ANCHOR_IN = 2 + 59 / 64;
|
||||||
|
|
||||||
if (hasMicrFont) {
|
if (hasMicrFont) {
|
||||||
doc.font('MICR').fontSize(12).fillColor('#000000')
|
doc.font('MICR').fontSize(12).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
} else {
|
} else {
|
||||||
doc.font('Courier').fontSize(10).fillColor('#000000')
|
doc.font('Courier').fontSize(10).fillColor('#000000');
|
||||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix = everything up to and including the second 'A' (first A + routing + second A).
|
||||||
|
const secondA = micrLine.indexOf('A', 1) + 1;
|
||||||
|
const prefix = micrLine.slice(0, secondA);
|
||||||
|
const prefixWidthPts = doc.widthOfString(prefix);
|
||||||
|
const anchorXPts = (ANCHOR_IN + offX) * POINTS_PER_INCH;
|
||||||
|
const micrXPts = anchorXPts - prefixWidthPts;
|
||||||
|
const micrYPts = (slotOriginY + MICR_Y_IN + offY) * POINTS_PER_INCH;
|
||||||
|
|
||||||
|
doc.text(micrLine, micrXPts, micrYPts, { lineBreak: false });
|
||||||
|
|
||||||
} // end slot loop
|
} // end slot loop
|
||||||
} // end page loop
|
} // end page loop
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user