/** * 夥伴對齊系統 - 前端應用程式 * 包含認證、儀表板、評估、回饋、排名等功能 */ class PartnerAlignmentApp { constructor() { this.currentUser = null; this.accessToken = localStorage.getItem('accessToken'); this.refreshToken = localStorage.getItem('refreshToken'); this.currentSection = 'dashboard'; this.init(); } init() { this.setupEventListeners(); this.checkAuthentication(); this.setupNavigation(); } setupEventListeners() { // Login/Register modals document.getElementById('loginBtn').addEventListener('click', () => this.showLoginModal()); document.getElementById('showLoginBtn').addEventListener('click', () => this.showLoginModal()); document.getElementById('showRegisterModal').addEventListener('click', () => this.showRegisterModal()); // Forms document.getElementById('loginForm').addEventListener('submit', (e) => this.handleLogin(e)); document.getElementById('registerForm').addEventListener('submit', (e) => this.handleRegister(e)); document.getElementById('assessmentForm').addEventListener('submit', (e) => this.handleAssessmentSubmit(e)); document.getElementById('starFeedbackForm').addEventListener('submit', (e) => this.handleStarFeedbackSubmit(e)); // Logout document.getElementById('logoutBtn').addEventListener('click', () => this.handleLogout()); // Navigation document.querySelectorAll('[data-section]').forEach(link => { link.addEventListener('click', (e) => this.handleNavigation(e)); }); // Ranking filters document.getElementById('applyFilters').addEventListener('click', () => this.loadAdvancedRankings()); // Notifications document.getElementById('markAllReadBtn').addEventListener('click', () => this.markAllNotificationsRead()); // Admin functions document.getElementById('refreshUsersBtn').addEventListener('click', () => this.loadAdminUsers()); // Department capabilities management document.getElementById('deptSelect').addEventListener('change', (e) => this.handleDepartmentSelect(e)); document.getElementById('loadDeptCapabilitiesBtn').addEventListener('click', () => this.loadDepartmentCapabilities()); document.getElementById('saveDeptCapabilitiesBtn').addEventListener('click', () => this.saveDepartmentCapabilities()); // Assessment department change - reload capabilities based on department document.getElementById('assessmentDepartment').addEventListener('change', (e) => this.handleAssessmentDepartmentChange(e)); // Capabilities CSV import document.getElementById('importCsvBtn').addEventListener('click', () => this.importCapabilitiesCsv()); document.getElementById('downloadTemplateCsvBtn').addEventListener('click', () => this.downloadCsvTemplate()); document.getElementById('refreshCapabilitiesBtn').addEventListener('click', () => this.loadCapabilitiesList()); } setupNavigation() { // Set default section this.showSection('dashboard'); } checkAuthentication() { if (this.accessToken) { // 嘗試從 localStorage 恢復用戶信息 const storedUser = localStorage.getItem('currentUser'); if (storedUser) { try { this.currentUser = JSON.parse(storedUser); } catch (e) { console.error('Failed to parse stored user:', e); } } if (this.currentUser) { this.showAuthenticatedUI(); this.loadDashboard(); this.loadNotifications(); } else { this.validateToken(); } } else { this.showLoginRequired(); } } async validateToken() { try { const response = await this.apiCall('/api/auth/protected', 'GET'); if (response.ok) { const data = await response.json(); this.currentUser = data; this.showAuthenticatedUI(); this.loadDashboard(); this.loadNotifications(); } else { this.handleTokenExpired(); } } catch (error) { console.error('Token validation failed:', error); this.handleTokenExpired(); } } handleTokenExpired() { this.clearTokens(); this.showLoginRequired(); this.showToast('登入已過期,請重新登入', 'warning'); } clearTokens() { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); this.accessToken = null; this.refreshToken = null; this.currentUser = null; } showLoginRequired() { document.getElementById('loginRequired').style.display = 'block'; document.querySelectorAll('.section').forEach(section => { section.style.display = 'none'; }); document.getElementById('authNav').style.display = 'block'; document.getElementById('userNav').style.display = 'none'; } showAuthenticatedUI() { // 隱藏登入提示,顯示用戶界面 const loginRequired = document.getElementById('loginRequired'); if (loginRequired) loginRequired.style.display = 'none'; // 隱藏登入按鈕,顯示用戶菜單 const authNav = document.getElementById('authNav'); if (authNav) authNav.style.display = 'none'; const userNav = document.getElementById('userNav'); if (userNav) userNav.style.display = 'block'; // 更新用戶顯示名稱 const userDisplayName = document.getElementById('userDisplayName'); if (userDisplayName && this.currentUser) { userDisplayName.textContent = this.currentUser.full_name || this.currentUser.username || '用戶'; } // 根據權限顯示/隱藏管理功能 const adminNavItem = document.getElementById('adminNavItem'); if (adminNavItem && this.currentUser) { // 檢查用戶名或角色以決定是否顯示管理功能 const username = this.currentUser.username || ''; const hasAdminAccess = username === 'admin' || username === 'hr_manager'; adminNavItem.style.display = hasAdminAccess ? 'block' : 'none'; } // 顯示儀表板 this.showSection('dashboard'); } showLoginModal() { const modal = new bootstrap.Modal(document.getElementById('loginModal')); modal.show(); } showRegisterModal() { const loginModal = bootstrap.Modal.getInstance(document.getElementById('loginModal')); if (loginModal) loginModal.hide(); const registerModal = new bootstrap.Modal(document.getElementById('registerModal')); registerModal.show(); } async handleLogin(e) { e.preventDefault(); const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; try { this.showLoading(true); const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }) }); if (response.ok) { const data = await response.json(); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.currentUser = data.user; // 保存用戶信息 localStorage.setItem('accessToken', this.accessToken); localStorage.setItem('refreshToken', this.refreshToken); localStorage.setItem('currentUser', JSON.stringify(this.currentUser)); // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('loginModal')); if (modal) modal.hide(); // 更新 UI this.showAuthenticatedUI(); this.loadDashboard(); this.loadNotifications(); this.showToast('登入成功!歡迎 ' + data.user.full_name, 'success'); } else { const error = await response.json(); this.showToast(error.error || error.message || '登入失敗', 'error'); } } catch (error) { console.error('Login error:', error); this.showToast('登入時發生錯誤', 'error'); } finally { this.showLoading(false); } } async handleRegister(e) { e.preventDefault(); const formData = { username: document.getElementById('regUsername').value, email: document.getElementById('regEmail').value, password: document.getElementById('regPassword').value, full_name: document.getElementById('regFullName').value, department: document.getElementById('regDepartment').value, position: document.getElementById('regPosition').value, employee_id: document.getElementById('regEmployeeId').value }; try { this.showLoading(true); const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData) }); if (response.ok) { const modal = bootstrap.Modal.getInstance(document.getElementById('registerModal')); modal.hide(); this.showToast('註冊成功!請登入', 'success'); this.showLoginModal(); } else { const error = await response.json(); this.showToast(error.message || '註冊失敗', 'error'); } } catch (error) { console.error('Register error:', error); this.showToast('註冊時發生錯誤', 'error'); } finally { this.showLoading(false); } } handleLogout() { this.clearTokens(); localStorage.removeItem('currentUser'); // 清除用戶信息 this.currentUser = null; this.showLoginRequired(); this.showToast('已登出', 'info'); } handleNavigation(e) { e.preventDefault(); const section = e.currentTarget.getAttribute('data-section'); this.showSection(section); } showSection(sectionName) { // Hide all sections document.querySelectorAll('.section').forEach(section => { section.style.display = 'none'; }); // Show target section const targetSection = document.getElementById(`${sectionName}-section`); if (targetSection) { targetSection.style.display = 'block'; this.currentSection = sectionName; // Update navigation document.querySelectorAll('[data-section]').forEach(link => { link.classList.remove('active'); }); document.querySelector(`[data-section="${sectionName}"]`).classList.add('active'); // Load section-specific data this.loadSectionData(sectionName); } } async loadSectionData(sectionName) { switch (sectionName) { case 'dashboard': await this.loadDashboard(); break; case 'assessment': await this.loadCapabilities(); break; case 'star-feedback': // STAR feedback form is static break; case 'rankings': await this.loadTotalRankings(); await this.loadMonthlyRankings(); break; case 'admin': await this.loadAdminData(); break; } } async loadDashboard() { try { const response = await this.apiCall('/api/dashboard/me', 'GET'); if (response.ok) { const data = await response.json(); this.renderDashboard(data); } } catch (error) { console.error('Failed to load dashboard:', error); } } renderDashboard(data) { const container = document.getElementById('dashboardContent'); container.innerHTML = `

總積分

${data.points_summary.total_points || 0}

本月積分

${data.points_summary.monthly_points || 0}

部門排名

${data.points_summary.department_rank || 'N/A'}

總排名

${data.points_summary.total_rank || 'N/A'}

最近通知
${this.renderNotifications(data.recent_notifications)}
`; } renderNotifications(notifications) { if (!notifications || notifications.length === 0) { return '

暫無通知

'; } return notifications.map(notification => ` `).join(''); } async markNotificationAsRead(notificationId) { try { await this.apiCall(`/api/dashboard/notifications/${notificationId}/read`, 'POST'); this.loadDashboard(); // Reload dashboard to update notifications } catch (error) { console.error('Failed to mark notification as read:', error); } } async loadCapabilities(department = null) { try { let url = '/api/capabilities'; // 如果指定了部門,則只載入該部門選擇的能力項目 if (department) { url = `/api/department-capabilities/${department}`; } const response = await this.apiCall(url, 'GET'); if (response.ok) { const data = await response.json(); this.renderCapabilities(data.capabilities); } } catch (error) { console.error('Failed to load capabilities:', error); } } async handleAssessmentDepartmentChange(e) { const department = e.target.value; const container = document.getElementById('capabilitiesContainer'); if (!department) { container.innerHTML = `
請先選擇部門以載入對應的能力評估項目
`; return; } // 根據選擇的部門載入能力項目 await this.loadCapabilities(department); } renderCapabilities(capabilities) { const container = document.getElementById('capabilitiesContainer'); if (!capabilities || capabilities.length === 0) { container.innerHTML = `
該部門尚未設定能力評估項目。請聯繫管理員進行設定。
`; return; } container.innerHTML = capabilities.map(capability => `
${capability.name}
`).join(''); } async handleAssessmentSubmit(e) { e.preventDefault(); const formData = { department: document.getElementById('assessmentDepartment').value, position: document.getElementById('assessmentPosition').value, employee_name: document.getElementById('assessmentEmployeeName').value, assessment_data: this.collectAssessmentData() }; try { this.showLoading(true); const response = await this.apiCall('/api/assessments', 'POST', formData); if (response.ok) { this.showToast('評估提交成功!', 'success'); document.getElementById('assessmentForm').reset(); } else { const error = await response.json(); this.showToast(error.error || '提交失敗', 'error'); } } catch (error) { console.error('Assessment submission error:', error); this.showToast('提交時發生錯誤', 'error'); } finally { this.showLoading(false); } } collectAssessmentData() { const data = {}; document.querySelectorAll('[name^="capability_"]:checked').forEach(radio => { const capabilityId = radio.name.split('_')[1]; data[capabilityId] = parseInt(radio.value); }); return data; } async handleStarFeedbackSubmit(e) { e.preventDefault(); const formData = { evaluator_name: document.getElementById('evaluatorName').value, evaluatee_name: document.getElementById('evaluateeName').value, evaluatee_department: document.getElementById('evaluateeDepartment').value, evaluatee_position: document.getElementById('evaluateePosition').value, situation: document.getElementById('situation').value, task: document.getElementById('task').value, action: document.getElementById('action').value, result: document.getElementById('result').value, score: parseInt(document.getElementById('score').value) }; try { this.showLoading(true); const response = await this.apiCall('/api/star-feedbacks', 'POST', formData); if (response.ok) { const data = await response.json(); this.showToast(`回饋提交成功!獲得 ${data.points_earned} 積分`, 'success'); document.getElementById('starFeedbackForm').reset(); } else { const error = await response.json(); this.showToast(error.error || '提交失敗', 'error'); } } catch (error) { console.error('STAR feedback submission error:', error); this.showToast('提交時發生錯誤', 'error'); } finally { this.showLoading(false); } } async loadTotalRankings() { try { const department = document.getElementById('totalRankingDepartment').value; const params = department ? `?department=${encodeURIComponent(department)}` : ''; const response = await this.apiCall(`/api/rankings/total${params}`, 'GET'); if (response.ok) { const data = await response.json(); this.renderTotalRankings(data.rankings); } } catch (error) { console.error('Failed to load total rankings:', error); } } renderTotalRankings(rankings) { const container = document.getElementById('totalRankingsList'); if (!rankings || rankings.length === 0) { container.innerHTML = '

暫無排名數據

'; return; } container.innerHTML = `
${rankings.map(ranking => ` `).join('')}
排名 姓名 部門 職位 總積分
${ranking.rank} ${ranking.employee_name} ${ranking.department} ${ranking.position} ${ranking.total_points}
`; } async loadMonthlyRankings() { try { const year = document.getElementById('monthlyRankingYear').value; const month = document.getElementById('monthlyRankingMonth').value; const response = await this.apiCall(`/api/rankings/monthly?year=${year}&month=${month}`, 'GET'); if (response.ok) { const data = await response.json(); this.renderMonthlyRankings(data.rankings); } } catch (error) { console.error('Failed to load monthly rankings:', error); } } renderMonthlyRankings(rankings) { const container = document.getElementById('monthlyRankingsList'); if (!rankings || rankings.length === 0) { container.innerHTML = '

暫無月度排名數據

'; return; } container.innerHTML = `
${rankings.map(ranking => ` `).join('')}
排名 姓名 部門 職位 月度積分
${ranking.ranking} ${ranking.employee_name} ${ranking.department} ${ranking.position} ${ranking.monthly_points}
`; } async loadAdminData() { // Load admin data based on user permissions if (this.currentUser && this.currentUser.roles) { const hasUserManagement = this.currentUser.roles.some(role => ['super_admin', 'admin', 'hr_manager'].includes(role) ); if (hasUserManagement) { await this.loadUsersManagement(); } } } async loadUsersManagement() { try { const response = await this.apiCall('/api/admin/users', 'GET'); if (response.ok) { const data = await response.json(); this.renderUsersManagement(data); } } catch (error) { console.error('Failed to load users management:', error); } } renderUsersManagement(users) { const container = document.getElementById('usersManagement'); container.innerHTML = `
用戶管理
${users.map(user => ` `).join('')}
ID 用戶名 姓名 部門 職位 狀態 角色 操作
${user.id} ${user.username} ${user.full_name} ${user.department} ${user.position} ${user.is_active ? '啟用' : '停用'} ${user.roles.map(role => `${role}`).join('')}
`; } async apiCall(url, method = 'GET', data = null) { const options = { method, headers: { 'Content-Type': 'application/json', } }; if (this.accessToken) { options.headers['Authorization'] = `Bearer ${this.accessToken}`; } if (data) { options.body = JSON.stringify(data); } const response = await fetch(url, options); // Handle token refresh if needed if (response.status === 401 && this.refreshToken) { const refreshed = await this.refreshAccessToken(); if (refreshed) { // Retry the original request options.headers['Authorization'] = `Bearer ${this.accessToken}`; return await fetch(url, options); } } return response; } async refreshAccessToken() { try { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token: this.refreshToken }) }); if (response.ok) { const data = await response.json(); this.accessToken = data.access_token; localStorage.setItem('accessToken', this.accessToken); return true; } } catch (error) { console.error('Token refresh failed:', error); } this.clearTokens(); return false; } showLoading(show) { document.getElementById('loadingSpinner').style.display = show ? 'block' : 'none'; } showToast(message, type = 'info') { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); toastMessage.textContent = message; // Update toast styling based on type const toastHeader = toast.querySelector('.toast-header'); const icon = toastHeader.querySelector('i'); icon.className = `bi me-2`; switch (type) { case 'success': icon.classList.add('bi-check-circle-fill', 'text-success'); break; case 'error': icon.classList.add('bi-exclamation-triangle-fill', 'text-danger'); break; case 'warning': icon.classList.add('bi-exclamation-circle-fill', 'text-warning'); break; default: icon.classList.add('bi-info-circle-fill', 'text-primary'); } const bsToast = new bootstrap.Toast(toast); bsToast.show(); } } // Enhanced dashboard rendering methods function renderRecentActivities(activities) { const container = document.getElementById('recentActivities'); if (!activities || activities.length === 0) { container.innerHTML = `

暫無最近活動

`; return; } const activitiesHtml = activities.map(activity => `
${activity.title}
${activity.description}
${new Date(activity.created_at).toLocaleString()}
+${activity.points || 0}
`).join(''); container.innerHTML = activitiesHtml; } function renderAchievements(achievements) { const container = document.getElementById('achievements'); if (!achievements || achievements.length === 0) { container.innerHTML = `

暫無成就

`; return; } const achievementsHtml = achievements.map(achievement => `
${achievement.name}
${achievement.description}
`).join(''); container.innerHTML = achievementsHtml; } function renderPerformanceChart(performanceData) { const ctx = document.getElementById('pointsChart'); if (!ctx) return; const chartCtx = ctx.getContext('2d'); // Destroy existing chart if it exists if (window.pointsChart) { window.pointsChart.destroy(); } const labels = performanceData.map(item => item.month); const data = performanceData.map(item => item.points); window.pointsChart = new Chart(chartCtx, { type: 'line', data: { labels: labels, datasets: [{ label: '積分', data: data, borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.2)', tension: 0.1 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } } }); } function getActivityIcon(type) { const icons = { 'assessment': 'clipboard-check', 'feedback': 'star-fill', 'achievement': 'trophy-fill', 'ranking': 'award' }; return icons[type] || 'circle'; } function getActivityBadgeColor(type) { const colors = { 'assessment': 'primary', 'feedback': 'success', 'achievement': 'warning', 'ranking': 'info' }; return colors[type] || 'secondary'; } // Advanced ranking system methods async function loadAdvancedRankings() { try { const department = document.getElementById('rankingDepartment').value; const position = document.getElementById('rankingPosition').value; const minPoints = document.getElementById('minPoints').value; const maxPoints = document.getElementById('maxPoints').value; const params = new URLSearchParams(); if (department) params.append('department', department); if (position) params.append('position', position); if (minPoints) params.append('min_points', minPoints); if (maxPoints) params.append('max_points', maxPoints); const response = await fetch(`/api/rankings/advanced?${params.toString()}`); const data = await response.json(); renderAdvancedRankings(data); } catch (error) { console.error('Failed to load advanced rankings:', error); } } function renderAdvancedRankings(data) { // Update statistics if (data.statistics) { document.getElementById('totalCount').textContent = data.statistics.total_count; document.getElementById('avgPoints').textContent = data.statistics.average_points; document.getElementById('medianPoints').textContent = data.statistics.median_points; document.getElementById('maxPoints').textContent = data.statistics.max_points; document.getElementById('minPoints').textContent = data.statistics.min_points; document.getElementById('stdDev').textContent = data.statistics.standard_deviation; document.getElementById('rankingStats').style.display = 'block'; } // Render rankings const container = document.getElementById('rankingsList'); if (!data.rankings || data.rankings.length === 0) { container.innerHTML = `

沒有找到符合條件的排名數據

`; return; } const rankingsHtml = data.rankings.map(ranking => `
#${ranking.rank} ${ranking.percentile}%
${ranking.employee_name}
${ranking.department} - ${ranking.position}
${ranking.tier.name} ${ranking.total_points} 分
本月: ${ranking.monthly_points} 分 ${ranking.vs_average >= 0 ? '+' : ''}${ranking.vs_average} vs 平均
`).join(''); container.innerHTML = rankingsHtml; } function getRankBadgeColor(rank) { if (rank === 1) return 'warning'; if (rank <= 3) return 'success'; if (rank <= 10) return 'info'; return 'secondary'; } // Notification system methods async function loadNotifications() { try { const response = await fetch('/api/notifications'); const data = await response.json(); renderNotifications(data); } catch (error) { console.error('Failed to load notifications:', error); } } function renderNotifications(data) { const container = document.getElementById('notificationsList'); const badge = document.getElementById('notificationBadge'); // Update badge if (data.unread_count > 0) { badge.textContent = data.unread_count; badge.style.display = 'block'; } else { badge.style.display = 'none'; } // Render notifications if (!data.notifications || data.notifications.length === 0) { container.innerHTML = `

暫無通知

`; return; } const notificationsHtml = data.notifications.map(notification => ` `).join(''); container.innerHTML = notificationsHtml; } async function markAllNotificationsRead() { try { const response = await fetch('/api/notifications/read-all', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { // Reload notifications loadNotifications(); showToast('所有通知已標記為已讀', 'success'); } } catch (error) { console.error('Failed to mark all notifications as read:', error); } } function formatNotificationTime(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return '剛剛'; if (diffMins < 60) return `${diffMins}分鐘前`; if (diffHours < 24) return `${diffHours}小時前`; if (diffDays < 7) return `${diffDays}天前`; return date.toLocaleDateString('zh-TW'); } function showToast(message, type = 'info') { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); const toastHeader = toast.querySelector('.toast-header i'); toastMessage.textContent = message; // Update icon based on type const icons = { 'success': 'check-circle-fill text-success', 'error': 'exclamation-triangle-fill text-danger', 'warning': 'exclamation-triangle-fill text-warning', 'info': 'info-circle-fill text-primary' }; toastHeader.className = `bi ${icons[type] || icons.info} me-2`; const bsToast = new bootstrap.Toast(toast); bsToast.show(); } // Admin management methods async function loadAdminData() { try { const response = await fetch('/api/admin/statistics'); const data = await response.json(); renderAdminStatistics(data); } catch (error) { console.error('Failed to load admin statistics:', error); } } function renderAdminStatistics(data) { // Update overview cards document.getElementById('adminTotalUsers').textContent = data.total_users; document.getElementById('adminActiveUsers').textContent = data.active_users; document.getElementById('adminTotalAssessments').textContent = data.total_assessments; document.getElementById('adminTotalFeedbacks').textContent = data.total_feedbacks; // Render department stats const deptContainer = document.getElementById('departmentStats'); if (data.department_stats && data.department_stats.length > 0) { const deptHtml = data.department_stats.map(dept => `
${dept.department} ${dept.count}
`).join(''); deptContainer.innerHTML = deptHtml; } else { deptContainer.innerHTML = '

暫無部門數據

'; } // Render points stats const pointsContainer = document.getElementById('pointsStats'); if (data.points_stats) { pointsContainer.innerHTML = `
平均積分
${data.points_stats.average}
最高積分
${data.points_stats.maximum}
最低積分
${data.points_stats.minimum}
`; } else { pointsContainer.innerHTML = '

暫無積分數據

'; } } async function loadAdminUsers() { try { const response = await fetch('/api/admin/users'); const data = await response.json(); renderAdminUsers(data.users); } catch (error) { console.error('Failed to load admin users:', error); } } function renderAdminUsers(users) { const container = document.getElementById('usersManagement'); if (!users || users.length === 0) { container.innerHTML = `

暫無用戶數據

`; return; } const usersHtml = users.map(user => `
${user.full_name}
${user.username} | ${user.email}
${user.department} - ${user.position}
${user.is_active ? '活躍' : '停用'}
員工編號: ${user.employee_id}
`).join(''); container.innerHTML = usersHtml; } // =================================== // Department Capabilities Management // =================================== PartnerAlignmentApp.prototype.handleDepartmentSelect = async function(e) { const department = e.target.value; const loadBtn = document.getElementById('loadDeptCapabilitiesBtn'); const saveBtn = document.getElementById('saveDeptCapabilitiesBtn'); if (!department) { loadBtn.disabled = true; saveBtn.disabled = true; document.getElementById('capabilitiesCheckboxList').innerHTML = `

請先選擇部門

`; return; } loadBtn.disabled = false; saveBtn.disabled = false; // 自動載入所有能力項目供選擇 await this.loadAllCapabilitiesForSelection(); // 自動載入該部門已選擇的能力項目 await this.loadDepartmentCapabilities(); } PartnerAlignmentApp.prototype.loadAllCapabilitiesForSelection = async function() { const container = document.getElementById('capabilitiesCheckboxList'); try { container.innerHTML = '
'; const response = await fetch('/api/capabilities'); const data = await response.json(); if (data.capabilities && data.capabilities.length > 0) { container.innerHTML = `
請勾選該部門要進行評分的能力項目
${data.capabilities.map(cap => `
L1: ${cap.l1_description || 'N/A'}
L2: ${cap.l2_description || 'N/A'}
L3: ${cap.l3_description || 'N/A'}
L4: ${cap.l4_description || 'N/A'}
L5: ${cap.l5_description || 'N/A'}
`).join('')}
`; } else { container.innerHTML = `
目前沒有可用的能力項目。請先由系統管理員建立能力項目。
`; } } catch (error) { console.error('載入能力項目失敗:', error); this.showError('載入能力項目時發生錯誤'); container.innerHTML = `
載入失敗,請稍後再試
`; } } PartnerAlignmentApp.prototype.loadDepartmentCapabilities = async function() { const department = document.getElementById('deptSelect').value; if (!department) { this.showError('請先選擇部門'); return; } try { const response = await fetch(`/api/department-capabilities/${department}`); const data = await response.json(); // 取消所有勾選 document.querySelectorAll('.capability-checkbox').forEach(cb => { cb.checked = false; }); // 勾選該部門已選擇的能力項目 if (data.capabilities && data.capabilities.length > 0) { data.capabilities.forEach(cap => { const checkbox = document.getElementById(`cap_${cap.id}`); if (checkbox) { checkbox.checked = true; } }); this.showSuccess(`已載入 ${department} 部門的能力項目設定 (${data.capabilities.length} 項)`); } else { this.showInfo(`${department} 部門尚未設定能力項目`); } } catch (error) { console.error('載入部門能力設定失敗:', error); this.showError('載入部門能力設定時發生錯誤'); } } PartnerAlignmentApp.prototype.saveDepartmentCapabilities = async function() { const department = document.getElementById('deptSelect').value; if (!department) { this.showError('請先選擇部門'); return; } // 收集已勾選的能力項目ID const selectedCapabilities = []; document.querySelectorAll('.capability-checkbox:checked').forEach(cb => { selectedCapabilities.push(parseInt(cb.value)); }); if (selectedCapabilities.length === 0) { this.showError('請至少選擇一個能力項目'); return; } try { const response = await fetch(`/api/department-capabilities/${department}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ capability_ids: selectedCapabilities }) }); const data = await response.json(); if (response.ok) { this.showSuccess(data.message || '儲存成功'); } else { this.showError(data.error || '儲存失敗'); } } catch (error) { console.error('儲存部門能力設定失敗:', error); this.showError('儲存時發生錯誤'); } } // =================================== // Capabilities CSV Import & Management // =================================== PartnerAlignmentApp.prototype.importCapabilitiesCsv = async function() { const fileInput = document.getElementById('csvFileInput'); const file = fileInput.files[0]; if (!file) { this.showError('請選擇 CSV 檔案'); return; } const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/capabilities/import-csv', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { this.showSuccess(data.message); if (data.errors && data.errors.length > 0) { console.warn('匯入警告:', data.errors); alert('匯入完成,但有以下警告:\n' + data.errors.join('\n')); } // 清空檔案選擇 fileInput.value = ''; // 自動刷新能力項目列表 await this.loadCapabilitiesList(); } else { this.showError(data.error || '匯入失敗'); } } catch (error) { console.error('CSV 匯入失敗:', error); this.showError('匯入時發生錯誤'); } } PartnerAlignmentApp.prototype.downloadCsvTemplate = function() { // 建立 CSV 範本內容 const csvContent = `name,l1_description,l2_description,l3_description,l4_description,l5_description 溝通能力,基本溝通,有效溝通,專業溝通,領導溝通,戰略溝通 技術能力,基礎技術,熟練技術,專業技術,專家技術,大師技術 領導能力,自我管理,團隊協作,團隊領導,部門領導,戰略領導`; // 建立 Blob 並下載 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', 'capabilities_template.csv'); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); this.showSuccess('CSV 範本已下載'); } PartnerAlignmentApp.prototype.loadCapabilitiesList = async function() { const container = document.getElementById('capabilitiesList'); try { container.innerHTML = '
'; const response = await fetch('/api/capabilities'); const data = await response.json(); if (data.capabilities && data.capabilities.length > 0) { container.innerHTML = `
${data.capabilities.map(cap => ` `).join('')}
能力名稱 L1 L2 L3 L4 L5 狀態
${cap.name} ${cap.l1_description || '-'} ${cap.l2_description || '-'} ${cap.l3_description || '-'} ${cap.l4_description || '-'} ${cap.l5_description || '-'} 啟用
共 ${data.capabilities.length} 個能力項目
`; } else { container.innerHTML = `
目前沒有能力項目。請使用 CSV 匯入功能新增。
`; } } catch (error) { console.error('載入能力項目列表失敗:', error); this.showError('載入能力項目列表時發生錯誤'); container.innerHTML = `
載入失敗,請稍後再試
`; } } // Global function to fill login form with test account credentials function fillLoginForm(username, password) { document.getElementById('loginUsername').value = username; document.getElementById('loginPassword').value = password; // Show a brief notification const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); toastMessage.textContent = `已填入 ${username} 的登入資訊`; const bsToast = new bootstrap.Toast(toast); bsToast.show(); } // Initialize the application const app = new PartnerAlignmentApp();