Files
tinker_tickets/views/admin/RecurringTicketsView.php
Jared Vititoe c3f7593f3c Harden CSP by removing unsafe-inline for scripts
Refactored all inline event handlers (onclick, onchange, onsubmit) to use
addEventListener with data-action attributes and event delegation pattern.

Changes:
- views/*.php: Replaced inline handlers with data-action attributes
- views/admin/*.php: Same refactoring for all admin views
- assets/js/dashboard.js: Added event delegation for bulk/quick action modals
- assets/js/ticket.js: Added event delegation for dynamic elements
- assets/js/markdown.js: Refactored toolbar button handlers
- assets/js/keyboard-shortcuts.js: Refactored modal close button
- SecurityHeadersMiddleware.php: Enabled strict CSP with nonces

The CSP now uses script-src 'self' 'nonce-{nonce}' instead of 'unsafe-inline',
significantly improving XSS protection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 13:15:55 -05:00

373 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Admin view for managing recurring tickets
// Receives $recurringTickets from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recurring Tickets - Admin</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">← Dashboard</a>
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: Recurring Tickets</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
<span class="user-name"><?php echo htmlspecialchars($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username']); ?></span>
<span class="admin-badge">Admin</span>
<?php endif; ?>
</div>
</div>
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Recurring Tickets Management</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Scheduled Tickets</h2>
<button data-action="show-create-modal" class="btn">+ New Recurring Ticket</button>
</div>
<table style="width: 100%;">
<thead>
<tr>
<th>ID</th>
<th>Title Template</th>
<th>Schedule</th>
<th>Category</th>
<th>Assigned To</th>
<th>Next Run</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($recurringTickets)): ?>
<tr>
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
No recurring tickets configured.
</td>
</tr>
<?php else: ?>
<?php foreach ($recurringTickets as $rt): ?>
<tr>
<td><?php echo $rt['recurring_id']; ?></td>
<td><?php echo htmlspecialchars($rt['title_template']); ?></td>
<td>
<?php
$schedule = ucfirst($rt['schedule_type']);
if ($rt['schedule_type'] === 'weekly') {
$days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
$schedule .= ' (' . ($days[$rt['schedule_day']] ?? '?') . ')';
} elseif ($rt['schedule_type'] === 'monthly') {
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
}
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
echo $schedule;
?>
</td>
<td><?php echo htmlspecialchars($rt['category']); ?></td>
<td><?php echo htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned'); ?></td>
<td><?php echo date('M d, Y H:i', strtotime($rt['next_run_at'])); ?></td>
<td>
<span style="color: <?php echo $rt['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
<?php echo $rt['is_active'] ? 'Active' : 'Inactive'; ?>
</span>
</td>
<td>
<button data-action="edit-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">Edit</button>
<button data-action="toggle-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">
<?php echo $rt['is_active'] ? 'Disable' : 'Enable'; ?>
</button>
<button data-action="delete-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="recurringModal" style="display: none;" data-action="close-modal-backdrop">
<div class="settings-content" style="max-width: 800px; width: 90%;">
<div class="settings-header">
<h3 id="modalTitle">Create Recurring Ticket</h3>
<button class="close-settings" data-action="close-modal">×</button>
</div>
<form id="recurringForm">
<input type="hidden" id="recurring_id" name="recurring_id">
<div class="settings-body">
<div class="setting-row">
<label for="title_template">Title Template *</label>
<input type="text" id="title_template" name="title_template" required style="width: 100%;" placeholder="Use {{date}}, {{month}}, etc.">
</div>
<div class="setting-row">
<label for="description_template">Description Template</label>
<textarea id="description_template" name="description_template" rows="8" style="width: 100%; min-height: 150px;"></textarea>
</div>
<div class="setting-row">
<label for="schedule_type">Schedule Type *</label>
<select id="schedule_type" name="schedule_type" required data-action="update-schedule-options">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div class="setting-row" id="schedule_day_row" style="display: none;">
<label for="schedule_day">Schedule Day</label>
<select id="schedule_day" name="schedule_day"></select>
</div>
<div class="setting-row">
<label for="schedule_time">Schedule Time *</label>
<input type="time" id="schedule_time" name="schedule_time" value="09:00" required>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
<div class="setting-row setting-row-compact">
<label for="category">Category</label>
<select id="category" name="category">
<option value="General">General</option>
<option value="Hardware">Hardware</option>
<option value="Software">Software</option>
<option value="Network">Network</option>
<option value="Security">Security</option>
</select>
</div>
<div class="setting-row setting-row-compact">
<label for="type">Type</label>
<select id="type" name="type">
<option value="Issue">Issue</option>
<option value="Maintenance">Maintenance</option>
<option value="Install">Install</option>
<option value="Task">Task</option>
<option value="Upgrade">Upgrade</option>
<option value="Problem">Problem</option>
</select>
</div>
<div class="setting-row setting-row-compact">
<label for="priority">Priority</label>
<select id="priority" name="priority">
<option value="1">P1 - Critical</option>
<option value="2">P2 - High</option>
<option value="3">P3 - Medium</option>
<option value="4" selected>P4 - Low</option>
<option value="5">P5 - Lowest</option>
</select>
</div>
<div class="setting-row setting-row-compact">
<label for="assigned_to">Assign To</label>
<select id="assigned_to" name="assigned_to">
<option value="">Unassigned</option>
<!-- Populated by JavaScript -->
</select>
</div>
</div>
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" data-action="close-modal">Cancel</button>
</div>
</form>
</div>
</div>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Recurring Ticket';
document.getElementById('recurringForm').reset();
document.getElementById('recurring_id').value = '';
updateScheduleOptions();
document.getElementById('recurringModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('recurringModal').style.display = 'none';
}
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'close-modal':
closeModal();
break;
case 'close-modal-backdrop':
if (event.target === target) closeModal();
break;
case 'edit-recurring':
editRecurring(target.dataset.id);
break;
case 'toggle-recurring':
toggleRecurring(target.dataset.id);
break;
case 'delete-recurring':
deleteRecurring(target.dataset.id);
break;
}
});
document.addEventListener('change', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
if (target.dataset.action === 'update-schedule-options') {
updateScheduleOptions();
}
});
// Form submit handler
document.getElementById('recurringForm').addEventListener('submit', function(e) {
saveRecurring(e);
});
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
function updateScheduleOptions() {
const type = document.getElementById('schedule_type').value;
const dayRow = document.getElementById('schedule_day_row');
const daySelect = document.getElementById('schedule_day');
daySelect.innerHTML = '';
if (type === 'daily') {
dayRow.style.display = 'none';
} else if (type === 'weekly') {
dayRow.style.display = 'flex';
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
days.forEach((day, i) => {
daySelect.innerHTML += `<option value="${i + 1}">${day}</option>`;
});
} else if (type === 'monthly') {
dayRow.style.display = 'flex';
for (let i = 1; i <= 28; i++) {
daySelect.innerHTML += `<option value="${i}">Day ${i}</option>`;
}
}
}
function saveRecurring(e) {
e.preventDefault();
const form = new FormData(document.getElementById('recurringForm'));
const data = Object.fromEntries(form);
const method = data.recurring_id ? 'PUT' : 'POST';
const url = '/api/manage_recurring.php' + (data.recurring_id ? '?id=' + data.recurring_id : '');
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
toast.error(data.error || 'Failed to save');
}
});
}
function toggleRecurring(id) {
fetch('/api/manage_recurring.php?action=toggle&id=' + id, {
method: 'POST',
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
})
.then(r => r.json())
.then(data => {
if (data.success) window.location.reload();
});
}
function deleteRecurring(id) {
if (!confirm('Delete this recurring ticket schedule?')) return;
fetch('/api/manage_recurring.php?id=' + id, {
method: 'DELETE',
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
})
.then(r => r.json())
.then(data => {
if (data.success) window.location.reload();
});
}
function editRecurring(id) {
fetch('/api/manage_recurring.php?id=' + id)
.then(r => r.json())
.then(data => {
if (data.success && data.recurring) {
const rt = data.recurring;
document.getElementById('recurring_id').value = rt.recurring_id;
document.getElementById('title_template').value = rt.title_template;
document.getElementById('description_template').value = rt.description_template || '';
document.getElementById('schedule_type').value = rt.schedule_type;
updateScheduleOptions();
document.getElementById('schedule_day').value = rt.schedule_day || '';
document.getElementById('schedule_time').value = rt.schedule_time ? rt.schedule_time.substring(0, 5) : '09:00';
document.getElementById('category').value = rt.category || 'General';
document.getElementById('type').value = rt.type || 'Issue';
document.getElementById('priority').value = rt.priority || 4;
document.getElementById('assigned_to').value = rt.assigned_to || '';
document.getElementById('modalTitle').textContent = 'Edit Recurring Ticket';
document.getElementById('recurringModal').style.display = 'flex';
}
});
}
// Load users for assignee dropdown
function loadUsers() {
fetch('/api/get_users.php')
.then(r => r.json())
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('assigned_to');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
}
});
}
// Initialize
updateScheduleOptions();
loadUsers();
</script>
</body>
</html>