fix: Resolve multiple UI and API bugs

- Remove is_active filter from get_users.php (column doesn't exist)
- Fix ticket ID validation regex in upload_attachment.php (9-digit format)
- Fix createSettingsModal reference to use openSettingsModal from settings.js
- Add error handling for dependencies tab to prevent infinite loading
- Add try-catch wrapper to ticket_dependencies.php API
- Make export dropdown visible only when tickets are selected
- Export only selected tickets instead of all filtered tickets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 15:16:14 -05:00
parent be505b7312
commit bc6a5cecf8
8 changed files with 124 additions and 19 deletions

View File

@@ -47,13 +47,31 @@ try {
$type = isset($_GET['type']) ? $_GET['type'] : null; $type = isset($_GET['type']) ? $_GET['type'] : null;
$search = isset($_GET['search']) ? trim($_GET['search']) : null; $search = isset($_GET['search']) ? trim($_GET['search']) : null;
$format = isset($_GET['format']) ? $_GET['format'] : 'csv'; $format = isset($_GET['format']) ? $_GET['format'] : 'csv';
$ticketIds = isset($_GET['ticket_ids']) ? $_GET['ticket_ids'] : null;
// Initialize model // Initialize model
$ticketModel = new TicketModel($conn); $ticketModel = new TicketModel($conn);
// Get all tickets (no pagination for export) // Check if specific ticket IDs are provided
if ($ticketIds) {
// Parse and validate ticket IDs
$ticketIdArray = array_filter(array_map('trim', explode(',', $ticketIds)));
if (empty($ticketIdArray)) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'No valid ticket IDs provided']);
exit;
}
// Get specific tickets by IDs
$tickets = $ticketModel->getTicketsByIds($ticketIdArray);
// Convert associative array to indexed array
$tickets = array_values($tickets);
} else {
// Get all tickets with filters (no pagination for export)
$result = $ticketModel->getAllTickets(1, 10000, $status, 'created_at', 'desc', $category, $type, $search); $result = $ticketModel->getAllTickets(1, 10000, $status, 'created_at', 'desc', $category, $type, $search);
$tickets = $result['tickets']; $tickets = $result['tickets'];
}
if ($format === 'csv') { if ($format === 'csv') {
// CSV Export // CSV Export

View File

@@ -34,8 +34,8 @@ try {
header('Content-Type: application/json'); header('Content-Type: application/json');
// Get all active users for mentions // Get all users for mentions/assignment
$sql = "SELECT user_id, username, display_name FROM users WHERE is_active = 1 ORDER BY display_name, username"; $sql = "SELECT user_id, username, display_name FROM users ORDER BY display_name, username";
$result = $conn->query($sql); $result = $conn->query($sql);
$users = []; $users = [];

View File

@@ -47,11 +47,16 @@ if ($conn->connect_error) {
ResponseHelper::serverError('Database connection failed'); ResponseHelper::serverError('Database connection failed');
} }
try {
$dependencyModel = new DependencyModel($conn); $dependencyModel = new DependencyModel($conn);
$auditLog = new AuditLogModel($conn); $auditLog = new AuditLogModel($conn);
} catch (Exception $e) {
ResponseHelper::serverError('Failed to initialize models: ' . $e->getMessage());
}
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
try {
switch ($method) { switch ($method) {
case 'GET': case 'GET':
// Get dependencies for a ticket // Get dependencies for a ticket
@@ -139,5 +144,8 @@ switch ($method) {
default: default:
ResponseHelper::error('Method not allowed', 405); ResponseHelper::error('Method not allowed', 405);
} }
} catch (Exception $e) {
ResponseHelper::serverError('An error occurred: ' . $e->getMessage());
}
$conn->close(); $conn->close();

View File

@@ -31,8 +31,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
ResponseHelper::error('Ticket ID is required'); ResponseHelper::error('Ticket ID is required');
} }
// Validate ticket ID format // Validate ticket ID format (9-digit number)
if (!preg_match('/^[A-Z]{3}\d{6}$/', $ticketId)) { if (!preg_match('/^\d{9}$/', $ticketId)) {
ResponseHelper::error('Invalid ticket ID format'); ResponseHelper::error('Invalid ticket ID format');
} }
@@ -72,8 +72,8 @@ if (empty($ticketId)) {
ResponseHelper::error('Ticket ID is required'); ResponseHelper::error('Ticket ID is required');
} }
// Validate ticket ID format // Validate ticket ID format (9-digit number)
if (!preg_match('/^[A-Z]{3}\d{6}$/', $ticketId)) { if (!preg_match('/^\d{9}$/', $ticketId)) {
ResponseHelper::error('Invalid ticket ID format'); ResponseHelper::error('Invalid ticket ID format');
} }

View File

@@ -3149,7 +3149,8 @@ tr:hover .quick-actions {
box-shadow: 0 0 10px rgba(0, 255, 65, 0.3); box-shadow: 0 0 10px rgba(0, 255, 65, 0.3);
} }
.export-dropdown:hover .export-dropdown-content { .export-dropdown:hover .export-dropdown-content,
.export-dropdown.open .export-dropdown-content {
display: block; display: block;
} }

View File

@@ -117,7 +117,10 @@ function initSettingsModal() {
if (settingsIcon) { if (settingsIcon) {
settingsIcon.addEventListener('click', function(e) { settingsIcon.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
createSettingsModal(); // openSettingsModal is defined in settings.js
if (typeof openSettingsModal === 'function') {
openSettingsModal();
}
}); });
} }
} }
@@ -482,6 +485,8 @@ function updateSelectionCount() {
const count = checkboxes.length; const count = checkboxes.length;
const toolbar = document.querySelector('.bulk-actions-inline'); const toolbar = document.querySelector('.bulk-actions-inline');
const countDisplay = document.getElementById('selected-count'); const countDisplay = document.getElementById('selected-count');
const exportDropdown = document.getElementById('exportDropdown');
const exportCount = document.getElementById('exportCount');
if (toolbar && countDisplay) { if (toolbar && countDisplay) {
if (count > 0) { if (count > 0) {
@@ -491,6 +496,16 @@ function updateSelectionCount() {
toolbar.style.display = 'none'; toolbar.style.display = 'none';
} }
} }
// Show/hide export dropdown based on selection
if (exportDropdown) {
if (count > 0) {
exportDropdown.style.display = '';
if (exportCount) exportCount.textContent = count;
} else {
exportDropdown.style.display = 'none';
}
}
} }
function getSelectedTicketIds() { function getSelectedTicketIds() {
@@ -1339,3 +1354,47 @@ function performQuickAssign(ticketId) {
toast.error('Error updating assignment', 4000); toast.error('Error updating assignment', 4000);
}); });
} }
/**
* Toggle export dropdown menu
*/
function toggleExportMenu(event) {
event.stopPropagation();
const dropdown = document.getElementById('exportDropdown');
const content = document.getElementById('exportDropdownContent');
if (dropdown && content) {
dropdown.classList.toggle('open');
}
}
// Close export dropdown when clicking outside
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('exportDropdown');
if (dropdown && !dropdown.contains(event.target)) {
dropdown.classList.remove('open');
}
});
/**
* Export selected tickets to CSV or JSON
*/
function exportSelectedTickets(format) {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
return;
}
// Build URL with selected ticket IDs
const params = new URLSearchParams();
params.set('format', format);
params.set('ticket_ids', ticketIds.join(','));
// Trigger download
window.location.href = '/api/export_tickets.php?' + params.toString();
// Close dropdown
const dropdown = document.getElementById('exportDropdown');
if (dropdown) dropdown.classList.remove('open');
}

View File

@@ -579,20 +579,39 @@ function loadDependencies() {
const ticketId = window.ticketData.id; const ticketId = window.ticketData.id;
fetch(`/api/ticket_dependencies.php?ticket_id=${ticketId}`) fetch(`/api/ticket_dependencies.php?ticket_id=${ticketId}`)
.then(response => response.json()) .then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderDependencies(data.dependencies); renderDependencies(data.dependencies);
renderDependents(data.dependents); renderDependents(data.dependents);
} else { } else {
console.error('Error loading dependencies:', data.error); console.error('Error loading dependencies:', data.error);
showDependencyError(data.error || 'Failed to load dependencies');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error loading dependencies:', error); console.error('Error loading dependencies:', error);
showDependencyError('Failed to load dependencies. The feature may not be available.');
}); });
} }
function showDependencyError(message) {
const dependenciesList = document.getElementById('dependenciesList');
const dependentsList = document.getElementById('dependentsList');
if (dependenciesList) {
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
}
if (dependentsList) {
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
}
}
function renderDependencies(dependencies) { function renderDependencies(dependencies) {
const container = document.getElementById('dependenciesList'); const container = document.getElementById('dependenciesList');
if (!container) return; if (!container) return;

View File

@@ -264,11 +264,11 @@
<!-- Center: Actions + Count --> <!-- Center: Actions + Count -->
<div class="toolbar-center"> <div class="toolbar-center">
<button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create'" class="btn create-ticket">+ New Ticket</button> <button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create'" class="btn create-ticket">+ New Ticket</button>
<div class="export-dropdown"> <div class="export-dropdown" id="exportDropdown" style="display: none;">
<button class="btn">↓ Export</button> <button class="btn" onclick="toggleExportMenu(event)">↓ Export Selected (<span id="exportCount">0</span>)</button>
<div class="export-dropdown-content"> <div class="export-dropdown-content" id="exportDropdownContent">
<a href="/api/export_tickets.php?format=csv<?php echo isset($_GET['status']) ? '&status=' . urlencode($_GET['status']) : ''; ?><?php echo isset($_GET['category']) ? '&category=' . urlencode($_GET['category']) : ''; ?>">CSV</a> <a href="#" onclick="exportSelectedTickets('csv'); return false;">CSV</a>
<a href="/api/export_tickets.php?format=json<?php echo isset($_GET['status']) ? '&status=' . urlencode($_GET['status']) : ''; ?><?php echo isset($_GET['category']) ? '&category=' . urlencode($_GET['category']) : ''; ?>">JSON</a> <a href="#" onclick="exportSelectedTickets('json'); return false;">JSON</a>
</div> </div>
</div> </div>
<span class="ticket-count">Total: <?php echo $totalTickets; ?></span> <span class="ticket-count">Total: <?php echo $totalTickets; ?></span>