// ── Admin Panel JavaScript ── const BASE_PATH = window.BASE_PATH || ''; function authHeaders() { return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') }; } async function apiGet(url) { const resp = await fetch(BASE_PATH + url, { headers: authHeaders() }); if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiPost(url, data) { const resp = await fetch(BASE_PATH + url, { method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiPut(url, data) { const resp = await fetch(BASE_PATH + url, { method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } async function apiDelete(url) { const resp = await fetch(BASE_PATH + url, { method: 'DELETE', headers: authHeaders() }); if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; } return resp.json(); } function showToast(msg, type = 'success') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function openModal(id) { document.getElementById(id).classList.add('open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } // ── Page Detection & Init ── document.addEventListener('DOMContentLoaded', () => { const path = window.location.pathname; if (path.includes('/admin/dashboard')) loadDashboard(); else if (path.includes('/admin/profile')) loadProfile(); else if (path.includes('/admin/education')) loadEducation(); else if (path.includes('/admin/experience')) loadExperience(); else if (path.includes('/admin/skills')) loadSkills(); else if (path.includes('/admin/tokens')) loadTokens(); else if (path.includes('/admin/messages')) loadMessages(); else if (path.includes('/admin/llm-config')) loadLLMConfig(); // Highlight active nav document.querySelectorAll('.admin-sidebar nav a').forEach(a => { if (a.getAttribute('href') === path) a.classList.add('active'); }); // Load unread badge on all admin pages loadUnreadBadge(); }); function adminLogout() { localStorage.removeItem('admin_token'); window.location.href = BASE_PATH + '/admin/login'; } // ── Dashboard ── async function loadDashboard() { const [skills, edu, exp, tokens] = await Promise.all([ apiGet('/api/admin/skills'), apiGet('/api/admin/education'), apiGet('/api/admin/experience'), apiGet('/api/admin/tokens'), ]); document.getElementById('stat-skills').textContent = skills ? skills.length : 0; document.getElementById('stat-education').textContent = edu ? edu.length : 0; document.getElementById('stat-experience').textContent = exp ? exp.length : 0; document.getElementById('stat-tokens').textContent = tokens ? tokens.filter(t => t.is_active).length : 0; } // ── Profile ── async function loadProfile() { const data = await apiGet('/api/admin/profile'); if (!data) return; ['name','phone','location','birthday','party','education_level','email','self_summary'].forEach(f => { const el = document.getElementById('field-' + f); if (el) el.value = data[f] || ''; }); if (data.photo_url) { document.getElementById('photo-preview').innerHTML = ``; } } async function saveProfile() { const fields = {}; ['name','phone','location','birthday','party','education_level','email','self_summary'].forEach(f => { const el = document.getElementById('field-' + f); if (el) fields[f] = el.value; }); const result = await apiPut('/api/admin/profile', fields); if (result) showToast('Profile saved'); } async function uploadPhoto() { const input = document.getElementById('photo-input'); if (!input.files[0]) return; const formData = new FormData(); formData.append('file', input.files[0]); const resp = await fetch(BASE_PATH + '/api/admin/profile/photo', { method: 'POST', headers: authHeaders(), body: formData, }); if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return; } const data = await resp.json(); if (data.photo_url) { document.getElementById('photo-preview').innerHTML = ``; showToast('Photo uploaded'); } } // ── Education CRUD ── let educationList = []; async function loadEducation() { educationList = await apiGet('/api/admin/education') || []; renderEducation(); } function renderEducation() { const tbody = document.getElementById('education-tbody'); tbody.innerHTML = educationList.map(e => ` ${e.start_date} - ${e.end_date} ${e.school} ${e.major} ${e.degree} `).join(''); } function showEducationModal(id = null) { const item = id ? educationList.find(e => e.id === id) : {}; document.getElementById('edu-id').value = id || ''; ['start_date','end_date','school','major','degree','details','sort_order'].forEach(f => { document.getElementById('edu-' + f).value = (item && item[f]) || ''; }); openModal('education-modal'); } function editEducation(id) { showEducationModal(id); } async function saveEducation() { const id = document.getElementById('edu-id').value; const data = {}; ['start_date','end_date','school','major','degree','details','sort_order'].forEach(f => { const val = document.getElementById('edu-' + f).value; data[f] = f === 'sort_order' ? parseInt(val) || 0 : val; }); if (id) await apiPut(`/api/admin/education/${id}`, data); else await apiPost('/api/admin/education', data); closeModal('education-modal'); showToast('Saved'); loadEducation(); } async function deleteEducation(id) { if (!confirm('Delete this entry?')) return; await apiDelete(`/api/admin/education/${id}`); showToast('Deleted'); loadEducation(); } // ── Experience CRUD ── let experienceList = []; async function loadExperience() { experienceList = await apiGet('/api/admin/experience') || []; renderExperience(); } function renderExperience() { const tbody = document.getElementById('experience-tbody'); tbody.innerHTML = experienceList.map(e => ` ${e.start_date} - ${e.end_date} ${e.company} ${e.position} `).join(''); } function showExperienceModal(id = null) { const item = id ? experienceList.find(e => e.id === id) : {}; document.getElementById('exp-id').value = id || ''; ['start_date','end_date','company','position','company_intro','responsibilities','achievements','sort_order'].forEach(f => { document.getElementById('exp-' + f).value = (item && item[f]) || ''; }); openModal('experience-modal'); } function editExperience(id) { showExperienceModal(id); } async function saveExperience() { const id = document.getElementById('exp-id').value; const data = {}; ['start_date','end_date','company','position','company_intro','responsibilities','achievements','sort_order'].forEach(f => { const val = document.getElementById('exp-' + f).value; data[f] = f === 'sort_order' ? parseInt(val) || 0 : val; }); if (id) await apiPut(`/api/admin/experience/${id}`, data); else await apiPost('/api/admin/experience', data); closeModal('experience-modal'); showToast('Saved'); loadExperience(); } async function deleteExperience(id) { if (!confirm('Delete this entry?')) return; await apiDelete(`/api/admin/experience/${id}`); showToast('Deleted'); loadExperience(); } // ── Skills CRUD ── let skillsList = []; async function loadSkills() { skillsList = await apiGet('/api/admin/skills') || []; renderSkills(); } function renderSkills() { const tbody = document.getElementById('skills-tbody'); tbody.innerHTML = skillsList.map(s => ` ${s.category} ${s.content.substring(0, 60)}${s.content.length > 60 ? '...' : ''} `).join(''); } function showSkillModal(id = null) { const item = id ? skillsList.find(s => s.id === id) : {}; document.getElementById('skill-id').value = id || ''; ['category','content','sort_order'].forEach(f => { document.getElementById('skill-' + f).value = (item && item[f]) || ''; }); openModal('skill-modal'); } function editSkill(id) { showSkillModal(id); } async function saveSkill() { const id = document.getElementById('skill-id').value; const data = {}; ['category','content','sort_order'].forEach(f => { const val = document.getElementById('skill-' + f).value; data[f] = f === 'sort_order' ? parseInt(val) || 0 : val; }); if (id) await apiPut(`/api/admin/skills/${id}`, data); else await apiPost('/api/admin/skills', data); closeModal('skill-modal'); showToast('Saved'); loadSkills(); } async function deleteSkill(id) { if (!confirm('Delete this entry?')) return; await apiDelete(`/api/admin/skills/${id}`); showToast('Deleted'); loadSkills(); } // ── Access Tokens ── let tokensList = []; async function loadTokens() { tokensList = await apiGet('/api/admin/tokens') || []; renderTokens(); } function renderTokens() { const tbody = document.getElementById('tokens-tbody'); tbody.innerHTML = tokensList.map(t => ` ${t.token.substring(0, 12)}... ${t.note || '-'} ${t.used_questions || 0} / ${t.max_questions || 10} ${t.is_active ? 'Active' : 'Inactive'} ${t.created_at ? new Date(t.created_at).toLocaleString() : '-'} `).join(''); } async function generateToken() { const note = prompt('Token note (optional):') || ''; const result = await apiPost('/api/admin/tokens', { note }); if (result && result.token) { showToast('Token created: ' + result.token); navigator.clipboard.writeText(result.token).catch(() => {}); loadTokens(); } } function copyToken(token) { navigator.clipboard.writeText(token).then(() => showToast('Copied!')).catch(() => showToast('Copy failed', 'error')); } function editToken(id) { const t = tokensList.find(x => x.id === id); if (!t) return; document.getElementById('token-edit-id').value = id; document.getElementById('token-edit-note').value = t.note || ''; document.getElementById('token-edit-max').value = t.max_questions || 10; document.getElementById('token-edit-used').value = t.used_questions || 0; document.getElementById('token-edit-active').value = t.is_active ? 'true' : 'false'; openModal('token-modal'); } async function saveToken() { const id = document.getElementById('token-edit-id').value; const data = { note: document.getElementById('token-edit-note').value, max_questions: parseInt(document.getElementById('token-edit-max').value) || 10, used_questions: parseInt(document.getElementById('token-edit-used').value) || 0, is_active: document.getElementById('token-edit-active').value === 'true', }; await apiPut(`/api/admin/tokens/${id}`, data); closeModal('token-modal'); showToast('Token updated'); loadTokens(); } async function deleteToken(id) { if (!confirm('Delete this token?')) return; await apiDelete(`/api/admin/tokens/${id}`); showToast('Deleted'); loadTokens(); } // ── LLM Config ── async function loadLLMConfig() { const data = await apiGet('/api/admin/llm-config'); if (!data) return; ['api_url','api_key','model_name','system_prompt'].forEach(f => { const el = document.getElementById('llm-' + f); if (el) el.value = data[f] || ''; }); const mt = document.getElementById('llm-max_tokens'); if (mt) mt.value = data.max_tokens || 2048; const mq = document.getElementById('llm-max_questions'); if (mq) mq.value = data.max_questions_per_session || 10; const temp = document.getElementById('llm-temperature'); if (temp) { temp.value = data.temperature || 0.7; document.getElementById('temp-value').textContent = temp.value; } } function setPreset(name) { const presets = { 'qwen': { url: 'https://dashscope.aliyuncs.com/compatible-mode', model: 'qwen-plus' }, 'deepseek': { url: 'https://api.deepseek.com', model: 'deepseek-chat' }, 'openai': { url: 'https://api.openai.com', model: 'gpt-4o' }, 'zhipu': { url: 'https://open.bigmodel.cn/api/paas', model: 'glm-4-flash' }, }; const p = presets[name]; if (p) { document.getElementById('llm-api_url').value = p.url; document.getElementById('llm-model_name').value = p.model; } } async function saveLLMConfig() { const data = { api_url: document.getElementById('llm-api_url').value, api_key: document.getElementById('llm-api_key').value, model_name: document.getElementById('llm-model_name').value, max_tokens: parseInt(document.getElementById('llm-max_tokens').value) || 2048, temperature: parseFloat(document.getElementById('llm-temperature').value) || 0.7, max_questions_per_session: parseInt(document.getElementById('llm-max_questions').value) || 10, system_prompt: document.getElementById('llm-system_prompt').value, }; const result = await apiPut('/api/admin/llm-config', data); if (result) showToast('LLM config saved'); } async function testLLMConnection() { // Auto-save before testing await saveLLMConfig(); showToast('Testing connection...'); try { const result = await apiPost('/api/admin/llm-config/test', {}); if (result && result.result) { showToast('Response: ' + result.result.substring(0, 100), 'success'); } else { showToast('No response from LLM', 'error'); } } catch (e) { showToast('Test failed: ' + e.message, 'error'); } } // ── Recruiter Messages ── async function loadUnreadBadge() { try { const data = await apiGet('/api/admin/messages/unread-count'); if (!data) return; const badges = document.querySelectorAll('.nav-badge'); badges.forEach(badge => { if (data.count > 0) { badge.textContent = data.count; badge.style.display = 'inline-block'; } else { badge.style.display = 'none'; } }); } catch (e) { /* ignore */ } } let messagesList = []; async function loadMessages() { messagesList = await apiGet('/api/admin/messages') || []; renderMessages(); } function renderMessages() { const container = document.getElementById('messages-list'); const noMsg = document.getElementById('no-messages'); if (!messagesList.length) { container.innerHTML = ''; noMsg.style.display = 'block'; return; } noMsg.style.display = 'none'; container.innerHTML = messagesList.map(m => `
${m.visitor_label || 'Unknown'} ${m.intent ? `${m.intent}` : ''} ${!m.is_read ? 'NEW' : ''}
${m.created_at ? new Date(m.created_at).toLocaleString() : ''}
${m.company ? `
Company: ${m.company}
` : ''} ${m.contact ? `
Contact: ${m.contact}
` : ''} ${m.summary ? `
${m.summary}
` : ''}
${!m.is_read ? `` : ''}
`).join(''); } async function markRead(id) { await apiPut(`/api/admin/messages/${id}/read`, {}); loadMessages(); loadUnreadBadge(); } async function markAllRead() { await apiPut('/api/admin/messages/read-all', {}); showToast('All marked as read'); loadMessages(); loadUnreadBadge(); } async function deleteMessage(id) { if (!confirm('Delete this message?')) return; await apiDelete(`/api/admin/messages/${id}`); showToast('Deleted'); loadMessages(); loadUnreadBadge(); }