- Consolidate the duplicated command palette to a single overlay + init in the footer; fix New Ticket to route to /ticket/create (was a 404 /create); keep the CSP nonce and all commands - TicketController create(): trim title, require a non-empty description, and honor the posted status (validated against the canonical list) instead of silently discarding it - UserActivityView: 'Active Users' counts only users active in the selected range, not every registered user - layout_footer/DashboardView: local esc() now escapes quotes so values used in HTML attributes can't break out - TicketView: comments tab badge shows the true total, not just page one - layout_header: gate the 'View activity log' link behind the admin flag - index.php: validate /admin/user-activity date params; anchor the legacy /ticket.php route; align the audit action-type whitelist with the dropdown - ApiKeysView: correct the external API sample to /create_ticket_api.php Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.6 KiB
PHP
132 lines
5.6 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
|
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
|
$pageTitle = 'User Activity';
|
|
$activeNav = 'admin-user-activity';
|
|
$_v = $GLOBALS['config']['ASSET_VERSION'] ?? '1';
|
|
$pageStyles = ["/assets/css/dashboard.css?v={$_v}"];
|
|
$pageScripts = ["/assets/js/keyboard-shortcuts.js?v={$_v}"];
|
|
include __DIR__ . '/../../views/layout_header.php';
|
|
?>
|
|
|
|
<div class="lt-page-header">
|
|
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
|
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
|
<span class="lt-text-muted lt-text-xs">/</span>
|
|
<span class="lt-text-muted lt-text-xs">Admin: User Activity</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">User Activity Report</div>
|
|
<div class="lt-section-body">
|
|
|
|
<!-- Date filter -->
|
|
<form method="GET" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-mb-md" role="search">
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="date_from">Date From</label>
|
|
<input type="date" name="date_from" id="date_from" class="lt-input lt-input-sm"
|
|
value="<?= htmlspecialchars($dateRange['from'] ?? '') ?>">
|
|
</div>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="date_to">Date To</label>
|
|
<input type="date" name="date_to" id="date_to" class="lt-input lt-input-sm"
|
|
value="<?= htmlspecialchars($dateRange['to'] ?? '') ?>">
|
|
</div>
|
|
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center" style="align-self:flex-end">
|
|
<button type="submit" class="lt-btn lt-btn-primary lt-btn-sm">APPLY</button>
|
|
<a href="?" class="lt-btn lt-btn-ghost lt-btn-sm">RESET</a>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Summary stats -->
|
|
<?php if (!empty($userStats)) : ?>
|
|
<?php
|
|
// "Active" = users with >=1 tracked action within the selected date range.
|
|
// The query LEFT JOINs from all users, so $userStats includes zero-activity users.
|
|
$activeUsers = 0;
|
|
foreach ($userStats as $_u) {
|
|
$_activity = ($_u['tickets_created'] ?? 0) + ($_u['tickets_resolved'] ?? 0)
|
|
+ ($_u['comments_added'] ?? 0) + ($_u['tickets_assigned'] ?? 0);
|
|
if ($_activity > 0) {
|
|
$activeUsers++;
|
|
}
|
|
}
|
|
?>
|
|
<div class="lt-stats-grid lt-mb-md">
|
|
<div class="lt-stat-card">
|
|
<div class="lt-stat-icon lt-text-cyan">[ # ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= (int)$activeUsers ?></div>
|
|
<div class="lt-stat-label">Active Users</div>
|
|
</div>
|
|
</div>
|
|
<div class="lt-stat-card">
|
|
<div class="lt-stat-icon">[ + ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'tickets_created')) ?></div>
|
|
<div class="lt-stat-label">Total Created</div>
|
|
</div>
|
|
</div>
|
|
<div class="lt-stat-card">
|
|
<div class="lt-stat-icon lt-text-muted">[ OK ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'tickets_resolved')) ?></div>
|
|
<div class="lt-stat-label">Total Resolved</div>
|
|
</div>
|
|
</div>
|
|
<div class="lt-stat-card">
|
|
<div class="lt-stat-icon lt-text-amber">[ > ]</div>
|
|
<div class="lt-stat-info">
|
|
<div class="lt-stat-value"><?= array_sum(array_column($userStats, 'comments_added')) ?></div>
|
|
<div class="lt-stat-label">Total Comments</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
<!-- User activity table -->
|
|
<div class="lt-table-wrap">
|
|
<table class="lt-table lt-table-responsive" aria-label="User activity">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">User</th>
|
|
<th scope="col">Tickets Created</th>
|
|
<th scope="col">Tickets Resolved</th>
|
|
<th scope="col">Comments</th>
|
|
<th scope="col">Assigned</th>
|
|
<th scope="col">Last Activity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($userStats)) : ?>
|
|
<tr><td colspan="6" class="lt-empty">No user activity data available.</td></tr>
|
|
<?php else :
|
|
foreach ($userStats as $u) : ?>
|
|
<tr>
|
|
<td data-label="User">
|
|
<strong><?= htmlspecialchars($u['display_name'] ?? $u['username']) ?></strong>
|
|
<?php if ($u['is_admin']) : ?>
|
|
<span class="lt-badge lt-badge-admin lt-text-xs">ADMIN</span>
|
|
<?php endif ?>
|
|
</td>
|
|
<td data-label="Created"><span class="lt-text-cyan"><?= (int)($u['tickets_created'] ?? 0) ?></span></td>
|
|
<td data-label="Resolved"><span class="lt-text-muted"><?= (int)($u['tickets_resolved'] ?? 0) ?></span></td>
|
|
<td data-label="Comments"><span class="lt-text-amber"><?= (int)($u['comments_added'] ?? 0) ?></span></td>
|
|
<td data-label="Assigned"><?= (int)($u['tickets_assigned'] ?? 0) ?></td>
|
|
<td data-label="Last Activity" class="lt-text-xs lt-text-muted">
|
|
<?= $u['last_activity'] ? date('M d, Y H:i', strtotime($u['last_activity'])) : 'Never' ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach;
|
|
endif ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|