/** * 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 = `