變更內容: - 所有資料表加上 HR_position_ 前綴 - 整理完整欄位顯示名稱與 ID 對照表 - 模組化 JS 檔案 (admin.js, ai.js, csv.js 等) - 專案結構優化 (docs/, scripts/, tests/ 等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
407 lines
15 KiB
JavaScript
407 lines
15 KiB
JavaScript
/**
|
|
* 管理功能模組
|
|
* 處理使用者管理、系統設定和統計功能
|
|
*/
|
|
|
|
import { CSVUtils } from './csv.js';
|
|
|
|
const API_BASE_URL = '/api';
|
|
|
|
// ==================== 使用者管理 ====================
|
|
|
|
let usersData = [
|
|
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },
|
|
{ employeeId: 'A002', name: '人資主管', email: 'hr_manager@company.com', role: 'admin', createdAt: '2024-01-15' },
|
|
{ employeeId: 'A003', name: '一般員工', email: 'employee@company.com', role: 'user', createdAt: '2024-02-01' }
|
|
];
|
|
let editingUserId = null;
|
|
|
|
export function showAddUserModal() {
|
|
editingUserId = null;
|
|
document.getElementById('userModalTitle').textContent = '新增使用者';
|
|
document.getElementById('userEmployeeId').value = '';
|
|
document.getElementById('userName').value = '';
|
|
document.getElementById('userEmail').value = '';
|
|
document.getElementById('userRole').value = '';
|
|
document.getElementById('userEmployeeId').disabled = false;
|
|
document.getElementById('userModal').style.display = 'flex';
|
|
}
|
|
|
|
export function editUser(employeeId) {
|
|
const user = usersData.find(u => u.employeeId === employeeId);
|
|
if (!user) return;
|
|
|
|
editingUserId = employeeId;
|
|
document.getElementById('userModalTitle').textContent = '編輯使用者';
|
|
document.getElementById('userEmployeeId').value = user.employeeId;
|
|
document.getElementById('userEmployeeId').disabled = true;
|
|
document.getElementById('userName').value = user.name;
|
|
document.getElementById('userEmail').value = user.email;
|
|
document.getElementById('userRole').value = user.role;
|
|
document.getElementById('userModal').style.display = 'flex';
|
|
}
|
|
|
|
export function closeUserModal() {
|
|
document.getElementById('userModal').style.display = 'none';
|
|
editingUserId = null;
|
|
}
|
|
|
|
export function saveUser(event) {
|
|
event.preventDefault();
|
|
const employeeId = document.getElementById('userEmployeeId').value;
|
|
const name = document.getElementById('userName').value;
|
|
const email = document.getElementById('userEmail').value;
|
|
const role = document.getElementById('userRole').value;
|
|
|
|
if (!employeeId || !name || !email || !role) {
|
|
if (typeof showToast === 'function') showToast('請填寫所有必填欄位');
|
|
return;
|
|
}
|
|
|
|
if (editingUserId) {
|
|
const index = usersData.findIndex(u => u.employeeId === editingUserId);
|
|
if (index > -1) {
|
|
usersData[index] = { ...usersData[index], name, email, role };
|
|
if (typeof showToast === 'function') showToast('使用者已更新');
|
|
}
|
|
} else {
|
|
if (usersData.some(u => u.employeeId === employeeId)) {
|
|
if (typeof showToast === 'function') showToast('工號已存在');
|
|
return;
|
|
}
|
|
usersData.push({
|
|
employeeId,
|
|
name,
|
|
email,
|
|
role,
|
|
createdAt: new Date().toISOString().split('T')[0]
|
|
});
|
|
if (typeof showToast === 'function') showToast('使用者已新增');
|
|
}
|
|
|
|
closeUserModal();
|
|
renderUserList();
|
|
}
|
|
|
|
export function deleteUser(employeeId) {
|
|
if (confirm('確定要刪除此使用者嗎?')) {
|
|
usersData = usersData.filter(u => u.employeeId !== employeeId);
|
|
renderUserList();
|
|
if (typeof showToast === 'function') showToast('使用者已刪除');
|
|
}
|
|
}
|
|
|
|
export function renderUserList() {
|
|
const tbody = document.getElementById('userListBody');
|
|
if (!tbody) return;
|
|
|
|
const roleLabels = {
|
|
'superadmin': { text: '最高權限管理者', color: '#e74c3c' },
|
|
'admin': { text: '管理者', color: '#f39c12' },
|
|
'user': { text: '一般使用者', color: '#27ae60' }
|
|
};
|
|
|
|
tbody.innerHTML = usersData.map(user => {
|
|
const roleInfo = roleLabels[user.role] || { text: user.role, color: '#999' };
|
|
const isSuperAdmin = user.role === 'superadmin';
|
|
return `
|
|
<tr>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.employeeId)}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.name)}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.email)}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
|
<span style="background: ${roleInfo.color}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${roleInfo.text}</span>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.createdAt)}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
|
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('${user.employeeId}')">編輯</button>
|
|
${!isSuperAdmin ? `<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('${user.employeeId}')">刪除</button>` : ''}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
export function exportUsersCSV() {
|
|
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
|
|
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
|
|
if (typeof showToast === 'function') showToast('使用者清單已匯出!');
|
|
}
|
|
|
|
// ==================== 用戶信息與登出功能 ====================
|
|
|
|
export function loadUserInfo() {
|
|
const currentUser = localStorage.getItem('currentUser');
|
|
|
|
if (currentUser) {
|
|
try {
|
|
const user = JSON.parse(currentUser);
|
|
|
|
const userNameEl = document.getElementById('userName');
|
|
if (userNameEl) {
|
|
userNameEl.textContent = user.name || user.username;
|
|
}
|
|
|
|
const userRoleEl = document.getElementById('userRole');
|
|
if (userRoleEl) {
|
|
let roleText = '';
|
|
switch(user.role) {
|
|
case 'user':
|
|
roleText = '一般使用者 ★☆☆';
|
|
break;
|
|
case 'admin':
|
|
roleText = '管理者 ★★☆';
|
|
break;
|
|
case 'superadmin':
|
|
roleText = '最高管理者 ★★★';
|
|
break;
|
|
default:
|
|
roleText = '一般使用者';
|
|
}
|
|
userRoleEl.textContent = roleText;
|
|
}
|
|
|
|
const userAvatarEl = document.getElementById('userAvatar');
|
|
if (userAvatarEl) {
|
|
const avatarText = (user.name || user.username || 'U').charAt(0).toUpperCase();
|
|
userAvatarEl.textContent = avatarText;
|
|
}
|
|
|
|
console.log('用戶信息已載入:', user.name, user.role);
|
|
} catch (error) {
|
|
console.error('載入用戶信息失敗:', error);
|
|
}
|
|
} else {
|
|
console.warn('未找到用戶信息,重定向到登入頁面');
|
|
window.location.href = 'login.html';
|
|
}
|
|
}
|
|
|
|
export function logout() {
|
|
if (confirm('確定要登出系統嗎?')) {
|
|
localStorage.removeItem('currentUser');
|
|
if (typeof showToast === 'function') showToast('已成功登出系統');
|
|
setTimeout(() => {
|
|
window.location.href = 'login.html';
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// ==================== Ollama 模型設定 ====================
|
|
|
|
export function saveOllamaModel() {
|
|
const saveBtn = document.getElementById('saveModelBtn');
|
|
if (saveBtn) {
|
|
saveBtn.style.display = 'inline-flex';
|
|
}
|
|
hideAllMessages();
|
|
}
|
|
|
|
export function saveOllamaModelWithConfirmation() {
|
|
const reasonerRadio = document.getElementById('model-reasoner');
|
|
const chatRadio = document.getElementById('model-chat');
|
|
|
|
let selectedModel = '';
|
|
if (reasonerRadio && reasonerRadio.checked) {
|
|
selectedModel = 'deepseek-reasoner';
|
|
} else if (chatRadio && chatRadio.checked) {
|
|
selectedModel = 'deepseek-chat';
|
|
}
|
|
|
|
if (selectedModel) {
|
|
localStorage.setItem('ollamaModel', selectedModel);
|
|
|
|
const saveBtn = document.getElementById('saveModelBtn');
|
|
if (saveBtn) {
|
|
saveBtn.style.display = 'none';
|
|
}
|
|
|
|
hideAllMessages();
|
|
const successDiv = document.getElementById('modelSaveSuccess');
|
|
if (successDiv) {
|
|
successDiv.style.display = 'block';
|
|
setTimeout(() => {
|
|
successDiv.style.display = 'none';
|
|
}, 3000);
|
|
}
|
|
|
|
console.log('Ollama 模型已設定為:', selectedModel);
|
|
}
|
|
}
|
|
|
|
export function loadOllamaModel() {
|
|
const savedModel = localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
|
|
|
|
const reasonerRadio = document.getElementById('model-reasoner');
|
|
const chatRadio = document.getElementById('model-chat');
|
|
const gptossRadio = document.getElementById('model-gptoss');
|
|
|
|
if (savedModel === 'deepseek-reasoner' && reasonerRadio) {
|
|
reasonerRadio.checked = true;
|
|
} else if (savedModel === 'deepseek-chat' && chatRadio) {
|
|
chatRadio.checked = true;
|
|
} else if (savedModel === 'gpt-oss:120b' && gptossRadio) {
|
|
gptossRadio.checked = true;
|
|
}
|
|
|
|
console.log('已載入 Ollama 模型設定:', savedModel);
|
|
}
|
|
|
|
export function getOllamaModel() {
|
|
return localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
|
|
}
|
|
|
|
export function hideAllMessages() {
|
|
const messages = ['connectionSuccess', 'connectionError', 'modelSaveSuccess'];
|
|
messages.forEach(id => {
|
|
const elem = document.getElementById(id);
|
|
if (elem) elem.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
export async function testOllamaConnection() {
|
|
const testBtn = document.getElementById('testConnectionBtn');
|
|
const reasonerRadio = document.getElementById('model-reasoner');
|
|
const chatRadio = document.getElementById('model-chat');
|
|
|
|
let selectedModel = '';
|
|
if (reasonerRadio && reasonerRadio.checked) {
|
|
selectedModel = 'deepseek-reasoner';
|
|
} else if (chatRadio && chatRadio.checked) {
|
|
selectedModel = 'deepseek-chat';
|
|
}
|
|
|
|
if (!selectedModel) {
|
|
if (typeof showToast === 'function') showToast('請先選擇一個模型');
|
|
return;
|
|
}
|
|
|
|
if (testBtn) {
|
|
testBtn.disabled = true;
|
|
testBtn.textContent = '測試中...';
|
|
}
|
|
|
|
hideAllMessages();
|
|
|
|
try {
|
|
const response = await fetch('/api/llm/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
api: 'ollama',
|
|
model: selectedModel,
|
|
prompt: '請用一句話說明你是誰',
|
|
max_tokens: 100
|
|
}),
|
|
timeout: 30000
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const successDiv = document.getElementById('connectionSuccess');
|
|
if (successDiv) {
|
|
successDiv.style.display = 'block';
|
|
setTimeout(() => {
|
|
successDiv.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
console.log('測試回應:', data.text);
|
|
} else {
|
|
throw new Error(data.error || '未知錯誤');
|
|
}
|
|
} catch (error) {
|
|
const errorDiv = document.getElementById('connectionError');
|
|
const errorMsg = document.getElementById('connectionErrorMessage');
|
|
if (errorDiv && errorMsg) {
|
|
errorMsg.textContent = error.message;
|
|
errorDiv.style.display = 'block';
|
|
setTimeout(() => {
|
|
errorDiv.style.display = 'none';
|
|
}, 10000);
|
|
}
|
|
console.error('連線測試失敗:', error);
|
|
} finally {
|
|
if (testBtn) {
|
|
testBtn.disabled = false;
|
|
testBtn.innerHTML = `
|
|
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
</svg>
|
|
測試連線
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== 崗位資料管理功能 ====================
|
|
|
|
export async function exportCompletePositionData() {
|
|
try {
|
|
if (typeof showToast === 'function') showToast('正在準備匯出資料...');
|
|
window.location.href = API_BASE_URL + '/position-list/export';
|
|
setTimeout(() => {
|
|
if (typeof showToast === 'function') showToast('崗位資料匯出成功!');
|
|
}, 1000);
|
|
} catch (error) {
|
|
console.error('匯出錯誤:', error);
|
|
alert('匯出失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
export async function refreshPositionStats() {
|
|
try {
|
|
if (typeof showToast === 'function') showToast('正在更新統計資料...');
|
|
|
|
const response = await fetch(API_BASE_URL + '/position-list');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const positions = result.data;
|
|
const total = positions.length;
|
|
const described = positions.filter(p => p.hasDescription).length;
|
|
const undescribed = total - described;
|
|
|
|
const totalEl = document.getElementById('totalPositionsCount');
|
|
const describedEl = document.getElementById('describedPositionsCount');
|
|
const undescribedEl = document.getElementById('undescribedPositionsCount');
|
|
|
|
if (totalEl) totalEl.textContent = total;
|
|
if (describedEl) describedEl.textContent = described;
|
|
if (undescribedEl) undescribedEl.textContent = undescribed;
|
|
|
|
if (typeof showToast === 'function') showToast('統計資料已更新');
|
|
} else {
|
|
alert('更新統計失敗: ' + (result.error || '未知錯誤'));
|
|
}
|
|
} catch (error) {
|
|
console.error('更新統計錯誤:', error);
|
|
alert('更新統計失敗: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// ==================== 工具函數 ====================
|
|
|
|
function sanitizeHTML(str) {
|
|
if (str === null || str === undefined) return '';
|
|
const temp = document.createElement('div');
|
|
temp.textContent = str;
|
|
return temp.innerHTML;
|
|
}
|
|
|
|
// 暴露到全域
|
|
if (typeof window !== 'undefined') {
|
|
window.showAddUserModal = showAddUserModal;
|
|
window.editUser = editUser;
|
|
window.closeUserModal = closeUserModal;
|
|
window.saveUser = saveUser;
|
|
window.deleteUser = deleteUser;
|
|
window.renderUserList = renderUserList;
|
|
window.exportUsersCSV = exportUsersCSV;
|
|
window.logout = logout;
|
|
window.testOllamaConnection = testOllamaConnection;
|
|
window.saveOllamaModelWithConfirmation = saveOllamaModelWithConfirmation;
|
|
window.exportCompletePositionData = exportCompletePositionData;
|
|
window.refreshPositionStats = refreshPositionStats;
|
|
}
|