Implement comprehensive improvement plan (Phases 1-6)

Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants

Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields

Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows

Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles

Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
  Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
  manage_recurring, custom_fields, get_users
- Add admin routes in index.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 09:55:01 -05:00
parent 8c7211d311
commit be505b7312
53 changed files with 6640 additions and 169 deletions

View File

@@ -1,3 +1,10 @@
// XSS prevention helper
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Main initialization
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing dashboard...');
@@ -999,6 +1006,10 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
};
const icon = icons[type] || icons.warning;
// Escape user-provided content to prevent XSS
const safeTitle = escapeHtml(title);
const safeMessage = escapeHtml(message);
const modalHtml = `
<div class="modal-overlay" id="${modalId}">
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
@@ -1006,14 +1017,14 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header" style="color: ${color};">
${icon} ${title}
${icon} ${safeTitle}
</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="modal-body" style="padding: 1.5rem; text-align: center;">
<p style="color: var(--terminal-green); white-space: pre-line;">
${message}
${safeMessage}
</p>
</div>
</div>
@@ -1070,6 +1081,11 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
const modalId = 'inputModal' + Date.now();
const inputId = modalId + '_input';
// Escape user-provided content to prevent XSS
const safeTitle = escapeHtml(title);
const safeLabel = escapeHtml(label);
const safePlaceholder = escapeHtml(placeholder);
const modalHtml = `
<div class="modal-overlay" id="${modalId}">
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
@@ -1077,20 +1093,20 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">
${title}
${safeTitle}
</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="modal-body" style="padding: 1.5rem;">
<label for="${inputId}" style="display: block; margin-bottom: 0.5rem; color: var(--terminal-green);">
${label}
${safeLabel}
</label>
<input
type="text"
id="${inputId}"
class="terminal-input"
placeholder="${placeholder}"
placeholder="${safePlaceholder}"
style="width: 100%; padding: 0.5rem; background: var(--bg-primary); border: 1px solid var(--terminal-green); color: var(--terminal-green); font-family: var(--font-mono);"
/>
</div>
@@ -1149,3 +1165,177 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
};
document.addEventListener('keydown', escHandler);
}
// ========================================
// QUICK ACTIONS
// ========================================
/**
* Quick status change from dashboard
*/
function quickStatusChange(ticketId, currentStatus) {
const statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
const otherStatuses = statuses.filter(s => s !== currentStatus);
const modalHtml = `
<div class="modal-overlay" id="quickStatusModal">
<div class="modal-content ascii-frame-outer" style="max-width: 400px;">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Quick Status Change</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="modal-body" style="padding: 1rem;">
<p style="margin-bottom: 1rem;">Ticket #${escapeHtml(ticketId)}</p>
<p style="margin-bottom: 0.5rem; color: var(--terminal-amber);">Current: ${escapeHtml(currentStatus)}</p>
<label for="quickStatusSelect">New Status:</label>
<select id="quickStatusSelect" class="editable" style="width: 100%; margin-top: 0.5rem;">
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
</select>
</div>
</div>
</div>
<div class="ascii-divider"></div>
<div class="ascii-content">
<div class="modal-footer">
<button onclick="performQuickStatusChange('${ticketId}')" class="btn btn-primary">Update</button>
<button onclick="closeQuickStatusModal()" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
function closeQuickStatusModal() {
const modal = document.getElementById('quickStatusModal');
if (modal) modal.remove();
}
function performQuickStatusChange(ticketId) {
const newStatus = document.getElementById('quickStatusSelect').value;
fetch('/api/update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
ticket_id: ticketId,
status: newStatus
})
})
.then(response => response.json())
.then(data => {
closeQuickStatusModal();
if (data.success) {
toast.success(`Status updated to ${newStatus}`, 3000);
setTimeout(() => window.location.reload(), 1000);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
closeQuickStatusModal();
console.error('Error:', error);
toast.error('Error updating status', 4000);
});
}
/**
* Quick assign from dashboard
*/
function quickAssign(ticketId) {
const modalHtml = `
<div class="modal-overlay" id="quickAssignModal">
<div class="modal-content ascii-frame-outer" style="max-width: 400px;">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Quick Assign</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="modal-body" style="padding: 1rem;">
<p style="margin-bottom: 1rem;">Ticket #${escapeHtml(ticketId)}</p>
<label for="quickAssignSelect">Assign to:</label>
<select id="quickAssignSelect" class="editable" style="width: 100%; margin-top: 0.5rem;">
<option value="">Unassigned</option>
</select>
</div>
</div>
</div>
<div class="ascii-divider"></div>
<div class="ascii-content">
<div class="modal-footer">
<button onclick="performQuickAssign('${ticketId}')" class="btn btn-primary">Assign</button>
<button onclick="closeQuickAssignModal()" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Load users
fetch('/api/get_users.php')
.then(response => response.json())
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('quickAssignSelect');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
}
})
.catch(error => console.error('Error loading users:', error));
}
function closeQuickAssignModal() {
const modal = document.getElementById('quickAssignModal');
if (modal) modal.remove();
}
function performQuickAssign(ticketId) {
const assignedTo = document.getElementById('quickAssignSelect').value || null;
fetch('/api/assign_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
ticket_id: ticketId,
assigned_to: assignedTo
})
})
.then(response => response.json())
.then(data => {
closeQuickAssignModal();
if (data.success) {
toast.success('Assignment updated', 3000);
setTimeout(() => window.location.reload(), 1000);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
closeQuickAssignModal();
console.error('Error:', error);
toast.error('Error updating assignment', 4000);
});
}