feat: Add 9 new features for enhanced UX and security

Quick Wins:
- Feature 1: Ticket linking in comments (#123456789 auto-links)
- Feature 6: Checkbox click area fix (click anywhere in cell)
- Feature 7: User groups display in settings modal

UI Enhancements:
- Feature 4: Collapsible sidebar with localStorage persistence
- Feature 5: Inline ticket preview popup on hover (300ms delay)
- Feature 2: Mobile responsive improvements (44px touch targets, iOS zoom fix)

Major Features:
- Feature 3: Kanban card view with status columns (toggle with localStorage)
- Feature 9: API key generation admin panel (/admin/api-keys)
- Feature 8: Ticket visibility levels (public/internal/confidential)

New files:
- views/admin/ApiKeysView.php
- api/generate_api_key.php
- api/revoke_api_key.php
- migrations/008_ticket_visibility.sql

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 10:01:50 -05:00
parent c32e9c871b
commit e86a5de3fd
19 changed files with 1933 additions and 39 deletions

View File

@@ -64,14 +64,14 @@ function formatDetails($details, $actionType) {
window.APP_TIMEZONE_ABBREV = '<?php echo $GLOBALS['config']['TIMEZONE_ABBREV']; ?>';
</script>
<script>
// Store ticket data in a global variable
// Store ticket data in a global variable (using json_encode for XSS safety)
window.ticketData = {
ticket_id: "<?php echo $ticket['ticket_id']; ?>",
title: "<?php echo htmlspecialchars($ticket['title']); ?>",
status: "<?php echo $ticket['status']; ?>",
priority: "<?php echo $ticket['priority']; ?>",
category: "<?php echo $ticket['category']; ?>",
type: "<?php echo $ticket['type']; ?>"
ticket_id: <?php echo json_encode($ticket['ticket_id']); ?>,
title: <?php echo json_encode($ticket['title']); ?>,
status: <?php echo json_encode($ticket['status']); ?>,
priority: <?php echo json_encode($ticket['priority']); ?>,
category: <?php echo json_encode($ticket['category']); ?>,
type: <?php echo json_encode($ticket['type']); ?>
};
</script>
</head>
@@ -167,6 +167,38 @@ function formatDetails($details, $actionType) {
</select>
</div>
</div>
<!-- Visibility Settings -->
<div class="ticket-visibility-settings" style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--terminal-green);">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem;">
<div class="metadata-field">
<label style="font-weight: 500; display: block; margin-bottom: 0.25rem; color: var(--terminal-cyan); font-family: var(--font-mono); font-size: 0.85rem;">Visibility:</label>
<select id="visibilitySelect" class="metadata-select editable-metadata" disabled onchange="toggleVisibilityGroups()" style="width: 100%; padding: 0.25rem 0.5rem; border-radius: 0; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
<?php $currentVisibility = $ticket['visibility'] ?? 'public'; ?>
<option value="public" <?php echo $currentVisibility == 'public' ? 'selected' : ''; ?>>Public</option>
<option value="internal" <?php echo $currentVisibility == 'internal' ? 'selected' : ''; ?>>Internal</option>
<option value="confidential" <?php echo $currentVisibility == 'confidential' ? 'selected' : ''; ?>>Confidential</option>
</select>
</div>
<div class="metadata-field" id="visibilityGroupsField" style="<?php echo $currentVisibility !== 'internal' ? 'opacity: 0.5;' : ''; ?>">
<label style="font-weight: 500; display: block; margin-bottom: 0.25rem; color: var(--terminal-cyan); font-family: var(--font-mono); font-size: 0.85rem;">Allowed Groups:</label>
<div class="visibility-groups-display" style="font-family: var(--font-mono); font-size: 0.85rem; padding: 0.25rem;">
<?php
$visibilityGroups = array_filter(array_map('trim', explode(',', $ticket['visibility_groups'] ?? '')));
if (!empty($visibilityGroups)):
foreach ($visibilityGroups as $group):
?>
<span class="group-badge" style="font-size: 0.75rem;"><?php echo htmlspecialchars($group); ?></span>
<?php
endforeach;
else:
?>
<span style="color: var(--text-muted);"><?php echo $currentVisibility === 'internal' ? 'No groups selected' : '-'; ?></span>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<div class="header-controls">
<div class="status-priority-group">
@@ -395,15 +427,8 @@ function formatDetails($details, $actionType) {
});
</script>
<script>
// Make ticket data available to JavaScript
window.ticketData = {
id: <?php echo json_encode($ticket['ticket_id']); ?>,
status: <?php echo json_encode($ticket['status']); ?>,
priority: <?php echo json_encode($ticket['priority']); ?>,
category: <?php echo json_encode($ticket['category']); ?>,
type: <?php echo json_encode($ticket['type']); ?>,
title: <?php echo json_encode($ticket['title']); ?>
};
// Ticket data already initialized in head, add id alias for compatibility
window.ticketData.id = window.ticketData.ticket_id;
console.log('Ticket data loaded:', window.ticketData);
</script>
@@ -515,6 +540,23 @@ function formatDetails($details, $actionType) {
<div><strong>Role:</strong></div>
<div><?php echo $GLOBALS['currentUser']['is_admin'] ? 'Administrator' : 'User'; ?></div>
<div><strong>Groups:</strong></div>
<div class="user-groups-list">
<?php
$groups = explode(',', $GLOBALS['currentUser']['groups'] ?? '');
foreach ($groups as $g):
if (trim($g)):
?>
<span class="group-badge"><?php echo htmlspecialchars(trim($g)); ?></span>
<?php
endif;
endforeach;
if (empty(trim($GLOBALS['currentUser']['groups'] ?? ''))):
?>
<span style="color: var(--text-muted);">No groups assigned</span>
<?php endif; ?>
</div>
</div>
</div>
</div>