fixes and refactor

This commit is contained in:
HotSwapp
2025-08-14 19:16:28 -05:00
parent 5111079149
commit bfc04a6909
61 changed files with 5689 additions and 767 deletions

View File

@@ -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,'&quot;')}">${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 = '';