Initial commit

This commit is contained in:
2025-10-28 15:50:53 +08:00
commit 297ef231c5
31 changed files with 12708 additions and 0 deletions

324
static/js/admin.js Normal file
View File

@@ -0,0 +1,324 @@
// Admin functionality JavaScript
class AdminManager {
constructor() {
this.initializeEventListeners();
}
initializeEventListeners() {
// Export buttons
document.addEventListener('click', (e) => {
if (e.target.matches('[onclick*="exportData"]')) {
e.preventDefault();
const onclick = e.target.getAttribute('onclick');
const matches = onclick.match(/exportData\('([^']+)',\s*'([^']+)'\)/);
if (matches) {
this.exportData(matches[1], matches[2]);
}
}
});
// Calculate rankings button
document.addEventListener('click', (e) => {
if (e.target.matches('[onclick*="calculateMonthlyRankings"]')) {
e.preventDefault();
this.calculateMonthlyRankings();
}
});
}
async exportData(type, format) {
try {
this.showLoading(true, `正在匯出${type === 'assessments' ? '評估資料' : 'STAR回饋'}...`);
const url = `/api/export/${type}?format=${format}`;
// Create a temporary link to download the file
const link = document.createElement('a');
link.href = url;
link.download = `${type}_${new Date().toISOString().split('T')[0]}.${format === 'excel' ? 'xlsx' : 'csv'}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showSuccess(`${type === 'assessments' ? '評估資料' : 'STAR回饋'}匯出成功`);
} catch (error) {
console.error('Export failed:', error);
this.showError('匯出失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
async calculateMonthlyRankings() {
try {
const year = document.getElementById('calc-year').value;
const month = document.getElementById('calc-month').value;
if (!year || !month) {
this.showError('請選擇年份和月份');
return;
}
this.showLoading(true, '正在計算月度排名...');
const response = await fetch('/api/rankings/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
year: parseInt(year),
month: parseInt(month)
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
this.showSuccess(result.message);
} catch (error) {
console.error('Calculate rankings failed:', error);
this.showError('排名計算失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
showLoading(show, message = '處理中...') {
// You can implement a more sophisticated loading indicator here
if (show) {
console.log('Loading:', message);
} else {
console.log('Loading finished');
}
}
showSuccess(message) {
console.log('Success:', message);
if (window.showSuccess) {
window.showSuccess(message);
}
}
showError(message) {
console.error('Error:', message);
if (window.showError) {
window.showError(message);
}
}
}
// Data management utilities
class DataManager {
constructor() {
this.initializeDataManagement();
}
initializeDataManagement() {
// Add any data management functionality here
this.setupDataValidation();
}
setupDataValidation() {
// Validate data integrity
this.validateDataIntegrity();
}
async validateDataIntegrity() {
try {
// Check for orphaned records, missing references, etc.
console.log('Validating data integrity...');
// You can add specific validation logic here
} catch (error) {
console.error('Data validation failed:', error);
}
}
async getSystemStats() {
try {
const [assessments, feedbacks, employees] = await Promise.all([
fetch('/api/assessments?per_page=1').then(r => r.json()),
fetch('/api/star-feedbacks?per_page=1').then(r => r.json()),
fetch('/api/rankings/total?limit=1').then(r => r.json())
]);
return {
totalAssessments: assessments.total || 0,
totalFeedbacks: feedbacks.total || 0,
totalEmployees: employees.rankings.length || 0
};
} catch (error) {
console.error('Failed to get system stats:', error);
return null;
}
}
}
// Export functionality
class ExportManager {
constructor() {
this.supportedFormats = ['excel', 'csv'];
}
async exportAssessments(format = 'excel') {
return this.exportData('assessments', format);
}
async exportStarFeedbacks(format = 'excel') {
return this.exportData('star-feedbacks', format);
}
async exportData(type, format) {
if (!this.supportedFormats.includes(format)) {
throw new Error(`Unsupported format: ${format}`);
}
const url = `/api/export/${type}?format=${format}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Export failed: ${response.status}`);
}
// Get filename from response headers or create default
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `${type}_${new Date().toISOString().split('T')[0]}.${format === 'excel' ? 'xlsx' : 'csv'}`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Create blob and download
const blob = await response.blob();
this.downloadBlob(blob, filename);
return filename;
} catch (error) {
console.error('Export failed:', error);
throw error;
}
}
downloadBlob(blob, filename) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
}
// Ranking calculation utilities
class RankingCalculator {
constructor() {
this.initializeRankingCalculation();
}
initializeRankingCalculation() {
// Add any ranking calculation setup here
}
async calculateMonthlyRankings(year, month) {
try {
const response = await fetch('/api/rankings/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ year, month })
});
if (!response.ok) {
throw new Error(`Calculation failed: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Ranking calculation failed:', error);
throw error;
}
}
async getRankingHistory(year, month) {
try {
const response = await fetch(`/api/rankings/monthly?year=${year}&month=${month}`);
if (!response.ok) {
throw new Error(`Failed to get ranking history: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Failed to get ranking history:', error);
throw error;
}
}
}
// Initialize admin functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize admin manager
window.adminManager = new AdminManager();
// Initialize data manager
window.dataManager = new DataManager();
// Initialize export manager
window.exportManager = new ExportManager();
// Initialize ranking calculator
window.rankingCalculator = new RankingCalculator();
});
// Global functions for HTML onclick handlers
function exportData(type, format) {
if (window.exportManager) {
window.exportManager.exportData(type, format);
}
}
function calculateMonthlyRankings() {
if (window.rankingCalculator) {
const year = document.getElementById('calc-year').value;
const month = document.getElementById('calc-month').value;
if (!year || !month) {
if (window.showError) {
window.showError('請選擇年份和月份');
}
return;
}
window.rankingCalculator.calculateMonthlyRankings(parseInt(year), parseInt(month))
.then(result => {
if (window.showSuccess) {
window.showSuccess(result.message);
}
})
.catch(error => {
if (window.showError) {
window.showError('排名計算失敗: ' + error.message);
}
});
}
}

1646
static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

386
static/js/assessment.js Normal file
View File

@@ -0,0 +1,386 @@
// Assessment-specific JavaScript functionality
// Enhanced drag and drop for assessment
class AssessmentDragDrop {
constructor() {
this.draggedElement = null;
this.initializeDragDrop();
}
initializeDragDrop() {
// Set up drag and drop for capability items
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('capability-item')) {
this.handleDragStart(e);
}
});
document.addEventListener('dragover', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDragOver(e);
}
});
document.addEventListener('dragleave', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDragLeave(e);
}
});
document.addEventListener('drop', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDrop(e);
}
});
document.addEventListener('dragend', (e) => {
if (e.target.classList.contains('capability-item')) {
this.handleDragEnd(e);
}
});
}
handleDragStart(e) {
this.draggedElement = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
e.dataTransfer.setData('text/plain', e.target.dataset.capabilityId);
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.add('drag-over');
}
}
handleDragLeave(e) {
const dropZone = e.target.closest('.drop-zone');
if (dropZone && !dropZone.contains(e.relatedTarget)) {
dropZone.classList.remove('drag-over');
}
}
handleDrop(e) {
e.preventDefault();
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.remove('drag-over');
if (this.draggedElement) {
this.moveCapability(this.draggedElement, dropZone);
}
}
}
handleDragEnd(e) {
e.target.classList.remove('dragging');
this.draggedElement = null;
}
moveCapability(capabilityElement, targetDropZone) {
// Remove from original position
capabilityElement.remove();
// Create new element in target drop zone
const newElement = this.createCapabilityElement(capabilityElement);
// Clear target drop zone and add new element
targetDropZone.innerHTML = '';
targetDropZone.appendChild(newElement);
targetDropZone.classList.add('has-items');
// Update visual state
this.updateDropZoneState(targetDropZone);
}
createCapabilityElement(originalElement) {
const newElement = document.createElement('div');
newElement.className = 'capability-item';
newElement.draggable = true;
newElement.dataset.capabilityId = originalElement.dataset.capabilityId;
newElement.dataset.capabilityName = originalElement.dataset.capabilityName;
newElement.textContent = originalElement.dataset.capabilityName;
// Add click to remove functionality
newElement.addEventListener('dblclick', () => {
this.removeCapabilityFromLevel(newElement);
});
return newElement;
}
removeCapabilityFromLevel(capabilityElement) {
const dropZone = capabilityElement.closest('.drop-zone');
if (dropZone) {
// Return to available capabilities
this.returnToAvailable(capabilityElement);
// Update drop zone state
this.updateDropZoneState(dropZone);
}
}
returnToAvailable(capabilityElement) {
const availableContainer = document.getElementById('available-capabilities');
if (availableContainer) {
const newElement = this.createCapabilityElement(capabilityElement);
availableContainer.appendChild(newElement);
}
}
updateDropZoneState(dropZone) {
const hasItems = dropZone.querySelector('.capability-item');
if (hasItems) {
dropZone.classList.add('has-items');
dropZone.classList.remove('empty');
} else {
dropZone.classList.remove('has-items');
dropZone.classList.add('empty');
dropZone.innerHTML = '<div class="drop-placeholder">拖放能力到此處</div>';
}
}
}
// Capability management
class CapabilityManager {
constructor() {
this.capabilities = [];
this.loadCapabilities();
}
async loadCapabilities() {
try {
const response = await fetch('/api/capabilities');
const data = await response.json();
this.capabilities = data.capabilities;
this.displayCapabilities();
} catch (error) {
console.error('Failed to load capabilities:', error);
this.showError('載入能力清單失敗');
}
}
displayCapabilities() {
const container = document.getElementById('available-capabilities');
if (!container) return;
container.innerHTML = '';
this.capabilities.forEach(capability => {
const capabilityElement = this.createCapabilityElement(capability);
container.appendChild(capabilityElement);
});
}
createCapabilityElement(capability) {
const element = document.createElement('div');
element.className = 'capability-item';
element.draggable = true;
element.dataset.capabilityId = capability.id;
element.dataset.capabilityName = capability.name;
element.textContent = capability.name;
// Add tooltip with description
element.title = this.getCapabilityDescription(capability);
return element;
}
getCapabilityDescription(capability) {
const descriptions = [
capability.l1_description,
capability.l2_description,
capability.l3_description,
capability.l4_description,
capability.l5_description
].filter(desc => desc && desc.trim());
return descriptions.join('\n\n');
}
showError(message) {
// You can implement a more sophisticated error display here
console.error(message);
}
}
// Assessment form validation and submission
class AssessmentForm {
constructor() {
this.form = document.getElementById('assessment-form');
this.initializeForm();
}
initializeForm() {
if (this.form) {
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
}
}
async handleSubmit() {
if (!this.validateForm()) {
return;
}
const assessmentData = this.collectAssessmentData();
try {
this.showLoading(true);
const response = await fetch('/api/assessments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(assessmentData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
this.showSuccess(result.message);
this.clearForm();
} catch (error) {
console.error('Assessment submission failed:', error);
this.showError('評估提交失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
validateForm() {
const department = document.getElementById('department').value.trim();
const position = document.getElementById('position').value.trim();
if (!department) {
this.showError('請填寫部門');
return false;
}
if (!position) {
this.showError('請填寫職位');
return false;
}
// Check if at least one capability is assigned
const hasAssignedCapabilities = this.hasAssignedCapabilities();
if (!hasAssignedCapabilities) {
this.showError('請至少分配一個能力到某個等級');
return false;
}
return true;
}
hasAssignedCapabilities() {
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
for (let level of levels) {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
if (dropZone && dropZone.querySelector('.capability-item')) {
return true;
}
}
return false;
}
collectAssessmentData() {
const formData = new FormData(this.form);
const assessmentData = {
department: formData.get('department'),
position: formData.get('position'),
employee_name: formData.get('employee_name') || null,
assessment_data: {}
};
// Collect capability assignments
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
levels.forEach(level => {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
const capabilityItems = dropZone.querySelectorAll('.capability-item');
assessmentData.assessment_data[level] = Array.from(capabilityItems).map(item => item.dataset.capabilityName);
});
return assessmentData;
}
clearForm() {
this.form.reset();
// Clear all drop zones
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
levels.forEach(level => {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
if (dropZone) {
dropZone.innerHTML = '<div class="drop-placeholder">拖放能力到此處</div>';
dropZone.classList.remove('has-items');
}
});
// Reload capabilities
if (window.capabilityManager) {
window.capabilityManager.loadCapabilities();
}
}
showLoading(show) {
const submitButton = this.form.querySelector('button[type="submit"]');
if (submitButton) {
if (show) {
submitButton.disabled = true;
submitButton.innerHTML = '<span class="loading"></span> 儲存中...';
} else {
submitButton.disabled = false;
submitButton.innerHTML = '<i class="bi bi-save me-1"></i>儲存評估';
}
}
}
showSuccess(message) {
// You can implement a more sophisticated success display here
console.log('Success:', message);
if (window.showSuccess) {
window.showSuccess(message);
}
}
showError(message) {
// You can implement a more sophisticated error display here
console.error('Error:', message);
if (window.showError) {
window.showError(message);
}
}
}
// Initialize assessment functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize drag and drop
window.assessmentDragDrop = new AssessmentDragDrop();
// Initialize capability manager
window.capabilityManager = new CapabilityManager();
// Initialize assessment form
window.assessmentForm = new AssessmentForm();
});
// Global function for clearing assessment (called from HTML)
function clearAssessment() {
if (window.assessmentForm) {
window.assessmentForm.clearForm();
}
}