/** * Global Error Handler for HR Position System * Provides unified error handling and user-friendly error dialogs */ class ErrorHandler { constructor() { this.init(); } init() { // Create error modal container if not exists if (!document.getElementById('error-modal-container')) { const container = document.createElement('div'); container.id = 'error-modal-container'; document.body.appendChild(container); } // Add error handler CSS this.injectStyles(); // Setup global error handlers this.setupGlobalHandlers(); } injectStyles() { if (document.getElementById('error-handler-styles')) { return; } const style = document.createElement('style'); style.id = 'error-handler-styles'; style.textContent = ` .error-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.3s ease; } .error-modal { background: white; border-radius: 12px; padding: 0; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); animation: slideDown 0.3s ease; overflow: hidden; } .error-modal-header { padding: 20px; border-bottom: 2px solid #f0f0f0; display: flex; align-items: center; gap: 15px; } .error-modal-header.error { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border-bottom-color: #c0392b; } .error-modal-header.warning { background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); color: white; border-bottom-color: #e67e22; } .error-modal-header.info { background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: white; border-bottom-color: #2980b9; } .error-modal-header.success { background: linear-gradient(135deg, #27ae60 0%, #229954 100%); color: white; border-bottom-color: #229954; } .error-icon { font-size: 2rem; line-height: 1; } .error-title { flex: 1; font-size: 1.3rem; font-weight: 600; margin: 0; } .error-modal-body { padding: 25px; max-height: 400px; overflow-y: auto; } .error-message { color: #333; line-height: 1.6; margin-bottom: 15px; font-size: 1rem; } .error-details { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 15px; margin-top: 15px; font-family: 'Courier New', monospace; font-size: 0.85rem; color: #666; max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; } .error-modal-footer { padding: 15px 25px; border-top: 1px solid #f0f0f0; display: flex; justify-content: flex-end; gap: 10px; } .error-btn { padding: 10px 25px; border: none; border-radius: 6px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.3s; font-family: 'Noto Sans TC', sans-serif; } .error-btn-primary { background: #3498db; color: white; } .error-btn-primary:hover { background: #2980b9; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); } .error-btn-secondary { background: #95a5a6; color: white; } .error-btn-secondary:hover { background: #7f8c8d; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideDown { from { transform: translateY(-50px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .error-toast { position: fixed; top: 20px; right: 20px; background: white; border-radius: 8px; padding: 15px 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 12px; z-index: 10001; animation: slideInRight 0.3s ease; max-width: 400px; } .error-toast.error { border-left: 4px solid #e74c3c; } .error-toast.warning { border-left: 4px solid #f39c12; } .error-toast.info { border-left: 4px solid #3498db; } .error-toast.success { border-left: 4px solid #27ae60; } .error-toast-icon { font-size: 1.5rem; } .error-toast-content { flex: 1; } .error-toast-title { font-weight: 600; margin-bottom: 5px; color: #333; } .error-toast-message { font-size: 0.9rem; color: #666; } .error-toast-close { background: none; border: none; font-size: 1.5rem; color: #999; cursor: pointer; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.2s; } .error-toast-close:hover { background: #f0f0f0; color: #666; } @keyframes slideInRight { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style); } setupGlobalHandlers() { // Handle uncaught errors window.addEventListener('error', (event) => { this.handleError({ title: '執行錯誤', message: event.message, details: `檔案: ${event.filename}\n行號: ${event.lineno}:${event.colno}\n錯誤: ${event.error?.stack || event.message}` }); }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { this.handleError({ title: 'Promise 錯誤', message: '發生未處理的 Promise 錯誤', details: event.reason?.stack || event.reason }); }); } /** * Show error modal * @param {Object} options - Error options * @param {string} options.title - Error title * @param {string} options.message - Error message * @param {string} options.details - Error details (optional) * @param {string} options.type - Error type: error, warning, info, success (default: error) * @param {Function} options.onClose - Callback when modal closes */ showError(options) { const { title = '錯誤', message = '發生未知錯誤', details = null, type = 'error', onClose = null } = options; const icons = { error: '❌', warning: '⚠️', info: 'ℹ️', success: '✅' }; const container = document.getElementById('error-modal-container'); const modal = document.createElement('div'); modal.className = 'error-modal-overlay'; modal.innerHTML = `
${icons[type]}

${this.escapeHtml(title)}

${this.escapeHtml(message)}
${details ? `
${this.escapeHtml(details)}
` : ''}
`; container.appendChild(modal); // Store close callback modal._onClose = onClose; // Close on overlay click modal.addEventListener('click', (e) => { if (e.target === modal) { this.closeModal(modal.querySelector('.error-btn')); } }); } /** * Show toast notification * @param {Object} options - Toast options * @param {string} options.title - Toast title * @param {string} options.message - Toast message * @param {string} options.type - Toast type: error, warning, info, success (default: info) * @param {number} options.duration - Duration in ms (default: 3000) */ showToast(options) { const { title = '', message = '', type = 'info', duration = 3000 } = options; const icons = { error: '❌', warning: '⚠️', info: 'ℹ️', success: '✅' }; const toast = document.createElement('div'); toast.className = `error-toast ${type}`; toast.innerHTML = `
${icons[type]}
${title ? `
${this.escapeHtml(title)}
` : ''}
${this.escapeHtml(message)}
`; document.body.appendChild(toast); // Auto remove after duration if (duration > 0) { setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); } } closeModal(button) { const modal = button.closest('.error-modal-overlay'); if (modal) { const onClose = modal._onClose; modal.style.animation = 'fadeOut 0.3s ease'; setTimeout(() => { modal.remove(); if (onClose && typeof onClose === 'function') { onClose(); } }, 300); } } /** * Handle API errors * @param {Error} error - Error object * @param {Object} options - Additional options */ async handleAPIError(error, options = {}) { const { showModal = true, showToast = false } = options; let message = '發生 API 錯誤'; let details = null; if (error.response) { // Server responded with error const data = error.response.data; message = data.error || data.message || `HTTP ${error.response.status} 錯誤`; details = JSON.stringify(data, null, 2); } else if (error.request) { // No response received message = '無法連接到伺服器,請檢查網路連線'; details = error.message; } else { // Request setup error message = error.message; details = error.stack; } if (showModal) { this.showError({ title: 'API 錯誤', message, details, type: 'error' }); } if (showToast) { this.showToast({ title: 'API 錯誤', message, type: 'error' }); } } /** * Handle general errors * @param {Object} options - Error options */ handleError(options) { console.error('[ErrorHandler]', options); this.showError(options); } /** * Escape HTML to prevent XSS * @param {string} text - Text to escape * @returns {string} - Escaped text */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Confirm dialog * @param {Object} options - Confirm options * @param {string} options.title - Title * @param {string} options.message - Message * @param {string} options.confirmText - Confirm button text * @param {string} options.cancelText - Cancel button text * @param {Function} options.onConfirm - Confirm callback * @param {Function} options.onCancel - Cancel callback */ confirm(options) { const { title = '確認', message = '確定要執行此操作嗎?', confirmText = '確定', cancelText = '取消', onConfirm = null, onCancel = null } = options; const container = document.getElementById('error-modal-container'); const modal = document.createElement('div'); modal.className = 'error-modal-overlay'; modal.innerHTML = `

${this.escapeHtml(title)}

${this.escapeHtml(message)}
`; container.appendChild(modal); // Handle button clicks modal.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-action'); modal.remove(); if (action === 'confirm' && onConfirm) { onConfirm(); } else if (action === 'cancel' && onCancel) { onCancel(); } }); }); } } // Initialize global error handler window.errorHandler = new ErrorHandler(); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = ErrorHandler; }