feat: Replace browser alerts with terminal-aesthetic notifications
Replaced all native browser dialogs with custom terminal-style UI:
**Utility Functions** (dashboard.js):
- showConfirmModal() - Reusable confirmation modal with type-based colors
- showInputModal() - Text input modal for user prompts
- Both support keyboard shortcuts (ESC to cancel, Enter to submit)
**Alert Replacements** (22 instances):
- Validation warnings → toast.warning() (amber, 2s)
- Error messages → toast.error() (red, 5s)
- Success messages → toast.success() or toast.warning() with details
- Example: "Bulk close: 5 succeeded, 2 failed" vs simple "Operation complete"
**Confirm Replacements** (3 instances):
- dashboard.js:509 - Bulk close confirmation → showConfirmModal()
- ticket.js:417 - Status change warning → showConfirmModal()
- advanced-search.js:321 - Delete filter → showConfirmModal('error' type)
**Prompt Replacement** (1 instance):
- advanced-search.js:151 - Save filter name → showInputModal()
**Benefits**:
✓ Visual consistency - matches terminal CRT aesthetic
✓ Non-blocking - toasts don't interrupt workflow
✓ Better UX - different colors for different message types
✓ Keyboard friendly - ESC/Enter support in modals
✓ Reusable - modal functions available for future use
All dialogs maintain retro aesthetic with:
- ASCII borders (╚ ╝)
- Terminal green glow
- Monospace fonts
- Color-coded by type (amber warning, red error, cyan info)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -148,8 +148,15 @@ function resetAdvancedSearch() {
|
||||
|
||||
// Save current search as a filter
|
||||
async function saveCurrentFilter() {
|
||||
const filterName = prompt('Enter a name for this filter:');
|
||||
if (!filterName || filterName.trim() === '') return;
|
||||
showInputModal(
|
||||
'Save Search Filter',
|
||||
'Enter a name for this filter:',
|
||||
'My Filter',
|
||||
async (filterName) => {
|
||||
if (!filterName || filterName.trim() === '') {
|
||||
toast.warning('Filter name cannot be empty', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const filterCriteria = getCurrentFilterCriteria();
|
||||
|
||||
@@ -169,21 +176,17 @@ async function saveCurrentFilter() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.success(`Filter "${filterName}" saved successfully!`);
|
||||
}
|
||||
toast.success(`Filter "${filterName}" saved successfully!`, 3000);
|
||||
loadSavedFilters();
|
||||
} else {
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.error('Failed to save filter: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
toast.error('Failed to save filter: ' + (result.error || 'Unknown error'), 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving filter:', error);
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.error('Error saving filter');
|
||||
toast.error('Error saving filter', 4000);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Get current filter criteria from form
|
||||
@@ -315,10 +318,11 @@ async function deleteSavedFilter() {
|
||||
const filterId = selectedOption.value;
|
||||
const filterName = selectedOption.textContent;
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the filter "${filterName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showConfirmModal(
|
||||
`Delete Filter "${filterName}"?`,
|
||||
'This action cannot be undone.',
|
||||
'error',
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch('/api/saved_filters.php', {
|
||||
method: 'DELETE',
|
||||
@@ -332,22 +336,18 @@ async function deleteSavedFilter() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.success('Filter deleted successfully');
|
||||
}
|
||||
toast.success('Filter deleted successfully', 3000);
|
||||
loadSavedFilters();
|
||||
resetAdvancedSearch();
|
||||
} else {
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.error('Failed to delete filter');
|
||||
}
|
||||
toast.error('Failed to delete filter', 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting filter:', error);
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.error('Error deleting filter');
|
||||
toast.error('Error deleting filter', 4000);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Keyboard shortcut (Ctrl+Shift+F)
|
||||
|
||||
@@ -342,12 +342,12 @@ function quickSave() {
|
||||
|
||||
} else {
|
||||
console.error('Error updating ticket:', result.error || 'Unknown error');
|
||||
alert('Error updating ticket: ' + (result.error || 'Unknown error'));
|
||||
toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating ticket:', error);
|
||||
alert('Error updating ticket: ' + error.message);
|
||||
toast.error('Error updating ticket: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -446,12 +446,12 @@ function loadTemplate() {
|
||||
console.log('Template loaded:', template.template_name);
|
||||
} else {
|
||||
console.error('Failed to load template:', data.error);
|
||||
alert('Failed to load template: ' + (data.error || 'Unknown error'));
|
||||
toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading template:', error);
|
||||
alert('Error loading template: ' + error.message);
|
||||
toast.error('Error loading template: ' + error.message, 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -502,13 +502,19 @@ function bulkClose() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
alert('No tickets selected');
|
||||
toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Close ${ticketIds.length} ticket(s)?`)) {
|
||||
return;
|
||||
}
|
||||
showConfirmModal(
|
||||
`Close ${ticketIds.length} Ticket(s)?`,
|
||||
'Are you sure you want to close these tickets?',
|
||||
'warning',
|
||||
() => performBulkCloseAction(ticketIds)
|
||||
);
|
||||
}
|
||||
|
||||
function performBulkCloseAction(ticketIds) {
|
||||
|
||||
fetch('/api/bulk_operation.php', {
|
||||
method: 'POST',
|
||||
@@ -524,15 +530,19 @@ function bulkClose() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Bulk Close Complete:\n${data.processed} succeeded\n${data.failed} failed`);
|
||||
window.location.reload();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error performing bulk close:', error);
|
||||
alert('Error performing bulk close: ' + error.message);
|
||||
toast.error('Bulk close failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -540,7 +550,7 @@ function showBulkAssignModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
alert('No tickets selected');
|
||||
toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -610,7 +620,7 @@ function performBulkAssign() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!userId) {
|
||||
alert('Please select a user');
|
||||
toast.warning('Please select a user', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -629,16 +639,20 @@ function performBulkAssign() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Bulk Assign Complete:\n${data.processed} succeeded\n${data.failed} failed`);
|
||||
closeBulkAssignModal();
|
||||
window.location.reload();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error performing bulk assign:', error);
|
||||
alert('Error performing bulk assign: ' + error.message);
|
||||
toast.error('Bulk assign failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,7 +660,7 @@ function showBulkPriorityModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
alert('No tickets selected');
|
||||
toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -701,7 +715,7 @@ function performBulkPriority() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!priority) {
|
||||
alert('Please select a priority');
|
||||
toast.warning('Please select a priority', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -720,16 +734,20 @@ function performBulkPriority() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(`Bulk Priority Update Complete:\n${data.processed} succeeded\n${data.failed} failed`);
|
||||
closeBulkPriorityModal();
|
||||
window.location.reload();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error performing bulk priority update:', error);
|
||||
alert('Error performing bulk priority update: ' + error.message);
|
||||
toast.error('Bulk priority update failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -777,7 +795,7 @@ function showBulkStatusModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
alert('No tickets selected');
|
||||
toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -831,7 +849,7 @@ function performBulkStatusChange() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!status) {
|
||||
alert('Please select a status');
|
||||
toast.warning('Please select a status', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -851,14 +869,19 @@ function performBulkStatusChange() {
|
||||
.then(data => {
|
||||
closeBulkStatusModal();
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error performing bulk status change:', error);
|
||||
alert('Error performing bulk status change: ' + error.message);
|
||||
toast.error('Bulk status change failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -867,7 +890,7 @@ function showBulkDeleteModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
alert('No tickets selected');
|
||||
toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -933,16 +956,196 @@ function performBulkDelete() {
|
||||
.then(data => {
|
||||
closeBulkDeleteModal();
|
||||
if (data.success) {
|
||||
if (typeof toast !== 'undefined') {
|
||||
toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error performing bulk delete:', error);
|
||||
alert('Error performing bulk delete: ' + error.message);
|
||||
toast.error('Bulk delete failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TERMINAL-STYLE MODAL UTILITIES
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Show a terminal-style confirmation modal
|
||||
* @param {string} title - Modal title
|
||||
* @param {string} message - Message body
|
||||
* @param {string} type - 'warning', 'error', 'info' (affects color)
|
||||
* @param {function} onConfirm - Callback when user confirms
|
||||
* @param {function} onCancel - Optional callback when user cancels
|
||||
*/
|
||||
function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel = null) {
|
||||
const modalId = 'confirmModal' + Date.now();
|
||||
|
||||
// Color scheme based on type
|
||||
const colors = {
|
||||
warning: 'var(--terminal-amber)',
|
||||
error: 'var(--status-closed)',
|
||||
info: 'var(--terminal-cyan)'
|
||||
};
|
||||
const color = colors[type] || colors.warning;
|
||||
|
||||
// Icon based on type
|
||||
const icons = {
|
||||
warning: '⚠',
|
||||
error: '✗',
|
||||
info: 'ℹ'
|
||||
};
|
||||
const icon = icons[type] || icons.warning;
|
||||
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="${modalId}">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header" style="color: ${color};">
|
||||
${icon} ${title}
|
||||
</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1.5rem; text-align: center;">
|
||||
<p style="color: var(--terminal-green); white-space: pre-line;">
|
||||
${message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" id="${modalId}_confirm">Confirm</button>
|
||||
<button class="btn btn-secondary" id="${modalId}_cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
const modal = document.getElementById(modalId);
|
||||
const confirmBtn = document.getElementById(`${modalId}_confirm`);
|
||||
const cancelBtn = document.getElementById(`${modalId}_cancel`);
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
if (onConfirm) onConfirm();
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
if (onCancel) onCancel();
|
||||
});
|
||||
|
||||
// ESC key to cancel
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
modal.remove();
|
||||
if (onCancel) onCancel();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a terminal-style input modal
|
||||
* @param {string} title - Modal title
|
||||
* @param {string} label - Input field label
|
||||
* @param {string} placeholder - Input placeholder text
|
||||
* @param {function} onSubmit - Callback with input value when submitted
|
||||
* @param {function} onCancel - Optional callback when cancelled
|
||||
*/
|
||||
function showInputModal(title, label, placeholder = '', onSubmit, onCancel = null) {
|
||||
const modalId = 'inputModal' + Date.now();
|
||||
const inputId = modalId + '_input';
|
||||
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay" id="${modalId}">
|
||||
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
|
||||
<span class="bottom-left-corner">╚</span>
|
||||
<span class="bottom-right-corner">╝</span>
|
||||
|
||||
<div class="ascii-section-header">
|
||||
${title}
|
||||
</div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="ascii-frame-inner">
|
||||
<div class="modal-body" style="padding: 1.5rem;">
|
||||
<label for="${inputId}" style="display: block; margin-bottom: 0.5rem; color: var(--terminal-green);">
|
||||
${label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="${inputId}"
|
||||
class="terminal-input"
|
||||
placeholder="${placeholder}"
|
||||
style="width: 100%; padding: 0.5rem; background: var(--bg-primary); border: 1px solid var(--terminal-green); color: var(--terminal-green); font-family: var(--font-mono);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ascii-divider"></div>
|
||||
|
||||
<div class="ascii-content">
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" id="${modalId}_submit">Save</button>
|
||||
<button class="btn btn-secondary" id="${modalId}_cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
const modal = document.getElementById(modalId);
|
||||
const input = document.getElementById(inputId);
|
||||
const submitBtn = document.getElementById(`${modalId}_submit`);
|
||||
const cancelBtn = document.getElementById(`${modalId}_cancel`);
|
||||
|
||||
// Focus input
|
||||
setTimeout(() => input.focus(), 100);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const value = input.value.trim();
|
||||
modal.remove();
|
||||
if (onSubmit) onSubmit(value);
|
||||
};
|
||||
|
||||
submitBtn.addEventListener('click', handleSubmit);
|
||||
|
||||
// Enter key to submit
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
if (onCancel) onCancel();
|
||||
});
|
||||
|
||||
// ESC key to cancel
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
modal.remove();
|
||||
if (onCancel) onCancel();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
}
|
||||
|
||||
@@ -414,14 +414,27 @@ function updateTicketStatus() {
|
||||
|
||||
// Warn if comment is required
|
||||
if (requiresComment) {
|
||||
const proceed = confirm(`This status change requires a comment. Please add a comment explaining the reason for this transition.\n\nProceed with status change to "${newStatus}"?`);
|
||||
if (!proceed) {
|
||||
// Reset to current status
|
||||
showConfirmModal(
|
||||
'Status Change Requires Comment',
|
||||
`This transition to "${newStatus}" requires a comment explaining the reason.\n\nPlease add a comment before changing the status.`,
|
||||
'warning',
|
||||
() => {
|
||||
// User confirmed, proceed with status change
|
||||
performStatusChange(statusSelect, newStatus);
|
||||
},
|
||||
() => {
|
||||
// User cancelled, reset to current status
|
||||
statusSelect.selectedIndex = 0;
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
performStatusChange(statusSelect, newStatus);
|
||||
});
|
||||
|
||||
// Extract status change logic into reusable function
|
||||
function performStatusChange(statusSelect, newStatus) {
|
||||
// Extract ticket ID
|
||||
let ticketId;
|
||||
if (window.location.href.includes('?id=')) {
|
||||
|
||||
Reference in New Issue
Block a user