Skip to content

Commit aa5aa44

Browse files
authored
Merge pull request #393 from qa-guru/develop
Develop
2 parents efc07bb + 6214f9c commit aa5aa44

File tree

15 files changed

+1438
-136
lines changed

15 files changed

+1438
-136
lines changed

src/api/graphql/training-lecture/training-lectures.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
query trainingLectures($id: ID!) {
22
trainingLectures(id: $id) {
33
id
4+
number
5+
locking
6+
availableFrom
7+
isAvailable
48
lecture {
59
id
610
subject
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
mutation updateTrainingLectureSettings(
2+
$id: ID!
3+
$input: TrainingLectureInput!
4+
) {
5+
updateTrainingLectureSettings(id: $id, input: $input) {
6+
id
7+
number
8+
locking
9+
availableFrom
10+
isAvailable
11+
lecture {
12+
id
13+
subject
14+
}
15+
}
16+
}

src/api/schema.graphql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ type Mutation {
5757
training lecture
5858
"""
5959
updateTrainingLecture(id: ID!, lectureIds: [ID!]): [TrainingLectureDto]
60+
updateTrainingLectureSettings(
61+
id: ID!
62+
input: TrainingLectureInput!
63+
): TrainingLectureDto
6064
"""
6165
studentHomeWork section
6266
"""
@@ -288,6 +292,7 @@ input TrainingLectureInput {
288292
lecture: ID!
289293
lastLecture: ID
290294
locking: Boolean
295+
availableFrom: LocalDateTime
291296
}
292297

293298
type TrainingLectureDto {
@@ -296,6 +301,8 @@ type TrainingLectureDto {
296301
lecture: LectureDto
297302
lastLecture: LectureDto
298303
locking: Boolean
304+
availableFrom: LocalDateTime
305+
isAvailable: Boolean
299306
}
300307

301308
input LectureInput {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./lecture-schedule-container";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { FC } from "react";
2+
import { useParams } from "react-router-dom";
3+
4+
import { AppSpinner } from "shared/components/spinners";
5+
import NoDataErrorMessage from "shared/components/no-data-error-message";
6+
import { useTrainingLecturesQuery } from "api/graphql/generated/graphql";
7+
import { FETCH_POLICY } from "shared/constants";
8+
9+
import LectureSchedule from "../../views/lecture-schedule";
10+
11+
const LectureScheduleContainer: FC = () => {
12+
const { trainingId } = useParams();
13+
14+
const { data, loading, refetch } = useTrainingLecturesQuery({
15+
variables: { id: trainingId! },
16+
fetchPolicy: FETCH_POLICY.NETWORK_ONLY,
17+
});
18+
19+
if (loading) return <AppSpinner />;
20+
if (!data?.trainingLectures) return <NoDataErrorMessage />;
21+
22+
return (
23+
<LectureSchedule
24+
trainingLectures={data.trainingLectures}
25+
refetch={refetch}
26+
/>
27+
);
28+
};
29+
30+
export default LectureScheduleContainer;

src/features/edit-training/views/edit-training/edit-training.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ const EditTraining: FC<IEditTraining> = ({ data, updateTraining }) => {
116116
>
117117
Сохранить
118118
</StyledSaveButton>
119+
<Button
120+
variant="outlined"
121+
onClick={() => navigate(`${location.pathname}/lecture-schedule`)}
122+
>
123+
Расписание
124+
</Button>
119125
<StyledContinueButton variant="contained" onClick={handleContinue}>
120126
Продолжить
121127
<StyledArrowForwardIosIcon />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./lecture-schedule";
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { FC, useState } from "react";
2+
import {
3+
Box,
4+
Button,
5+
Card,
6+
CardContent,
7+
Typography,
8+
Table,
9+
TableBody,
10+
TableCell,
11+
TableContainer,
12+
TableHead,
13+
TableRow,
14+
Paper,
15+
Switch,
16+
IconButton,
17+
Tooltip,
18+
TextField,
19+
} from "@mui/material";
20+
import {
21+
Schedule as ScheduleIcon,
22+
Lock as LockIcon,
23+
LockOpen as LockOpenIcon,
24+
Edit as EditIcon,
25+
} from "@mui/icons-material";
26+
import { useSnackbar } from "notistack";
27+
import dayjs from "dayjs";
28+
29+
import {
30+
TrainingLectureDto,
31+
useUpdateTrainingLectureSettingsMutation,
32+
} from "api/graphql/generated/graphql";
33+
34+
interface ILectureSchedule {
35+
trainingLectures: (TrainingLectureDto | null)[];
36+
refetch: () => void;
37+
}
38+
39+
const LectureSchedule: FC<ILectureSchedule> = ({
40+
trainingLectures,
41+
refetch,
42+
}) => {
43+
const { enqueueSnackbar } = useSnackbar();
44+
const [editingLectureId, setEditingLectureId] = useState<string | null>(null);
45+
const [selectedDate, setSelectedDate] = useState<string>("");
46+
47+
const [updateSettings] = useUpdateTrainingLectureSettingsMutation();
48+
49+
const handleToggleLocking = async (lecture: TrainingLectureDto) => {
50+
try {
51+
await updateSettings({
52+
variables: {
53+
id: lecture.id!,
54+
input: {
55+
lecture: lecture.lecture?.id!,
56+
locking: !lecture.locking,
57+
},
58+
},
59+
onCompleted: () => {
60+
enqueueSnackbar(
61+
`Урок "${lecture.lecture?.subject}" ${
62+
!lecture.locking ? "заблокирован" : "разблокирован"
63+
}`,
64+
{ variant: "success" }
65+
);
66+
refetch();
67+
},
68+
onError: () => {
69+
enqueueSnackbar("Не удалось изменить настройки урока", {
70+
variant: "error",
71+
});
72+
},
73+
});
74+
} catch (error) {
75+
console.error("Error toggling locking:", error);
76+
}
77+
};
78+
79+
const handleEditDate = (lecture: TrainingLectureDto) => {
80+
setEditingLectureId(lecture.id!);
81+
if (lecture.availableFrom) {
82+
// Backend returns LocalDateTime in format YYYY-MM-DDTHH:mm:ss
83+
// We need YYYY-MM-DDTHH:mm for datetime-local input
84+
const localDateTime = lecture.availableFrom.slice(0, 16);
85+
setSelectedDate(localDateTime);
86+
} else {
87+
setSelectedDate("");
88+
}
89+
};
90+
91+
const handleSaveDate = async (lecture: TrainingLectureDto) => {
92+
try {
93+
// Convert datetime-local format (YYYY-MM-DDTHH:mm) to LocalDateTime format (YYYY-MM-DDTHH:mm:ss)
94+
const formattedDate = selectedDate ? `${selectedDate}:00` : null;
95+
96+
await updateSettings({
97+
variables: {
98+
id: lecture.id!,
99+
input: {
100+
lecture: lecture.lecture?.id!,
101+
availableFrom: formattedDate,
102+
},
103+
},
104+
onCompleted: () => {
105+
enqueueSnackbar("Дата открытия урока обновлена", {
106+
variant: "success",
107+
});
108+
setEditingLectureId(null);
109+
setSelectedDate("");
110+
refetch();
111+
},
112+
onError: () => {
113+
enqueueSnackbar("Не удалось обновить дату", { variant: "error" });
114+
},
115+
});
116+
} catch (error) {
117+
console.error("Error saving date:", error);
118+
}
119+
};
120+
121+
const handleCancelEdit = () => {
122+
setEditingLectureId(null);
123+
setSelectedDate("");
124+
};
125+
126+
const formatDate = (date: string | null | undefined) => {
127+
if (!date) return "Не установлена";
128+
return dayjs(date).format("DD.MM.YYYY HH:mm");
129+
};
130+
131+
const getAvailabilityText = (lecture: TrainingLectureDto) => {
132+
if (lecture.locking) {
133+
return "Заблокирован";
134+
}
135+
if (!lecture.availableFrom) {
136+
return "Доступен сразу";
137+
}
138+
return lecture.isAvailable
139+
? "Доступен"
140+
: `Доступен с ${formatDate(lecture.availableFrom)}`;
141+
};
142+
143+
const getAvailabilityColor = (lecture: TrainingLectureDto) => {
144+
if (lecture.locking) return "error";
145+
if (lecture.isAvailable) return "success";
146+
return "warning";
147+
};
148+
149+
return (
150+
<Box>
151+
<Box sx={{ mb: 3, display: "flex", alignItems: "center", gap: 2 }}>
152+
<ScheduleIcon fontSize="large" color="primary" />
153+
<Typography variant="h4">Расписание уроков</Typography>
154+
</Box>
155+
156+
<Card sx={{ mb: 3 }}>
157+
<CardContent>
158+
<Typography variant="body2" color="text.secondary">
159+
Здесь вы можете настроить доступность уроков для студентов:
160+
</Typography>
161+
<Box component="ul" sx={{ mt: 1 }}>
162+
<Typography component="li" variant="body2">
163+
<strong>Дата начала</strong> - урок станет доступен в указанное
164+
время
165+
</Typography>
166+
<Typography component="li" variant="body2">
167+
<strong>Скрыть урок</strong> - урок будет виден, но недоступен для
168+
открытия
169+
</Typography>
170+
</Box>
171+
</CardContent>
172+
</Card>
173+
174+
<TableContainer component={Paper}>
175+
<Table>
176+
<TableHead>
177+
<TableRow>
178+
<TableCell></TableCell>
179+
<TableCell>Урок</TableCell>
180+
<TableCell>Дата начала</TableCell>
181+
<TableCell>Скрыть урок</TableCell>
182+
<TableCell>Статус</TableCell>
183+
</TableRow>
184+
</TableHead>
185+
<TableBody>
186+
{trainingLectures.map((lecture) => (
187+
<TableRow key={lecture?.id} hover>
188+
<TableCell>{lecture?.number}</TableCell>
189+
<TableCell>
190+
<Typography variant="body2" fontWeight="medium">
191+
{lecture?.lecture?.subject}
192+
</Typography>
193+
</TableCell>
194+
<TableCell>
195+
{editingLectureId === lecture?.id ? (
196+
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
197+
<TextField
198+
type="datetime-local"
199+
size="small"
200+
value={selectedDate}
201+
onChange={(e) => setSelectedDate(e.target.value)}
202+
sx={{ minWidth: 220 }}
203+
InputLabelProps={{
204+
shrink: true,
205+
}}
206+
/>
207+
<Button
208+
size="small"
209+
variant="contained"
210+
onClick={() => handleSaveDate(lecture)}
211+
>
212+
Сохранить
213+
</Button>
214+
<Button
215+
size="small"
216+
variant="outlined"
217+
onClick={handleCancelEdit}
218+
>
219+
Отмена
220+
</Button>
221+
</Box>
222+
) : (
223+
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
224+
<Typography variant="body2">
225+
{formatDate(lecture?.availableFrom)}
226+
</Typography>
227+
<Tooltip title="Изменить дату">
228+
<IconButton
229+
size="small"
230+
onClick={() => handleEditDate(lecture!)}
231+
>
232+
<EditIcon fontSize="small" />
233+
</IconButton>
234+
</Tooltip>
235+
</Box>
236+
)}
237+
</TableCell>
238+
<TableCell>
239+
<Tooltip
240+
title={
241+
lecture?.locking
242+
? "Разблокировать урок"
243+
: "Заблокировать урок"
244+
}
245+
>
246+
<Switch
247+
checked={!!lecture?.locking}
248+
onChange={() => handleToggleLocking(lecture!)}
249+
color="primary"
250+
/>
251+
</Tooltip>
252+
</TableCell>
253+
<TableCell>
254+
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
255+
{lecture?.locking ? (
256+
<LockIcon color="error" fontSize="small" />
257+
) : lecture?.isAvailable ? (
258+
<LockOpenIcon color="success" fontSize="small" />
259+
) : (
260+
<ScheduleIcon color="warning" fontSize="small" />
261+
)}
262+
<Typography
263+
variant="body2"
264+
color={`${getAvailabilityColor(lecture!)}.main`}
265+
fontWeight="medium"
266+
>
267+
{getAvailabilityText(lecture!)}
268+
</Typography>
269+
</Box>
270+
</TableCell>
271+
</TableRow>
272+
))}
273+
</TableBody>
274+
</Table>
275+
</TableContainer>
276+
277+
{trainingLectures.length === 0 && (
278+
<Box sx={{ textAlign: "center", py: 4 }}>
279+
<Typography variant="body1" color="text.secondary">
280+
Нет уроков в курсе. Добавьте уроки для настройки расписания.
281+
</Typography>
282+
</Box>
283+
)}
284+
</Box>
285+
);
286+
};
287+
288+
export default LectureSchedule;

0 commit comments

Comments
 (0)