|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace App\Controller; |
|
4
|
|
|
|
|
5
|
|
|
use App\Book\AbsenceSuggestion\SuggestionResolver; |
|
6
|
|
|
use App\Book\Lesson\LessonCancelHelper; |
|
7
|
|
|
use App\Book\Student\AbsenceExcuseResolver; |
|
8
|
|
|
use App\Entity\LessonAttendance; |
|
9
|
|
|
use App\Entity\LessonEntry; |
|
10
|
|
|
use App\Entity\Student as StudentEntity; |
|
11
|
|
|
use App\Entity\StudentAbsence; |
|
12
|
|
|
use App\Entity\TimetableLesson; |
|
13
|
|
|
use App\Entity\Tuition; |
|
14
|
|
|
use App\Markdown\Markdown; |
|
15
|
|
|
use App\Repository\LessonAttendanceRepositoryInterface; |
|
16
|
|
|
use App\Repository\StudentAbsenceRepositoryInterface; |
|
17
|
|
|
use App\Repository\StudentRepositoryInterface; |
|
18
|
|
|
use App\Repository\TeacherRepositoryInterface; |
|
19
|
|
|
use App\Repository\TimetableLessonRepositoryInterface; |
|
20
|
|
|
use App\Request\Book\CancelLessonRequest; |
|
21
|
|
|
use App\Request\Book\UpdateAttendanceRequest; |
|
22
|
|
|
use App\Response\Api\V1\Student; |
|
23
|
|
|
use App\Response\Api\V1\Subject; |
|
24
|
|
|
use App\Response\Api\V1\Teacher; |
|
25
|
|
|
use App\Response\Api\V1\Tuition as TuitionResponse; |
|
26
|
|
|
use App\Response\ViolationList; |
|
27
|
|
|
use App\Section\SectionResolverInterface; |
|
28
|
|
|
use App\Security\Voter\LessonEntryVoter; |
|
29
|
|
|
use App\Settings\BookSettings; |
|
30
|
|
|
use App\Utils\ArrayUtils; |
|
31
|
|
|
use DateTime; |
|
32
|
|
|
use JMS\Serializer\SerializerInterface; |
|
33
|
|
|
use Nelmio\ApiDocBundle\Annotation\Model; |
|
34
|
|
|
use OpenApi\Annotations as OA; |
|
35
|
|
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; |
|
36
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
37
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
38
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
|
39
|
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
|
40
|
|
|
use Symfony\Component\Routing\Annotation\Route; |
|
41
|
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
|
42
|
|
|
|
|
43
|
|
|
#[Route(path: '/book/xhr')] |
|
44
|
|
|
class BookXhrController extends AbstractController { |
|
45
|
|
|
|
|
46
|
|
|
use DateRequestTrait; |
|
|
|
|
|
|
47
|
|
|
|
|
48
|
|
|
private function returnJson($data, SerializerInterface $serializer): Response { |
|
49
|
|
|
$json = $serializer->serialize($data, 'json'); |
|
50
|
|
|
return new Response($json, Response::HTTP_OK, [ |
|
51
|
|
|
'Content-Type' => 'application/json' |
|
52
|
|
|
]); |
|
53
|
|
|
} |
|
54
|
|
|
|
|
55
|
|
|
#[Route(path: '/teachers', name: 'xhr_teachers')] |
|
56
|
|
|
public function teachers(TeacherRepositoryInterface $teacherRepository, SerializerInterface $serializer): Response { |
|
57
|
|
|
$teachers = []; |
|
58
|
|
|
|
|
59
|
|
|
foreach($teacherRepository->findAll() as $teacher) { |
|
60
|
|
|
$teachers[] = Teacher::fromEntity($teacher); |
|
61
|
|
|
} |
|
62
|
|
|
|
|
63
|
|
|
return $this->returnJson($teachers, $serializer); |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
#[Route(path: '/tuition/{uuid}', name: 'xhr_tuition')] |
|
67
|
|
|
public function tuition(Tuition $tuition, SerializerInterface $serializer): Response { |
|
68
|
|
|
return $this->returnJson(TuitionResponse::fromEntity($tuition), $serializer); |
|
69
|
|
|
} |
|
70
|
|
|
|
|
71
|
|
|
private function possiblyAbsentStudents(Tuition $tuition, DateTime $date, int $lesson, SuggestionResolver $suggestionResolver) { |
|
72
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
73
|
|
|
|
|
74
|
|
|
return $suggestionResolver->resolve($tuition, $date, $lesson); |
|
75
|
|
|
} |
|
76
|
|
|
|
|
77
|
|
|
#[Route(path: '/attendances/{uuid}', name: 'xhr_entry_attendances')] |
|
78
|
|
|
public function attendances(Request $request, LessonEntry $entry, SerializerInterface $serializer, SectionResolverInterface $sectionResolver): Response { |
|
79
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
80
|
|
|
|
|
81
|
|
|
$filter = $request->query->get('filter', null); |
|
82
|
|
|
$data = [ ]; |
|
83
|
|
|
|
|
84
|
|
|
/** @var LessonAttendance $attendance */ |
|
85
|
|
|
foreach($entry->getAttendances() as $attendance) { |
|
86
|
|
|
if($filter === null || intval($filter) === $attendance->getType()) { |
|
87
|
|
|
$data[] = [ |
|
88
|
|
|
'student' => Student::fromEntity($attendance->getStudent(), $sectionResolver->getCurrentSection()), |
|
89
|
|
|
'type' => $attendance->getType() |
|
90
|
|
|
]; |
|
91
|
|
|
} |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
return $this->returnJson($data, $serializer); |
|
95
|
|
|
} |
|
96
|
|
|
|
|
97
|
|
|
#[Route(path: '/students', name: 'xhr_students')] |
|
98
|
|
|
public function students(StudentRepositoryInterface $studentRepository, SectionResolverInterface $sectionResolver, SerializerInterface $serializer): Response { |
|
99
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
100
|
|
|
|
|
101
|
|
|
$students = [ ]; |
|
102
|
|
|
$section = $sectionResolver->getCurrentSection(); |
|
103
|
|
|
|
|
104
|
|
|
foreach($studentRepository->findAllBySection($section) as $studentEntity) { |
|
|
|
|
|
|
105
|
|
|
$students[] = Student::fromEntity($studentEntity, $section); |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
return $this->returnJson($students, $serializer); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
#[Route('/absence_note/{student}/{lesson}', name: 'xhr_student_absences')] |
|
112
|
|
|
#[ParamConverter('student', options: [ 'mapping' => ['student' => 'uuid']])] |
|
113
|
|
|
#[ParamConverter('lesson', options: [ 'mapping' => ['lesson' => 'uuid']])] |
|
114
|
|
|
public function absenceNote(StudentEntity $student, TimetableLesson $lesson, StudentAbsenceRepositoryInterface $absenceRepository, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator, Markdown $markdown): Response { |
|
115
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
116
|
|
|
|
|
117
|
|
|
$absences = [ ]; |
|
118
|
|
|
for($lessonNumber = $lesson->getLessonStart(); $lessonNumber <= $lesson->getLessonEnd(); $lessonNumber++) { |
|
119
|
|
|
$absences = array_merge( |
|
120
|
|
|
$absences, |
|
121
|
|
|
$absenceRepository->findByStudents([$student], null, $lesson->getDate(), $lessonNumber) |
|
122
|
|
|
); |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
$absences = ArrayUtils::unique($absences); |
|
126
|
|
|
$json = array_map(fn(StudentAbsence $absence) => [ |
|
127
|
|
|
'uuid' => $absence->getUuid()->toString(), |
|
128
|
|
|
'type' => $absence->getType()->getName(), |
|
129
|
|
|
'from' => [ |
|
130
|
|
|
'date' => $absence->getFrom()->getDate()->format('Y-m-d'), |
|
131
|
|
|
'lesson' => $absence->getFrom()->getLesson() |
|
132
|
|
|
], |
|
133
|
|
|
'until' => [ |
|
134
|
|
|
'date' => $absence->getUntil()->getDate()->format('Y-m-d'), |
|
135
|
|
|
'lesson' => $absence->getUntil()->getLesson() |
|
136
|
|
|
], |
|
137
|
|
|
'message' => $absence->getMessage(), |
|
138
|
|
|
'html' => $markdown->convertToHtml($absence->getMessage()), |
|
|
|
|
|
|
139
|
|
|
'url' => $urlGenerator->generate('show_student_absence', [ 'uuid' => $absence->getUuid()->toString() ]) |
|
140
|
|
|
], $absences); |
|
141
|
|
|
|
|
142
|
|
|
return $this->returnJson($json, $serializer); |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
/** |
|
146
|
|
|
* @OA\Get() |
|
147
|
|
|
* @OA\Parameter( |
|
148
|
|
|
* name="lesson", |
|
149
|
|
|
* in="query", |
|
150
|
|
|
* description="UUID of the lesson" |
|
151
|
|
|
* ) |
|
152
|
|
|
* @OA\Parameter( |
|
153
|
|
|
* name="start", |
|
154
|
|
|
* in="query", |
|
155
|
|
|
* description="Start lesson number" |
|
156
|
|
|
* ) |
|
157
|
|
|
* @OA\Parameter( |
|
158
|
|
|
* name="end", |
|
159
|
|
|
* in="path", |
|
160
|
|
|
* description="End lesson number" |
|
161
|
|
|
* ) |
|
162
|
|
|
*/ |
|
163
|
|
|
#[Route(path: '/entry', name: 'xhr_lesson_entry', methods: ['GET'])] |
|
164
|
|
|
public function entry(Request $request, TimetableLessonRepositoryInterface $lessonRepository, SuggestionResolver $suggestionResolver, |
|
165
|
|
|
SerializerInterface $serializer, AbsenceExcuseResolver $excuseResolver, BookSettings $settings): Response { |
|
166
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
167
|
|
|
|
|
168
|
|
|
$lesson = $lessonRepository->findOneByUuid($request->query->get('lesson')); |
|
169
|
|
|
if($lesson === null) { |
|
170
|
|
|
throw new NotFoundHttpException('Lesson not found.'); |
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
|
|
$start = $request->query->getInt('start'); |
|
174
|
|
|
if(!is_numeric($start)) { |
|
|
|
|
|
|
175
|
|
|
throw new BadRequestHttpException('Start and end must be numeric values.'); |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
if($start < $lesson->getLessonStart() || $start > $lesson->getLessonEnd()) { |
|
179
|
|
|
throw new BadRequestHttpException('Start must be inside lesson boundaries.'); |
|
180
|
|
|
} |
|
181
|
|
|
|
|
182
|
|
|
$entry = null; |
|
183
|
|
|
|
|
184
|
|
|
/** @var LessonEntry $lessonEntry */ |
|
185
|
|
|
foreach($lesson->getEntries() as $lessonEntry) { |
|
186
|
|
|
if($lessonEntry->getLessonStart() <= (int)$start && (int)$start <= $lessonEntry->getLessonEnd()) { |
|
187
|
|
|
$entry = $lessonEntry; |
|
188
|
|
|
break; |
|
189
|
|
|
} |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
$entryJson = null; |
|
193
|
|
|
|
|
194
|
|
|
if($entry !== null) { |
|
|
|
|
|
|
195
|
|
|
$attendances = [ ]; |
|
196
|
|
|
/** @var LessonAttendance $attendance */ |
|
197
|
|
|
foreach($entry->getAttendances() as $attendance) { |
|
198
|
|
|
$excuseInfo = $excuseResolver->resolve($attendance->getStudent(), [ $entry->getTuition() ]); |
|
199
|
|
|
$excuses = $excuseInfo->getExcuseCollectionForLesson($attendance); |
|
200
|
|
|
|
|
201
|
|
|
$attendances[] = [ |
|
202
|
|
|
'student' => Student::fromEntity($attendance->getStudent()), |
|
203
|
|
|
'minutes' => $attendance->getLateMinutes(), |
|
204
|
|
|
'lessons' => $attendance->getAbsentLessons(), |
|
205
|
|
|
'comment' => $attendance->getComment(), |
|
206
|
|
|
'excuse_status' => $attendance->getExcuseStatus(), |
|
207
|
|
|
'has_excuses' => $excuses->count() > 0, |
|
208
|
|
|
'type' => $attendance->getType() |
|
209
|
|
|
]; |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
$entryJson = [ |
|
213
|
|
|
'uuid' => $entry->getUuid()->toString(), |
|
214
|
|
|
'start' => $entry->getLessonStart(), |
|
215
|
|
|
'end' => $entry->getLessonEnd(), |
|
216
|
|
|
'subject' => Subject::fromEntity($entry->getSubject()), |
|
217
|
|
|
'replacement_subject' => $entry->getReplacementSubject(), |
|
218
|
|
|
'teacher' => Teacher::fromEntity($entry->getTeacher()), |
|
219
|
|
|
'replacement_teacher' => Teacher::fromEntity($entry->getReplacementTeacher()), |
|
220
|
|
|
'topic' => $entry->getTopic(), |
|
221
|
|
|
'exercises' => $entry->getExercises(), |
|
222
|
|
|
'comment' => $entry->getComment(), |
|
223
|
|
|
'is_cancelled' => $entry->isCancelled(), |
|
224
|
|
|
'cancel_reason' => $entry->getCancelReason(), |
|
225
|
|
|
'attendances' => $attendances |
|
226
|
|
|
]; |
|
227
|
|
|
} |
|
228
|
|
|
|
|
229
|
|
|
$students = [ ]; |
|
230
|
|
|
foreach($lesson->getTuition()->getStudyGroup()->getMemberships() as $membership) { |
|
231
|
|
|
if(in_array($membership->getStudent()->getStatus(), $settings->getExcludeStudentsStatus())) { |
|
232
|
|
|
// skip student |
|
233
|
|
|
continue; |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
$students[] = Student::fromEntity($membership->getStudent()); |
|
237
|
|
|
} |
|
238
|
|
|
|
|
239
|
|
|
$response = [ |
|
240
|
|
|
'lesson' => [ |
|
241
|
|
|
'uuid' => $lesson->getUuid()->toString(), |
|
242
|
|
|
'date' => $lesson->getDate()->format('Y-m-d'), |
|
243
|
|
|
'lesson_start' => $lesson->getLessonStart(), |
|
244
|
|
|
'lesson_end' => $lesson->getLessonEnd(), |
|
245
|
|
|
'tuition' => TuitionResponse::fromEntity($lesson->getTuition()) |
|
246
|
|
|
], |
|
247
|
|
|
'absences' => $this->possiblyAbsentStudents($lesson->getTuition(), $lesson->getDate(), $start, $suggestionResolver), |
|
|
|
|
|
|
248
|
|
|
'entry' => $entryJson, |
|
249
|
|
|
'students' => $students, |
|
250
|
|
|
'has_other_entries' => count($lesson->getEntries()) > 0 |
|
251
|
|
|
]; |
|
252
|
|
|
|
|
253
|
|
|
return $this->returnJson($response, $serializer); |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
/** |
|
257
|
|
|
* @OA\Post() |
|
258
|
|
|
* @OA\Parameter( |
|
259
|
|
|
* name="payload", |
|
260
|
|
|
* in="body", |
|
261
|
|
|
* @Model(type=CancelLessonRequest::class) |
|
262
|
|
|
* ) |
|
263
|
|
|
* @OA\Response( |
|
264
|
|
|
* response="201", |
|
265
|
|
|
* description="Lessons are cancelled successfully. Empty content." |
|
266
|
|
|
* ) |
|
267
|
|
|
* @OA\Response( |
|
268
|
|
|
* response="403", |
|
269
|
|
|
* description="Bad request.", |
|
270
|
|
|
* @Model(type=ViolationList::class) |
|
271
|
|
|
* ) |
|
272
|
|
|
*/ |
|
273
|
|
|
#[Route(path: '/cancel/{uuid}', name: 'xhr_cancel_lesson', methods: ['POST'])] |
|
274
|
|
|
public function cancelLesson(TimetableLesson $lesson, CancelLessonRequest $request, LessonCancelHelper $lessonCancelHelper): Response { |
|
275
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::New); |
|
276
|
|
|
$reason = $request->getReason(); |
|
277
|
|
|
$lessonCancelHelper->cancelLesson($lesson, $reason); |
|
|
|
|
|
|
278
|
|
|
|
|
279
|
|
|
return new Response('', Response::HTTP_CREATED, [ |
|
280
|
|
|
'Content-Type' => 'application/json' |
|
281
|
|
|
]); |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* @OA\Put() |
|
286
|
|
|
* @OA\Parameter( |
|
287
|
|
|
* name="payload", |
|
288
|
|
|
* in="body", |
|
289
|
|
|
* @Model(type=UpdateAttendanceRequest::class) |
|
290
|
|
|
* ) |
|
291
|
|
|
* @OA\Response( |
|
292
|
|
|
* response="200", |
|
293
|
|
|
* description="Attendance successfully updated" |
|
294
|
|
|
* ) |
|
295
|
|
|
* @OA\Response( |
|
296
|
|
|
* response="403", |
|
297
|
|
|
* description="Bad request.", |
|
298
|
|
|
* @Model(type=ViolationList::class) |
|
299
|
|
|
* ) |
|
300
|
|
|
*/ |
|
301
|
|
|
#[Route(path: '/attendance/{uuid}', name: 'xhr_update_attendance', methods: ['PUT'])] |
|
302
|
|
|
public function updateAttendance(LessonAttendance $attendance, UpdateAttendanceRequest $request, LessonAttendanceRepositoryInterface $repository): Response { |
|
303
|
|
|
$this->denyAccessUnlessGranted(LessonEntryVoter::Edit, $attendance->getEntry()); |
|
304
|
|
|
|
|
305
|
|
|
$attendance->setAbsentLessons($request->getAbsentLessons()); |
|
306
|
|
|
$attendance->setLateMinutes($request->getLateMinutes()); |
|
307
|
|
|
$attendance->setExcuseStatus($request->getExcuseStatus()); |
|
308
|
|
|
$attendance->setType($request->getType()); |
|
309
|
|
|
$attendance->setComment($request->getComment()); |
|
310
|
|
|
|
|
311
|
|
|
$repository->persist($attendance); |
|
312
|
|
|
|
|
313
|
|
|
return new Response('', Response::HTTP_OK, [ |
|
314
|
|
|
'Content-Type' => 'application/json' |
|
315
|
|
|
]); |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
#[Route(path: '/font/regular', name: 'xhr_font_regular')] |
|
319
|
|
|
public function regularFont(BookSettings $bookSettings) { |
|
320
|
|
|
$font = $bookSettings->getRegularFont(); |
|
321
|
|
|
|
|
322
|
|
|
if(empty($font)) { |
|
323
|
|
|
throw new NotFoundHttpException(); |
|
324
|
|
|
} |
|
325
|
|
|
|
|
326
|
|
|
return new Response($font); |
|
327
|
|
|
} |
|
328
|
|
|
|
|
329
|
|
|
#[Route(path: '/font/bold', name: 'xhr_font_bold')] |
|
330
|
|
|
public function boldFont(BookSettings $bookSettings) { |
|
331
|
|
|
$font = $bookSettings->getBoldFont(); |
|
332
|
|
|
|
|
333
|
|
|
if(empty($font)) { |
|
334
|
|
|
throw new NotFoundHttpException(); |
|
335
|
|
|
} |
|
336
|
|
|
|
|
337
|
|
|
return new Response($font); |
|
338
|
|
|
} |
|
339
|
|
|
} |