fixes and refactor
This commit is contained in:
@@ -137,6 +137,10 @@
|
||||
<i class="fa-solid fa-wrench"></i>
|
||||
<span>Maintenance</span>
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 border-transparent hover:border-primary-300 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-neutral-50 dark:hover:bg-neutral-700/50 transition-all duration-200" id="printers-tab" data-tab-target="#printers" type="button" role="tab">
|
||||
<i class="fa-solid fa-print"></i>
|
||||
<span>Printers</span>
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 border-transparent hover:border-primary-300 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-neutral-50 dark:hover:bg-neutral-700/50 transition-all duration-200" id="import-tab" data-tab-target="#import" type="button" role="tab">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span>Import</span>
|
||||
@@ -513,6 +517,110 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Printers Tab -->
|
||||
<div id="printers" role="tabpanel" class="hidden">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow">
|
||||
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700 flex items-center justify-between">
|
||||
<h5 class="m-0 font-semibold"><i class="fa-solid fa-list"></i> Printers</h5>
|
||||
<button type="button" class="px-3 py-1.5 bg-primary-600 hover:bg-primary-700 text-white rounded text-sm" onclick="showCreatePrinterForm()">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<ul id="printers-list" class="divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
<li class="py-2 text-neutral-500">Loading...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow">
|
||||
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h5 class="m-0 font-semibold"><i class="fa-solid fa-pen-to-square"></i> Edit Printer</h5>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<form id="printer-form" class="grid grid-cols-1 md:grid-cols-2 gap-4" onsubmit="return savePrinter(event)">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Printer Name *</label>
|
||||
<input id="printer_name" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Description</label>
|
||||
<input id="description" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Driver</label>
|
||||
<input id="driver" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Port</label>
|
||||
<input id="port" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Number</label>
|
||||
<input id="number" type="number" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-6">
|
||||
<input id="default_printer" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500">
|
||||
<label class="text-sm">Default Printer</label>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Page Break</label>
|
||||
<input id="page_break" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="e.g., \f">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Setup Sequence</label>
|
||||
<input id="setup_st" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Reset Sequence</label>
|
||||
<input id="reset_st" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Bold Start</label>
|
||||
<input id="b_bold" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Bold End</label>
|
||||
<input id="e_bold" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Underline Start</label>
|
||||
<input id="b_underline" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Underline End</label>
|
||||
<input id="e_underline" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 grid grid-cols-2 md:grid-cols-4 gap-3 mt-2">
|
||||
<label class="inline-flex items-center gap-2"><input id="phone_book" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Phone Book</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="rolodex_info" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Rolodex Info</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="envelope" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Envelope</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="file_cabinet" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> File Cabinet</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="accounts" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Accounts</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="statements" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Statements</label>
|
||||
<label class="inline-flex items-center gap-2"><input id="calendar" type="checkbox" class="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"> Calendar</label>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 flex gap-2 mt-4">
|
||||
<button type="submit" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Save
|
||||
</button>
|
||||
<button type="button" class="px-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded" onclick="clearPrinterForm()">Clear</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Import Tab -->
|
||||
<div id="import" role="tabpanel" class="hidden">
|
||||
<div class="flex flex-wrap -mx-4">
|
||||
@@ -1203,6 +1311,8 @@ function onTabShown(tabName) {
|
||||
loadUsers();
|
||||
} else if (tabName === 'settings') {
|
||||
loadSettings();
|
||||
} else if (tabName === 'printers') {
|
||||
loadPrinters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,6 +1331,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSettings();
|
||||
loadLookupTables();
|
||||
loadBackups();
|
||||
loadPrinters();
|
||||
// Tabs setup
|
||||
initializeTabs();
|
||||
|
||||
@@ -1720,6 +1831,157 @@ async function loadLookupTables() {
|
||||
}
|
||||
}
|
||||
|
||||
// Printers Management
|
||||
async function loadPrinters() {
|
||||
try {
|
||||
const response = await window.http.wrappedFetch('/api/admin/printers');
|
||||
const printers = await response.json();
|
||||
renderPrintersList(printers);
|
||||
} catch (err) {
|
||||
console.error('Failed to load printers:', err);
|
||||
const ul = document.getElementById('printers-list');
|
||||
if (ul) ul.innerHTML = '<li class="py-2 text-danger-600 dark:text-danger-400">Failed to load printers</li>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrintersList(printers) {
|
||||
const ul = document.getElementById('printers-list');
|
||||
if (!ul) return;
|
||||
if (!printers || printers.length === 0) {
|
||||
ul.innerHTML = '<li class="py-2 text-neutral-500">No printers found</li>';
|
||||
return;
|
||||
}
|
||||
ul.innerHTML = printers.map(p => `
|
||||
<li class="py-2 px-2 rounded hover:bg-neutral-50 dark:hover:bg-neutral-800/50">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex-1 cursor-pointer" onclick="selectPrinter('${encodeURIComponent(p.printer_name)}')">
|
||||
<div class="font-medium">${p.printer_name}</div>
|
||||
<div class="text-xs text-neutral-500">${p.description || ''}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
${p.default_printer ? '<span class="text-xs px-2 py-0.5 bg-primary-100 dark:bg-primary-800 text-primary-700 dark:text-primary-200 rounded">Default</span>' : ''}
|
||||
<button class="px-2 py-1 border border-danger-600 text-danger-700 dark:text-danger-200 rounded text-xs hover:bg-danger-50 dark:hover:bg-danger-900/20" onclick="deletePrinter(event, '${encodeURIComponent(p.printer_name)}')" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function selectPrinter(encodedName) {
|
||||
const name = decodeURIComponent(encodedName);
|
||||
try {
|
||||
const response = await window.http.wrappedFetch('/api/admin/printers/' + encodeURIComponent(name));
|
||||
const p = await response.json();
|
||||
fillPrinterForm(p);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch printer:', err);
|
||||
if (window.alerts) window.alerts.error('Failed to load printer details');
|
||||
}
|
||||
}
|
||||
|
||||
function fillPrinterForm(p) {
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.value = val == null ? '' : val; };
|
||||
const setb = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; };
|
||||
set('printer_name', p.printer_name || '');
|
||||
set('description', p.description);
|
||||
set('driver', p.driver);
|
||||
set('port', p.port);
|
||||
set('number', p.number);
|
||||
setb('default_printer', p.default_printer);
|
||||
set('page_break', p.page_break);
|
||||
set('setup_st', p.setup_st);
|
||||
set('reset_st', p.reset_st);
|
||||
set('b_bold', p.b_bold);
|
||||
set('e_bold', p.e_bold);
|
||||
set('b_underline', p.b_underline);
|
||||
set('e_underline', p.e_underline);
|
||||
setb('phone_book', p.phone_book);
|
||||
setb('rolodex_info', p.rolodex_info);
|
||||
setb('envelope', p.envelope);
|
||||
setb('file_cabinet', p.file_cabinet);
|
||||
setb('accounts', p.accounts);
|
||||
setb('statements', p.statements);
|
||||
setb('calendar', p.calendar);
|
||||
}
|
||||
|
||||
function showCreatePrinterForm() {
|
||||
clearPrinterForm();
|
||||
const nameEl = document.getElementById('printer_name');
|
||||
if (nameEl) nameEl.focus();
|
||||
}
|
||||
|
||||
function clearPrinterForm() {
|
||||
fillPrinterForm({});
|
||||
}
|
||||
|
||||
async function savePrinter(event) {
|
||||
event.preventDefault();
|
||||
const payload = {
|
||||
printer_name: document.getElementById('printer_name').value,
|
||||
description: document.getElementById('description').value,
|
||||
driver: document.getElementById('driver').value,
|
||||
port: document.getElementById('port').value,
|
||||
number: document.getElementById('number').value ? parseInt(document.getElementById('number').value, 10) : null,
|
||||
default_printer: document.getElementById('default_printer').checked,
|
||||
page_break: document.getElementById('page_break').value,
|
||||
setup_st: document.getElementById('setup_st').value,
|
||||
reset_st: document.getElementById('reset_st').value,
|
||||
b_bold: document.getElementById('b_bold').value,
|
||||
e_bold: document.getElementById('e_bold').value,
|
||||
b_underline: document.getElementById('b_underline').value,
|
||||
e_underline: document.getElementById('e_underline').value,
|
||||
phone_book: document.getElementById('phone_book').checked,
|
||||
rolodex_info: document.getElementById('rolodex_info').checked,
|
||||
envelope: document.getElementById('envelope').checked,
|
||||
file_cabinet: document.getElementById('file_cabinet').checked,
|
||||
accounts: document.getElementById('accounts').checked,
|
||||
statements: document.getElementById('statements').checked,
|
||||
calendar: document.getElementById('calendar').checked,
|
||||
};
|
||||
try {
|
||||
const existsResp = await window.http.wrappedFetch('/api/admin/printers/' + encodeURIComponent(payload.printer_name));
|
||||
if (existsResp.ok) {
|
||||
// update
|
||||
const resp = await window.http.wrappedFetch('/api/admin/printers/' + encodeURIComponent(payload.printer_name), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) throw await window.http.toError(resp, 'Failed to update printer');
|
||||
} else {
|
||||
// create
|
||||
const resp = await window.http.wrappedFetch('/api/admin/printers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) throw await window.http.toError(resp, 'Failed to create printer');
|
||||
}
|
||||
if (window.alerts) window.alerts.success('Printer saved');
|
||||
await loadPrinters();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (window.alerts) window.alerts.error(window.http.formatAlert(err, 'Printer save failed'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function deletePrinter(evt, encodedName) {
|
||||
evt.stopPropagation();
|
||||
const name = decodeURIComponent(encodedName);
|
||||
if (!confirm(`Delete printer "${name}"?`)) return;
|
||||
try {
|
||||
const resp = await window.http.wrappedFetch('/api/admin/printers/' + encodeURIComponent(name), { method: 'DELETE' });
|
||||
if (!resp.ok) throw await window.http.toError(resp, 'Failed to delete printer');
|
||||
if (window.alerts) window.alerts.success('Printer deleted');
|
||||
await loadPrinters();
|
||||
clearPrinterForm();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (window.alerts) window.alerts.error(window.http.formatAlert(err, 'Delete failed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function vacuumDatabase() {
|
||||
if (!confirm('This will optimize the database. Continue?')) return;
|
||||
|
||||
@@ -2576,6 +2838,17 @@ function displayAdminImportResults(result) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Add summary for printers
|
||||
if (result.file_type === 'PRINTERS.csv') {
|
||||
html += `
|
||||
<div class="mt-2 p-2 bg-neutral-50 dark:bg-neutral-800/50 rounded border border-neutral-200 dark:border-neutral-700 text-sm">
|
||||
<strong>Printers:</strong> ${result.created_count || 0} created, ${result.updated_count || 0} updated
|
||||
</div>
|
||||
`;
|
||||
// Auto-refresh printers tab list
|
||||
try { loadPrinters(); } catch (_) {}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -230,13 +230,16 @@
|
||||
<button type="submit" class="w-full px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors flex items-center justify-center gap-2">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> Search
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button type="button" class="w-full px-4 py-2 text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded-lg transition-colors flex items-center justify-center gap-2" id="saveSearchBtn">
|
||||
<i class="fa-solid fa-bookmark"></i> Save Search
|
||||
</button>
|
||||
<button type="button" class="w-full px-4 py-2 text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded-lg transition-colors flex items-center justify-center gap-2" id="resetSearchBtn">
|
||||
<i class="fa-solid fa-rotate-right"></i> Reset
|
||||
</button>
|
||||
<button type="button" class="w-full px-4 py-2 text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded-lg transition-colors flex items-center justify-center gap-2" id="restoreLastBtn">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i> Restore Last
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -412,10 +415,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setupEventHandlers();
|
||||
setupKeyboardShortcuts();
|
||||
|
||||
// Check for URL parameters to auto-load search
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('q')) {
|
||||
document.getElementById('searchQuery').value = urlParams.get('q');
|
||||
// Apply URL parameters to form and auto-perform search if any present
|
||||
const didApplyFromUrl = applyCriteriaFromUrl();
|
||||
if (didApplyFromUrl) {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
@@ -541,6 +543,8 @@ function setupEventHandlers() {
|
||||
document.getElementById('confirmSaveSearch').addEventListener('click', saveCurrentSearch);
|
||||
document.getElementById('savedSearchBtn').addEventListener('click', loadSavedSearches);
|
||||
document.getElementById('clearAllBtn').addEventListener('click', clearAll);
|
||||
const restoreBtn = document.getElementById('restoreLastBtn');
|
||||
if (restoreBtn) restoreBtn.addEventListener('click', restoreLastSearch);
|
||||
|
||||
// Sort change handlers
|
||||
document.getElementById('sortBy').addEventListener('change', () => {
|
||||
@@ -553,6 +557,19 @@ function setupEventHandlers() {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Facet chip click handler (event delegation)
|
||||
const facetsContainer = document.getElementById('facetsContainer');
|
||||
facetsContainer.addEventListener('click', function(e) {
|
||||
const chip = e.target.closest('.facet-chip');
|
||||
if (chip && facetsContainer.contains(chip)) {
|
||||
const facet = chip.getAttribute('data-facet');
|
||||
const value = chip.getAttribute('data-value');
|
||||
if (applyFacetFilter(facet, value)) {
|
||||
performSearch(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupKeyboardShortcuts() {
|
||||
@@ -569,8 +586,14 @@ function setupKeyboardShortcuts() {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
// Escape to clear search
|
||||
// Escape: hide suggestions if open; otherwise clear search
|
||||
if (e.key === 'Escape' && document.activeElement.id === 'searchQuery') {
|
||||
const dropdown = document.getElementById('searchSuggestions');
|
||||
if (dropdown && !dropdown.classList.contains('hidden')) {
|
||||
e.preventDefault();
|
||||
dropdown.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
clearAll();
|
||||
}
|
||||
});
|
||||
@@ -631,6 +654,10 @@ async function performSearch(offset = 0) {
|
||||
const criteria = buildSearchCriteria();
|
||||
criteria.offset = offset;
|
||||
currentSearchCriteria = criteria;
|
||||
// Sync URL with current criteria for shareable searches
|
||||
syncUrlWithCriteria(criteria);
|
||||
// Save last criteria best-effort
|
||||
saveLastCriteria(criteria);
|
||||
|
||||
try {
|
||||
const response = await window.http.wrappedFetch('/api/search/advanced', {
|
||||
@@ -651,6 +678,59 @@ async function performSearch(offset = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLastCriteria(criteria) {
|
||||
try {
|
||||
await window.http.wrappedFetch('/api/search/last_criteria', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(criteria)
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function restoreLastSearch() {
|
||||
try {
|
||||
const resp = await window.http.wrappedFetch('/api/search/last_criteria');
|
||||
if (!resp.ok) throw new Error('failed');
|
||||
const saved = await resp.json();
|
||||
if (!saved || Object.keys(saved).length === 0) {
|
||||
showAlert('No previous search found', 'info');
|
||||
return;
|
||||
}
|
||||
// Populate form from saved criteria
|
||||
document.getElementById('searchQuery').value = saved.query || '';
|
||||
document.getElementById('exactPhrase').checked = !!saved.exact_phrase;
|
||||
document.getElementById('caseSensitive').checked = !!saved.case_sensitive;
|
||||
document.getElementById('wholeWords').checked = !!saved.whole_words;
|
||||
if (Array.isArray(saved.search_types) && saved.search_types.length) {
|
||||
document.querySelectorAll('.search-type').forEach(cb => cb.checked = saved.search_types.includes(cb.value));
|
||||
}
|
||||
if (saved.date_field) document.getElementById('dateField').value = saved.date_field;
|
||||
if (saved.date_from) document.getElementById('dateFrom').value = saved.date_from;
|
||||
if (saved.date_to) document.getElementById('dateTo').value = saved.date_to;
|
||||
if (saved.amount_field) document.getElementById('amountField').value = saved.amount_field;
|
||||
if (saved.amount_min != null) document.getElementById('amountMin').value = saved.amount_min;
|
||||
if (saved.amount_max != null) document.getElementById('amountMax').value = saved.amount_max;
|
||||
const setMulti = (id, values) => {
|
||||
if (!values || !values.length) return;
|
||||
const select = document.getElementById(id);
|
||||
if (!select) return;
|
||||
const set = new Set(values.map(String));
|
||||
Array.from(select.options).forEach(o => { o.selected = set.has(String(o.value)); });
|
||||
};
|
||||
setMulti('fileTypes', saved.file_types);
|
||||
setMulti('fileStatuses', saved.file_statuses);
|
||||
setMulti('employees', saved.employees);
|
||||
setMulti('transactionTypes', saved.transaction_types);
|
||||
setMulti('states', saved.states);
|
||||
document.getElementById('activeOnly').checked = saved.active_only !== false;
|
||||
document.getElementById('hasBalance').checked = !!saved.has_balance;
|
||||
document.getElementById('isBilled').checked = !!saved.is_billed;
|
||||
performSearch(0);
|
||||
} catch (e) {
|
||||
showAlert('Could not restore last search', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
function buildSearchCriteria() {
|
||||
const searchTypes = [];
|
||||
document.querySelectorAll('.search-type:checked').forEach(checkbox => {
|
||||
@@ -833,9 +913,11 @@ function displayFacets(facets) {
|
||||
facetsHTML += `
|
||||
<div class="facet-group mb-2">
|
||||
<strong>${facetName.replace('_', ' ').toUpperCase()}:</strong>
|
||||
${Object.entries(facetData).map(([value, count]) =>
|
||||
`<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-1">${value} (${count})</span>`
|
||||
).join('')}
|
||||
${Object.entries(facetData).map(([value, count]) => {
|
||||
const isClickable = ['state','transaction_type','file_type','status','employee'].includes(facetName);
|
||||
const cls = isClickable ? 'facet-chip cursor-pointer hover:bg-neutral-300' : '';
|
||||
return `<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-1 ${cls}" data-facet="${facetName}" data-value="${String(value).replace(/"/g,'"')}">${value} (${count})</span>`
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -844,6 +926,106 @@ function displayFacets(facets) {
|
||||
container.innerHTML = facetsHTML;
|
||||
}
|
||||
|
||||
// Apply a clicked facet value to the appropriate filter control
|
||||
function applyFacetFilter(facetName, value) {
|
||||
const map = {
|
||||
'state': 'states',
|
||||
'transaction_type': 'transactionTypes',
|
||||
'file_type': 'fileTypes',
|
||||
'status': 'fileStatuses',
|
||||
'employee': 'employees'
|
||||
};
|
||||
const selectId = map[facetName];
|
||||
if (!selectId) return false;
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return false;
|
||||
const option = Array.from(select.options).find(o => String(o.value) === String(value));
|
||||
if (!option) return false;
|
||||
option.selected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sync URL query params with current criteria (shareable/bookmarkable)
|
||||
function syncUrlWithCriteria(criteria) {
|
||||
const params = new URLSearchParams();
|
||||
if (criteria.query) params.set('q', criteria.query);
|
||||
if (Array.isArray(criteria.search_types) && criteria.search_types.length) params.set('types', criteria.search_types.join(','));
|
||||
if (criteria.exact_phrase) params.set('exact_phrase', '1');
|
||||
if (criteria.case_sensitive) params.set('case_sensitive', '1');
|
||||
if (criteria.whole_words) params.set('whole_words', '1');
|
||||
if (criteria.sort_by) params.set('sort_by', criteria.sort_by);
|
||||
if (criteria.sort_order) params.set('sort_order', criteria.sort_order);
|
||||
if (criteria.date_field) params.set('date_field', criteria.date_field);
|
||||
if (criteria.date_from) params.set('date_from', criteria.date_from);
|
||||
if (criteria.date_to) params.set('date_to', criteria.date_to);
|
||||
if (criteria.amount_field) params.set('amount_field', criteria.amount_field);
|
||||
if (criteria.amount_min != null) params.set('amount_min', String(criteria.amount_min));
|
||||
if (criteria.amount_max != null) params.set('amount_max', String(criteria.amount_max));
|
||||
if (Array.isArray(criteria.file_types) && criteria.file_types.length) params.set('file_types', criteria.file_types.join(','));
|
||||
if (Array.isArray(criteria.file_statuses) && criteria.file_statuses.length) params.set('file_statuses', criteria.file_statuses.join(','));
|
||||
if (Array.isArray(criteria.employees) && criteria.employees.length) params.set('employees', criteria.employees.join(','));
|
||||
if (Array.isArray(criteria.transaction_types) && criteria.transaction_types.length) params.set('transaction_types', criteria.transaction_types.join(','));
|
||||
if (Array.isArray(criteria.states) && criteria.states.length) params.set('states', criteria.states.join(','));
|
||||
if (criteria.active_only === false) params.set('active_only', '0');
|
||||
if (criteria.has_balance === true) params.set('has_balance', '1');
|
||||
if (criteria.is_billed === true) params.set('is_billed', '1');
|
||||
const page = Math.floor((criteria.offset || 0) / (criteria.limit || 50)) + 1;
|
||||
if (page > 1) params.set('page', String(page));
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
function applyCriteriaFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let applied = false;
|
||||
const getBool = (key) => {
|
||||
const v = urlParams.get(key);
|
||||
return v === '1' || v === 'true';
|
||||
};
|
||||
const getCsv = (key) => {
|
||||
const v = urlParams.get(key);
|
||||
return v ? v.split(',').filter(Boolean) : [];
|
||||
};
|
||||
const q = urlParams.get('q');
|
||||
if (q) {
|
||||
document.getElementById('searchQuery').value = q;
|
||||
applied = true;
|
||||
}
|
||||
const types = getCsv('types').length ? getCsv('types') : getCsv('search_types');
|
||||
if (types.length) {
|
||||
document.querySelectorAll('.search-type').forEach(cb => cb.checked = types.includes(cb.value));
|
||||
applied = true;
|
||||
}
|
||||
if (urlParams.has('exact_phrase')) { document.getElementById('exactPhrase').checked = getBool('exact_phrase'); applied = true; }
|
||||
if (urlParams.has('case_sensitive')) { document.getElementById('caseSensitive').checked = getBool('case_sensitive'); applied = true; }
|
||||
if (urlParams.has('whole_words')) { document.getElementById('wholeWords').checked = getBool('whole_words'); applied = true; }
|
||||
if (urlParams.has('sort_by')) { document.getElementById('sortBy').value = urlParams.get('sort_by'); applied = true; }
|
||||
if (urlParams.has('sort_order')) { document.getElementById('sortOrder').value = urlParams.get('sort_order'); applied = true; }
|
||||
if (urlParams.has('date_field')) { document.getElementById('dateField').value = urlParams.get('date_field'); applied = true; }
|
||||
if (urlParams.has('date_from')) { document.getElementById('dateFrom').value = urlParams.get('date_from'); applied = true; }
|
||||
if (urlParams.has('date_to')) { document.getElementById('dateTo').value = urlParams.get('date_to'); applied = true; }
|
||||
if (urlParams.has('amount_field')) { document.getElementById('amountField').value = urlParams.get('amount_field'); applied = true; }
|
||||
if (urlParams.has('amount_min')) { document.getElementById('amountMin').value = urlParams.get('amount_min'); applied = true; }
|
||||
if (urlParams.has('amount_max')) { document.getElementById('amountMax').value = urlParams.get('amount_max'); applied = true; }
|
||||
const setMulti = (id, values) => {
|
||||
if (!values || !values.length) return false;
|
||||
const select = document.getElementById(id);
|
||||
if (!select) return false;
|
||||
const set = new Set(values.map(String));
|
||||
Array.from(select.options).forEach(o => { o.selected = set.has(String(o.value)); });
|
||||
return true;
|
||||
};
|
||||
if (setMulti('fileTypes', getCsv('file_types'))) applied = true;
|
||||
if (setMulti('fileStatuses', getCsv('file_statuses'))) applied = true;
|
||||
if (setMulti('employees', getCsv('employees'))) applied = true;
|
||||
if (setMulti('transactionTypes', getCsv('transaction_types'))) applied = true;
|
||||
if (setMulti('states', getCsv('states'))) applied = true;
|
||||
if (urlParams.has('active_only')) { document.getElementById('activeOnly').checked = getBool('active_only'); applied = true; }
|
||||
if (urlParams.has('has_balance')) { document.getElementById('hasBalance').checked = getBool('has_balance'); applied = true; }
|
||||
if (urlParams.has('is_billed')) { document.getElementById('isBilled').checked = getBool('is_billed'); applied = true; }
|
||||
return applied;
|
||||
}
|
||||
|
||||
function displayPagination(pageInfo) {
|
||||
const paginationContainer = document.getElementById('searchPagination');
|
||||
paginationContainer.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user