Add UI enhancements and new features

Keyboard Navigation:
- Add J/K keys for Gmail-style ticket list navigation
- Add N key for new ticket, C for comment focus
- Add G then D for go to dashboard (vim-style)
- Add 1-4 number keys for quick status changes on ticket page
- Add Enter to open selected ticket
- Update keyboard help modal with all new shortcuts

Ticket Age Indicator:
- Show "Last activity: X days ago" on ticket view
- Visual warning (yellow pulse) for tickets idle >5 days
- Critical warning (red pulse) for tickets idle >10 days

Ticket Clone Feature:
- Add "Clone" button on ticket view
- Creates copy with [CLONE] prefix in title
- Preserves description, priority, category, type, visibility
- Automatically creates "relates_to" dependency to original

Active Filter Badges:
- Show visual badges above ticket table for active filters
- Click X on badge to remove individual filter
- "Clear All" button to reset all filters
- Color-coded by filter type (status, priority, search)

Visual Enhancements:
- Add keyboard-selected row highlighting for J/K navigation
- Smooth animations for filter badges

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 19:21:36 -05:00
parent 9b40a714ed
commit 1c1eb19876
7 changed files with 565 additions and 6 deletions

View File

@@ -356,6 +356,49 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<?php endif; ?>
<!-- Active Filters Display -->
<?php
$activeFilters = [];
if (!empty($_GET['status'])) {
$statuses = explode(',', $_GET['status']);
foreach ($statuses as $s) {
$activeFilters[] = ['type' => 'status', 'value' => trim($s), 'label' => 'Status: ' . trim($s)];
}
}
if (!empty($_GET['priority'])) {
$priorities = is_array($_GET['priority']) ? $_GET['priority'] : explode(',', $_GET['priority']);
foreach ($priorities as $p) {
$activeFilters[] = ['type' => 'priority', 'value' => trim($p), 'label' => 'Priority: P' . trim($p)];
}
}
if (!empty($_GET['category'])) {
$activeFilters[] = ['type' => 'category', 'value' => $_GET['category'], 'label' => 'Category: ' . $_GET['category']];
}
if (!empty($_GET['type'])) {
$activeFilters[] = ['type' => 'type', 'value' => $_GET['type'], 'label' => 'Type: ' . $_GET['type']];
}
if (!empty($_GET['assigned_to'])) {
$activeFilters[] = ['type' => 'assigned_to', 'value' => $_GET['assigned_to'], 'label' => 'Assigned To: ' . ($_GET['assigned_to'] === 'unassigned' ? 'Unassigned' : 'User #' . $_GET['assigned_to'])];
}
if (!empty($_GET['search'])) {
$activeFilters[] = ['type' => 'search', 'value' => $_GET['search'], 'label' => 'Search: "' . htmlspecialchars(substr($_GET['search'], 0, 20)) . (strlen($_GET['search']) > 20 ? '...' : '') . '"'];
}
?>
<?php if (!empty($activeFilters)): ?>
<div class="active-filters-bar">
<span class="active-filters-label">Active Filters:</span>
<div class="active-filters-list">
<?php foreach ($activeFilters as $filter): ?>
<span class="filter-badge" data-filter-type="<?php echo htmlspecialchars($filter['type']); ?>" data-filter-value="<?php echo htmlspecialchars($filter['value']); ?>">
<?php echo htmlspecialchars($filter['label']); ?>
<button type="button" class="filter-remove" data-action="remove-filter" data-filter-type="<?php echo htmlspecialchars($filter['type']); ?>" data-filter-value="<?php echo htmlspecialchars($filter['value']); ?>" title="Remove filter">&times;</button>
</span>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-secondary btn-sm" data-action="clear-all-filters">Clear All</button>
</div>
<?php endif; ?>
<!-- Table -->
<div class="table-wrapper">
<table>