1175 lines
65 KiB
HTML
1175 lines
65 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Advanced Search - Delphi Database{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h2 class="text-2xl font-bold"><i class="fa-solid fa-magnifying-glass"></i> Advanced Search</h2>
|
|
<div class="flex gap-2">
|
|
<button class="px-4 py-2 bg-success-600 text-white rounded hover:bg-success-700" id="savedSearchBtn"><i class="fa-solid fa-bookmark"></i> Saved Searches</button>
|
|
<button class="px-4 py-2 bg-info-600 text-white rounded hover:bg-info-700" id="searchHistoryBtn"><i class="fa-solid fa-clock-rotate-left"></i> Search History</button>
|
|
<button class="px-4 py-2 bg-neutral-200 dark:bg-neutral-700 rounded hover:bg-neutral-300 dark:hover:bg-neutral-600" id="clearAllBtn"><i class="fa-solid fa-circle-xmark"></i> Clear All</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
<!-- Search Form Panel -->
|
|
<div class="lg:col-span-1">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow border border-neutral-200 dark:border-neutral-700 sticky top-5">
|
|
<div class="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="mb-0 text-lg font-semibold"><i class="fa-solid fa-filter"></i> Search Criteria</h5>
|
|
</div>
|
|
<div class="p-4">
|
|
<form id="advancedSearchForm">
|
|
<!-- Basic Search -->
|
|
<div class="mb-4">
|
|
<label for="searchQuery" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search Terms</label>
|
|
<div class="relative">
|
|
<input type="text" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="searchQuery"
|
|
placeholder="Enter search terms..." autocomplete="off">
|
|
<button class="absolute right-0 top-0 h-full px-3 text-neutral-400 hover:text-primary-600 dark:text-neutral-500 dark:hover:text-primary-400 transition-colors" type="button" id="voiceSearchBtn" title="Voice Search">
|
|
<i class="fa-solid fa-microphone"></i>
|
|
</button>
|
|
</div>
|
|
<div id="searchSuggestions" class="hidden absolute z-10 w-full bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 max-h-48 overflow-y-auto">
|
|
<!-- Suggestions will appear here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Options -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search Options</label>
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="exactPhrase">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="exactPhrase">Exact phrase</label>
|
|
</div>
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="caseSensitive">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="caseSensitive">Case sensitive</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="wholeWords">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="wholeWords">Whole words only</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Types -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search In</label>
|
|
<div class="flex flex-wrap -mx-2">
|
|
<div class="w-1/2 px-2">
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchCustomers" value="customer" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchCustomers">Customers</label>
|
|
</div>
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchFiles" value="file" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchFiles">Files</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchLedger" value="ledger" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchLedger">Financial</label>
|
|
</div>
|
|
</div>
|
|
<div class="w-1/2 px-2">
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchQdros" value="qdro" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchQdros">QDROs</label>
|
|
</div>
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchDocuments" value="document" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchDocuments">Documents</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500 search-type" type="checkbox" id="searchPhones" value="phone">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="searchPhones">Phones</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Filters -->
|
|
<div id="filtersAccordion">
|
|
<!-- Date Filters -->
|
|
<details class="border rounded mb-2">
|
|
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
|
|
<i class="fa-solid fa-calendar-days"></i> Date Filters
|
|
</summary>
|
|
<div class="px-4 py-2">
|
|
<div class="mb-4">
|
|
<label for="dateField" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Date Field</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" 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="flex flex-wrap -mx-2">
|
|
<div class="w-1/2 px-2">
|
|
<label for="dateFrom" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">From</label>
|
|
<input type="date" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="dateFrom">
|
|
</div>
|
|
<div class="w-1/2 px-2">
|
|
<label for="dateTo" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">To</label>
|
|
<input type="date" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="dateTo">
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 flex gap-2">
|
|
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetToday">Today</button>
|
|
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetWeek">This Week</button>
|
|
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetMonth">This Month</button>
|
|
<button type="button" class="px-3 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-200 dark:bg-neutral-700 hover:bg-neutral-300 dark:hover:bg-neutral-600 rounded transition-colors" id="datePresetYear">This Year</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Amount Filters -->
|
|
<details class="border rounded mb-2">
|
|
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
|
|
<i class="fa-solid fa-dollar-sign"></i> Amount Filters
|
|
</summary>
|
|
<div class="px-4 py-2">
|
|
<div class="mb-4">
|
|
<label for="amountField" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Amount Field</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" 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="flex flex-wrap -mx-2">
|
|
<div class="w-1/2 px-2">
|
|
<label for="amountMin" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Minimum</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 px-1 bg-neutral-200 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 rounded-l-md">
|
|
$
|
|
</span>
|
|
<input type="number" class="w-full pl-10 pr-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="amountMin" step="0.01" min="0">
|
|
</div>
|
|
</div>
|
|
<div class="w-1/2 px-2">
|
|
<label for="amountMax" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Maximum</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 px-1 bg-neutral-200 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 rounded-l-md">
|
|
$
|
|
</span>
|
|
<input type="number" class="w-full pl-10 pr-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="amountMax" step="0.01" min="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Category Filters -->
|
|
<details class="border rounded mb-2">
|
|
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
|
|
<i class="fa-solid fa-tags"></i> Category Filters
|
|
</summary>
|
|
<div class="px-4 py-2">
|
|
<div class="mb-4">
|
|
<label for="fileTypes" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">File Types</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="fileTypes" multiple>
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="fileStatuses" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">File Statuses</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="fileStatuses" multiple>
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="employees" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Employees</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="employees" multiple>
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="transactionTypes" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Transaction Types</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="transactionTypes" multiple>
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="states" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">States</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" id="states" multiple>
|
|
<!-- Options loaded dynamically -->
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Boolean Filters -->
|
|
<details class="border rounded mb-2">
|
|
<summary class="px-4 py-2 cursor-pointer flex items-center gap-2 font-medium">
|
|
<i class="fa-solid fa-sliders"></i> Additional Filters
|
|
</summary>
|
|
<div class="px-4 py-2">
|
|
<div class="flex items-center mb-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="activeOnly" checked>
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="activeOnly">Active records only</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="hasBalance">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="hasBalance">Has outstanding balance</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500" type="checkbox" id="isBilled">
|
|
<label class="ml-2 text-sm text-neutral-700 dark:text-neutral-300" for="isBilled">Billed items only</label>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<!-- Search Actions -->
|
|
<div class="mt-4 grid gap-2">
|
|
<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">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Panel -->
|
|
<div class="lg:col-span-2">
|
|
<!-- Search Status Bar -->
|
|
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md mb-3">
|
|
<div class="p-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
|
<div>
|
|
<div id="searchStatus">
|
|
<span class="text-neutral-500">Enter search terms to begin</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="grid grid-cols-2 gap-3 items-center">
|
|
<div>
|
|
<label for="sortBy" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Sort by:</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 transition-all duration-200" 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>
|
|
<label for="sortOrder" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Order:</label>
|
|
<select class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 transition-all duration-200" id="sortOrder">
|
|
<option value="desc">Descending</option>
|
|
<option value="asc">Ascending</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Facets/Filters Summary -->
|
|
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md mb-3" id="facetsCard" style="display: none;">
|
|
<div class="p-4">
|
|
<h6 class="font-semibold mb-2">Filter Results</h6>
|
|
<div id="facetsContainer">
|
|
<!-- Facets will be displayed here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Results -->
|
|
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md">
|
|
<div class="p-4">
|
|
<div id="searchResults">
|
|
<div class="text-center text-neutral-500 p-5">
|
|
<i class="fa-solid fa-magnifying-glass text-6xl"></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-neutral-500">
|
|
<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="inline-block w-6 h-6 border-2 border-neutral-300 border-t-transparent rounded-full animate-spin"></div>
|
|
<div class="mt-2">Searching database...</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<nav id="searchPagination" aria-label="Search results pagination" class="flex items-center justify-center" style="display: none;"></nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save Search Modal -->
|
|
<div id="saveSearchModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-md w-full">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold" id="saveSearchModalLabel">Save Search</h5>
|
|
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeSaveSearchModal()"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<form id="saveSearchForm">
|
|
<div class="mb-3">
|
|
<label for="searchName" class="block text-sm font-medium mb-1">Search Name *</label>
|
|
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="searchName" required placeholder="Enter a name for this search">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="searchDescription" class="block text-sm font-medium mb-1">Description</label>
|
|
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="searchDescription" rows="3" placeholder="Optional description of this search"></textarea>
|
|
</div>
|
|
<label class="flex items-center gap-2">
|
|
<input class="h-4 w-4 text-primary-600 border-neutral-300 rounded" type="checkbox" id="isPublicSearch">
|
|
<span class="text-sm">Make this search public (visible to all users)</span>
|
|
</label>
|
|
</form>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
|
|
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeSaveSearchModal()">Cancel</button>
|
|
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="confirmSaveSearch" onclick="saveCurrentSearch()"><i class="fa-solid fa-bookmark"></i> Save Search</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Saved Searches Modal -->
|
|
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="savedSearchesModal">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold" id="savedSearchesModalLabel">Saved Searches</h5>
|
|
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('savedSearchesModal')"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
|
|
<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="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
|
|
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('savedSearchesModal')">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Statistics Modal -->
|
|
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="searchStatsModal">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h5 class="text-lg font-semibold" id="searchStatsModalLabel">Search Statistics</h5>
|
|
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('searchStatsModal')"><i class="fa-solid fa-xmark"></i></button>
|
|
</div>
|
|
<div class="px-6 py-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="searchStatsContent">
|
|
<!-- Statistics will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
|
|
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('searchStatsModal')">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Advanced Search JavaScript
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize search components
|
|
initializeAdvancedSearch();
|
|
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 initializeAdvancedSearch() {
|
|
// 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) {
|
|
const item = e.target.closest('a');
|
|
if (item && suggestionsDropdown.contains(item)) {
|
|
e.preventDefault();
|
|
document.getElementById('searchQuery').value = item.textContent.trim();
|
|
suggestionsDropdown.classList.add('hidden');
|
|
performSearch();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Use shared highlight utilities
|
|
|
|
async function loadSearchFacets() {
|
|
try {
|
|
const response = await window.http.wrappedFetch('/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.add('hidden');
|
|
}
|
|
});
|
|
|
|
// 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) {
|
|
showSaveSearchModal();
|
|
} 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 window.http.wrappedFetch(`/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.add('hidden');
|
|
return;
|
|
}
|
|
|
|
suggestions.forEach(suggestion => {
|
|
const item = document.createElement('a');
|
|
item.className = 'block px-3 py-2 hover:bg-neutral-50';
|
|
item.href = '#';
|
|
item.innerHTML = `
|
|
<div class="flex justify-between">
|
|
<span>${suggestion.text}</span>
|
|
<small class="text-neutral-500">${suggestion.category}</small>
|
|
</div>
|
|
${suggestion.description ? `<small class="text-neutral-500">${suggestion.description}</small>` : ''}
|
|
`;
|
|
dropdown.appendChild(item);
|
|
});
|
|
|
|
dropdown.classList.remove('hidden');
|
|
}
|
|
|
|
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 window.http.wrappedFetch('/api/search/advanced', {
|
|
method: 'POST',
|
|
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');
|
|
const tokens = (window.highlightUtils && typeof window.highlightUtils.buildTokens === 'function')
|
|
? window.highlightUtils.buildTokens(currentSearchCriteria.query || '')
|
|
: [];
|
|
|
|
// Update status
|
|
const executionTime = data.stats?.search_execution_time || 0;
|
|
statusElement.innerHTML = `
|
|
<strong>${data.total_results}</strong> results found
|
|
<small class="text-neutral-500">(${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-neutral-500 p-5">
|
|
<i class="fa-solid fa-magnifying-glass text-6xl"></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);
|
|
const matchHtml = (window.highlightUtils && typeof window.highlightUtils.formatSnippet === 'function')
|
|
? window.highlightUtils.formatSnippet(result.highlight, tokens)
|
|
: (result.highlight || '');
|
|
|
|
resultsHTML += `
|
|
<div class="search-result-item border-b py-3">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0 mr-3">
|
|
<i class="${typeIcon} text-primary-600 text-xl"></i>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex justify-between items-start mb-1">
|
|
<h6 class="mb-1">
|
|
<a href="${result.url}" class="hover:underline">${window.highlightUtils ? window.highlightUtils.highlight(result.title || '', tokens) : (result.title || '')}</a>
|
|
${typeBadge}
|
|
</h6>
|
|
<div class="text-right">
|
|
${result.relevance_score ? `<small class="text-neutral-500">Score: ${result.relevance_score.toFixed(1)}</small>` : ''}
|
|
${result.updated_at ? `<br><small class="text-neutral-500">${formatDate(result.updated_at)}</small>` : ''}
|
|
</div>
|
|
</div>
|
|
<p class="mb-1 text-neutral-500">${window.highlightUtils ? window.highlightUtils.highlight(result.description || '', tokens) : (result.description || '')}</p>
|
|
${matchHtml ? `<div class="text-sm text-info-600"><strong>Match:</strong> ${matchHtml}</div>` : ''}
|
|
${displayResultMetadata(result.metadata, tokens)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
if (window.setSafeHTML) {
|
|
window.setSafeHTML(resultsContainer, resultsHTML);
|
|
} else {
|
|
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="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-1">${value} (${count})</span>`
|
|
).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
container.innerHTML = facetsHTML;
|
|
}
|
|
|
|
function displayPagination(pageInfo) {
|
|
const paginationContainer = document.getElementById('searchPagination');
|
|
paginationContainer.innerHTML = '';
|
|
|
|
// Previous button
|
|
const prevBtn = document.createElement('button');
|
|
prevBtn.className = `mx-1 px-3 py-1 rounded border ${pageInfo.has_previous ? 'border-neutral-300 hover:bg-neutral-100' : 'border-neutral-200 text-neutral-400 cursor-not-allowed'}`;
|
|
prevBtn.textContent = 'Previous';
|
|
prevBtn.dataset.page = pageInfo.current_page - 1;
|
|
prevBtn.disabled = !pageInfo.has_previous;
|
|
paginationContainer.appendChild(prevBtn);
|
|
|
|
// 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 pageBtn = document.createElement('button');
|
|
pageBtn.className = `mx-1 px-3 py-1 rounded border ${page === pageInfo.current_page ? 'bg-primary-600 text-white border-primary-600' : 'border-neutral-300 hover:bg-neutral-100'}`;
|
|
pageBtn.textContent = page;
|
|
pageBtn.dataset.page = page;
|
|
paginationContainer.appendChild(pageBtn);
|
|
}
|
|
|
|
// Next button
|
|
const nextBtn = document.createElement('button');
|
|
nextBtn.className = `mx-1 px-3 py-1 rounded border ${pageInfo.has_next ? 'border-neutral-300 hover:bg-neutral-100' : 'border-neutral-200 text-neutral-400 cursor-not-allowed'}`;
|
|
nextBtn.textContent = 'Next';
|
|
nextBtn.dataset.page = pageInfo.current_page + 1;
|
|
nextBtn.disabled = !pageInfo.has_next;
|
|
paginationContainer.appendChild(nextBtn);
|
|
|
|
// Add click handlers
|
|
paginationContainer.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (e.target.tagName === 'BUTTON' && !e.target.disabled) {
|
|
const page = parseInt(e.target.dataset.page);
|
|
const offset = (page - 1) * currentSearchCriteria.limit;
|
|
performSearch(offset);
|
|
}
|
|
});
|
|
|
|
paginationContainer.style.display = 'flex';
|
|
}
|
|
|
|
function getTypeIcon(type) {
|
|
const icons = {
|
|
'customer': 'fa-solid fa-user',
|
|
'file': 'fa-solid fa-folder',
|
|
'ledger': 'fa-solid fa-calculator',
|
|
'qdro': 'fa-regular fa-file-lines',
|
|
'document': 'fa-regular fa-file-lines',
|
|
'template': 'fa-regular fa-file-lines',
|
|
'phone': 'fa-solid fa-phone'
|
|
};
|
|
return icons[type] || 'fa-regular fa-file';
|
|
}
|
|
|
|
function getTypeBadge(type) {
|
|
const badges = {
|
|
'customer': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-primary-100 text-primary-700 ml-2">Customer</span>',
|
|
'file': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-success-50 text-success-700 ml-2">File</span>',
|
|
'ledger': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-warning-50 text-warning-700 ml-2">Financial</span>',
|
|
'qdro': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-info-50 text-info-700 ml-2">QDRO</span>',
|
|
'document': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-2">Document</span>',
|
|
'template': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700 ml-2">Template</span>',
|
|
'phone': '<span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-800 text-neutral-100 ml-2">Phone</span>'
|
|
};
|
|
return badges[type] || '';
|
|
}
|
|
|
|
function displayResultMetadata(metadata, tokens) {
|
|
if (!metadata) return '';
|
|
|
|
let metadataHTML = '<div class="text-sm text-neutral-500 mt-1">';
|
|
|
|
Object.entries(metadata).forEach(([key, value]) => {
|
|
if (value && key !== 'phones') { // Skip complex objects
|
|
const label = (window.highlightUtils && window.highlightUtils.escape)
|
|
? window.highlightUtils.escape(String(key).replace('_', ' '))
|
|
: String(key).replace('_', ' ');
|
|
const valStr = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
? String(value)
|
|
: '';
|
|
const valueHtml = (window.highlightUtils && typeof window.highlightUtils.highlight === 'function')
|
|
? window.highlightUtils.highlight(valStr, Array.isArray(tokens) ? tokens : [])
|
|
: valStr;
|
|
metadataHTML += `<span class="mr-3"><strong>${label}:</strong> ${valueHtml}</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-neutral-500 p-5">
|
|
<i class="fa-solid fa-magnifying-glass text-6xl"></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-neutral-500">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
|
|
closeSaveSearchModal();
|
|
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-neutral-500">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="flex items-center gap-2">
|
|
<button class="px-2 py-1 border border-blue-600 text-blue-600 rounded hover:bg-blue-100" onclick="loadSavedSearch(${search.id})" title="Load Search"><i class="fa-solid fa-play"></i></button>
|
|
<button class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100" onclick="deleteSavedSearch(${search.id})" title="Delete Search"><i class="fa-solid fa-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
tableBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
openModal('savedSearchesModal');
|
|
|
|
} 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
|
|
closeModal('savedSearchesModal');
|
|
|
|
// 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') {
|
|
if (window.alerts && typeof window.alerts.show === 'function') {
|
|
window.alerts.show(message, type);
|
|
} else if (window.showNotification) {
|
|
window.showNotification(message, type);
|
|
} else {
|
|
alert(String(message));
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
function showSaveSearchModal() {
|
|
document.getElementById('saveSearchModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeSaveSearchModal() {
|
|
document.getElementById('saveSearchModal').classList.add('hidden');
|
|
}
|
|
</script>
|
|
{% endblock %} |