Initial commit
This commit is contained in:
507
static/css/style.css
Normal file
507
static/css/style.css
Normal file
@@ -0,0 +1,507 @@
|
||||
/* 夥伴對齊系統 - 樣式表 */
|
||||
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #212529;
|
||||
--border-radius: 0.375rem;
|
||||
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* 全局樣式 */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 導航欄樣式 */
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
border-radius: var(--border-radius);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 卡片樣式 */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
transition: var(--transition);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* 儀表板卡片樣式 */
|
||||
.card.bg-primary,
|
||||
.card.bg-success,
|
||||
.card.bg-warning,
|
||||
.card.bg-info {
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||
}
|
||||
|
||||
.card.bg-success {
|
||||
background: linear-gradient(135deg, var(--success-color), #146c43);
|
||||
}
|
||||
|
||||
.card.bg-warning {
|
||||
background: linear-gradient(135deg, var(--warning-color), #e0a800);
|
||||
}
|
||||
|
||||
.card.bg-info {
|
||||
background: linear-gradient(135deg, var(--info-color), #0aa2c0);
|
||||
}
|
||||
|
||||
/* 按鈕樣式 */
|
||||
.btn {
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, var(--success-color), #146c43);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, var(--danger-color), #b02a37);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, var(--warning-color), #e0a800);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: linear-gradient(135deg, var(--info-color), #0aa2c0);
|
||||
}
|
||||
|
||||
/* 表單樣式 */
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #ced4da;
|
||||
transition: var(--transition);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 能力評估樣式 */
|
||||
.capability-card {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.capability-card:hover {
|
||||
border-left-color: var(--success-color);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 排名表格樣式 */
|
||||
.table {
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
/* 徽章樣式 */
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* 通知樣式 */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-primary {
|
||||
border-left-color: var(--primary-color);
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
border-left-color: var(--success-color);
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-left-color: var(--warning-color);
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
border-left-color: var(--danger-color);
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
}
|
||||
|
||||
/* 模態框樣式 */
|
||||
.modal-content {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* 載入動畫 */
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
/* Toast 樣式 */
|
||||
.toast {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* 標籤頁樣式 */
|
||||
.nav-tabs .nav-link {
|
||||
border: none;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
color: var(--secondary-color);
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
border-color: transparent;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 下拉選單樣式 */
|
||||
.dropdown-menu {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 響應式設計 */
|
||||
@media (max-width: 768px) {
|
||||
.container-fluid {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.navbar-brand {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 動畫效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* 自定義滾動條 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
/* 工具提示樣式 */
|
||||
.tooltip {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
background-color: var(--dark-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* 進度條樣式 */
|
||||
.progress {
|
||||
height: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: var(--border-radius);
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* 統計卡片特殊樣式 */
|
||||
.stats-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transform: translate(30px, -30px);
|
||||
}
|
||||
|
||||
.stats-card .card-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 排名特殊樣式 */
|
||||
.ranking-item {
|
||||
transition: var(--transition);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: white;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.ranking-item.top-3 {
|
||||
border-left-color: var(--warning-color);
|
||||
background: linear-gradient(135deg, #fff9e6, #ffffff);
|
||||
}
|
||||
|
||||
.ranking-item.top-1 {
|
||||
border-left-color: #ffd700;
|
||||
background: linear-gradient(135deg, #fffacd, #ffffff);
|
||||
}
|
||||
|
||||
/* 能力等級標籤 */
|
||||
.level-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.level-l1 { background-color: var(--danger-color); color: white; }
|
||||
.level-l2 { background-color: var(--warning-color); color: #000; }
|
||||
.level-l3 { background-color: var(--info-color); color: white; }
|
||||
.level-l4 { background-color: var(--success-color); color: white; }
|
||||
.level-l5 { background-color: var(--primary-color); color: white; }
|
||||
|
||||
/* 空狀態樣式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 載入狀態 */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* 成功/錯誤狀態 */
|
||||
.status-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
color: var(--info-color);
|
||||
}
|
||||
324
static/js/admin.js
Normal file
324
static/js/admin.js
Normal 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
1646
static/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
386
static/js/assessment.js
Normal file
386
static/js/assessment.js
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user