add account web

This commit is contained in:
2026-06-30 10:58:09 +07:00
parent dc86524ff0
commit fd08bee7ab
4 changed files with 115 additions and 44 deletions

View File

@@ -4,7 +4,7 @@ import { ClassesTab } from './components/ClassesTab';
import { StudentsTab } from './components/StudentsTab'; import { StudentsTab } from './components/StudentsTab';
import { LearningTab } from './components/LearningTab'; import { LearningTab } from './components/LearningTab';
import { NetworkTab } from './components/NetworkTab'; import { NetworkTab } from './components/NetworkTab';
import { AccountsTab } from './components/AccountsTab'; import { EmailDomainsTab, MyAccountTab } from './components/AccountsTab';
import { ChatWidget } from './components/ChatWidget'; import { ChatWidget } from './components/ChatWidget';
import { StaffChatSocket } from './hooks/useStaffChatSocket'; import { StaffChatSocket } from './hooks/useStaffChatSocket';
import { ClassWorkspace } from './components/ClassWorkspace'; import { ClassWorkspace } from './components/ClassWorkspace';
@@ -50,10 +50,10 @@ const IconNetwork = () => (
</svg> </svg>
); );
const IconAccounts = () => ( const IconEmail = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> <rect x="2" y="4" width="20" height="16" rx="2" />
<circle cx="12" cy="7" r="4" /> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg> </svg>
); );
@@ -144,21 +144,29 @@ function App() {
<div className="nav-header">Hệ thống</div> <div className="nav-header">Hệ thống</div>
<li className="nav-item"> <li className="nav-item">
<button <button
className={`nav-btn ${route.tab === 'accounts' && !route.classId ? 'active' : ''}`} className={`nav-btn ${route.tab === 'email-domains' && !route.classId ? 'active' : ''}`}
onClick={() => navigate('accounts')} onClick={() => navigate('email-domains')}
> >
<span className="nav-icon"><IconAccounts /></span> <span className="nav-icon"><IconEmail /></span>
Tài khoản Đuôi email
</button> </button>
</li> </li>
</ul> </ul>
</nav> </nav>
<div className="sidebar-footer"> <div className="sidebar-footer">
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', fontWeight: 500 }}> <button
{staff?.email} type="button"
</div> className={`sidebar-user-btn ${route.tab === 'profile' && !route.classId ? 'active' : ''}`}
<button type="button" className="link-btn" onClick={logout} style={{ marginTop: '0.35rem' }}> onClick={() => navigate('profile')}
title="Tài khoản của tôi"
>
<span className="sidebar-user-name">
{staff?.fullName?.trim() || staff?.email?.split('@')[0] || 'Tài khoản'}
</span>
<span className="sidebar-user-email">{staff?.email}</span>
</button>
<button type="button" className="link-btn" onClick={logout}>
Đăng xuất Đăng xuất
</button> </button>
</div> </div>
@@ -182,7 +190,8 @@ function App() {
{route.tab === 'students' && <StudentsTab />} {route.tab === 'students' && <StudentsTab />}
{route.tab === 'learning' && <LearningTab />} {route.tab === 'learning' && <LearningTab />}
{route.tab === 'network' && <NetworkTab />} {route.tab === 'network' && <NetworkTab />}
{route.tab === 'accounts' && <AccountsTab />} {route.tab === 'email-domains' && <EmailDomainsTab />}
{route.tab === 'profile' && <MyAccountTab />}
</> </>
)} )}
</div> </div>

View File

@@ -64,14 +64,9 @@ export function ChangePasswordPage({ forced }: { forced?: boolean }) {
); );
} }
export function AccountsTab() { export function EmailDomainsTab() {
const { staff, changePassword } = useAuth();
const [domains, setDomains] = useState<EmailDomainItem[]>([]); const [domains, setDomains] = useState<EmailDomainItem[]>([]);
const [newDomain, setNewDomain] = useState(''); const [newDomain, setNewDomain] = useState('');
const [oldPw, setOldPw] = useState('');
const [newPw, setNewPw] = useState('');
const [msg, setMsg] = useState('');
const [err, setErr] = useState('');
const load = async () => { const load = async () => {
const res = await apiAdmin.listEmailDomains(); const res = await apiAdmin.listEmailDomains();
@@ -95,32 +90,13 @@ export function AccountsTab() {
await load(); await load();
}; };
const submitPw = async (e: React.FormEvent) => {
e.preventDefault();
setErr('');
setMsg('');
try {
await changePassword(oldPw, newPw);
setMsg('Đã đổi mật khẩu');
setOldPw('');
setNewPw('');
} catch (e: any) {
setErr(e?.message || 'Lỗi');
}
};
return ( return (
<div className="page-stack"> <div className="page-stack">
<header className="page-header"> <header className="page-header">
<h1 className="page-title">Tài khoản & bảo mật</h1> <h1 className="page-title">Đuôi email</h1>
<p className="page-desc">Quản đuôi email đưc phép đi mật khẩu.</p> <p className="page-desc">Cấu hình đuôi email đưc phép đăng đăng nhập.</p>
</header> </header>
<div className="card" style={{ padding: '1.25rem' }}>
<h2 className="section-title">Tài khoản hiện tại</h2>
<p style={{ margin: 0, color: 'var(--text-muted)' }}>{staff?.email}</p>
</div>
<div className="card" style={{ padding: '1.25rem' }}> <div className="card" style={{ padding: '1.25rem' }}>
<h2 className="section-title">Đuôi email đưc phép</h2> <h2 className="section-title">Đuôi email đưc phép</h2>
<p className="page-desc" style={{ marginTop: 0 }}> <p className="page-desc" style={{ marginTop: 0 }}>
@@ -145,6 +121,45 @@ export function AccountsTab() {
))} ))}
</ul> </ul>
</div> </div>
</div>
);
}
export function MyAccountTab() {
const { staff, changePassword } = useAuth();
const [oldPw, setOldPw] = useState('');
const [newPw, setNewPw] = useState('');
const [msg, setMsg] = useState('');
const [err, setErr] = useState('');
const submitPw = async (e: React.FormEvent) => {
e.preventDefault();
setErr('');
setMsg('');
try {
await changePassword(oldPw, newPw);
setMsg('Đã đổi mật khẩu');
setOldPw('');
setNewPw('');
} catch (e: any) {
setErr(e?.message || 'Lỗi');
}
};
const displayName = staff?.fullName?.trim() || staff?.email?.split('@')[0] || 'Tài khoản';
return (
<div className="page-stack">
<header className="page-header">
<h1 className="page-title">Tài khoản của tôi</h1>
<p className="page-desc">Thông tin đăng nhập bảo mật nhân.</p>
</header>
<div className="card" style={{ padding: '1.25rem' }}>
<h2 className="section-title">Thông tin</h2>
<p style={{ margin: '0 0 0.25rem', fontWeight: 600 }}>{displayName}</p>
<p style={{ margin: 0, color: 'var(--text-muted)' }}>{staff?.email}</p>
</div>
<div className="card" style={{ padding: '1.25rem' }}> <div className="card" style={{ padding: '1.25rem' }}>
<h2 className="section-title">Đi mật khẩu</h2> <h2 className="section-title">Đi mật khẩu</h2>

View File

@@ -237,6 +237,51 @@ body {
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-user-btn {
width: 100%;
text-align: left;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: none;
padding: 0.5rem 0.6rem;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.15rem;
transition: background 0.15s, border-color 0.15s;
}
.sidebar-user-btn:hover {
background: var(--bg-subtle);
}
.sidebar-user-btn.active {
background: var(--accent-light);
border-color: var(--accent);
}
.sidebar-user-btn.active .sidebar-user-name,
.sidebar-user-btn.active .sidebar-user-email {
color: var(--accent);
}
.sidebar-user-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-user-email {
font-size: 0.72rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.token-badge { .token-badge {
background-color: var(--bg-subtle); background-color: var(--bg-subtle);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);

View File

@@ -1,4 +1,4 @@
export type TabId = 'dashboard' | 'classes' | 'students' | 'learning' | 'network' | 'accounts'; export type TabId = 'dashboard' | 'classes' | 'students' | 'learning' | 'network' | 'email-domains' | 'profile';
export interface NavEntry { export interface NavEntry {
id: string; id: string;
@@ -15,20 +15,22 @@ export const TAB_LABELS: Record<TabId, string> = {
students: 'Sinh viên', students: 'Sinh viên',
learning: 'Giám sát & Lịch học', learning: 'Giám sát & Lịch học',
network: 'Quản lý mạng', network: 'Quản lý mạng',
accounts: 'Tài khoản', 'email-domains': 'Đuôi email',
profile: 'Tài khoản của tôi',
}; };
const HISTORY_KEY = 'sc_nav_history'; const HISTORY_KEY = 'sc_nav_history';
const MAX_HISTORY = 10; const MAX_HISTORY = 10;
function isTabId(value: string | null): value is TabId { function isTabId(value: string | null): value is TabId {
return value === 'dashboard' || value === 'classes' || value === 'students' || value === 'learning' || value === 'network' || value === 'accounts'; return value === 'dashboard' || value === 'classes' || value === 'students' || value === 'learning' || value === 'network' || value === 'email-domains' || value === 'profile';
} }
export function parseRoute(): { tab: TabId; classId: number | null } { export function parseRoute(): { tab: TabId; classId: number | null } {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const tabParam = params.get('tab'); const tabParam = params.get('tab');
const tab: TabId = isTabId(tabParam) ? tabParam : 'dashboard'; let tab: TabId = isTabId(tabParam) ? tabParam : 'dashboard';
if (tabParam === 'accounts') tab = 'email-domains';
const classIdRaw = params.get('classId'); const classIdRaw = params.get('classId');
const classId = classIdRaw ? Number(classIdRaw) : null; const classId = classIdRaw ? Number(classIdRaw) : null;
return { return {