Security fixes: - Add HTTP method validation to delete_comment.php (block CSRF via GET) - Remove $_GET fallback in comment deletion (was CSRF bypass vector) - Guard session_start() with session_status() check across API files - Escape json_encode() data attributes with htmlspecialchars in views - Escape inline APP_TIMEZONE config values in DashboardView/TicketView - Validate timezone param against DateTimeZone::listIdentifiers() in index.php - Remove Database::escape() (was using real_escape_string, not safe) - Fix AttachmentModel hardcoded connection; inject via constructor Backend fixes: - Fix CommentModel bind_param type for ticket_id (s→i) - Fix buildCommentThread orphan parent guard - Fix StatsModel JOIN→LEFT JOIN so unassigned tickets aren't excluded - Add ticket ID validation in BulkOperationsModel before implode() - Add duplicate key retry in TicketModel::createTicket() for race conditions - Wrap SavedFiltersModel default filter changes in transactions - Add null result guards in WorkflowModel query methods Frontend JS: - Rewrite toast.js as lt.toast shim (base.js dependency) - Delegate escapeHtml() to lt.escHtml() - Rewrite keyboard-shortcuts.js using lt.keys.on() - Migrate settings.js to lt.api.* and lt.modal.open/close() - Migrate advanced-search.js to lt.api.* and lt.modal.open/close() - Migrate dashboard.js fetch calls to lt.api.*; update all dynamic modals (bulk ops, quick actions, confirm/input) to lt-modal structure - Migrate ticket.js fetchMentionUsers to lt.api.get() - Remove console.log/error/warn calls from JS files Views: - Add /web_template/base.css and base.js to all 10 view files - Call lt.keys.initDefaults() in DashboardView, TicketView, admin views - Migrate all modal HTML from settings-modal/settings-content to lt-modal-overlay/lt-modal/lt-modal-header/lt-modal-body/lt-modal-footer - Replace style="display:none" with aria-hidden="true" on all modals - Replace modal open/close style.display with lt.modal.open/close() - Update modal buttons to lt-btn lt-btn-primary/lt-btn-ghost classes - Remove manual ESC keydown handlers (replaced by lt.keys.initDefaults) - Fix unescaped timezone values in TicketView inline script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
/**
|
|
* 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) {
|
|
lt.modal.open('advancedSearchModal');
|
|
loadUsersForSearch();
|
|
populateCurrentFilters();
|
|
loadSavedFilters();
|
|
}
|
|
}
|
|
|
|
// Close advanced search modal
|
|
function closeAdvancedSearch() {
|
|
lt.modal.close('advancedSearchModal');
|
|
}
|
|
|
|
// 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 data = await lt.api.get('/api/get_users.php');
|
|
|
|
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) {
|
|
lt.toast.error('Error loading users');
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
showInputModal(
|
|
'Save Search Filter',
|
|
'Enter a name for this filter:',
|
|
'My Filter',
|
|
async (filterName) => {
|
|
if (!filterName || filterName.trim() === '') {
|
|
toast.warning('Filter name cannot be empty', 2000);
|
|
return;
|
|
}
|
|
|
|
const filterCriteria = getCurrentFilterCriteria();
|
|
|
|
try {
|
|
await lt.api.post('/api/saved_filters.php', {
|
|
filter_name: filterName.trim(),
|
|
filter_criteria: filterCriteria
|
|
});
|
|
lt.toast.success(`Filter "${filterName}" saved successfully!`, 3000);
|
|
loadSavedFilters();
|
|
} catch (error) {
|
|
lt.toast.error('Error saving filter: ' + (error.message || 'Unknown error'), 4000);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// 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 data = await lt.api.get('/api/saved_filters.php');
|
|
if (data.success && data.filters) {
|
|
populateSavedFiltersDropdown(data.filters);
|
|
}
|
|
} catch (error) {
|
|
lt.toast.error('Error loading saved filters');
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
lt.toast.error('Error loading filter');
|
|
}
|
|
}
|
|
|
|
// 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 === '') {
|
|
lt.toast.error('Please select a filter to delete');
|
|
return;
|
|
}
|
|
|
|
const filterId = selectedOption.value;
|
|
const filterName = selectedOption.textContent;
|
|
|
|
showConfirmModal(
|
|
`Delete Filter "${filterName}"?`,
|
|
'This action cannot be undone.',
|
|
'error',
|
|
async () => {
|
|
try {
|
|
await lt.api.delete('/api/saved_filters.php', { filter_id: filterId });
|
|
lt.toast.success('Filter deleted successfully', 3000);
|
|
loadSavedFilters();
|
|
resetAdvancedSearch();
|
|
} catch (error) {
|
|
lt.toast.error('Error deleting filter', 4000);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Keyboard shortcut (Ctrl+Shift+F) — ESC is handled globally by lt.keys.initDefaults()
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
|
e.preventDefault();
|
|
openAdvancedSearch();
|
|
}
|
|
});
|