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;
|
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);
|
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.className = `status-${data.status}`;
|
||||||
statusDisplay.textContent = data.status;
|
statusDisplay.textContent = data.status;
|
||||||
}
|
}
|
||||||
console.log('Ticket updated successfully');
|
toast.success('Ticket updated successfully');
|
||||||
} else {
|
} else {
|
||||||
console.error('Error in API response:', data.error || 'Unknown error');
|
console.error('Error in API response:', data.error || 'Unknown error');
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ function handleAssignmentChange() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
alert('Error updating assignment');
|
toast.error('Error updating assignment');
|
||||||
console.error(data.error);
|
console.error(data.error);
|
||||||
} else {
|
} else {
|
||||||
console.log('Assignment updated successfully');
|
console.log('Assignment updated successfully');
|
||||||
@@ -297,7 +297,7 @@ function handleAssignmentChange() {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error updating assignment:', 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(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
alert(`Error updating ${fieldName}`);
|
toast.error(`Error updating ${fieldName}`);
|
||||||
console.error(data.error);
|
console.error(data.error);
|
||||||
} else {
|
} else {
|
||||||
console.log(`${fieldName} updated successfully to:`, newValue);
|
console.log(`${fieldName} updated successfully to:`, newValue);
|
||||||
@@ -351,7 +351,7 @@ function handleMetadataChanges() {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`Error updating ${fieldName}:`, 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);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
console.error('Error updating status:', data.error || 'Unknown error');
|
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
|
// Reset to current status
|
||||||
statusSelect.selectedIndex = 0;
|
statusSelect.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error updating status:', error);
|
console.error('Error updating status:', error);
|
||||||
alert('Error updating status: ' + error.message);
|
toast.error('Error updating status: ' + error.message);
|
||||||
// Reset to current status
|
// Reset to current status
|
||||||
statusSelect.selectedIndex = 0;
|
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="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/dashboard.css">
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
<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>
|
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body data-categories='<?php echo json_encode($categories); ?>' data-types='<?php echo json_encode($types); ?>'>
|
<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 "<td>" . date('Y-m-d H:i', strtotime($row['updated_at'])) . "</td>";
|
||||||
echo "</tr>";
|
echo "</tr>";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$colspan = ($GLOBALS['currentUser']['is_admin'] ?? false) ? '11' : '10';
|
$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>
|
</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="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/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.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/dashboard.js"></script>
|
||||||
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.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>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user