Replace printed/unprinted tabs with unified list and inline filters
- All checks shown in one table by default (load all, no server-side printed filter) - Add payee search, date-from/to, and status filter controls to toolbar - Filtering is client-side; no extra API calls on filter changes - All checks get Edit/Delete buttons regardless of printed status - All checks get checkboxes for PDF selection - Remove separate Reprint button and reprintCheck function - Remove printed guard from PUT and DELETE endpoints
This commit is contained in:
+6
-3
@@ -18,10 +18,13 @@
|
|||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<label for="filter-status">Show:</label>
|
<input type="search" id="filter-payee" placeholder="Search payee…" style="width:160px">
|
||||||
|
<input type="date" id="filter-date-from" title="From date">
|
||||||
|
<span style="color:var(--text-muted)">–</span>
|
||||||
|
<input type="date" id="filter-date-to" title="To date">
|
||||||
<select id="filter-status">
|
<select id="filter-status">
|
||||||
<option value="">All</option>
|
<option value="" selected>All</option>
|
||||||
<option value="0" selected>Unprinted</option>
|
<option value="0">Unprinted</option>
|
||||||
<option value="1">Printed</option>
|
<option value="1">Printed</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
-40
@@ -3,7 +3,10 @@
|
|||||||
const state = {
|
const state = {
|
||||||
checks: [],
|
checks: [],
|
||||||
account: null,
|
account: null,
|
||||||
filter: '0', // '' = all, '0' = unprinted, '1' = printed
|
filterStatus: '', // '' = all, '0' = unprinted, '1' = printed
|
||||||
|
filterPayee: '',
|
||||||
|
filterDateFrom: '',
|
||||||
|
filterDateTo: '',
|
||||||
sortCol: 'check_no',
|
sortCol: 'check_no',
|
||||||
sortDir: 'desc',
|
sortDir: 'desc',
|
||||||
selected: new Set(),
|
selected: new Set(),
|
||||||
@@ -37,9 +40,7 @@ async function loadChecks() {
|
|||||||
const tbody = document.getElementById('checks-tbody');
|
const tbody = document.getElementById('checks-tbody');
|
||||||
tbody.innerHTML = '<tr class="loading-row"><td colspan="8">Loading…</td></tr>';
|
tbody.innerHTML = '<tr class="loading-row"><td colspan="8">Loading…</td></tr>';
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
state.checks = await apiFetch('GET', '/api/checks');
|
||||||
if (state.filter !== '') params.set('printed', state.filter);
|
|
||||||
state.checks = await apiFetch('GET', `/api/checks?${params}`);
|
|
||||||
state.selected.clear();
|
state.selected.clear();
|
||||||
renderTable();
|
renderTable();
|
||||||
refreshPdfButton();
|
refreshPdfButton();
|
||||||
@@ -58,7 +59,7 @@ function renderHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTable() {
|
function renderTable() {
|
||||||
const checks = sortedChecks();
|
const checks = filteredAndSortedChecks();
|
||||||
const tbody = document.getElementById('checks-tbody');
|
const tbody = document.getElementById('checks-tbody');
|
||||||
|
|
||||||
if (checks.length === 0) {
|
if (checks.length === 0) {
|
||||||
@@ -81,9 +82,6 @@ function renderTable() {
|
|||||||
tbody.querySelectorAll('.btn-delete').forEach(btn => {
|
tbody.querySelectorAll('.btn-delete').forEach(btn => {
|
||||||
btn.addEventListener('click', () => deleteCheck(parseInt(btn.dataset.id, 10)));
|
btn.addEventListener('click', () => deleteCheck(parseInt(btn.dataset.id, 10)));
|
||||||
});
|
});
|
||||||
tbody.querySelectorAll('.btn-reprint').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => reprintCheck(parseInt(btn.dataset.id, 10)));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRow(c) {
|
function renderRow(c) {
|
||||||
@@ -100,18 +98,14 @@ function renderRow(c) {
|
|||||||
})
|
})
|
||||||
: '—';
|
: '—';
|
||||||
|
|
||||||
const checkbox = printed
|
const checkbox = `<td class="col-select"><input type="checkbox" data-id="${c.id}"${selected ? ' checked' : ''}></td>`;
|
||||||
? '<td class="col-select"></td>'
|
|
||||||
: `<td class="col-select"><input type="checkbox" data-id="${c.id}"${selected ? ' checked' : ''}></td>`;
|
|
||||||
|
|
||||||
const statusBadge = printed
|
const statusBadge = printed
|
||||||
? '<span class="status-badge status-printed">Printed</span>'
|
? '<span class="status-badge status-printed">Printed</span>'
|
||||||
: '<span class="status-badge status-unprinted">Unprinted</span>';
|
: '<span class="status-badge status-unprinted">Unprinted</span>';
|
||||||
|
|
||||||
const actions = printed
|
const actions = `<button class="btn-sm btn-edit" data-id="${c.id}">Edit</button>` +
|
||||||
? `<button class="btn-sm btn-reprint" data-id="${c.id}">Reprint</button>`
|
`<button class="btn-sm btn-delete" data-id="${c.id}">Delete</button>`;
|
||||||
: `<button class="btn-sm btn-edit" data-id="${c.id}">Edit</button>` +
|
|
||||||
`<button class="btn-sm btn-delete" data-id="${c.id}">Delete</button>`;
|
|
||||||
|
|
||||||
return `<tr class="${printed ? 'printed' : ''}">
|
return `<tr class="${printed ? 'printed' : ''}">
|
||||||
${checkbox}
|
${checkbox}
|
||||||
@@ -125,10 +119,24 @@ function renderRow(c) {
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortedChecks() {
|
function filteredAndSortedChecks() {
|
||||||
|
const payee = state.filterPayee.toLowerCase();
|
||||||
|
const from = state.filterDateFrom;
|
||||||
|
const to = state.filterDateTo;
|
||||||
|
const status = state.filterStatus;
|
||||||
|
|
||||||
|
let list = state.checks.filter(c => {
|
||||||
|
if (payee && !c.payee.toLowerCase().includes(payee)) return false;
|
||||||
|
if (from && c.check_date < from) return false;
|
||||||
|
if (to && c.check_date > to) return false;
|
||||||
|
if (status === '0' && c.printed) return false;
|
||||||
|
if (status === '1' && !c.printed) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const col = state.sortCol;
|
const col = state.sortCol;
|
||||||
const dir = state.sortDir === 'asc' ? 1 : -1;
|
const dir = state.sortDir === 'asc' ? 1 : -1;
|
||||||
return [...state.checks].sort((a, b) => {
|
return list.sort((a, b) => {
|
||||||
let av = a[col];
|
let av = a[col];
|
||||||
let bv = b[col];
|
let bv = b[col];
|
||||||
if (col === 'amount') { av = parseFloat(av); bv = parseFloat(bv); }
|
if (col === 'amount') { av = parseFloat(av); bv = parseFloat(bv); }
|
||||||
@@ -318,26 +326,6 @@ async function generatePdf() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reprintCheck(id) {
|
|
||||||
const check = state.checks.find(c => c.id === id);
|
|
||||||
if (!check) return;
|
|
||||||
if (!confirm(`Reprint check #${check.check_no} to "${check.payee}"?\n(Will not re-mark as printed)`)) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/pdf?mark_printed=false', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ checkIds: [id] }),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
||||||
throw new Error(err.error || res.statusText);
|
|
||||||
}
|
|
||||||
const blob = await res.blob();
|
|
||||||
window.open(URL.createObjectURL(blob), '_blank');
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Reprint error: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Setup wizard ─────────────────────────────────────────────────────────────
|
// ── Setup wizard ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -530,10 +518,22 @@ function init() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter dropdown
|
// Filters (client-side; just re-render)
|
||||||
|
document.getElementById('filter-payee').addEventListener('input', e => {
|
||||||
|
state.filterPayee = e.target.value;
|
||||||
|
renderTable();
|
||||||
|
});
|
||||||
|
document.getElementById('filter-date-from').addEventListener('change', e => {
|
||||||
|
state.filterDateFrom = e.target.value;
|
||||||
|
renderTable();
|
||||||
|
});
|
||||||
|
document.getElementById('filter-date-to').addEventListener('change', e => {
|
||||||
|
state.filterDateTo = e.target.value;
|
||||||
|
renderTable();
|
||||||
|
});
|
||||||
document.getElementById('filter-status').addEventListener('change', e => {
|
document.getElementById('filter-status').addEventListener('change', e => {
|
||||||
state.filter = e.target.value;
|
state.filterStatus = e.target.value;
|
||||||
loadChecks();
|
renderTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
// New check
|
// New check
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ router.put('/:id', (req, res) => {
|
|||||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
|
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
|
||||||
if (!check) return res.status(404).json({ error: 'Check not found' });
|
if (!check) return res.status(404).json({ error: 'Check not found' });
|
||||||
|
|
||||||
if (check.printed) {
|
|
||||||
return res.status(409).json({ error: 'Cannot edit a check that has been printed.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { payee, amount, check_date, memo, note1, note2,
|
const { payee, amount, check_date, memo, note1, note2,
|
||||||
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
|
payee_address1, payee_address2, payee_address3, payee_address4 } = req.body;
|
||||||
|
|
||||||
@@ -120,10 +116,6 @@ router.delete('/:id', (req, res) => {
|
|||||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
|
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id);
|
||||||
if (!check) return res.status(404).json({ error: 'Check not found' });
|
if (!check) return res.status(404).json({ error: 'Check not found' });
|
||||||
|
|
||||||
if (check.printed) {
|
|
||||||
return res.status(409).json({ error: 'Cannot delete a check that has been printed.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id);
|
db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user