import { useEffect, useRef, useState } from 'react'; import { AlertDialog, ConfirmDialog } from '../../shared/ui/ConfirmDialog.jsx'; import { Pagination } from '../../shared/ui/Pagination.jsx'; import { DEPARTMENT_TABLE_COLUMNS, PROFILE_TABLE_COLUMNS, SPECIALTY_TABLE_COLUMNS, clampPage, createDepartment, createProfile, createSpecialty, deleteDepartment, deleteProfile, deleteSpecialty, departmentCode, departmentFields, departmentName, fetchAllProfiles, fetchDepartments, fetchProfiles, fetchSpecialties, filterListItems, pageSlice, profileFields, profileSpecialtyLabel, specialtyCode, specialtyFields, specialtyName, specialtyOptionLabel, updateDepartment, updateProfile, updateSpecialty } from '../university-structure-service.js'; const SEARCH_DEBOUNCE_MS = 250; const SECTIONS = ['departments', 'specialties', 'profiles']; const SECTION_TITLES = { departments: 'Кафедры', specialties: 'Специальности', profiles: 'Профили' }; function initialListStates() { return { departments: { query: '', page: 0, size: 25 }, specialties: { query: '', page: 0, size: 25 }, profiles: { query: '', page: 0, size: 25 } }; } export function UniversityStructureTab() { const [activeSection, setActiveSection] = useState('departments'); const [departments, setDepartments] = useState(null); const [specialties, setSpecialties] = useState(null); const [allProfiles, setAllProfiles] = useState(null); const [loadErrors, setLoadErrors] = useState({}); const [listStates, setListStates] = useState(initialListStates); const [searchInputs, setSearchInputs] = useState({ departments: '', specialties: '', profiles: '' }); const [createDept, setCreateDept] = useState({ name: '', code: '' }); const [createDeptAlert, setCreateDeptAlert] = useState({ text: '', isError: false }); const [createSpec, setCreateSpec] = useState({ name: '', code: '' }); const [createSpecAlert, setCreateSpecAlert] = useState({ text: '', isError: false }); const [profileForm, setProfileForm] = useState({ profileId: '', specId: '', name: '', description: '', editing: false }); const [profileAlert, setProfileAlert] = useState({ text: '', isError: false }); const [editDeptModal, setEditDeptModal] = useState(null); const [editSpecModal, setEditSpecModal] = useState(null); const [profilesModal, setProfilesModal] = useState(null); const [confirmState, setConfirmState] = useState(null); const [alertState, setAlertState] = useState(null); const searchTimers = useRef({}); const profileFormRef = useRef(null); useEffect(() => { void loadData(); return () => { Object.values(searchTimers.current).forEach(timer => window.clearTimeout(timer)); }; }, []); async function loadData() { const [deptResult, specResult, profilesResult] = await Promise.allSettled([ fetchDepartments(), fetchSpecialties(), fetchAllProfiles() ]); setDepartments(deptResult.status === 'fulfilled' ? deptResult.value || [] : null); setSpecialties(specResult.status === 'fulfilled' ? specResult.value || [] : null); setAllProfiles(profilesResult.status === 'fulfilled' ? profilesResult.value || [] : null); setLoadErrors({ departments: deptResult.status === 'rejected', specialties: specResult.status === 'rejected', profiles: profilesResult.status === 'rejected' }); } function handleSearchInput(section, value) { setSearchInputs(current => ({ ...current, [section]: value })); window.clearTimeout(searchTimers.current[section]); searchTimers.current[section] = window.setTimeout(() => { setListStates(current => ({ ...current, [section]: { ...current[section], query: value.trim(), page: 0 } })); }, SEARCH_DEBOUNCE_MS); } function applyListState(section, patch) { setListStates(current => ({ ...current, [section]: { ...current[section], ...patch } })); } function visibleItems(section, items, fields) { const listState = listStates[section]; const filtered = filterListItems(items, listState.query, fields); const totalPages = Math.ceil(filtered.length / listState.size); const page = clampPage(listState.page, totalPages); return { filtered, page, size: listState.size, totalPages, items: pageSlice(filtered, page, listState.size) }; } // --- Кафедры --- async function submitCreateDepartment(event) { event.preventDefault(); const name = createDept.name.trim(); const code = createDept.code.trim(); if (!name || !code) { setCreateDeptAlert({ text: 'Заполните все поля', isError: true }); return; } try { await createDepartment({ name, code }); setCreateDeptAlert({ text: `Кафедра «${name}» создана`, isError: false }); setCreateDept({ name: '', code: '' }); await loadData(); } catch (error) { setCreateDeptAlert({ text: error.message || 'Ошибка создания кафедры', isError: true }); } } function openEditDepartmentModal(department) { setEditDeptModal({ department, name: departmentName(department) || '', code: departmentCode(department) === null || departmentCode(department) === undefined ? '' : String(departmentCode(department)), alert: { text: '', isError: false } }); } async function submitEditDepartment(event) { event.preventDefault(); const modal = editDeptModal; const name = modal.name.trim(); const code = modal.code.trim(); if (!name || !code) { setEditDeptModal({ ...modal, alert: { text: 'Заполните все поля', isError: true } }); return; } try { await updateDepartment(modal.department.id, { name, code }); setEditDeptModal(null); setCreateDeptAlert({ text: `Кафедра «${name}» обновлена`, isError: false }); await loadData(); } catch (error) { setEditDeptModal({ ...modal, alert: { text: error.message || 'Ошибка обновления кафедры', isError: true } }); } } function confirmDeleteDepartment(department) { setConfirmState({ title: 'Удаление кафедры', message: 'Удалить кафедру?', confirmLabel: 'Удалить', danger: true, action: async () => { try { await deleteDepartment(department.id); setCreateDeptAlert({ text: 'Кафедра удалена', isError: false }); await loadData(); } catch (error) { setCreateDeptAlert({ text: error.message || 'Ошибка удаления кафедры', isError: true }); } } }); } // --- Специальности --- async function submitCreateSpecialty(event) { event.preventDefault(); const name = createSpec.name.trim(); const code = createSpec.code.trim(); if (!name || !code) { setCreateSpecAlert({ text: 'Заполните все поля', isError: true }); return; } try { await createSpecialty({ name, code }); setCreateSpecAlert({ text: `Специальность «${name}» создана`, isError: false }); setCreateSpec({ name: '', code: '' }); await loadData(); } catch (error) { setCreateSpecAlert({ text: error.message || 'Ошибка создания специальности', isError: true }); } } function openEditSpecialtyModal(speciality) { setEditSpecModal({ speciality, name: specialtyName(speciality) || '', code: specialtyCode(speciality) || '', alert: { text: '', isError: false } }); } async function submitEditSpecialty(event) { event.preventDefault(); const modal = editSpecModal; const name = modal.name.trim(); const code = modal.code.trim(); if (!name || !code) { setEditSpecModal({ ...modal, alert: { text: 'Заполните все поля', isError: true } }); return; } try { await updateSpecialty(modal.speciality.id, { name, code }); setEditSpecModal(null); setCreateSpecAlert({ text: `Специальность «${name}» обновлена`, isError: false }); await loadData(); } catch (error) { setEditSpecModal({ ...modal, alert: { text: error.message || 'Ошибка обновления специальности', isError: true } }); } } function confirmDeleteSpecialty(speciality) { setConfirmState({ title: 'Удаление специальности', message: 'Удалить специальность?', confirmLabel: 'Удалить', danger: true, action: async () => { try { await deleteSpecialty(speciality.id); setCreateSpecAlert({ text: 'Специальность удалена', isError: false }); await loadData(); } catch (error) { setCreateSpecAlert({ text: error.message || 'Ошибка удаления специальности', isError: true }); } } }); } // --- Профили: вкладка --- function resetProfileForm() { setProfileForm({ profileId: '', specId: '', name: '', description: '', editing: false }); } async function submitProfileForm(event) { event.preventDefault(); const { profileId, specId, name, description } = profileForm; const trimmedName = name.trim(); const trimmedDescription = description.trim(); if (!specId) { setProfileAlert({ text: 'Выберите специальность', isError: true }); return; } if (!trimmedName) { setProfileAlert({ text: 'Введите название профиля', isError: true }); return; } try { if (profileId) { await updateProfile(specId, profileId, { name: trimmedName, description: trimmedDescription }); setProfileAlert({ text: `Профиль «${trimmedName}» обновлен`, isError: false }); } else { await createProfile(specId, { name: trimmedName, description: trimmedDescription }); setProfileAlert({ text: `Профиль «${trimmedName}» создан`, isError: false }); } resetProfileForm(); await loadData(); } catch (error) { setProfileAlert({ text: error.message || 'Ошибка сохранения профиля', isError: true }); } } function editTabProfile(profile) { setProfileForm({ profileId: String(profile.id), specId: String(profile.specialityId), name: profile.name || '', description: profile.description || '', editing: true }); profileFormRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function confirmDeleteTabProfile(profile) { setConfirmState({ title: 'Удаление профиля', message: 'Удалить профиль обучения?', confirmLabel: 'Удалить', danger: true, action: async () => { try { await deleteProfile(profile.specialityId, profile.id); setProfileAlert({ text: 'Профиль удален', isError: false }); await loadData(); } catch (error) { setProfileAlert({ text: error.message || 'Ошибка удаления профиля', isError: true }); } } }); } // --- Профили: модальное окно специальности --- async function openProfilesModal(speciality) { setProfilesModal({ speciality, profiles: null, form: { profileId: '', name: '', description: '', editing: false }, alert: { text: '', isError: false } }); try { const profiles = await fetchProfiles(speciality.id); setProfilesModal(current => (current ? { ...current, profiles: profiles || [] } : current)); } catch (error) { setProfilesModal(current => ( current ? { ...current, profiles: [], alert: { text: error.message || 'Ошибка загрузки', isError: true } } : current )); } } function resetModalProfileForm() { setProfilesModal(current => ( current ? { ...current, form: { profileId: '', name: '', description: '', editing: false } } : current )); } async function submitModalProfile(event) { event.preventDefault(); const modal = profilesModal; const { profileId, name, description } = modal.form; const trimmedName = name.trim(); const trimmedDescription = description.trim(); if (!trimmedName) { setProfilesModal({ ...modal, alert: { text: 'Введите название профиля', isError: true } }); return; } try { if (profileId) { await updateProfile(modal.speciality.id, profileId, { name: trimmedName, description: trimmedDescription }); setProfilesModal(current => ( current ? { ...current, alert: { text: `Профиль «${trimmedName}» обновлен`, isError: false } } : current )); } else { await createProfile(modal.speciality.id, { name: trimmedName, description: trimmedDescription }); setProfilesModal(current => ( current ? { ...current, alert: { text: `Профиль «${trimmedName}» создан`, isError: false } } : current )); } resetModalProfileForm(); const profiles = await fetchProfiles(modal.speciality.id); setProfilesModal(current => (current ? { ...current, profiles: profiles || [] } : current)); } catch (error) { setProfilesModal(current => ( current ? { ...current, alert: { text: error.message || 'Ошибка сохранения профиля', isError: true } } : current )); } } function editModalProfile(profile) { setProfilesModal(current => ( current ? { ...current, form: { profileId: String(profile.id), name: profile.name || '', description: profile.description || '', editing: true } } : current )); } function confirmDeleteModalProfile(profile) { const modal = profilesModal; if (!modal) return; setConfirmState({ title: 'Удаление профиля', message: 'Удалить профиль обучения?', confirmLabel: 'Удалить', danger: true, action: async () => { try { await deleteProfile(modal.speciality.id, profile.id); setProfilesModal(current => ( current ? { ...current, alert: { text: 'Профиль удален', isError: false } } : current )); const profiles = await fetchProfiles(modal.speciality.id); setProfilesModal(current => (current ? { ...current, profiles: profiles || [] } : current)); } catch (error) { setProfilesModal(current => ( current ? { ...current, alert: { text: error.message || 'Ошибка удаления профиля', isError: true } } : current )); } } }); } async function runConfirm() { const confirm = confirmState; setConfirmState(null); await confirm.action(); } const departmentsView = visibleItems('departments', departments || [], departmentFields); const specialtiesView = visibleItems('specialties', specialties || [], specialtyFields); const profilesView = visibleItems('profiles', allProfiles || [], profileFields); return ( <>
{SECTIONS.map(section => ( ))}
{/* Раздел Кафедр */} {/* Раздел Специальностей */} {/* Раздел Профилей */}
{editDeptModal && (
{ if (event.target === event.currentTarget) setEditDeptModal(null); }} >

Редактирование кафедры

setEditDeptModal(current => ( current ? { ...current, name: event.target.value } : current ))} />
setEditDeptModal(current => ( current ? { ...current, code: event.target.value } : current ))} />
{editDeptModal.alert.text}
)} {editSpecModal && (
{ if (event.target === event.currentTarget) setEditSpecModal(null); }} >

Редактирование специальности

setEditSpecModal(current => ( current ? { ...current, name: event.target.value } : current ))} />
setEditSpecModal(current => ( current ? { ...current, code: event.target.value } : current ))} />
{editSpecModal.alert.text}
)} {profilesModal && (
{ if (event.target === event.currentTarget) setProfilesModal(null); }} >

Профили обучения

{specialtyCode(profilesModal.speciality)} · {specialtyName(profilesModal.speciality)}

{profilesModal.form.editing ? 'Редактирование профиля' : 'Создание профиля'}

setProfilesModal(current => ( current ? { ...current, form: { ...current.form, name: event.target.value } } : current ))} />
setProfilesModal(current => ( current ? { ...current, form: { ...current.form, description: event.target.value } } : current ))} />
{profilesModal.form.editing && ( )}
{profilesModal.alert.text}
{profilesModal.profiles === null ? ( ) : profilesModal.profiles.length === 0 ? ( ) : profilesModal.profiles.map(profile => ( ))}
ID Название Описание Действия
Загрузка...
Профили не созданы
{profile.id} {profile.name} {profile.description || '-'}
)} {confirmState && ( void runConfirm()} onClose={() => setConfirmState(null)} /> )} {alertState && ( setAlertState(null)} /> )} ); }