diff --git a/src/api/graphql/training-lecture/training-lectures.graphql b/src/api/graphql/training-lecture/training-lectures.graphql index 06570eaf..01e23f42 100644 --- a/src/api/graphql/training-lecture/training-lectures.graphql +++ b/src/api/graphql/training-lecture/training-lectures.graphql @@ -1,6 +1,10 @@ query trainingLectures($id: ID!) { trainingLectures(id: $id) { id + number + locking + availableFrom + isAvailable lecture { id subject diff --git a/src/api/graphql/training-lecture/update-training-lecture-settings.graphql b/src/api/graphql/training-lecture/update-training-lecture-settings.graphql new file mode 100644 index 00000000..70be939e --- /dev/null +++ b/src/api/graphql/training-lecture/update-training-lecture-settings.graphql @@ -0,0 +1,16 @@ +mutation updateTrainingLectureSettings( + $id: ID! + $input: TrainingLectureInput! +) { + updateTrainingLectureSettings(id: $id, input: $input) { + id + number + locking + availableFrom + isAvailable + lecture { + id + subject + } + } +} diff --git a/src/api/schema.graphql b/src/api/schema.graphql index 143fbcbb..ca93551e 100644 --- a/src/api/schema.graphql +++ b/src/api/schema.graphql @@ -57,6 +57,10 @@ type Mutation { training lecture """ updateTrainingLecture(id: ID!, lectureIds: [ID!]): [TrainingLectureDto] + updateTrainingLectureSettings( + id: ID! + input: TrainingLectureInput! + ): TrainingLectureDto """ studentHomeWork section """ @@ -288,6 +292,7 @@ input TrainingLectureInput { lecture: ID! lastLecture: ID locking: Boolean + availableFrom: LocalDateTime } type TrainingLectureDto { @@ -296,6 +301,8 @@ type TrainingLectureDto { lecture: LectureDto lastLecture: LectureDto locking: Boolean + availableFrom: LocalDateTime + isAvailable: Boolean } input LectureInput { diff --git a/src/features/edit-training/containers/lecture-schedule/index.ts b/src/features/edit-training/containers/lecture-schedule/index.ts new file mode 100644 index 00000000..fb3d8dc6 --- /dev/null +++ b/src/features/edit-training/containers/lecture-schedule/index.ts @@ -0,0 +1 @@ +export { default } from "./lecture-schedule-container"; diff --git a/src/features/edit-training/containers/lecture-schedule/lecture-schedule-container.tsx b/src/features/edit-training/containers/lecture-schedule/lecture-schedule-container.tsx new file mode 100644 index 00000000..7c1f37cf --- /dev/null +++ b/src/features/edit-training/containers/lecture-schedule/lecture-schedule-container.tsx @@ -0,0 +1,30 @@ +import { FC } from "react"; +import { useParams } from "react-router-dom"; + +import { AppSpinner } from "shared/components/spinners"; +import NoDataErrorMessage from "shared/components/no-data-error-message"; +import { useTrainingLecturesQuery } from "api/graphql/generated/graphql"; +import { FETCH_POLICY } from "shared/constants"; + +import LectureSchedule from "../../views/lecture-schedule"; + +const LectureScheduleContainer: FC = () => { + const { trainingId } = useParams(); + + const { data, loading, refetch } = useTrainingLecturesQuery({ + variables: { id: trainingId! }, + fetchPolicy: FETCH_POLICY.NETWORK_ONLY, + }); + + if (loading) return ; + if (!data?.trainingLectures) return ; + + return ( + + ); +}; + +export default LectureScheduleContainer; diff --git a/src/features/edit-training/views/edit-training/edit-training.tsx b/src/features/edit-training/views/edit-training/edit-training.tsx index 69bf9ab9..1e7597e4 100644 --- a/src/features/edit-training/views/edit-training/edit-training.tsx +++ b/src/features/edit-training/views/edit-training/edit-training.tsx @@ -116,6 +116,12 @@ const EditTraining: FC = ({ data, updateTraining }) => { > Сохранить + Продолжить diff --git a/src/features/edit-training/views/lecture-schedule/index.ts b/src/features/edit-training/views/lecture-schedule/index.ts new file mode 100644 index 00000000..ebe6ccad --- /dev/null +++ b/src/features/edit-training/views/lecture-schedule/index.ts @@ -0,0 +1 @@ +export { default } from "./lecture-schedule"; diff --git a/src/features/edit-training/views/lecture-schedule/lecture-schedule.tsx b/src/features/edit-training/views/lecture-schedule/lecture-schedule.tsx new file mode 100644 index 00000000..de61d21e --- /dev/null +++ b/src/features/edit-training/views/lecture-schedule/lecture-schedule.tsx @@ -0,0 +1,288 @@ +import { FC, useState } from "react"; +import { + Box, + Button, + Card, + CardContent, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Switch, + IconButton, + Tooltip, + TextField, +} from "@mui/material"; +import { + Schedule as ScheduleIcon, + Lock as LockIcon, + LockOpen as LockOpenIcon, + Edit as EditIcon, +} from "@mui/icons-material"; +import { useSnackbar } from "notistack"; +import dayjs from "dayjs"; + +import { + TrainingLectureDto, + useUpdateTrainingLectureSettingsMutation, +} from "api/graphql/generated/graphql"; + +interface ILectureSchedule { + trainingLectures: (TrainingLectureDto | null)[]; + refetch: () => void; +} + +const LectureSchedule: FC = ({ + trainingLectures, + refetch, +}) => { + const { enqueueSnackbar } = useSnackbar(); + const [editingLectureId, setEditingLectureId] = useState(null); + const [selectedDate, setSelectedDate] = useState(""); + + const [updateSettings] = useUpdateTrainingLectureSettingsMutation(); + + const handleToggleLocking = async (lecture: TrainingLectureDto) => { + try { + await updateSettings({ + variables: { + id: lecture.id!, + input: { + lecture: lecture.lecture?.id!, + locking: !lecture.locking, + }, + }, + onCompleted: () => { + enqueueSnackbar( + `Урок "${lecture.lecture?.subject}" ${ + !lecture.locking ? "заблокирован" : "разблокирован" + }`, + { variant: "success" } + ); + refetch(); + }, + onError: () => { + enqueueSnackbar("Не удалось изменить настройки урока", { + variant: "error", + }); + }, + }); + } catch (error) { + console.error("Error toggling locking:", error); + } + }; + + const handleEditDate = (lecture: TrainingLectureDto) => { + setEditingLectureId(lecture.id!); + if (lecture.availableFrom) { + // Backend returns LocalDateTime in format YYYY-MM-DDTHH:mm:ss + // We need YYYY-MM-DDTHH:mm for datetime-local input + const localDateTime = lecture.availableFrom.slice(0, 16); + setSelectedDate(localDateTime); + } else { + setSelectedDate(""); + } + }; + + const handleSaveDate = async (lecture: TrainingLectureDto) => { + try { + // Convert datetime-local format (YYYY-MM-DDTHH:mm) to LocalDateTime format (YYYY-MM-DDTHH:mm:ss) + const formattedDate = selectedDate ? `${selectedDate}:00` : null; + + await updateSettings({ + variables: { + id: lecture.id!, + input: { + lecture: lecture.lecture?.id!, + availableFrom: formattedDate, + }, + }, + onCompleted: () => { + enqueueSnackbar("Дата открытия урока обновлена", { + variant: "success", + }); + setEditingLectureId(null); + setSelectedDate(""); + refetch(); + }, + onError: () => { + enqueueSnackbar("Не удалось обновить дату", { variant: "error" }); + }, + }); + } catch (error) { + console.error("Error saving date:", error); + } + }; + + const handleCancelEdit = () => { + setEditingLectureId(null); + setSelectedDate(""); + }; + + const formatDate = (date: string | null | undefined) => { + if (!date) return "Не установлена"; + return dayjs(date).format("DD.MM.YYYY HH:mm"); + }; + + const getAvailabilityText = (lecture: TrainingLectureDto) => { + if (lecture.locking) { + return "Заблокирован"; + } + if (!lecture.availableFrom) { + return "Доступен сразу"; + } + return lecture.isAvailable + ? "Доступен" + : `Доступен с ${formatDate(lecture.availableFrom)}`; + }; + + const getAvailabilityColor = (lecture: TrainingLectureDto) => { + if (lecture.locking) return "error"; + if (lecture.isAvailable) return "success"; + return "warning"; + }; + + return ( + + + + Расписание уроков + + + + + + Здесь вы можете настроить доступность уроков для студентов: + + + + Дата начала - урок станет доступен в указанное + время + + + Скрыть урок - урок будет виден, но недоступен для + открытия + + + + + + + + + + + Урок + Дата начала + Скрыть урок + Статус + + + + {trainingLectures.map((lecture) => ( + + {lecture?.number} + + + {lecture?.lecture?.subject} + + + + {editingLectureId === lecture?.id ? ( + + setSelectedDate(e.target.value)} + sx={{ minWidth: 220 }} + InputLabelProps={{ + shrink: true, + }} + /> + + + + ) : ( + + + {formatDate(lecture?.availableFrom)} + + + handleEditDate(lecture!)} + > + + + + + )} + + + + handleToggleLocking(lecture!)} + color="primary" + /> + + + + + {lecture?.locking ? ( + + ) : lecture?.isAvailable ? ( + + ) : ( + + )} + + {getAvailabilityText(lecture!)} + + + + + ))} + +
+
+ + {trainingLectures.length === 0 && ( + + + Нет уроков в курсе. Добавьте уроки для настройки расписания. + + + )} +
+ ); +}; + +export default LectureSchedule; diff --git a/src/features/lecture-detail/containers/lecture-detail/lecture-detail-container.tsx b/src/features/lecture-detail/containers/lecture-detail/lecture-detail-container.tsx index 16b76eb8..9ad35aba 100644 --- a/src/features/lecture-detail/containers/lecture-detail/lecture-detail-container.tsx +++ b/src/features/lecture-detail/containers/lecture-detail/lecture-detail-container.tsx @@ -1,5 +1,6 @@ -import { FC } from "react"; -import { useParams } from "react-router-dom"; +import { FC, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { useSnackbar } from "notistack"; import { AppSpinner } from "shared/components/spinners"; import NoDataErrorMessage from "shared/components/no-data-error-message"; @@ -15,6 +16,8 @@ import useTariff from "../../hooks/use-tariff"; const LectureDetailContainer: FC = () => { const { lectureId, trainingId } = useParams(); + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); const { tariffHomework } = useTariff({ trainingId }); @@ -36,6 +39,24 @@ const LectureDetailContainer: FC = () => { fetchPolicy: FETCH_POLICY.CACHE_AND_NETWORK, }); + useEffect(() => { + if (dataTrainingLectures?.trainingLectures && lectureId) { + const currentLecture = dataTrainingLectures.trainingLectures.find( + (tl) => tl?.lecture?.id === lectureId + ); + + if (currentLecture) { + const isLocked = currentLecture.locking; + const {isAvailable} = currentLecture; + + if (isLocked || !isAvailable) { + enqueueSnackbar("Этот урок пока недоступен", { variant: "warning" }); + navigate(`/training/${trainingId}`); + } + } + } + }, [dataTrainingLectures, lectureId, navigate, trainingId, enqueueSnackbar]); + if (loadingLecture || loadingTrainingLectures) { return ; } diff --git a/src/features/lecture-detail/views/stepper-buttons/stepper-buttons.tsx b/src/features/lecture-detail/views/stepper-buttons/stepper-buttons.tsx index a02c6550..7e776195 100644 --- a/src/features/lecture-detail/views/stepper-buttons/stepper-buttons.tsx +++ b/src/features/lecture-detail/views/stepper-buttons/stepper-buttons.tsx @@ -19,9 +19,36 @@ const StepperButtons: FC = ({ (lecture) => lecture?.lecture?.id === lectureId ); - const isAtFirstLecture = activeStep === 0; const isAtLastLecture = activeStep === lectures.length - 1; - const canGoToNextLecture = activeStep < lectures.length - 1; + + const isLectureAccessible = (index: number) => { + const lecture = lectures[index]; + return lecture && !lecture.locking && lecture.isAvailable; + }; + + const findNextAccessibleLecture = () => { + for (let i = activeStep + 1; i < lectures.length; i++) { + if (isLectureAccessible(i)) { + return i; + } + } + return -1; + }; + + const findPreviousAccessibleLecture = () => { + for (let i = activeStep - 1; i >= 0; i--) { + if (isLectureAccessible(i)) { + return i; + } + } + return -1; + }; + + const nextAccessibleIndex = findNextAccessibleLecture(); + const previousAccessibleIndex = findPreviousAccessibleLecture(); + + const canGoToNextLecture = nextAccessibleIndex !== -1; + const canGoToPreviousLecture = previousAccessibleIndex !== -1; const handleNavigation = (step: number) => { const lecture = lectures[step]?.lecture; @@ -31,14 +58,14 @@ const StepperButtons: FC = ({ }; const goToPreviousLecture = () => { - if (activeStep && activeStep > 0) { - handleNavigation(activeStep - 1); + if (previousAccessibleIndex !== -1) { + handleNavigation(previousAccessibleIndex); } }; const goToNextLecture = () => { - if (activeStep !== undefined && activeStep < lectures.length - 1) { - handleNavigation(activeStep + 1); + if (nextAccessibleIndex !== -1) { + handleNavigation(nextAccessibleIndex); } }; @@ -50,7 +77,7 @@ const StepperButtons: FC = ({