Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ee95dbb09 | |||
| a2de7e2d9d | |||
| c4e4a8c246 | |||
| b692791436 | |||
| 37d70b4d82 | |||
| 7d854d4e01 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ezcheck",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ezcheck",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ezcheck",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"description": "Self-hosted check printing web app",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
|
||||
+35
-5
@@ -867,19 +867,49 @@ input[type="file"] {
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
height: 64px;
|
||||
}
|
||||
.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 */
|
||||
@media (max-width: 768px), (orientation: portrait) {
|
||||
|
||||
+11
-8
@@ -149,14 +149,17 @@
|
||||
<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 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>
|
||||
|
||||
+46
-10
@@ -316,8 +316,8 @@ function renderUfAccountCheckboxes() {
|
||||
const acctRole = assignment ? assignment.role : 'viewer';
|
||||
return `<label class="account-checkbox-label">
|
||||
<input type="checkbox" name="uf-account" value="${a.id}"${checked ? ' checked' : ''}>
|
||||
${escHtml(a.company1 || a.bank_name || `Account ${a.id}`)}
|
||||
<select name="uf-account-role" data-account-id="${a.id}" style="margin-left:6px;font-size:12px">
|
||||
<span>${escHtml(a.company1 || a.bank_name || `Account ${a.id}`)}</span>
|
||||
<select name="uf-account-role" data-account-id="${a.id}" style="font-size:12px">
|
||||
<option value="editor"${acctRole === 'editor' ? ' selected' : ''}>Editor</option>
|
||||
<option value="viewer"${acctRole === 'viewer' ? ' selected' : ''}>Viewer</option>
|
||||
</select>
|
||||
@@ -2082,6 +2082,12 @@ function populateLayoutDropdown() {
|
||||
).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';
|
||||
function svgEl(tag, attrs, text) {
|
||||
const el = document.createElementNS(SVG_NS, tag);
|
||||
@@ -2104,11 +2110,41 @@ function renderLayoutCanvas() {
|
||||
// White check background
|
||||
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
|
||||
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('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) {
|
||||
const g = createFieldSvgElement(f, SCALE, layoutState.selectedId === f.id);
|
||||
svg.appendChild(g);
|
||||
@@ -2333,11 +2369,11 @@ function onLayoutDragMove(e) {
|
||||
const dy = (e.clientY - layoutDrag.mouseY) / layoutState.scale;
|
||||
const f = layoutState.fields.find(x => x.id === layoutDrag.fieldId);
|
||||
if (!f) return;
|
||||
f.x_pos = clampIn(round16(layoutDrag.origX + dx), 0, 8.5);
|
||||
f.y_pos = clampIn(round16(layoutDrag.origY + dy), 0, 3.5);
|
||||
f.x_pos = clampIn(round16(layoutDrag.origX + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||
f.y_pos = clampIn(round16(layoutDrag.origY + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||
if (layoutDrag.moveEnd) {
|
||||
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), 0, 8.5);
|
||||
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), 0, 3.5);
|
||||
f.x_end_pos = clampIn(round16(layoutDrag.origX2 + dx), SAFE_LEFT, SAFE_RIGHT);
|
||||
f.y_end_pos = clampIn(round16(layoutDrag.origY2 + dy), SAFE_TOP, SAFE_BOTTOM);
|
||||
}
|
||||
// Update just the dragged element for smooth performance
|
||||
const svg = document.querySelector('#layout-canvas-container svg');
|
||||
@@ -2364,11 +2400,11 @@ function nudgeLayoutField(dx, dy) {
|
||||
const f = layoutState.fields.find(x => x.id === layoutState.selectedId);
|
||||
if (!f) return;
|
||||
const S = 1 / 16;
|
||||
f.x_pos = clampIn(round16(f.x_pos + dx * S), 0, 8.5);
|
||||
f.y_pos = clampIn(round16(f.y_pos + dy * S), 0, 3.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), SAFE_TOP, SAFE_BOTTOM);
|
||||
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.y_end_pos = clampIn(round16(f.y_end_pos + dy * S), 0, 3.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), SAFE_TOP, SAFE_BOTTOM);
|
||||
}
|
||||
updateLayoutSidebar(f);
|
||||
renderLayoutCanvas();
|
||||
|
||||
@@ -235,17 +235,27 @@ function generateCheckPdf(account, checks, fields) {
|
||||
}
|
||||
|
||||
// --- 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 micrPos = pt(0.3, MICR_Y_IN);
|
||||
const ANCHOR_IN = 2 + 59 / 64;
|
||||
|
||||
if (hasMicrFont) {
|
||||
doc.font('MICR').fontSize(12).fillColor('#000000')
|
||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
||||
doc.font('MICR').fontSize(12).fillColor('#000000');
|
||||
} else {
|
||||
doc.font('Courier').fontSize(10).fillColor('#000000')
|
||||
.text(micrLine, micrPos.x, micrPos.y, { lineBreak: false });
|
||||
doc.font('Courier').fontSize(10).fillColor('#000000');
|
||||
}
|
||||
|
||||
// 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 page loop
|
||||
|
||||
|
||||
Reference in New Issue
Block a user