UI: фамилия с инициалами в шапке, подпись автора у тестов
- Хедер: отображать Фамилия И. О., полное ФИО в title. - Список тестов и карточка: «Автор: Вы» для своих, иначе «Автор: Фамилия И. О.». - API: в каталоге и summary/versions — created_by, author full_name (camelCase в JSON). Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api';
|
||||
import { formatSurnameWithInitials } from '../utils/formatUserName';
|
||||
|
||||
export default function CabinetLayout() {
|
||||
const nav = useNavigate();
|
||||
const [user, setUser] = useState(null);
|
||||
/** Backend: `devUi` при NODE_ENV=development */
|
||||
const [devUi, setDevUi] = useState(false);
|
||||
/** Каталог HR + блок назначения (env CLINIC_ASSIGNMENT_ENABLED в prod) */
|
||||
const [assignmentUi, setAssignmentUi] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState(null);
|
||||
|
||||
const loadMe = useCallback(async () => {
|
||||
setErr(null);
|
||||
try {
|
||||
const me = await api('/api/auth/me');
|
||||
setUser(me.user);
|
||||
setDevUi(!!me.devUi);
|
||||
setAssignmentUi(!!me.assignmentUi);
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
nav('/login', { replace: true });
|
||||
return;
|
||||
}
|
||||
setErr(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [nav]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMe();
|
||||
}, [loadMe]);
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await api('/api/auth/logout', { method: 'POST' });
|
||||
} catch {
|
||||
// ignore; still go to login
|
||||
}
|
||||
nav('/login');
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="cabinet-app cabinet-page--center">
|
||||
<p className="text-muted">Загрузка…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<div className="cabinet-app cabinet-page--center">
|
||||
<p className="callout callout--error" role="alert">
|
||||
{err}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cabinet-app">
|
||||
<header className="cabinet-header">
|
||||
<div className="cabinet-header__inner">
|
||||
<Link to="/tests" className="cabinet-brand">
|
||||
<span
|
||||
className="material-symbols-outlined cabinet-brand__icon"
|
||||
aria-hidden
|
||||
>
|
||||
school
|
||||
</span>
|
||||
<div>
|
||||
<div className="cabinet-brand__title">Система тестрования</div>
|
||||
<div className="cabinet-brand__subtitle">Портал</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="cabinet-header__actions">
|
||||
{user && (
|
||||
<span
|
||||
className="cabinet-user"
|
||||
title={user.fullName ? `${user.fullName} · ${user.role}` : user.role}
|
||||
>
|
||||
{formatSurnameWithInitials(user.fullName) || user.login || '—'}
|
||||
<span className="cabinet-user__role"> · {user.role}</span>
|
||||
</span>
|
||||
)}
|
||||
<button type="button" className="btn btn-ghost" onClick={logout}>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="cabinet-main">
|
||||
<Outlet context={{ user, devUi, assignmentUi, refreshUser: loadMe }} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user