Files
delphi-database/templates/search.html
2025-08-08 15:55:15 -05:00

1205 lines
57 KiB
HTML

{% extends "base.html" %}
{% block title %}Advanced Search - Delphi Database{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-search"></i> Advanced Search</h2>
<div>
<button class="btn btn-success" id="savedSearchBtn">
<i class="bi bi-bookmark-star"></i> Saved Searches
</button>
<button class="btn btn-info" id="searchHistoryBtn">
<i class="bi bi-clock-history"></i> Search History
</button>
<button class="btn btn-secondary" id="clearAllBtn">
<i class="bi bi-x-circle"></i> Clear All
</button>
</div>
</div>
<div class="row">
<!-- Search Form Panel -->
<div class="col-lg-4">
<div class="card sticky-top" style="top: 20px;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-funnel"></i> Search Criteria</h5>
</div>
<div class="card-body">
<form id="advancedSearchForm">
<!-- Basic Search -->
<div class="mb-3">
<label for="searchQuery" class="form-label">Search Terms</label>
<div class="input-group">
<input type="text" class="form-control" id="searchQuery"
placeholder="Enter search terms..." autocomplete="off">
<button class="btn btn-outline-secondary" type="button" id="voiceSearchBtn" title="Voice Search">
<i class="bi bi-mic"></i>
</button>
</div>
<div id="searchSuggestions" class="dropdown-menu w-100" style="max-height: 200px; overflow-y: auto;">
<!-- Suggestions will appear here -->
</div>
</div>
<!-- Search Options -->
<div class="mb-3">
<label class="form-label">Search Options</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="exactPhrase">
<label class="form-check-label" for="exactPhrase">Exact phrase</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="caseSensitive">
<label class="form-check-label" for="caseSensitive">Case sensitive</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="wholeWords">
<label class="form-check-label" for="wholeWords">Whole words only</label>
</div>
</div>
<!-- Search Types -->
<div class="mb-3">
<label class="form-label">Search In</label>
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchCustomers" value="customer" checked>
<label class="form-check-label" for="searchCustomers">Customers</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchFiles" value="file" checked>
<label class="form-check-label" for="searchFiles">Files</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchLedger" value="ledger" checked>
<label class="form-check-label" for="searchLedger">Financial</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchQdros" value="qdro" checked>
<label class="form-check-label" for="searchQdros">QDROs</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchDocuments" value="document" checked>
<label class="form-check-label" for="searchDocuments">Documents</label>
</div>
<div class="form-check">
<input class="form-check-input search-type" type="checkbox" id="searchPhones" value="phone">
<label class="form-check-label" for="searchPhones">Phones</label>
</div>
</div>
</div>
</div>
<!-- Advanced Filters -->
<div class="accordion" id="filtersAccordion">
<!-- Date Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="dateFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#dateFilters" aria-expanded="false">
<i class="bi bi-calendar3"></i>&nbsp; Date Filters
</button>
</h2>
<div id="dateFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="dateField" class="form-label">Date Field</label>
<select class="form-select" id="dateField">
<option value="">Select date field...</option>
<option value="created">Created Date</option>
<option value="updated">Updated Date</option>
<option value="opened">File Opened Date</option>
<option value="closed">File Closed Date</option>
</select>
</div>
<div class="row">
<div class="col-6">
<label for="dateFrom" class="form-label">From</label>
<input type="date" class="form-control" id="dateFrom">
</div>
<div class="col-6">
<label for="dateTo" class="form-label">To</label>
<input type="date" class="form-control" id="dateTo">
</div>
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetToday">Today</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetWeek">This Week</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetMonth">This Month</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="datePresetYear">This Year</button>
</div>
</div>
</div>
</div>
<!-- Amount Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="amountFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#amountFilters" aria-expanded="false">
<i class="bi bi-currency-dollar"></i>&nbsp; Amount Filters
</button>
</h2>
<div id="amountFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="amountField" class="form-label">Amount Field</label>
<select class="form-select" id="amountField">
<option value="">Select amount field...</option>
<option value="amount">Transaction Amount</option>
<option value="balance">Account Balance</option>
<option value="total_charges">Total Charges</option>
</select>
</div>
<div class="row">
<div class="col-6">
<label for="amountMin" class="form-label">Minimum</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="amountMin" step="0.01" min="0">
</div>
</div>
<div class="col-6">
<label for="amountMax" class="form-label">Maximum</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="amountMax" step="0.01" min="0">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Category Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="categoryFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#categoryFilters" aria-expanded="false">
<i class="bi bi-tags"></i>&nbsp; Category Filters
</button>
</h2>
<div id="categoryFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="mb-3">
<label for="fileTypes" class="form-label">File Types</label>
<select class="form-select" id="fileTypes" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="fileStatuses" class="form-label">File Statuses</label>
<select class="form-select" id="fileStatuses" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="employees" class="form-label">Employees</label>
<select class="form-select" id="employees" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="transactionTypes" class="form-label">Transaction Types</label>
<select class="form-select" id="transactionTypes" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
<div class="mb-3">
<label for="states" class="form-label">States</label>
<select class="form-select" id="states" multiple>
<!-- Options loaded dynamically -->
</select>
</div>
</div>
</div>
</div>
<!-- Boolean Filters -->
<div class="accordion-item">
<h2 class="accordion-header" id="booleanFiltersHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#booleanFilters" aria-expanded="false">
<i class="bi bi-toggles"></i>&nbsp; Additional Filters
</button>
</h2>
<div id="booleanFilters" class="accordion-collapse collapse" data-bs-parent="#filtersAccordion">
<div class="accordion-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="activeOnly" checked>
<label class="form-check-label" for="activeOnly">Active records only</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="hasBalance">
<label class="form-check-label" for="hasBalance">Has outstanding balance</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isBilled">
<label class="form-check-label" for="isBilled">Billed items only</label>
</div>
</div>
</div>
</div>
</div>
<!-- Search Actions -->
<div class="mt-4 d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Search
</button>
<div class="row g-2">
<div class="col-6">
<button type="button" class="btn btn-outline-secondary w-100" id="saveSearchBtn">
<i class="bi bi-bookmark-plus"></i> Save Search
</button>
</div>
<div class="col-6">
<button type="button" class="btn btn-outline-secondary w-100" id="resetSearchBtn">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Results Panel -->
<div class="col-lg-8">
<!-- Search Status Bar -->
<div class="card mb-3">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<div id="searchStatus">
<span class="text-muted">Enter search terms to begin</span>
</div>
</div>
<div class="col-md-6">
<div class="row align-items-center">
<div class="col-md-6">
<label for="sortBy" class="form-label mb-0">Sort by:</label>
<select class="form-select form-select-sm" id="sortBy">
<option value="relevance">Relevance</option>
<option value="date">Date</option>
<option value="amount">Amount</option>
<option value="title">Title</option>
</select>
</div>
<div class="col-md-6">
<label for="sortOrder" class="form-label mb-0">Order:</label>
<select class="form-select form-select-sm" id="sortOrder">
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Facets/Filters Summary -->
<div class="card mb-3" id="facetsCard" style="display: none;">
<div class="card-body">
<h6 class="card-title">Filter Results</h6>
<div id="facetsContainer">
<!-- Facets will be displayed here -->
</div>
</div>
</div>
<!-- Search Results -->
<div class="card">
<div class="card-body">
<div id="searchResults">
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<h4>Advanced Search</h4>
<p>Use the search form on the left to find customers, files, transactions, documents, and more across the entire database.</p>
<div class="mt-3">
<small class="text-muted">
<strong>Quick Tips:</strong><br>
• Use quotes for exact phrases: "John Smith"<br>
• Use filters to narrow results<br>
• Save frequently used searches<br>
• Export results for further analysis
</small>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="searchLoading" class="text-center p-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<div class="mt-2">Searching database...</div>
</div>
<!-- Pagination -->
<nav id="searchPagination" aria-label="Search results pagination" style="display: none;">
<ul class="pagination justify-content-center">
<!-- Pagination will be populated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Save Search Modal -->
<div class="modal fade" id="saveSearchModal" tabindex="-1" aria-labelledby="saveSearchModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveSearchModalLabel">Save Search</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="saveSearchForm">
<div class="mb-3">
<label for="searchName" class="form-label">Search Name *</label>
<input type="text" class="form-control" id="searchName" required placeholder="Enter a name for this search">
</div>
<div class="mb-3">
<label for="searchDescription" class="form-label">Description</label>
<textarea class="form-control" id="searchDescription" rows="3" placeholder="Optional description of this search"></textarea>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPublicSearch">
<label class="form-check-label" for="isPublicSearch">
Make this search public (visible to all users)
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmSaveSearch">
<i class="bi bi-bookmark-plus"></i> Save Search
</button>
</div>
</div>
</div>
</div>
<!-- Saved Searches Modal -->
<div class="modal fade" id="savedSearchesModal" tabindex="-1" aria-labelledby="savedSearchesModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="savedSearchesModalLabel">Saved Searches</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Last Used</th>
<th>Use Count</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="savedSearchesTableBody">
<!-- Saved searches will be loaded here -->
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Search Statistics Modal -->
<div class="modal fade" id="searchStatsModal" tabindex="-1" aria-labelledby="searchStatsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="searchStatsModalLabel">Search Statistics</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row" id="searchStatsContent">
<!-- Statistics will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
// Advanced Search JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Initialize search components
initializeSearch();
loadSearchFacets();
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');
performSearch();
}
});
let currentSearchCriteria = {};
let searchTimeout;
let facetsData = {};
function initializeSearch() {
// Set default date for today
const today = new Date().toISOString().split('T')[0];
// Initialize search suggestions dropdown
const suggestionsDropdown = document.getElementById('searchSuggestions');
suggestionsDropdown.addEventListener('click', function(e) {
if (e.target.classList.contains('dropdown-item')) {
document.getElementById('searchQuery').value = e.target.textContent;
suggestionsDropdown.classList.remove('show');
performSearch();
}
});
}
async function loadSearchFacets() {
try {
const response = await fetch('/api/search/facets');
if (!response.ok) throw new Error('Failed to load search facets');
facetsData = await response.json();
populateFilterDropdowns();
} catch (error) {
console.error('Error loading search facets:', error);
}
}
function populateFilterDropdowns() {
// Populate file types
const fileTypesSelect = document.getElementById('fileTypes');
facetsData.file_types?.forEach(type => {
const option = document.createElement('option');
option.value = type.code;
option.textContent = type.name;
fileTypesSelect.appendChild(option);
});
// Populate file statuses
const fileStatusesSelect = document.getElementById('fileStatuses');
facetsData.file_statuses?.forEach(status => {
const option = document.createElement('option');
option.value = status.code;
option.textContent = status.name;
fileStatusesSelect.appendChild(option);
});
// Populate employees
const employeesSelect = document.getElementById('employees');
facetsData.employees?.forEach(emp => {
const option = document.createElement('option');
option.value = emp.code;
option.textContent = emp.name;
employeesSelect.appendChild(option);
});
// Populate transaction types
const transactionTypesSelect = document.getElementById('transactionTypes');
facetsData.transaction_types?.forEach(type => {
const option = document.createElement('option');
option.value = type.code;
option.textContent = type.name;
transactionTypesSelect.appendChild(option);
});
// Populate states
const statesSelect = document.getElementById('states');
facetsData.states?.forEach(state => {
const option = document.createElement('option');
option.value = state.code;
option.textContent = state.name;
statesSelect.appendChild(option);
});
}
function setupEventHandlers() {
// Form submission
document.getElementById('advancedSearchForm').addEventListener('submit', function(e) {
e.preventDefault();
performSearch();
});
// Search input with suggestions
const searchInput = document.getElementById('searchQuery');
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
if (this.value.length > 1) {
searchTimeout = setTimeout(() => {
loadSearchSuggestions(this.value);
}, 300);
} else {
document.getElementById('searchSuggestions').classList.remove('show');
}
});
// Date presets
document.getElementById('datePresetToday').addEventListener('click', () => setDatePreset('today'));
document.getElementById('datePresetWeek').addEventListener('click', () => setDatePreset('week'));
document.getElementById('datePresetMonth').addEventListener('click', () => setDatePreset('month'));
document.getElementById('datePresetYear').addEventListener('click', () => setDatePreset('year'));
// Action buttons
document.getElementById('resetSearchBtn').addEventListener('click', resetSearch);
document.getElementById('saveSearchBtn').addEventListener('click', () => {
if (Object.keys(currentSearchCriteria).length > 0) {
const modal = new bootstrap.Modal(document.getElementById('saveSearchModal'));
modal.show();
} else {
showAlert('Please perform a search before saving', 'warning');
}
});
document.getElementById('confirmSaveSearch').addEventListener('click', saveCurrentSearch);
document.getElementById('savedSearchBtn').addEventListener('click', loadSavedSearches);
document.getElementById('clearAllBtn').addEventListener('click', clearAll);
// Sort change handlers
document.getElementById('sortBy').addEventListener('change', () => {
if (Object.keys(currentSearchCriteria).length > 0) {
performSearch();
}
});
document.getElementById('sortOrder').addEventListener('change', () => {
if (Object.keys(currentSearchCriteria).length > 0) {
performSearch();
}
});
}
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
// Ctrl+F to focus search
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.getElementById('searchQuery').focus();
}
// Ctrl+Enter to search
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
performSearch();
}
// Escape to clear search
if (e.key === 'Escape' && document.activeElement.id === 'searchQuery') {
clearAll();
}
});
}
async function loadSearchSuggestions(query) {
try {
const response = await fetch(`/api/search/suggestions?q=${encodeURIComponent(query)}&limit=10`);
if (!response.ok) throw new Error('Failed to load suggestions');
const data = await response.json();
displaySearchSuggestions(data.suggestions);
} catch (error) {
console.error('Error loading suggestions:', error);
}
}
function displaySearchSuggestions(suggestions) {
const dropdown = document.getElementById('searchSuggestions');
dropdown.innerHTML = '';
if (suggestions.length === 0) {
dropdown.classList.remove('show');
return;
}
suggestions.forEach(suggestion => {
const item = document.createElement('a');
item.className = 'dropdown-item';
item.href = '#';
item.innerHTML = `
<div class="d-flex justify-content-between">
<span>${suggestion.text}</span>
<small class="text-muted">${suggestion.category}</small>
</div>
${suggestion.description ? `<small class="text-muted">${suggestion.description}</small>` : ''}
`;
dropdown.appendChild(item);
});
dropdown.classList.add('show');
}
async function performSearch(offset = 0) {
const searchQuery = document.getElementById('searchQuery').value.trim();
if (!searchQuery && !hasActiveFilters()) {
showAlert('Please enter search terms or apply filters', 'warning');
return;
}
// Show loading
document.getElementById('searchLoading').style.display = 'block';
document.getElementById('searchResults').innerHTML = '';
document.getElementById('searchPagination').style.display = 'none';
// Build search criteria
const criteria = buildSearchCriteria();
criteria.offset = offset;
currentSearchCriteria = criteria;
try {
const response = await fetch('/api/search/advanced', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(criteria)
});
if (!response.ok) throw new Error('Search failed');
const data = await response.json();
displaySearchResults(data);
} catch (error) {
console.error('Search error:', error);
showAlert('Search failed: ' + error.message, 'danger');
} finally {
document.getElementById('searchLoading').style.display = 'none';
}
}
function buildSearchCriteria() {
const searchTypes = [];
document.querySelectorAll('.search-type:checked').forEach(checkbox => {
searchTypes.push(checkbox.value);
});
const criteria = {
query: document.getElementById('searchQuery').value.trim() || null,
search_types: searchTypes,
exact_phrase: document.getElementById('exactPhrase').checked,
case_sensitive: document.getElementById('caseSensitive').checked,
whole_words: document.getElementById('wholeWords').checked,
sort_by: document.getElementById('sortBy').value,
sort_order: document.getElementById('sortOrder').value,
limit: 50
};
// Date filters
const dateField = document.getElementById('dateField').value;
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
if (dateField && (dateFrom || dateTo)) {
criteria.date_field = dateField;
if (dateFrom) criteria.date_from = dateFrom;
if (dateTo) criteria.date_to = dateTo;
}
// Amount filters
const amountField = document.getElementById('amountField').value;
const amountMin = document.getElementById('amountMin').value;
const amountMax = document.getElementById('amountMax').value;
if (amountField && (amountMin || amountMax)) {
criteria.amount_field = amountField;
if (amountMin) criteria.amount_min = parseFloat(amountMin);
if (amountMax) criteria.amount_max = parseFloat(amountMax);
}
// Category filters
const fileTypes = Array.from(document.getElementById('fileTypes').selectedOptions).map(o => o.value);
const fileStatuses = Array.from(document.getElementById('fileStatuses').selectedOptions).map(o => o.value);
const employees = Array.from(document.getElementById('employees').selectedOptions).map(o => o.value);
const transactionTypes = Array.from(document.getElementById('transactionTypes').selectedOptions).map(o => o.value);
const states = Array.from(document.getElementById('states').selectedOptions).map(o => o.value);
if (fileTypes.length > 0) criteria.file_types = fileTypes;
if (fileStatuses.length > 0) criteria.file_statuses = fileStatuses;
if (employees.length > 0) criteria.employees = employees;
if (transactionTypes.length > 0) criteria.transaction_types = transactionTypes;
if (states.length > 0) criteria.states = states;
// Boolean filters
criteria.active_only = document.getElementById('activeOnly').checked;
const hasBalanceCheckbox = document.getElementById('hasBalance');
if (hasBalanceCheckbox.checked) {
criteria.has_balance = true;
}
const isBilledCheckbox = document.getElementById('isBilled');
if (isBilledCheckbox.checked) {
criteria.is_billed = true;
}
return criteria;
}
function hasActiveFilters() {
const dateField = document.getElementById('dateField').value;
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
const amountField = document.getElementById('amountField').value;
const amountMin = document.getElementById('amountMin').value;
const amountMax = document.getElementById('amountMax').value;
const fileTypes = document.getElementById('fileTypes').selectedOptions.length > 0;
const fileStatuses = document.getElementById('fileStatuses').selectedOptions.length > 0;
const employees = document.getElementById('employees').selectedOptions.length > 0;
const transactionTypes = document.getElementById('transactionTypes').selectedOptions.length > 0;
const states = document.getElementById('states').selectedOptions.length > 0;
const hasBalance = document.getElementById('hasBalance').checked;
const isBilled = document.getElementById('isBilled').checked;
return (dateField && (dateFrom || dateTo)) ||
(amountField && (amountMin || amountMax)) ||
fileTypes || fileStatuses || employees || transactionTypes || states ||
hasBalance || isBilled;
}
function displaySearchResults(data) {
const resultsContainer = document.getElementById('searchResults');
const statusElement = document.getElementById('searchStatus');
const facetsCard = document.getElementById('facetsCard');
// Update status
const executionTime = data.stats?.search_execution_time || 0;
statusElement.innerHTML = `
<strong>${data.total_results}</strong> results found
<small class="text-muted">(${executionTime.toFixed(3)}s)</small>
`;
// Display facets
if (Object.keys(data.facets).some(key => Object.keys(data.facets[key]).length > 0)) {
displayFacets(data.facets);
facetsCard.style.display = 'block';
} else {
facetsCard.style.display = 'none';
}
// Display results
if (data.results.length === 0) {
resultsContainer.innerHTML = `
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<h4>No Results Found</h4>
<p>Try adjusting your search terms or filters</p>
</div>
`;
return;
}
let resultsHTML = '';
data.results.forEach(result => {
const typeIcon = getTypeIcon(result.type);
const typeBadge = getTypeBadge(result.type);
resultsHTML += `
<div class="search-result-item border-bottom py-3">
<div class="d-flex">
<div class="flex-shrink-0 me-3">
<i class="${typeIcon} fs-4 text-primary"></i>
</div>
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start mb-1">
<h6 class="mb-1">
<a href="${result.url}" class="text-decoration-none">${result.title}</a>
${typeBadge}
</h6>
<div class="text-end">
${result.relevance_score ? `<small class="text-muted">Score: ${result.relevance_score.toFixed(1)}</small>` : ''}
${result.updated_at ? `<br><small class="text-muted">${formatDate(result.updated_at)}</small>` : ''}
</div>
</div>
<p class="mb-1 text-muted">${result.description}</p>
${result.highlight ? `<div class="small text-info"><strong>Match:</strong> ${result.highlight}</div>` : ''}
${displayResultMetadata(result.metadata)}
</div>
</div>
</div>
`;
});
resultsContainer.innerHTML = resultsHTML;
// Display pagination
if (data.page_info.total_pages > 1) {
displayPagination(data.page_info);
}
}
function displayFacets(facets) {
const container = document.getElementById('facetsContainer');
let facetsHTML = '';
Object.entries(facets).forEach(([facetName, facetData]) => {
if (Object.keys(facetData).length > 0) {
facetsHTML += `
<div class="facet-group mb-2">
<strong>${facetName.replace('_', ' ').toUpperCase()}:</strong>
${Object.entries(facetData).map(([value, count]) =>
`<span class="badge bg-secondary ms-1">${value} (${count})</span>`
).join('')}
</div>
`;
}
});
container.innerHTML = facetsHTML;
}
function displayPagination(pageInfo) {
const paginationContainer = document.getElementById('searchPagination');
const pagination = paginationContainer.querySelector('.pagination');
pagination.innerHTML = '';
// Previous button
const prevItem = document.createElement('li');
prevItem.className = `page-item ${pageInfo.has_previous ? '' : 'disabled'}`;
prevItem.innerHTML = `<a class="page-link" href="#" data-page="${pageInfo.current_page - 1}">Previous</a>`;
pagination.appendChild(prevItem);
// Page numbers
const startPage = Math.max(1, pageInfo.current_page - 2);
const endPage = Math.min(pageInfo.total_pages, pageInfo.current_page + 2);
for (let page = startPage; page <= endPage; page++) {
const pageItem = document.createElement('li');
pageItem.className = `page-item ${page === pageInfo.current_page ? 'active' : ''}`;
pageItem.innerHTML = `<a class="page-link" href="#" data-page="${page}">${page}</a>`;
pagination.appendChild(pageItem);
}
// Next button
const nextItem = document.createElement('li');
nextItem.className = `page-item ${pageInfo.has_next ? '' : 'disabled'}`;
nextItem.innerHTML = `<a class="page-link" href="#" data-page="${pageInfo.current_page + 1}">Next</a>`;
pagination.appendChild(nextItem);
// Add click handlers
pagination.addEventListener('click', function(e) {
e.preventDefault();
if (e.target.classList.contains('page-link') && !e.target.parentElement.classList.contains('disabled')) {
const page = parseInt(e.target.dataset.page);
const offset = (page - 1) * currentSearchCriteria.limit;
performSearch(offset);
}
});
paginationContainer.style.display = 'block';
}
function getTypeIcon(type) {
const icons = {
'customer': 'bi-person-fill',
'file': 'bi-folder-fill',
'ledger': 'bi-calculator',
'qdro': 'bi-file-earmark-ruled',
'document': 'bi-file-earmark-text',
'template': 'bi-file-text',
'phone': 'bi-telephone'
};
return icons[type] || 'bi-file';
}
function getTypeBadge(type) {
const badges = {
'customer': '<span class="badge bg-primary ms-2">Customer</span>',
'file': '<span class="badge bg-success ms-2">File</span>',
'ledger': '<span class="badge bg-warning ms-2">Financial</span>',
'qdro': '<span class="badge bg-info ms-2">QDRO</span>',
'document': '<span class="badge bg-secondary ms-2">Document</span>',
'template': '<span class="badge bg-secondary ms-2">Template</span>',
'phone': '<span class="badge bg-dark ms-2">Phone</span>'
};
return badges[type] || '';
}
function displayResultMetadata(metadata) {
if (!metadata) return '';
let metadataHTML = '<div class="small text-muted mt-1">';
Object.entries(metadata).forEach(([key, value]) => {
if (value && key !== 'phones') { // Skip complex objects
metadataHTML += `<span class="me-3"><strong>${key.replace('_', ' ')}:</strong> ${value}</span>`;
}
});
metadataHTML += '</div>';
return metadataHTML;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString();
}
function setDatePreset(preset) {
const today = new Date();
let fromDate, toDate;
switch (preset) {
case 'today':
fromDate = toDate = today.toISOString().split('T')[0];
break;
case 'week':
const weekStart = new Date(today.setDate(today.getDate() - today.getDay()));
fromDate = weekStart.toISOString().split('T')[0];
toDate = new Date().toISOString().split('T')[0];
break;
case 'month':
fromDate = new Date(today.getFullYear(), today.getMonth(), 1).toISOString().split('T')[0];
toDate = new Date().toISOString().split('T')[0];
break;
case 'year':
fromDate = new Date(today.getFullYear(), 0, 1).toISOString().split('T')[0];
toDate = new Date().toISOString().split('T')[0];
break;
}
document.getElementById('dateFrom').value = fromDate;
document.getElementById('dateTo').value = toDate;
}
function resetSearch() {
// Reset form
document.getElementById('advancedSearchForm').reset();
// Reset checkboxes to defaults
document.getElementById('activeOnly').checked = true;
document.querySelectorAll('.search-type').forEach(cb => cb.checked = true);
document.getElementById('searchPhones').checked = false;
// Clear multi-selects
['fileTypes', 'fileStatuses', 'employees', 'transactionTypes', 'states'].forEach(id => {
const select = document.getElementById(id);
Array.from(select.options).forEach(option => option.selected = false);
});
// Clear results
document.getElementById('searchResults').innerHTML = `
<div class="text-center text-muted p-5">
<i class="bi bi-search display-1"></i>
<h4>Advanced Search</h4>
<p>Use the search form on the left to find customers, files, transactions, documents, and more across the entire database.</p>
</div>
`;
document.getElementById('searchStatus').innerHTML = '<span class="text-muted">Enter search terms to begin</span>';
document.getElementById('facetsCard').style.display = 'none';
document.getElementById('searchPagination').style.display = 'none';
currentSearchCriteria = {};
}
function clearAll() {
resetSearch();
document.getElementById('searchQuery').focus();
}
async function saveCurrentSearch() {
const name = document.getElementById('searchName').value.trim();
const description = document.getElementById('searchDescription').value.trim();
const isPublic = document.getElementById('isPublicSearch').checked;
if (!name) {
showAlert('Please enter a name for the search', 'warning');
return;
}
const savedSearch = {
name: name,
description: description,
criteria: currentSearchCriteria,
is_public: isPublic
};
try {
// This would typically be saved to a backend API
// For now, save to localStorage
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
savedSearch.id = Date.now();
savedSearch.created_at = new Date().toISOString();
savedSearches.push(savedSearch);
localStorage.setItem('savedSearches', JSON.stringify(savedSearches));
showAlert('Search saved successfully', 'success');
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('saveSearchModal'));
modal.hide();
document.getElementById('saveSearchForm').reset();
} catch (error) {
console.error('Error saving search:', error);
showAlert('Failed to save search', 'danger');
}
}
async function loadSavedSearches() {
try {
// Load from localStorage (would typically be from API)
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
const tableBody = document.getElementById('savedSearchesTableBody');
tableBody.innerHTML = '';
if (savedSearches.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No saved searches found</td></tr>';
} else {
savedSearches.forEach(search => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${search.name}</strong></td>
<td>${search.description || '<em>No description</em>'}</td>
<td>${search.last_used ? formatDate(search.last_used) : 'Never'}</td>
<td>${search.use_count || 0}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="loadSavedSearch(${search.id})" title="Load Search">
<i class="bi bi-play"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteSavedSearch(${search.id})" title="Delete Search">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
tableBody.appendChild(row);
});
}
const modal = new bootstrap.Modal(document.getElementById('savedSearchesModal'));
modal.show();
} catch (error) {
console.error('Error loading saved searches:', error);
showAlert('Failed to load saved searches', 'danger');
}
}
function loadSavedSearch(searchId) {
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
const search = savedSearches.find(s => s.id === searchId);
if (!search) {
showAlert('Saved search not found', 'warning');
return;
}
// Load search criteria into form
const criteria = search.criteria;
// Set basic search
document.getElementById('searchQuery').value = criteria.query || '';
document.getElementById('exactPhrase').checked = criteria.exact_phrase || false;
document.getElementById('caseSensitive').checked = criteria.case_sensitive || false;
document.getElementById('wholeWords').checked = criteria.whole_words || false;
// Set search types
document.querySelectorAll('.search-type').forEach(checkbox => {
checkbox.checked = criteria.search_types.includes(checkbox.value);
});
// Set filters
if (criteria.date_field) {
document.getElementById('dateField').value = criteria.date_field;
document.getElementById('dateFrom').value = criteria.date_from || '';
document.getElementById('dateTo').value = criteria.date_to || '';
}
if (criteria.amount_field) {
document.getElementById('amountField').value = criteria.amount_field;
document.getElementById('amountMin').value = criteria.amount_min || '';
document.getElementById('amountMax').value = criteria.amount_max || '';
}
// Set sort options
document.getElementById('sortBy').value = criteria.sort_by || 'relevance';
document.getElementById('sortOrder').value = criteria.sort_order || 'desc';
// Close modal and perform search
const modal = bootstrap.Modal.getInstance(document.getElementById('savedSearchesModal'));
modal.hide();
// Update use count
search.use_count = (search.use_count || 0) + 1;
search.last_used = new Date().toISOString();
localStorage.setItem('savedSearches', JSON.stringify(savedSearches));
performSearch();
}
function deleteSavedSearch(searchId) {
if (confirm('Are you sure you want to delete this saved search?')) {
const savedSearches = JSON.parse(localStorage.getItem('savedSearches') || '[]');
const filteredSearches = savedSearches.filter(s => s.id !== searchId);
localStorage.setItem('savedSearches', JSON.stringify(filteredSearches));
loadSavedSearches(); // Refresh the list
showAlert('Saved search deleted', 'success');
}
}
// Utility functions
function showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 end-0 m-3`;
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// Export functionality
function exportSearchResults() {
if (!currentSearchCriteria || Object.keys(currentSearchCriteria).length === 0) {
showAlert('No search results to export', 'warning');
return;
}
// This would typically call an export API endpoint
showAlert('Export functionality will be implemented in a future update', 'info');
}
</script>
{% endblock %}