ui improvements, keyboard shortcuts, and toast not

This commit is contained in:
2026-01-08 22:49:48 -05:00
parent 1a74536079
commit eda9c61724
7 changed files with 245 additions and 9 deletions

View File

@@ -2548,3 +2548,59 @@ body.dark-mode select option {
box-shadow: none !important;
}
}
/* ===== TERMINAL TOAST NOTIFICATIONS ===== */
.terminal-toast {
position: fixed;
top: 20px;
right: 20px;
background: var(--bg-secondary);
border: 2px solid var(--terminal-green);
padding: 1rem 1.5rem;
font-family: var(--font-mono);
font-size: 0.9rem;
color: var(--terminal-green);
text-shadow: var(--glow-green);
box-shadow: 0 0 20px rgba(0, 255, 65, 0.3);
z-index: 10001;
opacity: 0;
transform: translateX(400px);
transition: all 0.3s ease;
max-width: 400px;
word-wrap: break-word;
}
.terminal-toast.show {
opacity: 1;
transform: translateX(0);
}
.toast-icon {
display: inline-block;
margin-right: 0.5rem;
font-weight: bold;
}
.toast-success {
border-color: var(--status-open);
color: var(--status-open);
text-shadow: 0 0 5px var(--status-open);
}
.toast-error {
border-color: var(--status-closed);
color: var(--status-closed);
text-shadow: 0 0 5px var(--status-closed);
}
.toast-warning {
border-color: var(--status-in-progress);
color: var(--status-in-progress);
text-shadow: 0 0 5px var(--status-in-progress);
}
.toast-info {
border-color: var(--terminal-cyan);
color: var(--terminal-cyan);
text-shadow: 0 0 5px var(--terminal-cyan);
}

View File

@@ -775,3 +775,42 @@ function performBulkPriority() {
alert('Error performing bulk priority update: ' + error.message);
});
}
// Make table rows clickable
document.addEventListener('DOMContentLoaded', function() {
const tableRows = document.querySelectorAll('tbody tr');
tableRows.forEach(row => {
// Skip if row already has click handler
if (row.dataset.clickable) return;
row.dataset.clickable = 'true';
row.style.cursor = 'pointer';
row.addEventListener('click', function(e) {
// Don't navigate if clicking on a link, button, checkbox, or select
if (e.target.tagName === 'A' ||
e.target.tagName === 'BUTTON' ||
e.target.tagName === 'INPUT' ||
e.target.tagName === 'SELECT' ||
e.target.closest('a') ||
e.target.closest('button')) {
return;
}
// Find the ticket link in the row
const ticketLink = row.querySelector('.ticket-link');
if (ticketLink) {
window.location.href = ticketLink.href;
}
});
// Add hover effect
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = 'rgba(0, 255, 65, 0.08)';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
});

View File

@@ -0,0 +1,81 @@
/**
* Keyboard shortcuts for power users
*/
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('keydown', function(e) {
// Skip if user is typing in an input/textarea
if (e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' ||
e.target.isContentEditable) {
// Allow ESC to exit edit mode even when in input
if (e.key === 'Escape') {
e.target.blur();
const editButton = document.getElementById('editButton');
if (editButton && editButton.classList.contains('active')) {
editButton.click();
}
}
return;
}
// Ctrl/Cmd + E: Toggle edit mode (on ticket pages)
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
e.preventDefault();
const editButton = document.getElementById('editButton');
if (editButton) {
editButton.click();
toast.info('Edit mode ' + (editButton.classList.contains('active') ? 'enabled' : 'disabled'));
}
}
// Ctrl/Cmd + S: Save ticket (on ticket pages)
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
const editButton = document.getElementById('editButton');
if (editButton && editButton.classList.contains('active')) {
editButton.click();
toast.success('Saving ticket...');
}
}
// ESC: Cancel edit mode
if (e.key === 'Escape') {
const editButton = document.getElementById('editButton');
if (editButton && editButton.classList.contains('active')) {
// Reset without saving
window.location.reload();
}
}
// Ctrl/Cmd + K: Focus search (on dashboard)
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchBox = document.querySelector('.search-box');
if (searchBox) {
searchBox.focus();
searchBox.select();
}
}
// ? : Show keyboard shortcuts help
if (e.key === '?' && !e.shiftKey) {
showKeyboardHelp();
}
});
});
function showKeyboardHelp() {
const helpText = `
╔════════════════════════════════════════╗
║ KEYBOARD SHORTCUTS ║
╠════════════════════════════════════════╣
║ Ctrl/Cmd + E : Toggle Edit Mode ║
║ Ctrl/Cmd + S : Save Changes ║
║ Ctrl/Cmd + K : Focus Search ║
║ ESC : Cancel Edit/Close ║
║ ? : Show This Help ║
╚════════════════════════════════════════╝
`;
toast.info(helpText, 5000);
}

View File

@@ -56,7 +56,7 @@ function saveTicket() {
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
}
console.log('Ticket updated successfully');
toast.success('Ticket updated successfully');
} else {
console.error('Error in API response:', data.error || 'Unknown error');
}
@@ -289,7 +289,7 @@ function handleAssignmentChange() {
.then(response => response.json())
.then(data => {
if (!data.success) {
alert('Error updating assignment');
toast.error('Error updating assignment');
console.error(data.error);
} else {
console.log('Assignment updated successfully');
@@ -297,7 +297,7 @@ function handleAssignmentChange() {
})
.catch(error => {
console.error('Error updating assignment:', error);
alert('Error updating assignment: ' + error.message);
toast.error('Error updating assignment: ' + error.message);
});
});
}
@@ -325,7 +325,7 @@ function handleMetadataChanges() {
.then(response => response.json())
.then(data => {
if (!data.success) {
alert(`Error updating ${fieldName}`);
toast.error(`Error updating ${fieldName}`);
console.error(data.error);
} else {
console.log(`${fieldName} updated successfully to:`, newValue);
@@ -351,7 +351,7 @@ function handleMetadataChanges() {
})
.catch(error => {
console.error(`Error updating ${fieldName}:`, error);
alert(`Error updating ${fieldName}: ` + error.message);
toast.error(`Error updating ${fieldName}: ` + error.message);
});
}
@@ -467,14 +467,14 @@ function updateTicketStatus() {
}, 500);
} else {
console.error('Error updating status:', data.error || 'Unknown error');
alert('Error updating status: ' + (data.error || 'Unknown error'));
toast.error('Error updating status: ' + (data.error || 'Unknown error'));
// Reset to current status
statusSelect.selectedIndex = 0;
}
})
.catch(error => {
console.error('Error updating status:', error);
alert('Error updating status: ' + error.message);
toast.error('Error updating status: ' + error.message);
// Reset to current status
statusSelect.selectedIndex = 0;
});

48
assets/js/toast.js Normal file
View File

@@ -0,0 +1,48 @@
/**
* Terminal-style toast notification system
*/
function showToast(message, type = 'info', duration = 3000) {
// Remove any existing toasts
const existingToast = document.querySelector('.terminal-toast');
if (existingToast) {
existingToast.remove();
}
// Create toast element
const toast = document.createElement('div');
toast.className = `terminal-toast toast-${type}`;
// Icon based on type
const icons = {
success: '✓',
error: '✗',
info: '',
warning: '⚠'
};
toast.innerHTML = `
<span class="toast-icon">[${icons[type] || ''}]</span>
<span class="toast-message">${message}</span>
`;
// Add to document
document.body.appendChild(toast);
// Trigger animation
setTimeout(() => toast.classList.add('show'), 10);
// Auto-remove after duration
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
// Convenience functions
window.toast = {
success: (msg, duration) => showToast(msg, 'success', duration),
error: (msg, duration) => showToast(msg, 'error', duration),
info: (msg, duration) => showToast(msg, 'info', duration),
warning: (msg, duration) => showToast(msg, 'warning', duration)
};

View File

@@ -11,6 +11,7 @@
<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">
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
</head>
<body data-categories='<?php echo json_encode($categories); ?>' data-types='<?php echo json_encode($types); ?>'>
@@ -326,7 +327,17 @@
}
} else {
$colspan = ($GLOBALS['currentUser']['is_admin'] ?? false) ? '11' : '10';
echo "<tr><td colspan='$colspan'>No tickets found</td></tr>";
echo "<tr><td colspan='$colspan' style='text-align: center; padding: 3rem;'>";
echo "<pre style='color: var(--terminal-green); text-shadow: var(--glow-green); font-size: 0.8rem; line-height: 1.2;'>";
echo "╔════════════════════════════════════════╗\n";
echo "║ ║\n";
echo "║ NO TICKETS FOUND ║\n";
echo "║ ║\n";
echo "║ [ ] Empty queue - all clear! ║\n";
echo "║ ║\n";
echo "╚════════════════════════════════════════╝";
echo "</pre>";
echo "</td></tr>";
}
?>
</tbody>

View File

@@ -48,6 +48,7 @@ function formatDetails($details, $actionType) {
<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 src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>