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:
157
views/admin/AuditLogView.php
Normal file
157
views/admin/AuditLogView.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
// Admin view for browsing audit logs
|
||||
// Receives $auditLogs, $totalPages, $page, $filters from controller
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Log - 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">
|
||||
</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: Audit Log</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: 1400px; margin: 2rem auto;">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">Audit Log Browser</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<!-- Filters -->
|
||||
<form method="GET" style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Action Type</label>
|
||||
<select name="action_type" class="setting-select">
|
||||
<option value="">All Actions</option>
|
||||
<option value="create" <?php echo ($filters['action_type'] ?? '') === 'create' ? 'selected' : ''; ?>>Create</option>
|
||||
<option value="update" <?php echo ($filters['action_type'] ?? '') === 'update' ? 'selected' : ''; ?>>Update</option>
|
||||
<option value="delete" <?php echo ($filters['action_type'] ?? '') === 'delete' ? 'selected' : ''; ?>>Delete</option>
|
||||
<option value="comment" <?php echo ($filters['action_type'] ?? '') === 'comment' ? 'selected' : ''; ?>>Comment</option>
|
||||
<option value="assign" <?php echo ($filters['action_type'] ?? '') === 'assign' ? 'selected' : ''; ?>>Assign</option>
|
||||
<option value="status_change" <?php echo ($filters['action_type'] ?? '') === 'status_change' ? 'selected' : ''; ?>>Status Change</option>
|
||||
<option value="login" <?php echo ($filters['action_type'] ?? '') === 'login' ? 'selected' : ''; ?>>Login</option>
|
||||
<option value="security" <?php echo ($filters['action_type'] ?? '') === 'security' ? 'selected' : ''; ?>>Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">User</label>
|
||||
<select name="user_id" class="setting-select">
|
||||
<option value="">All Users</option>
|
||||
<?php if (isset($users)): foreach ($users as $user): ?>
|
||||
<option value="<?php echo $user['user_id']; ?>" <?php echo ($filters['user_id'] ?? '') == $user['user_id'] ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
|
||||
</option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date From</label>
|
||||
<input type="date" name="date_from" value="<?php echo htmlspecialchars($filters['date_from'] ?? ''); ?>" class="setting-select">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date To</label>
|
||||
<input type="date" name="date_to" value="<?php echo htmlspecialchars($filters['date_to'] ?? ''); ?>" class="setting-select">
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn">Filter</button>
|
||||
<a href="?" class="btn btn-secondary" style="margin-left: 0.5rem;">Reset</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Log Table -->
|
||||
<table style="width: 100%; font-size: 0.9rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
<th>Entity ID</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($auditLogs)): ?>
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||||
No audit log entries found.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($auditLogs as $log): ?>
|
||||
<tr>
|
||||
<td style="white-space: nowrap;"><?php echo date('Y-m-d H:i:s', strtotime($log['created_at'])); ?></td>
|
||||
<td><?php echo htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System'); ?></td>
|
||||
<td>
|
||||
<span style="color: var(--terminal-amber);"><?php echo htmlspecialchars($log['action_type']); ?></span>
|
||||
</td>
|
||||
<td><?php echo htmlspecialchars($log['entity_type'] ?? '-'); ?></td>
|
||||
<td>
|
||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
||||
<a href="/ticket/<?php echo htmlspecialchars($log['entity_id']); ?>" style="color: var(--terminal-green);">
|
||||
<?php echo htmlspecialchars($log['entity_id']); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
|
||||
<?php
|
||||
if ($log['details']) {
|
||||
$details = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||
if (is_array($details)) {
|
||||
echo '<code style="font-size: 0.8rem;">' . htmlspecialchars(json_encode($details)) . '</code>';
|
||||
} else {
|
||||
echo htmlspecialchars($log['details']);
|
||||
}
|
||||
} else {
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td style="white-space: nowrap; font-size: 0.85rem;"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination" style="margin-top: 1rem; text-align: center;">
|
||||
<?php
|
||||
$params = $_GET;
|
||||
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
||||
$params['page'] = $i;
|
||||
$activeClass = ($i == $page) ? 'active' : '';
|
||||
$url = '?' . http_build_query($params);
|
||||
echo "<a href='$url' class='btn btn-small $activeClass'>$i</a> ";
|
||||
}
|
||||
if ($totalPages > 10) {
|
||||
echo "...";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
254
views/admin/CustomFieldsView.php
Normal file
254
views/admin/CustomFieldsView.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
// Admin view for managing custom fields
|
||||
// Receives $customFields from controller
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom Fields - 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>
|
||||
window.CSRF_TOKEN = '<?php
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.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: Custom Fields</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">Custom Fields 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;">Custom Field Definitions</h2>
|
||||
<button onclick="showCreateModal()" class="btn">+ New Field</button>
|
||||
</div>
|
||||
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Field Name</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Required</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($customFields)): ?>
|
||||
<tr>
|
||||
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||||
No custom fields defined.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($customFields as $field): ?>
|
||||
<tr>
|
||||
<td><?php echo $field['display_order']; ?></td>
|
||||
<td><code><?php echo htmlspecialchars($field['field_name']); ?></code></td>
|
||||
<td><?php echo htmlspecialchars($field['field_label']); ?></td>
|
||||
<td><?php echo ucfirst($field['field_type']); ?></td>
|
||||
<td><?php echo htmlspecialchars($field['category'] ?? 'All'); ?></td>
|
||||
<td><?php echo $field['is_required'] ? 'Yes' : 'No'; ?></td>
|
||||
<td>
|
||||
<span style="color: <?php echo $field['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
||||
<?php echo $field['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="editField(<?php echo $field['field_id']; ?>)" class="btn btn-small">Edit</button>
|
||||
<button onclick="deleteField(<?php echo $field['field_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="fieldModal" style="display: none;">
|
||||
<div class="settings-content" style="max-width: 500px;">
|
||||
<div class="settings-header">
|
||||
<h3 id="modalTitle">Create Custom Field</h3>
|
||||
<button class="close-settings" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="fieldForm" onsubmit="saveField(event)">
|
||||
<input type="hidden" id="field_id" name="field_id">
|
||||
<div class="settings-body">
|
||||
<div class="setting-row">
|
||||
<label for="field_name">Field Name * (internal)</label>
|
||||
<input type="text" id="field_name" name="field_name" required pattern="[a-z_]+" placeholder="e.g., server_name">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="field_label">Field Label * (display)</label>
|
||||
<input type="text" id="field_label" name="field_label" required placeholder="e.g., Server Name">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="field_type">Field Type *</label>
|
||||
<select id="field_type" name="field_type" required onchange="toggleOptionsField()">
|
||||
<option value="text">Text</option>
|
||||
<option value="textarea">Text Area</option>
|
||||
<option value="select">Dropdown (Select)</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="number">Number</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row" id="options_row" style="display: none;">
|
||||
<label for="field_options">Options (one per line)</label>
|
||||
<textarea id="field_options" name="field_options" rows="4" placeholder="Option 1 Option 2 Option 3"></textarea>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="category">Category (empty = all)</label>
|
||||
<select id="category" name="category">
|
||||
<option value="">All Categories</option>
|
||||
<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">
|
||||
<label for="display_order">Display Order</label>
|
||||
<input type="number" id="display_order" name="display_order" value="0" min="0">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="is_required" name="is_required"> Required field</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script>
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Custom Field';
|
||||
document.getElementById('fieldForm').reset();
|
||||
document.getElementById('field_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
toggleOptionsField();
|
||||
document.getElementById('fieldModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('fieldModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function toggleOptionsField() {
|
||||
const type = document.getElementById('field_type').value;
|
||||
document.getElementById('options_row').style.display = type === 'select' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function saveField(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('fieldForm');
|
||||
const data = {
|
||||
field_id: document.getElementById('field_id').value,
|
||||
field_name: document.getElementById('field_name').value,
|
||||
field_label: document.getElementById('field_label').value,
|
||||
field_type: document.getElementById('field_type').value,
|
||||
category: document.getElementById('category').value || null,
|
||||
display_order: parseInt(document.getElementById('display_order').value) || 0,
|
||||
is_required: document.getElementById('is_required').checked ? 1 : 0,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
|
||||
if (data.field_type === 'select') {
|
||||
const options = document.getElementById('field_options').value.split('\n').filter(o => o.trim());
|
||||
data.field_options = { options: options };
|
||||
}
|
||||
|
||||
const method = data.field_id ? 'PUT' : 'POST';
|
||||
const url = '/api/custom_fields.php' + (data.field_id ? '?id=' + data.field_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(result => {
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to save');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editField(id) {
|
||||
fetch('/api/custom_fields.php?id=' + id)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success && data.field) {
|
||||
const f = data.field;
|
||||
document.getElementById('field_id').value = f.field_id;
|
||||
document.getElementById('field_name').value = f.field_name;
|
||||
document.getElementById('field_label').value = f.field_label;
|
||||
document.getElementById('field_type').value = f.field_type;
|
||||
document.getElementById('category').value = f.category || '';
|
||||
document.getElementById('display_order').value = f.display_order;
|
||||
document.getElementById('is_required').checked = f.is_required == 1;
|
||||
document.getElementById('is_active').checked = f.is_active == 1;
|
||||
toggleOptionsField();
|
||||
if (f.field_options && f.field_options.options) {
|
||||
document.getElementById('field_options').value = f.field_options.options.join('\n');
|
||||
}
|
||||
document.getElementById('modalTitle').textContent = 'Edit Custom Field';
|
||||
document.getElementById('fieldModal').style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteField(id) {
|
||||
if (!confirm('Delete this custom field? All values will be lost.')) return;
|
||||
fetch('/api/custom_fields.php?id=' + id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
283
views/admin/RecurringTicketsView.php
Normal file
283
views/admin/RecurringTicketsView.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
// Admin view for managing recurring tickets
|
||||
// Receives $recurringTickets from controller
|
||||
?>
|
||||
<!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>
|
||||
window.CSRF_TOKEN = '<?php
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.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 onclick="showCreateModal()" 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 onclick="editRecurring(<?php echo $rt['recurring_id']; ?>)" class="btn btn-small">Edit</button>
|
||||
<button onclick="toggleRecurring(<?php echo $rt['recurring_id']; ?>)" class="btn btn-small">
|
||||
<?php echo $rt['is_active'] ? 'Disable' : 'Enable'; ?>
|
||||
</button>
|
||||
<button onclick="deleteRecurring(<?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;">
|
||||
<div class="settings-content" style="max-width: 600px;">
|
||||
<div class="settings-header">
|
||||
<h3 id="modalTitle">Create Recurring Ticket</h3>
|
||||
<button class="close-settings" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="recurringForm" onsubmit="saveRecurring(event)">
|
||||
<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="4" style="width: 100%;"></textarea>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="schedule_type">Schedule Type *</label>
|
||||
<select id="schedule_type" name="schedule_type" required onchange="updateScheduleOptions()">
|
||||
<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 class="setting-row">
|
||||
<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">
|
||||
<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>
|
||||
<div class="settings-footer">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script>
|
||||
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';
|
||||
}
|
||||
|
||||
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('priority').value = rt.priority || 4;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Recurring Ticket';
|
||||
document.getElementById('recurringModal').style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
updateScheduleOptions();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
241
views/admin/TemplatesView.php
Normal file
241
views/admin/TemplatesView.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
// Admin view for managing ticket templates
|
||||
// Receives $templates from controller
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Template Management - 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>
|
||||
window.CSRF_TOKEN = '<?php
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.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: Templates</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">Ticket Template 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;">Ticket Templates</h2>
|
||||
<button onclick="showCreateModal()" class="btn">+ New Template</button>
|
||||
</div>
|
||||
|
||||
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
|
||||
Templates pre-fill ticket creation forms with standard content for common ticket types.
|
||||
</p>
|
||||
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Template Name</th>
|
||||
<th>Category</th>
|
||||
<th>Type</th>
|
||||
<th>Priority</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||||
No templates defined. Create templates to speed up ticket creation.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<tr>
|
||||
<td><strong><?php echo htmlspecialchars($tpl['template_name']); ?></strong></td>
|
||||
<td><?php echo htmlspecialchars($tpl['category'] ?? 'Any'); ?></td>
|
||||
<td><?php echo htmlspecialchars($tpl['type'] ?? 'Any'); ?></td>
|
||||
<td>P<?php echo $tpl['priority'] ?? '4'; ?></td>
|
||||
<td>
|
||||
<span style="color: <?php echo ($tpl['is_active'] ?? 1) ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
||||
<?php echo ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="editTemplate(<?php echo $tpl['template_id']; ?>)" class="btn btn-small">Edit</button>
|
||||
<button onclick="deleteTemplate(<?php echo $tpl['template_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="templateModal" style="display: none;">
|
||||
<div class="settings-content" style="max-width: 600px;">
|
||||
<div class="settings-header">
|
||||
<h3 id="modalTitle">Create Template</h3>
|
||||
<button class="close-settings" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="templateForm" onsubmit="saveTemplate(event)">
|
||||
<input type="hidden" id="template_id" name="template_id">
|
||||
<div class="settings-body">
|
||||
<div class="setting-row">
|
||||
<label for="template_name">Template Name *</label>
|
||||
<input type="text" id="template_name" name="template_name" required style="width: 100%;">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="title_template">Title Template</label>
|
||||
<input type="text" id="title_template" name="title_template" style="width: 100%;" placeholder="Pre-filled title text">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="description_template">Description Template</label>
|
||||
<textarea id="description_template" name="description_template" rows="6" style="width: 100%;" placeholder="Pre-filled description content"></textarea>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
|
||||
<div class="setting-row">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category">
|
||||
<option value="">Any</option>
|
||||
<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">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="">Any</option>
|
||||
<option value="Maintenance">Maintenance</option>
|
||||
<option value="Install">Install</option>
|
||||
<option value="Task">Task</option>
|
||||
<option value="Upgrade">Upgrade</option>
|
||||
<option value="Issue">Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="priority">Priority</label>
|
||||
<select id="priority" name="priority">
|
||||
<option value="1">P1</option>
|
||||
<option value="2">P2</option>
|
||||
<option value="3">P3</option>
|
||||
<option value="4" selected>P4</option>
|
||||
<option value="5">P5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script>
|
||||
const templates = <?php echo json_encode($templates ?? []); ?>;
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Template';
|
||||
document.getElementById('templateForm').reset();
|
||||
document.getElementById('template_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
document.getElementById('templateModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('templateModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveTemplate(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
template_id: document.getElementById('template_id').value,
|
||||
template_name: document.getElementById('template_name').value,
|
||||
title_template: document.getElementById('title_template').value,
|
||||
description_template: document.getElementById('description_template').value,
|
||||
category: document.getElementById('category').value || null,
|
||||
type: document.getElementById('type').value || null,
|
||||
priority: parseInt(document.getElementById('priority').value) || 4,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
|
||||
const method = data.template_id ? 'PUT' : 'POST';
|
||||
const url = '/api/manage_templates.php' + (data.template_id ? '?id=' + data.template_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(result => {
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to save');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editTemplate(id) {
|
||||
const tpl = templates.find(t => t.template_id == id);
|
||||
if (!tpl) return;
|
||||
|
||||
document.getElementById('template_id').value = tpl.template_id;
|
||||
document.getElementById('template_name').value = tpl.template_name;
|
||||
document.getElementById('title_template').value = tpl.title_template || '';
|
||||
document.getElementById('description_template').value = tpl.description_template || '';
|
||||
document.getElementById('category').value = tpl.category || '';
|
||||
document.getElementById('type').value = tpl.type || '';
|
||||
document.getElementById('priority').value = tpl.priority || 4;
|
||||
document.getElementById('is_active').checked = (tpl.is_active ?? 1) == 1;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Template';
|
||||
document.getElementById('templateModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function deleteTemplate(id) {
|
||||
if (!confirm('Delete this template?')) return;
|
||||
fetch('/api/manage_templates.php?id=' + id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
135
views/admin/UserActivityView.php
Normal file
135
views/admin/UserActivityView.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
// Admin view for user activity reports
|
||||
// Receives $userStats, $dateRange from controller
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Activity - 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">
|
||||
</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: User Activity</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">User Activity Report</div>
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<!-- Date Range Filter -->
|
||||
<form method="GET" style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: flex-end;">
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date From</label>
|
||||
<input type="date" name="date_from" value="<?php echo htmlspecialchars($dateRange['from'] ?? ''); ?>" class="setting-select">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber);">Date To</label>
|
||||
<input type="date" name="date_to" value="<?php echo htmlspecialchars($dateRange['to'] ?? ''); ?>" class="setting-select">
|
||||
</div>
|
||||
<button type="submit" class="btn">Apply</button>
|
||||
<a href="?" class="btn btn-secondary">Reset</a>
|
||||
</form>
|
||||
|
||||
<!-- User Activity Table -->
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th style="text-align: center;">Tickets Created</th>
|
||||
<th style="text-align: center;">Tickets Resolved</th>
|
||||
<th style="text-align: center;">Comments Added</th>
|
||||
<th style="text-align: center;">Tickets Assigned</th>
|
||||
<th style="text-align: center;">Last Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($userStats)): ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||||
No user activity data available.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($userStats as $user): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?></strong>
|
||||
<?php if ($user['is_admin']): ?>
|
||||
<span class="admin-badge" style="font-size: 0.7rem;">Admin</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<span style="color: var(--terminal-green); font-weight: bold;"><?php echo $user['tickets_created'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<span style="color: var(--status-open); font-weight: bold;"><?php echo $user['tickets_resolved'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<span style="color: var(--terminal-cyan); font-weight: bold;"><?php echo $user['comments_added'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<span style="color: var(--terminal-amber); font-weight: bold;"><?php echo $user['tickets_assigned'] ?? 0; ?></span>
|
||||
</td>
|
||||
<td style="text-align: center; font-size: 0.9rem;">
|
||||
<?php echo $user['last_activity'] ? date('M d, Y H:i', strtotime($user['last_activity'])) : 'Never'; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<?php if (!empty($userStats)): ?>
|
||||
<div style="margin-top: 2rem; padding: 1rem; border: 1px solid var(--terminal-green);">
|
||||
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Summary</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center;">
|
||||
<div>
|
||||
<div style="font-size: 1.5rem; color: var(--terminal-green); font-weight: bold;">
|
||||
<?php echo array_sum(array_column($userStats, 'tickets_created')); ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Created</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 1.5rem; color: var(--status-open); font-weight: bold;">
|
||||
<?php echo array_sum(array_column($userStats, 'tickets_resolved')); ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Resolved</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 1.5rem; color: var(--terminal-cyan); font-weight: bold;">
|
||||
<?php echo array_sum(array_column($userStats, 'comments_added')); ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Total Comments</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 1.5rem; color: var(--terminal-amber); font-weight: bold;">
|
||||
<?php echo count($userStats); ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim);">Active Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
255
views/admin/WorkflowDesignerView.php
Normal file
255
views/admin/WorkflowDesignerView.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
// Admin view for workflow/status transitions designer
|
||||
// Receives $workflows from controller
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Workflow Designer - 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>
|
||||
window.CSRF_TOKEN = '<?php
|
||||
require_once __DIR__ . '/../../middleware/CsrfMiddleware.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: Workflow Designer</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">Status Workflow Designer</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;">Status Transitions</h2>
|
||||
<button onclick="showCreateModal()" class="btn">+ New Transition</button>
|
||||
</div>
|
||||
|
||||
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
|
||||
Define which status transitions are allowed. This controls what options appear in the status dropdown.
|
||||
</p>
|
||||
|
||||
<!-- Visual Workflow Diagram -->
|
||||
<div style="margin-bottom: 2rem; padding: 1rem; border: 1px solid var(--terminal-green); background: var(--bg-secondary);">
|
||||
<h4 style="color: var(--terminal-amber); margin-bottom: 1rem;">Workflow Diagram</h4>
|
||||
<div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
|
||||
<?php
|
||||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
foreach ($statuses as $status):
|
||||
$statusClass = 'status-' . str_replace(' ', '-', strtolower($status));
|
||||
?>
|
||||
<div style="text-align: center;">
|
||||
<div class="<?php echo $statusClass; ?>" style="padding: 0.5rem 1rem; display: inline-block;">
|
||||
<?php echo $status; ?>
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--terminal-green-dim); margin-top: 0.5rem;">
|
||||
<?php
|
||||
$toCount = 0;
|
||||
if (isset($workflows)) {
|
||||
foreach ($workflows as $w) {
|
||||
if ($w['from_status'] === $status) $toCount++;
|
||||
}
|
||||
}
|
||||
echo "→ $toCount transitions";
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transitions Table -->
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From Status</th>
|
||||
<th>→</th>
|
||||
<th>To Status</th>
|
||||
<th>Requires Comment</th>
|
||||
<th>Requires Admin</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($workflows)): ?>
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
|
||||
No transitions defined. Add transitions to enable status changes.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($workflows as $wf): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['from_status'])); ?>">
|
||||
<?php echo htmlspecialchars($wf['from_status']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: center; color: var(--terminal-amber);">→</td>
|
||||
<td>
|
||||
<span class="status-<?php echo str_replace(' ', '-', strtolower($wf['to_status'])); ?>">
|
||||
<?php echo htmlspecialchars($wf['to_status']); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: center;"><?php echo $wf['requires_comment'] ? '✓' : '−'; ?></td>
|
||||
<td style="text-align: center;"><?php echo $wf['requires_admin'] ? '✓' : '−'; ?></td>
|
||||
<td style="text-align: center;">
|
||||
<span style="color: <?php echo $wf['is_active'] ? 'var(--status-open)' : 'var(--status-closed)'; ?>;">
|
||||
<?php echo $wf['is_active'] ? '✓' : '✗'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="editTransition(<?php echo $wf['transition_id']; ?>)" class="btn btn-small">Edit</button>
|
||||
<button onclick="deleteTransition(<?php echo $wf['transition_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="workflowModal" style="display: none;">
|
||||
<div class="settings-content" style="max-width: 450px;">
|
||||
<div class="settings-header">
|
||||
<h3 id="modalTitle">Create Transition</h3>
|
||||
<button class="close-settings" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<form id="workflowForm" onsubmit="saveTransition(event)">
|
||||
<input type="hidden" id="transition_id" name="transition_id">
|
||||
<div class="settings-body">
|
||||
<div class="setting-row">
|
||||
<label for="from_status">From Status *</label>
|
||||
<select id="from_status" name="from_status" required>
|
||||
<option value="Open">Open</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="to_status">To Status *</label>
|
||||
<select id="to_status" name="to_status" required>
|
||||
<option value="Open">Open</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="requires_comment" name="requires_comment"> Requires comment</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="requires_admin" name="requires_admin"> Requires admin privileges</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label><input type="checkbox" id="is_active" name="is_active" checked> Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-footer">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||
<script>
|
||||
const workflows = <?php echo json_encode($workflows ?? []); ?>;
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Create Transition';
|
||||
document.getElementById('workflowForm').reset();
|
||||
document.getElementById('transition_id').value = '';
|
||||
document.getElementById('is_active').checked = true;
|
||||
document.getElementById('workflowModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('workflowModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveTransition(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
transition_id: document.getElementById('transition_id').value,
|
||||
from_status: document.getElementById('from_status').value,
|
||||
to_status: document.getElementById('to_status').value,
|
||||
requires_comment: document.getElementById('requires_comment').checked ? 1 : 0,
|
||||
requires_admin: document.getElementById('requires_admin').checked ? 1 : 0,
|
||||
is_active: document.getElementById('is_active').checked ? 1 : 0
|
||||
};
|
||||
|
||||
const method = data.transition_id ? 'PUT' : 'POST';
|
||||
const url = '/api/manage_workflows.php' + (data.transition_id ? '?id=' + data.transition_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(result => {
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to save');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editTransition(id) {
|
||||
const wf = workflows.find(w => w.transition_id == id);
|
||||
if (!wf) return;
|
||||
|
||||
document.getElementById('transition_id').value = wf.transition_id;
|
||||
document.getElementById('from_status').value = wf.from_status;
|
||||
document.getElementById('to_status').value = wf.to_status;
|
||||
document.getElementById('requires_comment').checked = wf.requires_comment == 1;
|
||||
document.getElementById('requires_admin').checked = wf.requires_admin == 1;
|
||||
document.getElementById('is_active').checked = wf.is_active == 1;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Transition';
|
||||
document.getElementById('workflowModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function deleteTransition(id) {
|
||||
if (!confirm('Delete this status transition?')) return;
|
||||
fetch('/api/manage_workflows.php?id=' + id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': window.CSRF_TOKEN }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user