Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8e6dcf7fa | |||
| 6b76496640 |
@@ -85,11 +85,16 @@ body.edit-mode .editable-metadata {
|
|||||||
border: 1px solid rgba(0, 255, 65, 0.2);
|
border: 1px solid rgba(0, 255, 65, 0.2);
|
||||||
}
|
}
|
||||||
.comment-reply {
|
.comment-reply {
|
||||||
|
/* depth-1 default; .thread-depth-N overrides for deeper nesting */
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
border-color: rgba(0, 255, 65, 0.12);
|
border-color: rgba(0, 255, 65, 0.12);
|
||||||
}
|
}
|
||||||
.thread-depth-2 { margin-left: 3rem; }
|
/* Explicit depth overrides using combined selectors — higher specificity prevents
|
||||||
.thread-depth-3 { margin-left: 4.5rem; }
|
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 {
|
.thread-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
+6
-309
@@ -99,18 +99,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (isDashboard) {
|
if (isDashboard) {
|
||||||
// Dashboard-specific initialization
|
// Dashboard-specific initialization
|
||||||
initStatusFilter();
|
|
||||||
initTableSorting();
|
|
||||||
initSidebarFilters();
|
initSidebarFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize for all pages
|
// Initialize for all pages
|
||||||
initSettingsModal();
|
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
|
// Event delegation for dynamically created modals
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
const target = e.target.closest('[data-action]');
|
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
|
// 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)
|
* Bulk Actions Functions (Admin only)
|
||||||
@@ -1067,9 +834,9 @@ function showBulkDeleteModal() {
|
|||||||
<span class="lt-modal-title" id="bulkDeleteModalTitle">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
|
<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>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body text-center">
|
<div class="lt-modal-body" style="text-align:center">
|
||||||
<p class="modal-warning-text">This action cannot be undone!</p>
|
<p class="lt-text-danger lt-text-sm">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>
|
<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>
|
||||||
<div class="lt-modal-footer">
|
<div class="lt-modal-footer">
|
||||||
<button data-action="perform-bulk-delete" class="lt-btn lt-btn-danger">DELETE PERMANENTLY</button>
|
<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>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p class="mb-half">Ticket #${lt.escHtml(ticketId)}</p>
|
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
|
||||||
<p class="text-amber mb-half">Current: ${lt.escHtml(currentStatus)}</p>
|
<p class="lt-text-amber lt-mb-xs">Current: ${lt.escHtml(currentStatus)}</p>
|
||||||
<label for="quickStatusSelect">New Status:</label>
|
<label for="quickStatusSelect">New Status:</label>
|
||||||
<select id="quickStatusSelect" class="lt-select">
|
<select id="quickStatusSelect" class="lt-select">
|
||||||
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
${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>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<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>
|
<label for="quickAssignSelect">Assign to:</label>
|
||||||
<select id="quickAssignSelect" class="lt-select">
|
<select id="quickAssignSelect" class="lt-select">
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
@@ -1561,74 +1328,6 @@ function exportSelectedTickets(format) {
|
|||||||
if (dropdown) dropdown.classList.remove('open');
|
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
|
* Show loading overlay on element
|
||||||
@@ -1706,7 +1405,5 @@ setInterval(initRelativeTimes, 60000);
|
|||||||
|
|
||||||
|
|
||||||
// Export for use in other scripts
|
// Export for use in other scripts
|
||||||
window.generateSkeletonRows = generateSkeletonRows;
|
|
||||||
window.generateSkeletonComments = generateSkeletonComments;
|
|
||||||
window.showLoadingOverlay = showLoadingOverlay;
|
window.showLoadingOverlay = showLoadingOverlay;
|
||||||
window.hideLoadingOverlay = hideLoadingOverlay;
|
window.hideLoadingOverlay = hideLoadingOverlay;
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ class TicketController {
|
|||||||
|
|
||||||
// Check if form was submitted
|
// Check if form was submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// Validate CSRF token
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
$csrfToken = $_POST['csrf_token'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
$error = "Invalid or expired security token. Please try again.";
|
||||||
|
$templates = $this->templateModel->getAllTemplates();
|
||||||
|
$allUsers = $this->userModel->getAllUsers();
|
||||||
|
$conn = $this->conn;
|
||||||
|
include dirname(__DIR__) . '/views/CreateTicketView.php';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle visibility groups (comes as array from checkboxes)
|
// Handle visibility groups (comes as array from checkboxes)
|
||||||
$visibilityGroups = null;
|
$visibilityGroups = null;
|
||||||
if (isset($_POST['visibility_groups']) && is_array($_POST['visibility_groups'])) {
|
if (isset($_POST['visibility_groups']) && is_array($_POST['visibility_groups'])) {
|
||||||
|
|||||||
@@ -242,7 +242,9 @@ switch (true) {
|
|||||||
$params = [];
|
$params = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
|
|
||||||
if (!empty($_GET['action_type'])) {
|
$allowedActionTypes = ['create','update','delete','comment','assign','status_change','login','security',
|
||||||
|
'ticket_create','ticket_update','ticket_delete','attachment_delete','attachment_upload'];
|
||||||
|
if (!empty($_GET['action_type']) && in_array($_GET['action_type'], $allowedActionTypes, true)) {
|
||||||
$whereConditions[] = "al.action_type = ?";
|
$whereConditions[] = "al.action_type = ?";
|
||||||
$params[] = $_GET['action_type'];
|
$params[] = $_GET['action_type'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
@@ -252,15 +254,15 @@ switch (true) {
|
|||||||
$whereConditions[] = "al.user_id = ?";
|
$whereConditions[] = "al.user_id = ?";
|
||||||
$params[] = (int)$_GET['user_id'];
|
$params[] = (int)$_GET['user_id'];
|
||||||
$types .= 'i';
|
$types .= 'i';
|
||||||
$filters['user_id'] = $_GET['user_id'];
|
$filters['user_id'] = (int)$_GET['user_id'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['date_from'])) {
|
if (!empty($_GET['date_from']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_from'])) {
|
||||||
$whereConditions[] = "DATE(al.created_at) >= ?";
|
$whereConditions[] = "DATE(al.created_at) >= ?";
|
||||||
$params[] = $_GET['date_from'];
|
$params[] = $_GET['date_from'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
$filters['date_from'] = $_GET['date_from'];
|
$filters['date_from'] = $_GET['date_from'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['date_to'])) {
|
if (!empty($_GET['date_to']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_to'])) {
|
||||||
$whereConditions[] = "DATE(al.created_at) <= ?";
|
$whereConditions[] = "DATE(al.created_at) <= ?";
|
||||||
$params[] = $_GET['date_to'];
|
$params[] = $_GET['date_to'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ include __DIR__ . '/layout_header.php';
|
|||||||
class="create-ticket-form"
|
class="create-ticket-form"
|
||||||
novalidate>
|
novalidate>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token"
|
||||||
|
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
|
||||||
<?php if (isset($error)): ?>
|
<?php if (isset($error)): ?>
|
||||||
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
||||||
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||||
@@ -255,26 +258,41 @@ include __DIR__ . '/layout_header.php';
|
|||||||
});
|
});
|
||||||
|
|
||||||
function checkDuplicates(title) {
|
function checkDuplicates(title) {
|
||||||
|
if (!window.lt || typeof lt.api === 'undefined') return;
|
||||||
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
var warn = document.getElementById('duplicateWarning');
|
var warn = document.getElementById('duplicateWarning');
|
||||||
var list = document.getElementById('duplicatesList');
|
var list = document.getElementById('duplicatesList');
|
||||||
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
||||||
var html = '<ul class="duplicate-list lt-text-sm">';
|
var ul = document.createElement('ul');
|
||||||
|
ul.className = 'duplicate-list lt-text-sm';
|
||||||
data.duplicates.forEach(function (dup) {
|
data.duplicates.forEach(function (dup) {
|
||||||
html += '<li><a href="/ticket/' + lt.escHtml(dup.ticket_id) + '" target="_blank">#' +
|
var li = document.createElement('li');
|
||||||
lt.escHtml(dup.ticket_id) + '</a> — ' + lt.escHtml(dup.title) +
|
var a = document.createElement('a');
|
||||||
' <span class="lt-text-muted">(' + dup.similarity + '% match, ' +
|
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
|
||||||
lt.escHtml(dup.status) + ')</span></li>';
|
a.target = '_blank';
|
||||||
|
a.textContent = '#' + dup.ticket_id;
|
||||||
|
var dash = document.createTextNode(' \u2014 ' + dup.title + ' ');
|
||||||
|
var badge = document.createElement('span');
|
||||||
|
badge.className = 'lt-text-muted';
|
||||||
|
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
|
||||||
|
li.appendChild(a);
|
||||||
|
li.appendChild(dash);
|
||||||
|
li.appendChild(badge);
|
||||||
|
ul.appendChild(li);
|
||||||
});
|
});
|
||||||
html += '</ul><p class="lt-text-xs lt-text-muted lt-mt-sm">Check these before creating a new ticket.</p>';
|
var hint = document.createElement('p');
|
||||||
list.innerHTML = html;
|
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
|
||||||
|
hint.textContent = 'Check these before creating a new ticket.';
|
||||||
|
list.innerHTML = '';
|
||||||
|
list.appendChild(ul);
|
||||||
|
list.appendChild(hint);
|
||||||
warn.classList.remove('is-hidden');
|
warn.classList.remove('is-hidden');
|
||||||
} else {
|
} else {
|
||||||
warn.classList.add('is-hidden');
|
warn.classList.add('is-hidden');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () { /* silent */ });
|
.catch(function () { /* silent — duplicate check is non-critical */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Visibility groups toggle ──────────────────────────────
|
// ── Visibility groups toggle ──────────────────────────────
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
||||||
<option value="">All Actions</option>
|
<option value="">All Actions</option>
|
||||||
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a): ?>
|
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a): ?>
|
||||||
<option value="<?= $a ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= ucfirst(str_replace('_',' ',$a)) ?></option>
|
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8') ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $a)), ENT_QUOTES, 'UTF-8') ?></option>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -204,12 +204,18 @@ function updateScheduleOptions() {
|
|||||||
} else if (type === 'weekly') {
|
} else if (type === 'weekly') {
|
||||||
dayRow.classList.remove('is-hidden');
|
dayRow.classList.remove('is-hidden');
|
||||||
['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'].forEach(function (day, i) {
|
['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'].forEach(function (day, i) {
|
||||||
daySelect.innerHTML += '<option value="' + (i + 1) + '">' + day + '</option>';
|
var opt = document.createElement('option');
|
||||||
|
opt.value = String(i + 1);
|
||||||
|
opt.textContent = day;
|
||||||
|
daySelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
} else if (type === 'monthly') {
|
} else if (type === 'monthly') {
|
||||||
dayRow.classList.remove('is-hidden');
|
dayRow.classList.remove('is-hidden');
|
||||||
for (var i = 1; i <= 28; i++) {
|
for (var i = 1; i <= 28; i++) {
|
||||||
daySelect.innerHTML += '<option value="' + i + '">Day ' + i + '</option>';
|
var opt = document.createElement('option');
|
||||||
|
opt.value = String(i);
|
||||||
|
opt.textContent = 'Day ' + i;
|
||||||
|
daySelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user