diff --git a/README.md b/README.md index 49fcb7a..7b1ec50 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ When the backend is running, open `/index.html` to use the lightweight dashboard - request a login token - load project statistics - query recent projects with keyword, type, level, and status filters +- query the user directory with keyword, role, and status filters +- load the active teacher roster +- view and update the current user's profile details +- change the current user's password - visualize status and level breakdowns - view leader and advisor names directly in project rows diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html index 9a7ff4f..0e07d11 100644 --- a/backend/src/main/resources/static/index.html +++ b/backend/src/main/resources/static/index.html @@ -198,6 +198,7 @@ Use satoken header Supports quick login Reads stats and project pages + Includes user management
Seed accounts share the bundled demo password: @@ -290,6 +291,74 @@
+ +
+
People
+

User management

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
Profile
+

Current account tools

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
@@ -343,6 +412,38 @@ + +
+
Users
+

User directory

+
+ + + + + + + + + + + + + +
UserRoleStatusContactUpdated
No user data loaded yet.
+
+
+ +
+
Teachers
+

Advisor roster

+
+
+
+ Sign in to load user directory, teacher roster, and editable profile details. +
+
+
@@ -358,10 +459,22 @@ projectType: document.getElementById('projectType'), projectLevel: document.getElementById('projectLevel'), status: document.getElementById('status'), + userKeyword: document.getElementById('userKeyword'), + userRole: document.getElementById('userRole'), + userStatus: document.getElementById('userStatus'), + profileRealName: document.getElementById('profileRealName'), + profilePhone: document.getElementById('profilePhone'), + profileEmail: document.getElementById('profileEmail'), + profileAvatar: document.getElementById('profileAvatar'), + oldPassword: document.getElementById('oldPassword'), + newPassword: document.getElementById('newPassword'), statusBox: document.getElementById('statusBox'), projectTable: document.getElementById('projectTable'), + userTable: document.getElementById('userTable'), statusList: document.getElementById('statusList'), - levelList: document.getElementById('levelList') + levelList: document.getElementById('levelList'), + teacherList: document.getElementById('teacherList'), + sessionSummary: document.getElementById('sessionSummary') }; const statRefs = { @@ -382,12 +495,24 @@ student001: { username: 'student001', password: 'admin123' } }; + const sessionState = { + userId: null, + username: '', + roleType: null, + realName: '' + }; + refs.baseUrl.value = window.location.origin || 'http://localhost:8080'; applyPresetAccount(); document.getElementById('loginBtn').addEventListener('click', login); document.getElementById('statsBtn').addEventListener('click', loadStats); document.getElementById('projectsBtn').addEventListener('click', loadProjects); + document.getElementById('usersBtn').addEventListener('click', loadUsers); + document.getElementById('teachersBtn').addEventListener('click', loadTeachers); + document.getElementById('profileBtn').addEventListener('click', loadProfile); + document.getElementById('saveProfileBtn').addEventListener('click', saveProfile); + document.getElementById('passwordBtn').addEventListener('click', changePassword); refs.accountPreset.addEventListener('change', applyPresetAccount); document.getElementById('resetBtn').addEventListener('click', () => { refs.keyword.value = ''; @@ -412,10 +537,20 @@ if (payload.code !== 200) { throw new Error(payload.message || 'Login failed.'); } + sessionState.userId = payload.data.userId || null; + sessionState.username = payload.data.username || ''; + sessionState.roleType = payload.data.roleType ?? null; + sessionState.realName = payload.data.realName || ''; refs.token.value = payload.data.token || ''; + updateSessionSummary(); setStatus('Login succeeded. Token stored in the satoken field.', 'success'); - await loadStats(); - await loadProjects(); + await Promise.allSettled([ + loadStats(), + loadProjects(), + loadUsers(), + loadTeachers(), + loadProfile() + ]); } catch (error) { setStatus(error.message, 'error'); } @@ -484,13 +619,143 @@ } } - async function apiFetch(path) { - const headers = {}; + async function loadUsers() { + setStatus('Loading user directory...'); + const params = new URLSearchParams(); + if (refs.userKeyword.value.trim()) params.set('keyword', refs.userKeyword.value.trim()); + if (refs.userRole.value) params.set('roleType', refs.userRole.value); + if (refs.userStatus.value) params.set('status', refs.userStatus.value); + params.set('size', '8'); + + try { + const payload = await apiFetch('/api/users?' + params.toString()); + const page = payload.data || {}; + const records = page.records || []; + if (!records.length) { + refs.userTable.innerHTML = 'No matching users found.'; + } else { + refs.userTable.innerHTML = records.map((item) => ` + + + ${escapeHtml(item.realName || item.username || 'Unnamed user')}
+ ${escapeHtml(item.username || '-')} + + ${escapeHtml(item.roleTypeLabel || '-')} + ${escapeHtml(item.statusLabel || '-')} + + ${escapeHtml(item.phone || '-')}
+ ${escapeHtml(item.email || '-')} + + ${formatDateTime(item.updateTime || item.createTime)} + + `).join(''); + } + setStatus(`Loaded ${records.length} user record(s).`, 'success'); + } catch (error) { + setStatus(error.message, 'error'); + } + } + + async function loadTeachers() { + setStatus('Loading teacher roster...'); + try { + const payload = await apiFetch('/api/users/teachers'); + const records = payload.data || []; + renderTeacherList(records); + setStatus(`Loaded ${records.length} teacher record(s).`, 'success'); + } catch (error) { + setStatus(error.message, 'error'); + } + } + + async function loadProfile() { + if (!sessionState.userId) { + setStatus('Login first so the dashboard knows which profile to load.', 'error'); + return; + } + + setStatus('Loading current profile...'); + try { + const payload = await apiFetch('/api/users/' + sessionState.userId); + const profile = payload.data || {}; + refs.profileRealName.value = profile.realName || ''; + refs.profilePhone.value = profile.phone || ''; + refs.profileEmail.value = profile.email || ''; + refs.profileAvatar.value = profile.avatar || ''; + sessionState.realName = profile.realName || sessionState.realName; + sessionState.roleType = profile.roleType ?? sessionState.roleType; + updateSessionSummary(); + setStatus('Profile loaded successfully.', 'success'); + } catch (error) { + setStatus(error.message, 'error'); + } + } + + async function saveProfile() { + if (!sessionState.userId) { + setStatus('Login first so the dashboard can update your profile.', 'error'); + return; + } + + setStatus('Saving current profile...'); + try { + const payload = await apiFetch('/api/users/profile', { + method: 'PUT', + body: JSON.stringify({ + realName: refs.profileRealName.value.trim(), + phone: refs.profilePhone.value.trim(), + email: refs.profileEmail.value.trim(), + avatar: refs.profileAvatar.value.trim() + }) + }); + const profile = payload.data || {}; + sessionState.realName = profile.realName || sessionState.realName; + updateSessionSummary(); + setStatus('Profile saved successfully.', 'success'); + await loadUsers(); + await loadTeachers(); + } catch (error) { + setStatus(error.message, 'error'); + } + } + + async function changePassword() { + if (!sessionState.userId) { + setStatus('Login first so the dashboard can change your password.', 'error'); + return; + } + if (!refs.oldPassword.value || !refs.newPassword.value) { + setStatus('Enter both the current password and the new password.', 'error'); + return; + } + + setStatus('Changing password...'); + try { + await apiFetch('/api/users/change-password', { + method: 'POST', + body: JSON.stringify({ + oldPassword: refs.oldPassword.value, + newPassword: refs.newPassword.value + }) + }); + refs.oldPassword.value = ''; + refs.newPassword.value = ''; + setStatus('Password changed successfully.', 'success'); + } catch (error) { + setStatus(error.message, 'error'); + } + } + + async function apiFetch(path, options = {}) { + const headers = { ...(options.headers || {}) }; const token = refs.token.value.trim(); if (token) { headers.satoken = token; } - const response = await fetch(buildUrl(path), { headers }); + if (options.body && !headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } + const response = await fetch(buildUrl(path), { ...options, headers }); const payload = await response.json(); if (payload.code !== 200) { throw new Error(payload.message || 'Request failed.'); @@ -511,6 +776,33 @@ `).join(''); } + function renderTeacherList(items) { + if (!items.length) { + refs.teacherList.innerHTML = '
No teachers loaded-
'; + return; + } + refs.teacherList.innerHTML = items.map((item) => ` +
+ + ${escapeHtml(item.realName || item.username || 'Teacher')}
+ ${escapeHtml(item.email || item.phone || '-')} +
+ ${escapeHtml(item.roleTypeLabel || 'Teacher')} +
+ `).join(''); + } + + function updateSessionSummary() { + const name = sessionState.realName || sessionState.username || 'Unknown user'; + const role = roleLabel(sessionState.roleType); + refs.sessionSummary.innerHTML = ` + Current session: +
${escapeHtml(name)} +
${escapeHtml(sessionState.username || '-')} +
Role: ${escapeHtml(role)} + `; + } + function buildUrl(path) { const base = refs.baseUrl.value.trim().replace(/\/$/, ''); return (base || 'http://localhost:8080') + path; @@ -538,6 +830,23 @@ return parts.length ? parts.join('
') : '-'; } + function formatDateTime(value) { + if (!value) { + return '-'; + } + const normalized = String(value).replace('T', ' '); + return escapeHtml(normalized); + } + + function roleLabel(value) { + switch (Number(value)) { + case 1: return 'Student'; + case 2: return 'Teacher'; + case 3: return 'Administrator'; + default: return 'Unknown'; + } + } + function applyPresetAccount() { const preset = accountPresets[refs.accountPreset.value] || accountPresets.admin; refs.username.value = preset.username;