|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace App\Controller; |
|
4
|
|
|
|
|
5
|
|
|
use App\Security\Voter\TimetableVoter; |
|
6
|
|
|
use Symfony\Component\HttpFoundation\Response; |
|
7
|
|
|
use App\Date\WeekOfYear; |
|
8
|
|
|
use App\Entity\IcsAccessToken; |
|
9
|
|
|
use App\Entity\IcsAccessTokenType; |
|
10
|
|
|
use App\Entity\MessageScope; |
|
11
|
|
|
use App\Entity\StudyGroupMembership; |
|
12
|
|
|
use App\Entity\Subject; |
|
13
|
|
|
use App\Entity\TimetableLesson; |
|
14
|
|
|
use App\Entity\TimetableSupervision; |
|
15
|
|
|
use App\Entity\User; |
|
16
|
|
|
use App\Export\TimetableIcsExporter; |
|
17
|
|
|
use App\Form\IcsAccessTokenType as DeviceTokenTypeForm; |
|
18
|
|
|
use App\Grouping\Grouper; |
|
19
|
|
|
use App\Message\DismissedMessagesHelper; |
|
20
|
|
|
use App\Repository\ImportDateTypeRepositoryInterface; |
|
21
|
|
|
use App\Repository\MessageRepositoryInterface; |
|
22
|
|
|
use App\Repository\SubjectRepositoryInterface; |
|
23
|
|
|
use App\Repository\TimetableLessonRepositoryInterface; |
|
24
|
|
|
use App\Repository\TimetableSupervisionRepositoryInterface; |
|
25
|
|
|
use App\Repository\UserRepositoryInterface; |
|
26
|
|
|
use App\Security\IcsAccessToken\IcsAccessTokenManager; |
|
27
|
|
|
use App\Settings\TimetableSettings; |
|
28
|
|
|
use App\Sorting\Sorter; |
|
29
|
|
|
use App\Timetable\TimetableFilter; |
|
30
|
|
|
use App\Timetable\TimetableHelper; |
|
31
|
|
|
use App\Utils\ArrayUtils; |
|
32
|
|
|
use App\View\Filter\GradeFilter; |
|
33
|
|
|
use App\View\Filter\RoomFilter; |
|
34
|
|
|
use App\View\Filter\SectionFilter; |
|
35
|
|
|
use App\View\Filter\StudentFilter; |
|
36
|
|
|
use App\View\Filter\SubjectsFilter; |
|
37
|
|
|
use App\View\Filter\TeachersFilter; |
|
38
|
|
|
use DateTime; |
|
39
|
|
|
use SchulIT\CommonBundle\Helper\DateHelper; |
|
40
|
|
|
use SchulIT\CommonBundle\Utils\RefererHelper; |
|
41
|
|
|
use Symfony\Component\HttpFoundation\Request; |
|
42
|
|
|
use Symfony\Component\Routing\Annotation\Route; |
|
43
|
|
|
|
|
44
|
|
|
#[Route(path: '/timetable')] |
|
45
|
|
|
class TimetableController extends AbstractControllerWithMessages { |
|
46
|
|
|
|
|
47
|
|
|
private const OnlyOneWeek = 'timetable.only_one_week'; |
|
48
|
|
|
|
|
49
|
|
|
use RequestTrait; |
|
50
|
|
|
use CalendarWeeksTrait; |
|
|
|
|
|
|
51
|
|
|
|
|
52
|
|
|
public function __construct(MessageRepositoryInterface $messageRepository, DismissedMessagesHelper $dismissedMessagesHelper, |
|
53
|
|
|
DateHelper $dateHelper, private TimetableHelper $timetableHelper, private TimetableSettings $timetableSettings, |
|
54
|
|
|
RefererHelper $refererHelper) { |
|
55
|
|
|
parent::__construct($messageRepository, $dismissedMessagesHelper, $dateHelper, $refererHelper); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
#[Route(path: '', name: 'timetable')] |
|
59
|
|
|
public function index(StudentFilter $studentFilter, TeachersFilter $teachersFilter, GradeFilter $gradeFilter, RoomFilter $roomFilter, SubjectsFilter $subjectFilter, |
|
60
|
|
|
TimetableLessonRepositoryInterface $lessonRepository, TimetableSupervisionRepositoryInterface $supervisionRepository, TimetableFilter $timetableFilter, ImportDateTypeRepositoryInterface $importDateTypeRepository, |
|
61
|
|
|
SubjectRepositoryInterface $subjectRepository, SectionFilter $sectionFilter, Request $request, UserRepositoryInterface $userRepository): Response { |
|
62
|
|
|
/** @var User $user */ |
|
63
|
|
|
$user = $this->getUser(); |
|
64
|
|
|
|
|
65
|
|
|
if($request->isMethod('POST')) { |
|
66
|
|
|
$onlyOneWeek = $request->request->getBoolean('only_one_week'); |
|
67
|
|
|
$user->setData(self::OnlyOneWeek, $onlyOneWeek); |
|
68
|
|
|
|
|
69
|
|
|
$userRepository->persist($user); |
|
70
|
|
|
return $this->redirectToRoute('timetable', $request->query->all()); |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
$sectionFilterView = $sectionFilter->handle($request->query->get('section', null)); |
|
74
|
|
|
$gradeFilterView = $gradeFilter->handle($request->query->get('grade', null), $sectionFilterView->getCurrentSection(), $user); |
|
75
|
|
|
$roomFilterView = $roomFilter->handle($request->query->get('room', null), $user); |
|
76
|
|
|
$subjectFilterView = $subjectFilter->handle($this->getArrayOrNull($request->query->all('subjects')), $user); |
|
77
|
|
|
$studentFilterView = $studentFilter->handle($request->query->get('student', null), $sectionFilterView->getCurrentSection(), $user, $gradeFilterView->getCurrentGrade() === null && $roomFilterView->getCurrentRoom() === null && (is_countable($subjectFilterView->getCurrentSubjects()) ? count($subjectFilterView->getCurrentSubjects()) : 0) === 0); |
|
78
|
|
|
$teachersFilterView = $teachersFilter->handle($this->getArrayOrNull($request->query->all('teachers')), $sectionFilterView->getCurrentSection(), $user, $studentFilterView->getCurrentStudent() === null && $gradeFilterView->getCurrentGrade() === null && $roomFilterView->getCurrentRoom() === null && (is_countable($subjectFilterView->getCurrentSubjects()) ? count($subjectFilterView->getCurrentSubjects()) : 0) === 0); |
|
79
|
|
|
|
|
80
|
|
|
$selectedDate = $this->resolveSelectedDate($request, $sectionFilterView->getCurrentSection(), $this->dateHelper); |
|
81
|
|
|
|
|
82
|
|
|
$start = max( |
|
83
|
|
|
$selectedDate, |
|
84
|
|
|
$startDate = $this->timetableSettings->getStartDate($user->getUserType()) |
|
85
|
|
|
); |
|
86
|
|
|
$end = min( |
|
87
|
|
|
(clone $start)->modify('+13 days'), |
|
88
|
|
|
$endDate = $this->timetableSettings->getEndDate($user->getUserType()) |
|
89
|
|
|
); |
|
90
|
|
|
|
|
91
|
|
|
$lessons = [ ]; |
|
92
|
|
|
$supervisions = [ ]; |
|
93
|
|
|
$membershipsTypes = [ ]; |
|
94
|
|
|
|
|
95
|
|
|
if($start <= $end) { |
|
96
|
|
|
if ($studentFilterView->getCurrentStudent() !== null) { |
|
97
|
|
|
$lessons = $lessonRepository->findAllByStudent($start, $end, $studentFilterView->getCurrentStudent()); |
|
98
|
|
|
$lessons = $timetableFilter->filterStudentLessons($lessons); |
|
99
|
|
|
|
|
100
|
|
|
$gradeIdsWithMembershipTypes = $this->timetableSettings->getGradeIdsWithMembershipTypes(); |
|
101
|
|
|
|
|
102
|
|
|
/** @var StudyGroupMembership $membership */ |
|
103
|
|
|
foreach($studentFilterView->getCurrentStudent()->getStudyGroupMemberships() as $membership) { |
|
104
|
|
|
foreach($membership->getStudyGroup()->getGrades() as $grade) { |
|
105
|
|
|
if (in_array($grade->getId(), $gradeIdsWithMembershipTypes)) { |
|
106
|
|
|
$membershipsTypes[$membership->getStudyGroup()->getId()] = $membership->getType(); |
|
107
|
|
|
} |
|
108
|
|
|
} |
|
109
|
|
|
} |
|
110
|
|
|
} else if (count($teachersFilterView->getCurrentTeachers()) > 0) { |
|
111
|
|
|
$lessons = [ ]; |
|
112
|
|
|
$supervisions = [ ]; |
|
113
|
|
|
|
|
114
|
|
|
foreach($teachersFilterView->getCurrentTeachers() as $teacher) { |
|
115
|
|
|
$lessons = array_merge($lessons, $timetableFilter->filterTeacherLessons($lessonRepository->findAllByTeacher($start, $end, $teacher))); |
|
116
|
|
|
$supervisions = array_merge($supervisions, $supervisionRepository->findAllByTeacher($start, $end, $teacher)); |
|
117
|
|
|
} |
|
118
|
|
|
} else if ($gradeFilterView->getCurrentGrade() !== null) { |
|
119
|
|
|
$lessons = $lessonRepository->findAllByGrade($start, $end, $gradeFilterView->getCurrentGrade()); |
|
120
|
|
|
$lessons = $timetableFilter->filterGradeLessons($lessons); |
|
121
|
|
|
} else if ($roomFilterView->getCurrentRoom() !== null) { |
|
122
|
|
|
$lessons = $lessonRepository->findAllByRoom($start, $end, $roomFilterView->getCurrentRoom()); |
|
123
|
|
|
$lessons = $timetableFilter->filterRoomLessons($lessons); |
|
124
|
|
|
} else if ((is_countable($subjectFilterView->getSubjects()) ? count($subjectFilterView->getSubjects()) : 0) > 0) { |
|
125
|
|
|
$lessons = $lessonRepository->findAllBySubjects($start, $end, $subjectFilterView->getCurrentSubjects()); |
|
126
|
|
|
$lessons = $timetableFilter->filterSubjectsLessons($lessons); |
|
127
|
|
|
} |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
if((is_countable($lessons) ? count($lessons) : 0) === 0 && count($supervisions) === 0) { |
|
131
|
|
|
$timetable = null; |
|
132
|
|
|
} else { |
|
133
|
|
|
$weeks = $this->getWeeksToDisplay($start, $end, $user->getData(self::OnlyOneWeek, false)); |
|
134
|
|
|
$timetable = $this->timetableHelper->makeTimetable($weeks, $lessons, $supervisions); |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
$startTimes = [ ]; |
|
138
|
|
|
$endTimes = [ ]; |
|
139
|
|
|
|
|
140
|
|
|
for($lesson = 1; $lesson <= $this->timetableSettings->getMaxLessons(); $lesson++) { |
|
141
|
|
|
$startTimes[$lesson] = $this->timetableSettings->getStart($lesson); |
|
142
|
|
|
$endTimes[$lesson] = $this->timetableSettings->getEnd($lesson); |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
$template = 'timetable/index.html.twig'; |
|
146
|
|
|
|
|
147
|
|
|
if($request->query->getBoolean('print', false) === true) { |
|
148
|
|
|
$template = 'timetable/index_print.html.twig'; |
|
149
|
|
|
|
|
150
|
|
|
if($timetable === null) { |
|
151
|
|
|
$query = $request->query->all(); |
|
152
|
|
|
unset($query['print']); |
|
153
|
|
|
$this->addFlash('info', 'plans.timetable.print.empty'); |
|
154
|
|
|
return $this->redirectToRoute('timetable', $query); |
|
155
|
|
|
} |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
$supervisionLabels = [ ]; |
|
159
|
|
|
for($i = 1; $i <= $this->timetableSettings->getMaxLessons(); $i++) { |
|
160
|
|
|
$supervisionLabels[$i] = $this->timetableSettings->getDescriptionBeforeLesson($i); |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
$subjects = ArrayUtils::createArrayWithKeys( |
|
164
|
|
|
$subjectRepository->findAll(), |
|
165
|
|
|
fn(Subject $subject) => $subject->getAbbreviation() |
|
166
|
|
|
); |
|
167
|
|
|
|
|
168
|
|
|
$weekStarts = [ ]; |
|
169
|
|
|
|
|
170
|
|
|
if($startDate !== null && $endDate !== null && $sectionFilterView->getCurrentSection() !== null) { |
|
171
|
|
|
$weekStarts = $this->listCalendarWeeks( |
|
172
|
|
|
max($startDate, $sectionFilterView->getCurrentSection()->getStart()), |
|
173
|
|
|
min($endDate, $sectionFilterView->getCurrentSection()->getEnd()) |
|
174
|
|
|
); |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
return $this->renderWithMessages($template, [ |
|
178
|
|
|
'compact' => count($teachersFilterView->getCurrentTeachers()) > 1, |
|
179
|
|
|
'timetable' => $timetable, |
|
180
|
|
|
'studentFilter' => $studentFilterView, |
|
181
|
|
|
'teachersFilter' => $teachersFilterView, |
|
182
|
|
|
'gradeFilter' => $gradeFilterView, |
|
183
|
|
|
'roomFilter'=> $roomFilterView, |
|
184
|
|
|
'subjectFilter' => $subjectFilterView, |
|
185
|
|
|
'sectionFilter' => $sectionFilterView, |
|
186
|
|
|
'startTimes' => $startTimes, |
|
187
|
|
|
'endTimes' => $endTimes, |
|
188
|
|
|
'gradesWithCourseNames' => $this->timetableSettings->getGradeIdsWithCourseNames(), |
|
189
|
|
|
'memberships' => $membershipsTypes, |
|
190
|
|
|
'query' => $request->query->all(), |
|
191
|
|
|
'supervisionLabels' => $supervisionLabels, |
|
192
|
|
|
'supervisionSubject' => $this->timetableSettings->getSupervisionLabel(), |
|
193
|
|
|
'supervisionColor' => $this->timetableSettings->getSupervisionColor(), |
|
194
|
|
|
'last_import_lessons' => $importDateTypeRepository->findOneByEntityClass(TimetableLesson::class), |
|
195
|
|
|
'last_import_supervisions' => $importDateTypeRepository->findOneByEntityClass(TimetableSupervision::class), |
|
196
|
|
|
'subjects' => $subjects, |
|
197
|
|
|
'selectedDate' => $selectedDate, |
|
198
|
|
|
'weekStarts' => $weekStarts, |
|
199
|
|
|
'onlyOneWeek' => $user->getData(self::OnlyOneWeek, false) |
|
200
|
|
|
]); |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* @param DateTime $start |
|
205
|
|
|
* @param DateTime $end |
|
206
|
|
|
* @param bool $onlyOneWeek |
|
207
|
|
|
* @return WeekOfYear[] |
|
208
|
|
|
*/ |
|
209
|
|
|
private function getWeeksToDisplay(DateTime $start, DateTime $end, bool $onlyOneWeek): array { |
|
210
|
|
|
$weeks = [ |
|
211
|
|
|
(new WeekOfYear((int)$start->format('Y'), (int)$start->format('W'))), |
|
212
|
|
|
(new WeekOfYear((int)$end->format('Y'), (int)$end->format('W'))) |
|
213
|
|
|
]; |
|
214
|
|
|
|
|
215
|
|
|
if($weeks[0]->getWeekNumber() === $weeks[1]->getWeekNumber() && $weeks[0]->getYear() === $weeks[1]->getYear()) { |
|
216
|
|
|
$weeks = [ $weeks[0] ]; |
|
217
|
|
|
} |
|
218
|
|
|
|
|
219
|
|
|
if($onlyOneWeek === true) { |
|
220
|
|
|
$weeks = [ $weeks[0] ]; |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
return $weeks; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
#[Route(path: '/supervisions', name: 'timetable_supervisions')] |
|
227
|
|
|
public function supervisions(TimetableSupervisionRepositoryInterface $supervisionRepository, TimetableFilter $timetableFilter, ImportDateTypeRepositoryInterface $importDateTypeRepository, |
|
|
|
|
|
|
228
|
|
|
SectionFilter $sectionFilter, Request $request, UserRepositoryInterface $userRepository) { |
|
229
|
|
|
$this->denyAccessUnlessGranted(TimetableVoter::Supervisions); |
|
230
|
|
|
|
|
231
|
|
|
/** @var User $user */ |
|
232
|
|
|
$user = $this->getUser(); |
|
233
|
|
|
|
|
234
|
|
|
if($request->isMethod('POST')) { |
|
235
|
|
|
$onlyOneWeek = $request->request->getBoolean('only_one_week'); |
|
236
|
|
|
$user->setData(self::OnlyOneWeek, $onlyOneWeek); |
|
237
|
|
|
|
|
238
|
|
|
$userRepository->persist($user); |
|
239
|
|
|
return $this->redirectToRoute('timetable_supervisions', $request->query->all()); |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
$sectionFilterView = $sectionFilter->handle($request->query->get('section', null)); |
|
243
|
|
|
$selectedDate = $this->resolveSelectedDate($request, $sectionFilterView->getCurrentSection(), $this->dateHelper); |
|
244
|
|
|
|
|
245
|
|
|
$start = max( |
|
246
|
|
|
$selectedDate, |
|
247
|
|
|
$startDate = $this->timetableSettings->getStartDate($user->getUserType()) |
|
248
|
|
|
); |
|
249
|
|
|
$end = min( |
|
250
|
|
|
(clone $start)->modify('+13 days'), |
|
251
|
|
|
$endDate = $this->timetableSettings->getEndDate($user->getUserType()) |
|
252
|
|
|
); |
|
253
|
|
|
|
|
254
|
|
|
$timetable = null; |
|
255
|
|
|
|
|
256
|
|
|
if($start <= $end) { |
|
257
|
|
|
$supervisions = $supervisionRepository->findAllByRange($start, $end); |
|
258
|
|
|
$weeks = $this->getWeeksToDisplay($start, $end, $user->getData(self::OnlyOneWeek, false)); |
|
259
|
|
|
|
|
260
|
|
|
if(count($supervisions) > 0) { |
|
261
|
|
|
$timetable = $this->timetableHelper->makeTimetable($weeks, [], $supervisions); |
|
262
|
|
|
} |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
$startTimes = [ ]; |
|
266
|
|
|
$endTimes = [ ]; |
|
267
|
|
|
|
|
268
|
|
|
for($lesson = 1; $lesson <= $this->timetableSettings->getMaxLessons(); $lesson++) { |
|
269
|
|
|
$startTimes[$lesson] = $this->timetableSettings->getStart($lesson); |
|
270
|
|
|
$endTimes[$lesson] = $this->timetableSettings->getEnd($lesson); |
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
$template = 'timetable/index.html.twig'; |
|
|
|
|
|
|
274
|
|
|
|
|
275
|
|
|
if($request->query->getBoolean('print', false) === true) { |
|
276
|
|
|
$template = 'timetable/index_print.html.twig'; |
|
277
|
|
|
|
|
278
|
|
|
if($timetable === null) { |
|
279
|
|
|
$query = $request->query->all(); |
|
280
|
|
|
unset($query['print']); |
|
281
|
|
|
$this->addFlash('info', 'plans.timetable.print.empty'); |
|
282
|
|
|
return $this->redirectToRoute('timetable', $query); |
|
283
|
|
|
} |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
$supervisionLabels = [ ]; |
|
287
|
|
|
for($i = 1; $i <= $this->timetableSettings->getMaxLessons(); $i++) { |
|
288
|
|
|
$supervisionLabels[$i] = $this->timetableSettings->getDescriptionBeforeLesson($i); |
|
289
|
|
|
} |
|
290
|
|
|
|
|
291
|
|
|
$weekStarts = [ ]; |
|
292
|
|
|
|
|
293
|
|
|
if($startDate !== null && $endDate !== null && $sectionFilterView->getCurrentSection() !== null) { |
|
294
|
|
|
$weekStarts = $this->listCalendarWeeks( |
|
295
|
|
|
max($startDate, $sectionFilterView->getCurrentSection()->getStart()), |
|
296
|
|
|
min($endDate, $sectionFilterView->getCurrentSection()->getEnd()) |
|
297
|
|
|
); |
|
298
|
|
|
} |
|
299
|
|
|
|
|
300
|
|
|
return $this->renderWithMessages('timetable/supervisions.html.twig', [ |
|
301
|
|
|
'timetable' => $timetable, |
|
302
|
|
|
'sectionFilter' => $sectionFilterView, |
|
303
|
|
|
'startTimes' => $startTimes, |
|
304
|
|
|
'endTimes' => $endTimes, |
|
305
|
|
|
'query' => $request->query->all(), |
|
306
|
|
|
'supervisionLabels' => $supervisionLabels, |
|
307
|
|
|
'supervisionSubject' => $this->timetableSettings->getSupervisionLabel(), |
|
308
|
|
|
'supervisionColor' => $this->timetableSettings->getSupervisionColor(), |
|
309
|
|
|
'last_import_lessons' => $importDateTypeRepository->findOneByEntityClass(TimetableLesson::class), |
|
310
|
|
|
'last_import_supervisions' => $importDateTypeRepository->findOneByEntityClass(TimetableSupervision::class), |
|
311
|
|
|
'selectedDate' => $selectedDate, |
|
312
|
|
|
'weekStarts' => $weekStarts, |
|
313
|
|
|
'onlyOneWeek' => $user->getData(self::OnlyOneWeek, false) |
|
314
|
|
|
]); |
|
315
|
|
|
|
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
#[Route(path: '/export', name: 'timetable_export')] |
|
319
|
|
|
public function export(Request $request, IcsAccessTokenManager $manager): Response { |
|
320
|
|
|
/** @var User $user */ |
|
321
|
|
|
$user = $this->getUser(); |
|
322
|
|
|
|
|
323
|
|
|
$deviceToken = (new IcsAccessToken()) |
|
324
|
|
|
->setType(IcsAccessTokenType::Timetable) |
|
325
|
|
|
->setUser($user); |
|
326
|
|
|
|
|
327
|
|
|
$form = $this->createForm(DeviceTokenTypeForm::class, $deviceToken); |
|
328
|
|
|
$form->handleRequest($request); |
|
329
|
|
|
|
|
330
|
|
|
if($form->isSubmitted() && $form->isValid()) { |
|
331
|
|
|
$deviceToken = $manager->persistToken($deviceToken); |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
|
return $this->renderWithMessages('timetable/export.html.twig', [ |
|
335
|
|
|
'form' => $form->createView(), |
|
336
|
|
|
'token' => $deviceToken |
|
337
|
|
|
]); |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
#[Route(path: '/ics/download', name: 'timetable_ics')] |
|
341
|
|
|
#[Route(path: '/ics/downloads/{token}', name: 'timetable_ics_token')] |
|
342
|
|
|
public function ics(TimetableIcsExporter $icsExporter): Response { |
|
343
|
|
|
/** @var User $user */ |
|
344
|
|
|
$user = $this->getUser(); |
|
345
|
|
|
|
|
346
|
|
|
return $icsExporter->getIcsResponse($user); |
|
347
|
|
|
} |
|
348
|
|
|
|
|
349
|
|
|
protected function getMessageScope(): MessageScope { |
|
350
|
|
|
return MessageScope::Timetable; |
|
351
|
|
|
} |
|
352
|
|
|
} |