2026-01-09 11:20:27 -05:00
|
|
|
/**
|
|
|
|
|
* Advanced Search Functionality
|
|
|
|
|
* Handles complex search queries with date ranges, user filters, and multiple criteria
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Open advanced search modal
|
|
|
|
|
function openAdvancedSearch() {
|
|
|
|
|
const modal = document.getElementById('advancedSearchModal');
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.style.display = 'flex';
|
|
|
|
|
document.body.classList.add('modal-open');
|
|
|
|
|
loadUsersForSearch();
|
|
|
|
|
populateCurrentFilters();
|
|
|
|
|
loadSavedFilters();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close advanced search modal
|
|
|
|
|
function closeAdvancedSearch() {
|
|
|
|
|
const modal = document.getElementById('advancedSearchModal');
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.style.display = 'none';
|
|
|
|
|
document.body.classList.remove('modal-open');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close modal when clicking on backdrop
|
|
|
|
|
function closeOnAdvancedSearchBackdropClick(event) {
|
|
|
|
|
const modal = document.getElementById('advancedSearchModal');
|
|
|
|
|
if (event.target === modal) {
|
|
|
|
|
closeAdvancedSearch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load users for dropdown
|
|
|
|
|
async function loadUsersForSearch() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/get_users.php');
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success && data.users) {
|
|
|
|
|
const createdBySelect = document.getElementById('adv-created-by');
|
|
|
|
|
const assignedToSelect = document.getElementById('adv-assigned-to');
|
|
|
|
|
|
|
|
|
|
// Clear existing options (except first default option)
|
|
|
|
|
while (createdBySelect.options.length > 1) {
|
|
|
|
|
createdBySelect.remove(1);
|
|
|
|
|
}
|
|
|
|
|
while (assignedToSelect.options.length > 2) { // Keep "Any" and "Unassigned"
|
|
|
|
|
assignedToSelect.remove(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add users to both dropdowns
|
|
|
|
|
data.users.forEach(user => {
|
|
|
|
|
const displayName = user.display_name || user.username;
|
|
|
|
|
|
|
|
|
|
const option1 = document.createElement('option');
|
|
|
|
|
option1.value = user.user_id;
|
|
|
|
|
option1.textContent = displayName;
|
|
|
|
|
createdBySelect.appendChild(option1);
|
|
|
|
|
|
|
|
|
|
const option2 = document.createElement('option');
|
|
|
|
|
option2.value = user.user_id;
|
|
|
|
|
option2.textContent = displayName;
|
|
|
|
|
assignedToSelect.appendChild(option2);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading users:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate form with current URL parameters
|
|
|
|
|
function populateCurrentFilters() {
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
|
|
|
|
|
// Search text
|
|
|
|
|
if (urlParams.has('search')) {
|
|
|
|
|
document.getElementById('adv-search-text').value = urlParams.get('search');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status
|
|
|
|
|
if (urlParams.has('status')) {
|
|
|
|
|
const statuses = urlParams.get('status').split(',');
|
|
|
|
|
const statusSelect = document.getElementById('adv-status');
|
|
|
|
|
Array.from(statusSelect.options).forEach(option => {
|
|
|
|
|
option.selected = statuses.includes(option.value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Perform advanced search
|
|
|
|
|
function performAdvancedSearch(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
|
|
|
|
// Search text
|
|
|
|
|
const searchText = document.getElementById('adv-search-text').value.trim();
|
|
|
|
|
if (searchText) {
|
|
|
|
|
params.set('search', searchText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Date ranges
|
|
|
|
|
const createdFrom = document.getElementById('adv-created-from').value;
|
|
|
|
|
const createdTo = document.getElementById('adv-created-to').value;
|
|
|
|
|
const updatedFrom = document.getElementById('adv-updated-from').value;
|
|
|
|
|
const updatedTo = document.getElementById('adv-updated-to').value;
|
|
|
|
|
|
|
|
|
|
if (createdFrom) params.set('created_from', createdFrom);
|
|
|
|
|
if (createdTo) params.set('created_to', createdTo);
|
|
|
|
|
if (updatedFrom) params.set('updated_from', updatedFrom);
|
|
|
|
|
if (updatedTo) params.set('updated_to', updatedTo);
|
|
|
|
|
|
|
|
|
|
// Status (multi-select)
|
|
|
|
|
const statusSelect = document.getElementById('adv-status');
|
|
|
|
|
const selectedStatuses = Array.from(statusSelect.selectedOptions).map(opt => opt.value);
|
|
|
|
|
if (selectedStatuses.length > 0) {
|
|
|
|
|
params.set('status', selectedStatuses.join(','));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority range
|
|
|
|
|
const priorityMin = document.getElementById('adv-priority-min').value;
|
|
|
|
|
const priorityMax = document.getElementById('adv-priority-max').value;
|
|
|
|
|
if (priorityMin) params.set('priority_min', priorityMin);
|
|
|
|
|
if (priorityMax) params.set('priority_max', priorityMax);
|
|
|
|
|
|
|
|
|
|
// Users
|
|
|
|
|
const createdBy = document.getElementById('adv-created-by').value;
|
|
|
|
|
const assignedTo = document.getElementById('adv-assigned-to').value;
|
|
|
|
|
if (createdBy) params.set('created_by', createdBy);
|
|
|
|
|
if (assignedTo) params.set('assigned_to', assignedTo);
|
|
|
|
|
|
|
|
|
|
// Redirect to dashboard with params
|
|
|
|
|
window.location.href = '/?' + params.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset advanced search form
|
|
|
|
|
function resetAdvancedSearch() {
|
|
|
|
|
document.getElementById('advancedSearchForm').reset();
|
|
|
|
|
|
|
|
|
|
// Unselect all multi-select options
|
|
|
|
|
const statusSelect = document.getElementById('adv-status');
|
|
|
|
|
Array.from(statusSelect.options).forEach(option => {
|
|
|
|
|
option.selected = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save current search as a filter
|
|
|
|
|
async function saveCurrentFilter() {
|
|
|
|
|
const filterName = prompt('Enter a name for this filter:');
|
|
|
|
|
if (!filterName || filterName.trim() === '') return;
|
|
|
|
|
|
|
|
|
|
const filterCriteria = getCurrentFilterCriteria();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/saved_filters.php', {
|
|
|
|
|
method: 'POST',
|
2026-01-09 16:13:13 -05:00
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-Token': window.CSRF_TOKEN
|
|
|
|
|
},
|
2026-01-09 11:20:27 -05:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
filter_name: filterName.trim(),
|
|
|
|
|
filter_criteria: filterCriteria
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.success(`Filter "${filterName}" saved successfully!`);
|
|
|
|
|
}
|
|
|
|
|
loadSavedFilters();
|
|
|
|
|
} else {
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.error('Failed to save filter: ' + (result.error || 'Unknown error'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error saving filter:', error);
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.error('Error saving filter');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current filter criteria from form
|
|
|
|
|
function getCurrentFilterCriteria() {
|
|
|
|
|
const criteria = {};
|
|
|
|
|
|
|
|
|
|
const searchText = document.getElementById('adv-search-text').value.trim();
|
|
|
|
|
if (searchText) criteria.search = searchText;
|
|
|
|
|
|
|
|
|
|
const createdFrom = document.getElementById('adv-created-from').value;
|
|
|
|
|
if (createdFrom) criteria.created_from = createdFrom;
|
|
|
|
|
|
|
|
|
|
const createdTo = document.getElementById('adv-created-to').value;
|
|
|
|
|
if (createdTo) criteria.created_to = createdTo;
|
|
|
|
|
|
|
|
|
|
const updatedFrom = document.getElementById('adv-updated-from').value;
|
|
|
|
|
if (updatedFrom) criteria.updated_from = updatedFrom;
|
|
|
|
|
|
|
|
|
|
const updatedTo = document.getElementById('adv-updated-to').value;
|
|
|
|
|
if (updatedTo) criteria.updated_to = updatedTo;
|
|
|
|
|
|
|
|
|
|
const statusSelect = document.getElementById('adv-status');
|
|
|
|
|
const selectedStatuses = Array.from(statusSelect.selectedOptions).map(opt => opt.value);
|
|
|
|
|
if (selectedStatuses.length > 0) criteria.status = selectedStatuses.join(',');
|
|
|
|
|
|
|
|
|
|
const priorityMin = document.getElementById('adv-priority-min').value;
|
|
|
|
|
if (priorityMin) criteria.priority_min = priorityMin;
|
|
|
|
|
|
|
|
|
|
const priorityMax = document.getElementById('adv-priority-max').value;
|
|
|
|
|
if (priorityMax) criteria.priority_max = priorityMax;
|
|
|
|
|
|
|
|
|
|
const createdBy = document.getElementById('adv-created-by').value;
|
|
|
|
|
if (createdBy) criteria.created_by = createdBy;
|
|
|
|
|
|
|
|
|
|
const assignedTo = document.getElementById('adv-assigned-to').value;
|
|
|
|
|
if (assignedTo) criteria.assigned_to = assignedTo;
|
|
|
|
|
|
|
|
|
|
return criteria;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load saved filters
|
|
|
|
|
async function loadSavedFilters() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/saved_filters.php');
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success && data.filters) {
|
|
|
|
|
populateSavedFiltersDropdown(data.filters);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading saved filters:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate saved filters dropdown
|
|
|
|
|
function populateSavedFiltersDropdown(filters) {
|
|
|
|
|
const dropdown = document.getElementById('saved-filters-select');
|
|
|
|
|
if (!dropdown) return;
|
|
|
|
|
|
|
|
|
|
// Clear existing options except the first (placeholder)
|
|
|
|
|
while (dropdown.options.length > 1) {
|
|
|
|
|
dropdown.remove(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add saved filters
|
|
|
|
|
filters.forEach(filter => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = filter.filter_id;
|
|
|
|
|
option.textContent = filter.filter_name + (filter.is_default ? ' ⭐' : '');
|
|
|
|
|
option.dataset.criteria = JSON.stringify(filter.filter_criteria);
|
|
|
|
|
dropdown.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load a saved filter
|
|
|
|
|
function loadSavedFilter() {
|
|
|
|
|
const dropdown = document.getElementById('saved-filters-select');
|
|
|
|
|
const selectedOption = dropdown.options[dropdown.selectedIndex];
|
|
|
|
|
|
|
|
|
|
if (!selectedOption || !selectedOption.dataset.criteria) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const criteria = JSON.parse(selectedOption.dataset.criteria);
|
|
|
|
|
applySavedFilterCriteria(criteria);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading filter:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply saved filter criteria to form
|
|
|
|
|
function applySavedFilterCriteria(criteria) {
|
|
|
|
|
// Search text
|
|
|
|
|
document.getElementById('adv-search-text').value = criteria.search || '';
|
|
|
|
|
|
|
|
|
|
// Date ranges
|
|
|
|
|
document.getElementById('adv-created-from').value = criteria.created_from || '';
|
|
|
|
|
document.getElementById('adv-created-to').value = criteria.created_to || '';
|
|
|
|
|
document.getElementById('adv-updated-from').value = criteria.updated_from || '';
|
|
|
|
|
document.getElementById('adv-updated-to').value = criteria.updated_to || '';
|
|
|
|
|
|
|
|
|
|
// Status
|
|
|
|
|
const statusSelect = document.getElementById('adv-status');
|
|
|
|
|
const statuses = criteria.status ? criteria.status.split(',') : [];
|
|
|
|
|
Array.from(statusSelect.options).forEach(option => {
|
|
|
|
|
option.selected = statuses.includes(option.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Priority
|
|
|
|
|
document.getElementById('adv-priority-min').value = criteria.priority_min || '';
|
|
|
|
|
document.getElementById('adv-priority-max').value = criteria.priority_max || '';
|
|
|
|
|
|
|
|
|
|
// Users
|
|
|
|
|
document.getElementById('adv-created-by').value = criteria.created_by || '';
|
|
|
|
|
document.getElementById('adv-assigned-to').value = criteria.assigned_to || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete saved filter
|
|
|
|
|
async function deleteSavedFilter() {
|
|
|
|
|
const dropdown = document.getElementById('saved-filters-select');
|
|
|
|
|
const selectedOption = dropdown.options[dropdown.selectedIndex];
|
|
|
|
|
|
|
|
|
|
if (!selectedOption || selectedOption.value === '') {
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.error('Please select a filter to delete');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filterId = selectedOption.value;
|
|
|
|
|
const filterName = selectedOption.textContent;
|
|
|
|
|
|
|
|
|
|
if (!confirm(`Are you sure you want to delete the filter "${filterName}"?`)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/saved_filters.php', {
|
|
|
|
|
method: 'DELETE',
|
2026-01-09 16:13:13 -05:00
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-Token': window.CSRF_TOKEN
|
|
|
|
|
},
|
2026-01-09 11:20:27 -05:00
|
|
|
body: JSON.stringify({ filter_id: filterId })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.success('Filter deleted successfully');
|
|
|
|
|
}
|
|
|
|
|
loadSavedFilters();
|
|
|
|
|
resetAdvancedSearch();
|
|
|
|
|
} else {
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.error('Failed to delete filter');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting filter:', error);
|
|
|
|
|
if (typeof toast !== 'undefined') {
|
|
|
|
|
toast.error('Error deleting filter');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcut (Ctrl+Shift+F)
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
openAdvancedSearch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ESC to close
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
const modal = document.getElementById('advancedSearchModal');
|
|
|
|
|
if (modal && modal.style.display === 'flex') {
|
|
|
|
|
closeAdvancedSearch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|