fix: CSS nesting conflict, dashboard.js dead code removal, admin view escaping

CSS:
- ticket.css: use combined .comment.thread-depth-N selectors to resolve the
  margin-left conflict between .comment-reply and .thread-depth-N classes

dashboard.js:
- Remove legacy initStatusFilter() (superseded by TDS v1.2 sidebar filters)
- Remove initTableSorting() call (client-side sort conflicts with server ?sort=)
- Remove quickSave() + saveTicket() (old hamburger-menu ticket page functions)
- Remove global loadTemplate() (duplicate of IIFE-scoped version in CreateTicketView)
- Remove generateSkeletonRows/Comments/Stats helpers (never called, used
  unregistered CSS class names like .skeleton-row-tr)
- Remove "force dark mode" lines that overrode the user theme preference
- Fix non-TDS CSS classes in modal templates: text-center → style, text-green →
  lt-text-cyan, mb-half → lt-mb-xs, modal-warning-text → lt-text-danger

Admin views:
- RecurringTicketsView: replace innerHTML += loop with createElement/appendChild
  (avoids serial DOM re-parsing on each iteration)
- AuditLogView: add htmlspecialchars() to action_type option values (consistency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 21:34:34 -04:00
parent 6b76496640
commit d8e6dcf7fa
4 changed files with 22 additions and 314 deletions
+7 -2
View File
@@ -85,11 +85,16 @@ body.edit-mode .editable-metadata {
border: 1px solid rgba(0, 255, 65, 0.2);
}
.comment-reply {
/* depth-1 default; .thread-depth-N overrides for deeper nesting */
margin-left: 1.5rem;
border-color: rgba(0, 255, 65, 0.12);
}
.thread-depth-2 { margin-left: 3rem; }
.thread-depth-3 { margin-left: 4.5rem; }
/* Explicit depth overrides using combined selectors — higher specificity prevents
cascade order from determining winner when both classes are present */
.comment.thread-depth-2,
.comment-reply.thread-depth-2 { margin-left: 3rem; }
.comment.thread-depth-3,
.comment-reply.thread-depth-3 { margin-left: 4.5rem; }
.thread-line {
position: absolute;
+6 -309
View File
@@ -99,18 +99,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (isDashboard) {
// Dashboard-specific initialization
initStatusFilter();
initTableSorting();
initSidebarFilters();
}
// Initialize for all pages
initSettingsModal();
// Force dark mode only (terminal aesthetic - no theme switching)
document.documentElement.setAttribute('data-theme', 'dark');
document.body.classList.add('dark-mode');
// Event delegation for dynamically created modals
document.addEventListener('click', function(e) {
const target = e.target.closest('[data-action]');
@@ -451,233 +445,6 @@ function sortTable(table, column) {
// Old settings modal functions removed - now using settings.js with new settings modal
function initStatusFilter() {
const filterContainer = document.createElement('div');
filterContainer.className = 'status-filter-container';
const dropdown = document.createElement('div');
dropdown.className = 'status-dropdown';
const dropdownHeader = document.createElement('div');
dropdownHeader.className = 'dropdown-header';
dropdownHeader.textContent = 'Status Filter';
const dropdownContent = document.createElement('div');
dropdownContent.className = 'dropdown-content';
const statuses = ['Open', 'In Progress', 'Closed'];
statuses.forEach(status => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = status;
checkbox.id = `status-${status.toLowerCase().replace(/\s+/g, '-')}`;
const urlParams = new URLSearchParams(window.location.search);
const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : [];
const showAll = urlParams.get('show_all');
// FIXED LOGIC: Determine checkbox state
if (showAll === '1') {
// If show_all=1 parameter exists, all should be checked
checkbox.checked = true;
} else if (currentStatuses.length === 0) {
// No status parameter - default: Open and In Progress checked, Closed unchecked
checkbox.checked = status !== 'Closed';
} else {
// Status parameter exists - check if this status is in the list
checkbox.checked = currentStatuses.includes(status);
}
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + status));
dropdownContent.appendChild(label);
});
const saveButton = document.createElement('button');
saveButton.className = 'btn save-filter';
saveButton.textContent = 'Apply Filter';
saveButton.onclick = () => {
const checkedBoxes = dropdownContent.querySelectorAll('input:checked');
const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value);
const params = new URLSearchParams(window.location.search);
if (selectedStatuses.length === 0) {
// No statuses selected - show default (Open + In Progress)
params.delete('status');
params.delete('show_all');
} else if (selectedStatuses.length === 3) {
// All statuses selected - show all tickets
params.delete('status');
params.set('show_all', '1');
} else {
// Some statuses selected - set the parameter
params.set('status', selectedStatuses.join(','));
params.delete('show_all');
}
params.set('page', '1');
window.location.search = params.toString();
dropdown.classList.remove('active');
};
dropdownHeader.onclick = () => {
dropdown.classList.toggle('active');
};
dropdown.appendChild(dropdownHeader);
dropdown.appendChild(dropdownContent);
dropdownContent.appendChild(saveButton);
filterContainer.appendChild(dropdown);
const tableActions = document.querySelector('.table-controls .table-actions');
if (tableActions) {
tableActions.prepend(filterContainer);
}
}
function quickSave() {
if (!window.ticketData) {
return;
}
const statusSelect = document.getElementById('status-select');
const prioritySelect = document.getElementById('priority-select');
if (!statusSelect || !prioritySelect) {
return;
}
const data = {
ticket_id: parseInt(window.ticketData.id),
status: statusSelect.value,
priority: parseInt(prioritySelect.value)
};
lt.api.post('/api/update_ticket.php', data)
.then(result => {
if (result.success) {
// Update the hamburger menu display
const hamburgerStatus = document.getElementById('hamburger-status');
const hamburgerPriority = document.getElementById('hamburger-priority');
if (hamburgerStatus) hamburgerStatus.textContent = statusSelect.value;
if (hamburgerPriority) hamburgerPriority.textContent = 'P' + prioritySelect.value;
// Update window.ticketData
window.ticketData.status = statusSelect.value;
window.ticketData.priority = parseInt(prioritySelect.value);
// Update main page elements if they exist
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
statusDisplay.className = `status-${statusSelect.value}`;
statusDisplay.textContent = statusSelect.value;
}
// Close hamburger menu after successful save
const hamburgerContent = document.querySelector('.hamburger-content');
if (hamburgerContent) {
hamburgerContent.classList.remove('open');
document.body.classList.remove('menu-open');
}
} else {
lt.toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
}
})
.catch(error => {
lt.toast.error('Error updating ticket: ' + error.message, 5000);
});
}
// Ticket page functions (if needed)
function saveTicket() {
const editables = document.querySelectorAll('.editable');
const data = {};
const ticketId = getTicketIdFromUrl();
editables.forEach(field => {
if (field.dataset.field) {
data[field.dataset.field] = field.value;
}
});
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data })
.then(data => {
if(data.success) {
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
}
} else {
lt.toast.error('Error saving ticket: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
lt.toast.error('Error saving ticket: ' + error.message);
});
}
/**
* Load template data into the create ticket form
*/
function loadTemplate() {
const templateSelect = document.getElementById('templateSelect');
if (!templateSelect) return;
const templateId = templateSelect.value;
if (!templateId) {
// Clear form when "No Template" is selected
document.getElementById('title').value = '';
document.getElementById('description').value = '';
document.getElementById('priority').value = '4';
document.getElementById('category').value = 'General';
document.getElementById('type').value = 'Issue';
const assignedToSelect = document.getElementById('assigned_to');
if (assignedToSelect) {
assignedToSelect.value = '';
}
return;
}
// Fetch template data
lt.api.get(`/api/get_template.php?template_id=${templateId}`)
.then(data => {
if (data.success && data.template) {
const template = data.template;
// Populate form fields with template data
if (template.title_template) {
document.getElementById('title').value = template.title_template;
}
if (template.description_template) {
document.getElementById('description').value = template.description_template;
}
if (template.category) {
document.getElementById('category').value = template.category;
}
if (template.type) {
document.getElementById('type').value = template.type;
}
if (template.default_priority) {
document.getElementById('priority').value = template.default_priority;
}
} else {
lt.toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
lt.toast.error('Error loading template: ' + error.message, 4000);
});
}
/**
* Bulk Actions Functions (Admin only)
@@ -1067,9 +834,9 @@ function showBulkDeleteModal() {
<span class="lt-modal-title" id="bulkDeleteModalTitle">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body text-center">
<p class="modal-warning-text">This action cannot be undone!</p>
<p class="text-green">You are about to permanently delete ${ticketIds.length} ticket(s).<br>All associated comments and history will be lost.</p>
<div class="lt-modal-body" style="text-align:center">
<p class="lt-text-danger lt-text-sm">This action cannot be undone!</p>
<p class="lt-text-cyan">You are about to permanently delete ${ticketIds.length} ticket(s).<br>All associated comments and history will be lost.</p>
</div>
<div class="lt-modal-footer">
<button data-action="perform-bulk-delete" class="lt-btn lt-btn-danger">DELETE PERMANENTLY</button>
@@ -1191,8 +958,8 @@ function quickStatusChange(ticketId, currentStatus) {
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body">
<p class="mb-half">Ticket #${lt.escHtml(ticketId)}</p>
<p class="text-amber mb-half">Current: ${lt.escHtml(currentStatus)}</p>
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
<p class="lt-text-amber lt-mb-xs">Current: ${lt.escHtml(currentStatus)}</p>
<label for="quickStatusSelect">New Status:</label>
<select id="quickStatusSelect" class="lt-select">
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
@@ -1247,7 +1014,7 @@ function quickAssign(ticketId) {
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div>
<div class="lt-modal-body">
<p class="mb-half">Ticket #${lt.escHtml(ticketId)}</p>
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
<label for="quickAssignSelect">Assign to:</label>
<select id="quickAssignSelect" class="lt-select">
<option value="">Unassigned</option>
@@ -1561,74 +1328,6 @@ function exportSelectedTickets(format) {
if (dropdown) dropdown.classList.remove('open');
}
// ========================================
// Skeleton Loading Helpers
// ========================================
/**
* Generate skeleton table rows
*/
function generateSkeletonRows(count = 5) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<tr class="skeleton-row-tr">
<td><div class="skeleton skeleton-text" style="width: 80px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 40px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: ${70 + Math.random() * 30}%;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 60px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 50px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 70px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 80px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 80px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 70px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 70px;"></div></td>
<td><div class="skeleton skeleton-text" style="width: 60px;"></div></td>
</tr>
`;
}
return html;
}
/**
* Generate skeleton comments
*/
function generateSkeletonComments(count = 3) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="skeleton-comment">
<div class="skeleton-comment-header">
<div class="skeleton skeleton-avatar"></div>
<div class="skeleton-comment-meta">
<div class="skeleton skeleton-text short"></div>
<div class="skeleton skeleton-text" style="width: 100px;"></div>
</div>
</div>
<div class="skeleton skeleton-text long"></div>
<div class="skeleton skeleton-text medium"></div>
<div class="skeleton skeleton-text short"></div>
</div>
`;
}
return html;
}
/**
* Generate skeleton stat cards
*/
function generateSkeletonStats(count = 4) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="skeleton-stat skeleton">
<div class="skeleton skeleton-value"></div>
<div class="skeleton skeleton-label"></div>
</div>
`;
}
return html;
}
/**
* Show loading overlay on element
@@ -1706,7 +1405,5 @@ setInterval(initRelativeTimes, 60000);
// Export for use in other scripts
window.generateSkeletonRows = generateSkeletonRows;
window.generateSkeletonComments = generateSkeletonComments;
window.showLoadingOverlay = showLoadingOverlay;
window.hideLoadingOverlay = hideLoadingOverlay;