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

@@ -104,6 +104,34 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ticket-subheader">
<div class="ticket-metadata">
<div class="ticket-id">UUID <?php echo $ticket['ticket_id']; ?></div>
<?php
// Calculate ticket age
$lastUpdate = !empty($ticket['updated_at']) ? strtotime($ticket['updated_at']) : strtotime($ticket['created_at']);
$ageSeconds = time() - $lastUpdate;
$ageDays = floor($ageSeconds / 86400);
$ageHours = floor(($ageSeconds % 86400) / 3600);
// Determine age class for styling
$ageClass = 'age-normal';
if ($ticket['status'] !== 'Closed') {
if ($ageDays >= 10) {
$ageClass = 'age-critical';
} elseif ($ageDays >= 5) {
$ageClass = 'age-warning';
}
}
// Format age string
if ($ageDays > 0) {
$ageStr = $ageDays . ' day' . ($ageDays != 1 ? 's' : '');
} else {
$ageStr = $ageHours . ' hour' . ($ageHours != 1 ? 's' : '');
}
?>
<div class="ticket-age <?php echo $ageClass; ?>" title="Time since last update">
<span class="age-icon"><?php echo $ageClass === 'age-critical' ? '⚠️' : ($ageClass === 'age-warning' ? '⏰' : '📅'); ?></span>
<span class="age-text">Last activity: <?php echo $ageStr; ?> ago</span>
</div>
<div class="ticket-user-info" style="font-size: 0.85rem; color: #666; margin-top: 0.25rem;">
<?php
$creator = $ticket['creator_display_name'] ?? $ticket['creator_username'] ?? 'System';
@@ -227,6 +255,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</select>
</div>
<button id="editButton" class="btn">Edit Ticket</button>
<button id="cloneButton" class="btn btn-secondary" title="Create a copy of this ticket">Clone</button>
</div>
</div>
</div>
@@ -483,6 +512,46 @@ $nonce = SecurityHeadersMiddleware::getNonce();
});
}
// Clone button
var cloneBtn = document.getElementById('cloneButton');
if (cloneBtn) {
cloneBtn.addEventListener('click', function() {
if (confirm('Create a copy of this ticket? The new ticket will have the same title, description, priority, category, and type.')) {
cloneBtn.disabled = true;
cloneBtn.textContent = 'Cloning...';
fetch('/api/clone_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
ticket_id: window.ticketData.ticket_id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
toast.success('Ticket cloned successfully!');
setTimeout(function() {
window.location.href = '/ticket/' + data.new_ticket_id;
}, 1000);
} else {
toast.error('Failed to clone ticket: ' + (data.error || 'Unknown error'));
cloneBtn.disabled = false;
cloneBtn.textContent = 'Clone';
}
})
.catch(function(error) {
toast.error('Failed to clone ticket: ' + error.message);
cloneBtn.disabled = false;
cloneBtn.textContent = 'Clone';
});
}
});
}
// Add comment button
var addCommentBtn = document.getElementById('addCommentBtn');
if (addCommentBtn) {