ui improvements, keyboard shortcuts, and toast not
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
81
assets/js/keyboard-shortcuts.js
Normal file
81
assets/js/keyboard-shortcuts.js
Normal 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);
|
||||
}
|
||||
@@ -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
48
assets/js/toast.js
Normal 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)
|
||||
};
|
||||
@@ -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); ?>'>
|
||||
@@ -324,9 +325,19 @@
|
||||
echo "<td>" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
} else {
|
||||
} 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user