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:
@@ -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
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -47,11 +47,16 @@ if ($conn->connect_error) {
|
|||||||
ResponseHelper::serverError('Database connection failed');
|
ResponseHelper::serverError('Database connection failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
$dependencyModel = new DependencyModel($conn);
|
try {
|
||||||
$auditLog = new AuditLogModel($conn);
|
$dependencyModel = new DependencyModel($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();
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user