Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
775 lines
37 KiB
JavaScript
775 lines
37 KiB
JavaScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
import { getSession } from '../../../admin/js/api.js';
|
|
import { addDays, toIsoDate } from '../../shared/dates.js';
|
|
import { requestCountLabel } from '../admin-service.js';
|
|
import {
|
|
DEPARTMENT_REQUEST_TABLE_COLUMNS,
|
|
SUBJECT_TABLE_COLUMNS,
|
|
assignTeacher,
|
|
assignTeacherError,
|
|
availableTeachers,
|
|
defaultWorkloadPeriod,
|
|
departmentOptionLabel,
|
|
fetchAllTeachers,
|
|
fetchDepartmentSubjects,
|
|
fetchDepartmentTeacherRequests,
|
|
fetchDepartmentTeachers,
|
|
fetchDepartments,
|
|
fetchSemesters,
|
|
fetchSubjectComments,
|
|
fetchTeacherWorkload,
|
|
formatDateTime,
|
|
importSubjects,
|
|
importSubjectsPayload,
|
|
requestStatusClass,
|
|
requestStatusLabel,
|
|
saveSubjectComment,
|
|
subjectCountLabel,
|
|
submitTeacherRequest,
|
|
teacherMetrics,
|
|
teacherOptionLabel,
|
|
teacherRequestPayload,
|
|
workloadLabel
|
|
} from '../department-workspace-service.js';
|
|
|
|
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
|
|
|
export function DepartmentWorkspaceTab() {
|
|
const [session] = useState(() => getSession());
|
|
const role = session?.role;
|
|
const userDepartmentId = session?.departmentId;
|
|
const isDepartmentRole = role === ROLE_DEPARTMENT;
|
|
|
|
const departmentIdRef = useRef('');
|
|
const startDateRef = useRef(toIsoDate(new Date()));
|
|
const endDateRef = useRef(toIsoDate(addDays(new Date(), 7)));
|
|
|
|
const [departments, setDepartments] = useState([]);
|
|
const [departmentId, setDepartmentId] = useState('');
|
|
const [startDate, setStartDate] = useState(startDateRef.current);
|
|
const [endDate, setEndDate] = useState(endDateRef.current);
|
|
const [workspaceAlert, setWorkspaceAlert] = useState({ text: '', isError: false });
|
|
|
|
const [subjects, setSubjects] = useState({ loading: true, items: [], error: '' });
|
|
const [teachers, setTeachers] = useState({ items: null, error: '' });
|
|
const [workload, setWorkload] = useState({ items: null, error: '' });
|
|
const [requests, setRequests] = useState({ items: null, error: '' });
|
|
const [allTeachers, setAllTeachers] = useState([]);
|
|
const [allTeachersError, setAllTeachersError] = useState('');
|
|
|
|
const [importRows, setImportRows] = useState([{ code: '', name: '' }]);
|
|
const [importAlert, setImportAlert] = useState({ text: '', isError: false });
|
|
|
|
const [assignTeacherId, setAssignTeacherId] = useState('');
|
|
const [assignComment, setAssignComment] = useState('');
|
|
const [assignAlert, setAssignAlert] = useState({ text: '', isError: false });
|
|
|
|
const [requestForm, setRequestForm] = useState({ username: '', fullName: '', jobTitle: '', comment: '' });
|
|
const [requestAlert, setRequestAlert] = useState({ text: '', isError: false });
|
|
|
|
const [comments, setComments] = useState(null);
|
|
const [commentText, setCommentText] = useState('');
|
|
const [commentAlert, setCommentAlert] = useState({ text: '', isError: false });
|
|
|
|
function currentDepartmentId() {
|
|
return isDepartmentRole ? userDepartmentId : departmentIdRef.current;
|
|
}
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
(async () => {
|
|
try {
|
|
const loaded = await fetchDepartments();
|
|
if (!active) return;
|
|
const list = loaded || [];
|
|
setDepartments(list);
|
|
if (isDepartmentRole) {
|
|
departmentIdRef.current = userDepartmentId || '';
|
|
setDepartmentId(departmentIdRef.current);
|
|
} else if (list.length) {
|
|
departmentIdRef.current = String(list[0].id);
|
|
setDepartmentId(departmentIdRef.current);
|
|
}
|
|
try {
|
|
const semesters = await fetchSemesters();
|
|
const period = defaultWorkloadPeriod(semesters, new Date());
|
|
startDateRef.current = period.startDate;
|
|
endDateRef.current = period.endDate;
|
|
if (active) {
|
|
setStartDate(period.startDate);
|
|
setEndDate(period.endDate);
|
|
}
|
|
} catch {
|
|
// Текущий диапазон остаётся безопасным fallback, если справочник
|
|
// семестров недоступен.
|
|
}
|
|
try {
|
|
const loadedTeachers = await fetchAllTeachers();
|
|
if (active) setAllTeachers(Array.isArray(loadedTeachers) ? loadedTeachers : []);
|
|
} catch (error) {
|
|
if (active) setAllTeachersError(error.message || 'Ошибка загрузки преподавателей');
|
|
}
|
|
if (!active) return;
|
|
await loadWorkspace();
|
|
} catch (error) {
|
|
if (!active) return;
|
|
setWorkspaceAlert({ text: error.message || 'Ошибка загрузки кабинета кафедры', isError: true });
|
|
}
|
|
})();
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
async function loadWorkspace() {
|
|
setWorkspaceAlert({ text: '', isError: false });
|
|
setTeachers({ items: null, error: '' });
|
|
setWorkload({ items: null, error: '' });
|
|
setRequests({ items: null, error: '' });
|
|
await Promise.all([loadSubjects(), loadTeachers(), loadWorkload(), loadTeacherRequests()]);
|
|
}
|
|
|
|
async function loadSubjects() {
|
|
setSubjects({ loading: true, items: [], error: '' });
|
|
try {
|
|
const items = await fetchDepartmentSubjects(currentDepartmentId());
|
|
setSubjects({ loading: false, items: Array.isArray(items) ? items : [], error: '' });
|
|
} catch (error) {
|
|
setSubjects({ loading: false, items: [], error: error.message || 'Ошибка загрузки дисциплин' });
|
|
}
|
|
}
|
|
|
|
async function loadTeachers() {
|
|
setTeachers({ items: null, error: '' });
|
|
try {
|
|
const items = await fetchDepartmentTeachers(currentDepartmentId());
|
|
setTeachers({ items: Array.isArray(items) ? items : [], error: '' });
|
|
} catch (error) {
|
|
setTeachers({ items: [], error: error.message || 'Ошибка загрузки преподавателей' });
|
|
}
|
|
}
|
|
|
|
async function loadWorkload() {
|
|
if (!startDateRef.current || !endDateRef.current) {
|
|
setWorkload({ items: [], error: 'Выберите начало и окончание периода' });
|
|
return;
|
|
}
|
|
setWorkload({ items: null, error: '' });
|
|
try {
|
|
const items = await fetchTeacherWorkload({
|
|
startDate: startDateRef.current,
|
|
endDate: endDateRef.current,
|
|
departmentId: currentDepartmentId()
|
|
});
|
|
setWorkload({ items: Array.isArray(items) ? items : [], error: '' });
|
|
} catch (error) {
|
|
setWorkload({ items: [], error: error.message || 'Ошибка расчёта нагрузки' });
|
|
}
|
|
}
|
|
|
|
async function loadTeacherRequests() {
|
|
setRequests({ items: null, error: '' });
|
|
try {
|
|
const items = await fetchDepartmentTeacherRequests(currentDepartmentId());
|
|
setRequests({ items: Array.isArray(items) ? items : [], error: '' });
|
|
} catch (error) {
|
|
setRequests({ items: [], error: error.message || 'Ошибка загрузки заявок' });
|
|
}
|
|
}
|
|
|
|
function handleDepartmentChange(value) {
|
|
departmentIdRef.current = value;
|
|
setDepartmentId(value);
|
|
void loadWorkspace();
|
|
}
|
|
|
|
function handleStartDateChange(value) {
|
|
startDateRef.current = value;
|
|
setStartDate(value);
|
|
if (value && endDateRef.current && value > endDateRef.current) {
|
|
endDateRef.current = '';
|
|
setEndDate('');
|
|
}
|
|
void loadWorkload();
|
|
}
|
|
|
|
function handleEndDateChange(value) {
|
|
endDateRef.current = value;
|
|
setEndDate(value);
|
|
void loadWorkload();
|
|
}
|
|
|
|
// --- Загрузка дисциплин ---
|
|
|
|
function addImportRow() {
|
|
setImportRows(current => [...current, { code: '', name: '' }]);
|
|
}
|
|
|
|
function updateImportRow(index, patch) {
|
|
setImportRows(current => current.map((row, rowIndex) => (
|
|
rowIndex === index ? { ...row, ...patch } : row
|
|
)));
|
|
}
|
|
|
|
function removeImportRow(index) {
|
|
setImportRows(current => {
|
|
const rows = current.filter((_, rowIndex) => rowIndex !== index);
|
|
return rows.length ? rows : [{ code: '', name: '' }];
|
|
});
|
|
}
|
|
|
|
async function submitImport(event) {
|
|
event.preventDefault();
|
|
setImportAlert({ text: '', isError: false });
|
|
const rows = importRows.map(row => ({
|
|
code: (row.code || '').trim(),
|
|
name: (row.name || '').trim()
|
|
}));
|
|
const { payload, error } = importSubjectsPayload(rows, currentDepartmentId());
|
|
if (error) {
|
|
setImportAlert({ text: error, isError: true });
|
|
return;
|
|
}
|
|
try {
|
|
const result = await importSubjects(payload);
|
|
setImportAlert({ text: result?.message || 'Дисциплины загружены', isError: false });
|
|
setImportRows([{ code: '', name: '' }]);
|
|
await loadSubjects();
|
|
} catch (err) {
|
|
setImportAlert({ text: err.message || 'Ошибка загрузки дисциплин', isError: true });
|
|
}
|
|
}
|
|
|
|
// --- Привязка преподавателя ---
|
|
|
|
async function submitAssign(event) {
|
|
event.preventDefault();
|
|
setAssignAlert({ text: '', isError: false });
|
|
const error = assignTeacherError(currentDepartmentId(), assignTeacherId);
|
|
if (error) {
|
|
setAssignAlert({ text: error, isError: true });
|
|
return;
|
|
}
|
|
try {
|
|
await assignTeacher(assignTeacherId, currentDepartmentId(), assignComment.trim());
|
|
setAssignAlert({ text: 'Преподаватель привязан к кафедре', isError: false });
|
|
setAssignComment('');
|
|
await Promise.all([loadTeachers(), loadWorkload()]);
|
|
} catch (err) {
|
|
setAssignAlert({ text: err.message || 'Ошибка привязки преподавателя', isError: true });
|
|
}
|
|
}
|
|
|
|
// --- Заявка на преподавателя ---
|
|
|
|
async function submitRequest(event) {
|
|
event.preventDefault();
|
|
setRequestAlert({ text: '', isError: false });
|
|
const { payload, error } = teacherRequestPayload(requestForm, currentDepartmentId());
|
|
if (error) {
|
|
setRequestAlert({ text: error, isError: true });
|
|
return;
|
|
}
|
|
try {
|
|
await submitTeacherRequest(payload);
|
|
setRequestAlert({ text: 'Заявка отправлена администратору', isError: false });
|
|
setRequestForm({ username: '', fullName: '', jobTitle: '', comment: '' });
|
|
await loadTeacherRequests();
|
|
} catch (err) {
|
|
setRequestAlert({ text: err.message || 'Ошибка отправки заявки', isError: true });
|
|
}
|
|
}
|
|
|
|
// --- Комментарии к дисциплинам ---
|
|
|
|
async function openComments(subjectId, subjectName) {
|
|
setComments({ subjectId, subjectName, items: null, error: '' });
|
|
setCommentText('');
|
|
setCommentAlert({ text: '', isError: false });
|
|
try {
|
|
const items = await fetchSubjectComments(subjectId);
|
|
setComments(current => (
|
|
current && current.subjectId === subjectId
|
|
? { ...current, items: Array.isArray(items) ? items : [], error: '' }
|
|
: current
|
|
));
|
|
} catch (error) {
|
|
setComments(current => (
|
|
current && current.subjectId === subjectId
|
|
? { ...current, items: [], error: error.message || 'Ошибка загрузки комментариев' }
|
|
: current
|
|
));
|
|
}
|
|
}
|
|
|
|
async function submitComment(event) {
|
|
event.preventDefault();
|
|
const subjectId = comments?.subjectId;
|
|
const comment = commentText.trim();
|
|
if (!subjectId) {
|
|
setCommentAlert({ text: 'Выберите дисциплину', isError: true });
|
|
return;
|
|
}
|
|
if (!comment) {
|
|
setCommentAlert({ text: 'Комментарий не должен быть пустым', isError: true });
|
|
return;
|
|
}
|
|
try {
|
|
await saveSubjectComment(subjectId, comment);
|
|
setCommentAlert({ text: 'Комментарий сохранён', isError: false });
|
|
await openComments(subjectId, comments.subjectName);
|
|
} catch (error) {
|
|
setCommentAlert({ text: error.message || 'Ошибка сохранения комментария', isError: true });
|
|
}
|
|
}
|
|
|
|
const metricRows = teachers.items === null || workload.items === null
|
|
? null
|
|
: teacherMetrics(teachers.items, workload.items);
|
|
const freeTeachers = teachers.items === null
|
|
? null
|
|
: availableTeachers(allTeachers, teachers.items);
|
|
const requestsCountLabel = requests.error
|
|
? '0 заявок'
|
|
: (requests.items === null ? 'Загрузка...' : requestCountLabel(requests.items.length));
|
|
const subjectsCountLabel = subjects.error
|
|
? '0 дисциплин'
|
|
: (subjects.loading ? 'Загрузка...' : subjectCountLabel(subjects.items.length));
|
|
|
|
return (
|
|
<>
|
|
<div className="card department-workspace-control-card">
|
|
<div className="card-header-row">
|
|
<h2>Рабочая область кафедры</h2>
|
|
<button
|
|
type="button"
|
|
className="btn btn-md btn-secondary"
|
|
id="department-workspace-refresh"
|
|
onClick={() => void loadWorkspace()}
|
|
>
|
|
Обновить
|
|
</button>
|
|
</div>
|
|
|
|
<div className="department-workspace-filter-grid">
|
|
{!isDepartmentRole && (
|
|
<div className="form-group" id="department-workspace-department-field">
|
|
<label htmlFor="department-workspace-department">Кафедра</label>
|
|
<select
|
|
id="department-workspace-department"
|
|
value={departmentId}
|
|
onChange={event => handleDepartmentChange(event.target.value)}
|
|
>
|
|
<option value="">Выберите кафедру</option>
|
|
{departments.map(department => (
|
|
<option key={department.id} value={department.id}>
|
|
{departmentOptionLabel(department)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
<div className="form-group">
|
|
<label htmlFor="department-workspace-start">Начало периода</label>
|
|
<input
|
|
type="date"
|
|
id="department-workspace-start"
|
|
value={startDate}
|
|
onChange={event => handleStartDateChange(event.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="department-workspace-end">Окончание периода</label>
|
|
<input
|
|
type="date"
|
|
id="department-workspace-end"
|
|
value={endDate}
|
|
onChange={event => handleEndDateChange(event.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`form-alert${workspaceAlert.text ? (workspaceAlert.isError ? ' error' : ' success') : ''}`}
|
|
id="department-workspace-alert"
|
|
role="alert"
|
|
>
|
|
{workspaceAlert.text}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="department-workspace-grid department-workspace-grid-wide department-workspace-actions-grid">
|
|
<div className="card create-card department-subject-import-card">
|
|
<div className="card-header-row">
|
|
<h2>Загрузка дисциплин</h2>
|
|
</div>
|
|
<form id="department-subject-import-form" className="stack-form" onSubmit={submitImport}>
|
|
<div className="subject-import-slots-panel">
|
|
<div className="card-header-row">
|
|
<label className="schedule-section-label schedule-section-label--flush">
|
|
Дисциплины для загрузки
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-secondary"
|
|
id="subject-import-add"
|
|
onClick={addImportRow}
|
|
>
|
|
Добавить строку
|
|
</button>
|
|
</div>
|
|
<div id="subject-import-rows" className="subject-import-rows-list">
|
|
{importRows.map((row, index) => (
|
|
<div className="subject-import-row" key={index}>
|
|
<div className="form-group">
|
|
<label>Код</label>
|
|
<input
|
|
type="text"
|
|
className="subject-import-code"
|
|
placeholder="DB201"
|
|
value={row.code}
|
|
onChange={event => updateImportRow(index, { code: event.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Название</label>
|
|
<input
|
|
type="text"
|
|
className="subject-import-name"
|
|
placeholder="Базы данных"
|
|
value={row.name}
|
|
onChange={event => updateImportRow(index, { name: event.target.value })}
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-danger subject-import-remove"
|
|
onClick={() => removeImportRow(index)}
|
|
>
|
|
Удалить
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<button type="submit" className="btn btn-md btn-secondary">Загрузить</button>
|
|
<div
|
|
className={`form-alert${importAlert.text ? (importAlert.isError ? ' error' : ' success') : ''}`}
|
|
id="department-subject-import-alert"
|
|
role="alert"
|
|
>
|
|
{importAlert.text}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="card create-card department-teacher-assign-card">
|
|
<div className="card-header-row">
|
|
<h2>Добавить преподавателя</h2>
|
|
</div>
|
|
<form id="department-teacher-assign-form" className="stack-form" onSubmit={submitAssign}>
|
|
<div className="form-group">
|
|
<label htmlFor="department-teacher-select">Преподаватель</label>
|
|
<select
|
|
id="department-teacher-select"
|
|
value={assignTeacherId}
|
|
onChange={event => setAssignTeacherId(event.target.value)}
|
|
>
|
|
{freeTeachers === null ? (
|
|
<option value="">Загрузка...</option>
|
|
) : allTeachersError ? (
|
|
<option value="">{allTeachersError}</option>
|
|
) : freeTeachers.length === 0 ? (
|
|
<option value="">Нет доступных преподавателей</option>
|
|
) : (
|
|
<>
|
|
<option value="">Выберите преподавателя</option>
|
|
{freeTeachers.map(teacher => (
|
|
<option key={teacher.id} value={teacher.id}>
|
|
{teacherOptionLabel(teacher)}
|
|
</option>
|
|
))}
|
|
</>
|
|
)}
|
|
</select>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="department-teacher-assign-comment">Комментарий</label>
|
|
<input
|
|
type="text"
|
|
id="department-teacher-assign-comment"
|
|
placeholder="Например, ведет лабораторные работы"
|
|
value={assignComment}
|
|
onChange={event => setAssignComment(event.target.value)}
|
|
/>
|
|
</div>
|
|
<button type="submit" className="btn btn-md btn-secondary">Привязать к кафедре</button>
|
|
<div
|
|
className={`form-alert${assignAlert.text ? (assignAlert.isError ? ' error' : ' success') : ''}`}
|
|
id="department-teacher-assign-alert"
|
|
role="alert"
|
|
>
|
|
{assignAlert.text}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="card create-card department-teacher-request-card">
|
|
<div className="card-header-row">
|
|
<h2>Заявка на преподавателя</h2>
|
|
</div>
|
|
<form id="department-teacher-request-form" className="stack-form" onSubmit={submitRequest}>
|
|
<div className="form-group">
|
|
<label htmlFor="teacher-request-username">Логин</label>
|
|
<input
|
|
type="text"
|
|
id="teacher-request-username"
|
|
placeholder="ivanov_teacher"
|
|
maxLength={50}
|
|
required
|
|
value={requestForm.username}
|
|
onChange={event => setRequestForm(current => (
|
|
{ ...current, username: event.target.value }
|
|
))}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="teacher-request-fullname">ФИО</label>
|
|
<input
|
|
type="text"
|
|
id="teacher-request-fullname"
|
|
placeholder="Иванов Иван Иванович"
|
|
maxLength={255}
|
|
required
|
|
value={requestForm.fullName}
|
|
onChange={event => setRequestForm(current => (
|
|
{ ...current, fullName: event.target.value }
|
|
))}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="teacher-request-jobtitle">Должность</label>
|
|
<input
|
|
type="text"
|
|
id="teacher-request-jobtitle"
|
|
placeholder="Доцент"
|
|
maxLength={255}
|
|
value={requestForm.jobTitle}
|
|
onChange={event => setRequestForm(current => (
|
|
{ ...current, jobTitle: event.target.value }
|
|
))}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label htmlFor="teacher-request-comment">Комментарий</label>
|
|
<input
|
|
type="text"
|
|
id="teacher-request-comment"
|
|
placeholder="Что важно проверить администратору"
|
|
value={requestForm.comment}
|
|
onChange={event => setRequestForm(current => (
|
|
{ ...current, comment: event.target.value }
|
|
))}
|
|
/>
|
|
</div>
|
|
<button type="submit" className="btn btn-md btn-primary">Отправить заявку</button>
|
|
<div
|
|
className={`form-alert${requestAlert.text ? (requestAlert.isError ? ' error' : ' success') : ''}`}
|
|
id="department-teacher-request-alert"
|
|
role="alert"
|
|
>
|
|
{requestAlert.text}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="card department-teachers-card">
|
|
<div className="card-header-row">
|
|
<h2>Преподаватели и нагрузка</h2>
|
|
</div>
|
|
<div className="metric-grid" id="department-teachers-list">
|
|
{metricRows === null ? (
|
|
'Загрузка...'
|
|
) : metricRows.length === 0 ? (
|
|
<div className="loading-row">
|
|
{teachers.error || 'Преподаватели не найдены'}
|
|
</div>
|
|
) : metricRows.map((row, index) => (
|
|
<article className="metric-card" key={index}>
|
|
<strong>{row.name || '-'}</strong>
|
|
<span>{row.meta || 'Должность не указана'}</span>
|
|
<small>{workloadLabel(row.workload, workload.error)}</small>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card department-workspace-table-card">
|
|
<div className="card-header-row">
|
|
<h2>Заявки кафедры</h2>
|
|
<span className="muted" id="department-teacher-request-count">{requestsCountLabel}</span>
|
|
</div>
|
|
<div className="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Статус</th>
|
|
<th>Логин</th>
|
|
<th>ФИО</th>
|
|
<th>Должность</th>
|
|
<th>Комментарий</th>
|
|
<th>Рассмотрение</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="department-teacher-requests-tbody">
|
|
{requests.items === null && !requests.error ? (
|
|
<tr>
|
|
<td colSpan={DEPARTMENT_REQUEST_TABLE_COLUMNS} className="loading-row">
|
|
Загрузка...
|
|
</td>
|
|
</tr>
|
|
) : requests.error ? (
|
|
<tr>
|
|
<td colSpan={DEPARTMENT_REQUEST_TABLE_COLUMNS} className="loading-row">
|
|
{requests.error}
|
|
</td>
|
|
</tr>
|
|
) : requests.items.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={DEPARTMENT_REQUEST_TABLE_COLUMNS} className="loading-row">
|
|
Заявок пока нет
|
|
</td>
|
|
</tr>
|
|
) : requests.items.map(request => (
|
|
<tr key={request.id}>
|
|
<td>
|
|
<span className={`badge ${requestStatusClass(request.status)}`}>
|
|
{requestStatusLabel(request.status)}
|
|
</span>
|
|
</td>
|
|
<td>{request.username || '-'}</td>
|
|
<td>{request.fullName || '-'}</td>
|
|
<td>{request.jobTitle || '-'}</td>
|
|
<td>{request.comment || '-'}</td>
|
|
<td>{request.reviewComment || formatDateTime(request.reviewedAt) || '-'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card department-workspace-table-card">
|
|
<div className="card-header-row">
|
|
<h2>Дисциплины кафедры</h2>
|
|
<span className="muted" id="department-subject-count">{subjectsCountLabel}</span>
|
|
</div>
|
|
<div className="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Код</th>
|
|
<th>Название</th>
|
|
<th>Описание</th>
|
|
<th>Комментарии</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="department-subjects-tbody">
|
|
{subjects.loading ? (
|
|
<tr>
|
|
<td colSpan={SUBJECT_TABLE_COLUMNS} className="loading-row">
|
|
Загрузка...
|
|
</td>
|
|
</tr>
|
|
) : subjects.error ? (
|
|
<tr>
|
|
<td colSpan={SUBJECT_TABLE_COLUMNS} className="loading-row">
|
|
{subjects.error}
|
|
</td>
|
|
</tr>
|
|
) : subjects.items.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={SUBJECT_TABLE_COLUMNS} className="loading-row">
|
|
Дисциплины не найдены
|
|
</td>
|
|
</tr>
|
|
) : subjects.items.map(subject => (
|
|
<tr key={subject.id}>
|
|
<td>{subject.code || '-'}</td>
|
|
<td>{subject.name || '-'}</td>
|
|
<td>{subject.description || '-'}</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-secondary department-comments-open"
|
|
onClick={() => void openComments(subject.id, subject.name || '')}
|
|
>
|
|
Открыть
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{comments && (
|
|
<div className="card department-workspace-table-card" id="department-comments-card">
|
|
<div className="card-header-row">
|
|
<h2 id="department-comments-title">
|
|
Комментарии: {comments.subjectName || 'дисциплина'}
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-ghost"
|
|
id="department-comments-close"
|
|
onClick={() => setComments(null)}
|
|
>
|
|
Скрыть
|
|
</button>
|
|
</div>
|
|
|
|
<form id="department-comment-form" className="form-row" onSubmit={submitComment}>
|
|
<div className="form-group">
|
|
<label htmlFor="department-comment-text">Новый комментарий</label>
|
|
<input
|
|
type="text"
|
|
id="department-comment-text"
|
|
placeholder="Добавьте примечание по дисциплине"
|
|
value={commentText}
|
|
onChange={event => setCommentText(event.target.value)}
|
|
/>
|
|
</div>
|
|
<button type="submit" className="btn btn-md btn-primary">Сохранить</button>
|
|
</form>
|
|
<div
|
|
className={`form-alert${commentAlert.text ? (commentAlert.isError ? ' error' : ' success') : ''}`}
|
|
id="department-comment-alert"
|
|
role="alert"
|
|
>
|
|
{commentAlert.text}
|
|
</div>
|
|
<div className="department-comment-list" id="department-comment-list">
|
|
{comments.items === null ? (
|
|
'Загрузка...'
|
|
) : comments.error ? (
|
|
<div className="loading-row">{comments.error}</div>
|
|
) : comments.items.length === 0 ? (
|
|
<div className="loading-row">Комментариев пока нет</div>
|
|
) : comments.items.map((comment, index) => (
|
|
<article className="department-comment-item" key={index}>
|
|
<strong>{comment.authorName || 'Автор не указан'}</strong>
|
|
<span>{formatDateTime(comment.createdAt)}</span>
|
|
<p>{comment.comment}</p>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|