/**
* 夥伴對齊系統 - 前端應用程式
* 包含認證、儀表板、評估、回饋、排名等功能
*/
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 => `
${notification.title}
${notification.message}
${new Date(notification.created_at).toLocaleString()}
${!notification.is_read ? ` ` : ''}
`).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 => `
`).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 => `
${ranking.rank}
${ranking.employee_name}
${ranking.department}
${ranking.position}
${ranking.total_points}
`).join('')}
`;
}
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 => `
${ranking.ranking}
${ranking.employee_name}
${ranking.department}
${ranking.position}
${ranking.monthly_points}
`).join('')}
`;
}
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 = `
用戶管理
新增用戶
ID
用戶名
姓名
部門
職位
狀態
角色
操作
${users.map(user => `
${user.id}
${user.username}
${user.full_name}
${user.department}
${user.position}
${user.is_active ? '啟用' : '停用'}
${user.roles.map(role => `${role} `).join('')}
`).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 => `
${notification.title}
${notification.message}
${formatNotificationTime(notification.created_at)}
${!notification.is_read ? '
新 ' : ''}
`).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 => `
${cap.name}
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 = `
能力名稱
L1
L2
L3
L4
L5
狀態
${data.capabilities.map(cap => `
${cap.name}
${cap.l1_description || '-'}
${cap.l2_description || '-'}
${cap.l3_description || '-'}
${cap.l4_description || '-'}
${cap.l5_description || '-'}
啟用
`).join('')}
共 ${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();