diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 348631a..d8feca7 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -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); +} diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index ec3e066..4d92b19 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -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 = ''; + }); + }); +}); diff --git a/assets/js/keyboard-shortcuts.js b/assets/js/keyboard-shortcuts.js new file mode 100644 index 0000000..e27710e --- /dev/null +++ b/assets/js/keyboard-shortcuts.js @@ -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); +} diff --git a/assets/js/ticket.js b/assets/js/ticket.js index e2d241a..8afd663 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -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; }); diff --git a/assets/js/toast.js b/assets/js/toast.js new file mode 100644 index 0000000..8b5775a --- /dev/null +++ b/assets/js/toast.js @@ -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 = ` + + + `; + + // 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) +}; diff --git a/views/DashboardView.php b/views/DashboardView.php index 5d24c4a..a5a235c 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -11,6 +11,7 @@ +
@@ -324,9 +325,19 @@ echo ""; + echo "╔════════════════════════════════════════╗\n"; + echo "║ ║\n"; + echo "║ NO TICKETS FOUND ║\n"; + echo "║ ║\n"; + echo "║ [ ] Empty queue - all clear! ║\n"; + echo "║ ║\n"; + echo "╚════════════════════════════════════════╝"; + echo ""; + echo "