add account web
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 lý đuôi email được phép và đổi mật khẩu.</p>
|
<p className="page-desc">Cấu hình đuôi email được phép đăng ký và đă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 và bảo mật cá 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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user