src/Controller/TutorNoShowIncidentController.php line 29

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Lesson;
  4. use App\Entity\TutorNoShowIncident;
  5. use App\Repository\LessonChangeLogRepository;
  6. use App\Repository\TutorNoShowIncidentRepository;
  7. use Doctrine\ORM\QueryBuilder;
  8. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  9. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  10. use Symfony\Component\HttpFoundation\JsonResponse;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\Routing\Annotation\Route;
  14. /**
  15. * @Route("/tutor-no-show-incidents")
  16. */
  17. class TutorNoShowIncidentController extends AbstractController
  18. {
  19. private const DATE_FORMAT = 'Y-m-d H:i';
  20. private const DELETED_LESSON_LABEL = 'Ištrintas užsiėmimas';
  21. /**
  22. * @Route("/", name="tutor_no_show_incidents_index", methods={"GET"})
  23. * @Security("is_granted('ROLE_ADMIN') and is_granted('IS_AUTHENTICATED_FULLY')")
  24. */
  25. public function index(): Response
  26. {
  27. return $this->render('tutor_no_show_incidents/index.html.twig');
  28. }
  29. /**
  30. * @Route("/ajax", name="tutor_no_show_incidents_ajax", methods={"POST"})
  31. * @Security("is_granted('ROLE_ADMIN') and is_granted('IS_AUTHENTICATED_FULLY')")
  32. */
  33. public function ajax(
  34. Request $request,
  35. TutorNoShowIncidentRepository $incidentRepository,
  36. LessonChangeLogRepository $changeLogRepository
  37. ): Response {
  38. $page = max(1, (int) $request->request->get('page', 1));
  39. $perPage = max(1, (int) $request->request->get('perPage', 20));
  40. $dateFrom = $request->request->get('dateFrom');
  41. $dateTo = $request->request->get('dateTo');
  42. $filters = $request->request->all('filters');
  43. $qb = $incidentRepository->createFilteredQueryBuilder($dateFrom, $dateTo)
  44. ->leftJoin('i.lesson', 'l')
  45. ->leftJoin('i.teacher', 't')
  46. ->leftJoin('i.child', 'c');
  47. $this->applyTableFilters($qb, $filters);
  48. $qb->orderBy('i.recordedAt', 'DESC');
  49. $totalCount = (clone $qb)
  50. ->select('COUNT(i.id)')
  51. ->getQuery()
  52. ->getSingleScalarResult();
  53. $incidents = (clone $qb)
  54. ->select('i, l, t, c')
  55. ->setFirstResult(($page - 1) * $perPage)
  56. ->setMaxResults($perPage)
  57. ->getQuery()
  58. ->getResult();
  59. $lessonIds = array_filter(array_map(fn($i) => $i->getLesson()?->getId(), $incidents));
  60. $statusChangeLogs = $this->fetchStatusChangeLogs($lessonIds, $changeLogRepository);
  61. $data = array_map(
  62. fn($incident) => $this->formatIncidentRow($incident, $statusChangeLogs),
  63. $incidents
  64. );
  65. return new JsonResponse([
  66. 'last_page' => (int) ceil($totalCount / $perPage),
  67. 'data' => $data,
  68. 'kpis' => $this->buildKpis($incidentRepository, $dateFrom, $dateTo),
  69. ]);
  70. }
  71. /**
  72. * @Route("/csv", name="tutor_no_show_incidents_csv", methods={"GET"})
  73. * @Security("is_granted('ROLE_ADMIN') and is_granted('IS_AUTHENTICATED_FULLY')")
  74. */
  75. public function csv(
  76. Request $request,
  77. TutorNoShowIncidentRepository $incidentRepository,
  78. LessonChangeLogRepository $changeLogRepository
  79. ): Response {
  80. $dateFrom = $request->query->get('dateFrom');
  81. $dateTo = $request->query->get('dateTo');
  82. $incidents = $incidentRepository->createFilteredQueryBuilder($dateFrom, $dateTo)
  83. ->leftJoin('i.lesson', 'l')
  84. ->select('i, l')
  85. ->orderBy('i.recordedAt', 'DESC')
  86. ->getQuery()
  87. ->getResult();
  88. $lessonIds = array_filter(array_map(fn($i) => $i->getLesson()?->getId(), $incidents));
  89. $statusChangeLogs = $this->fetchStatusChangeLogs($lessonIds, $changeLogRepository);
  90. $f = fopen('php://memory', 'w');
  91. fputcsv($f, [
  92. 'Korepetitorius',
  93. 'Korepetitoriaus ID',
  94. 'Mokinys',
  95. 'Pamoka',
  96. 'Korepetitoriaus neinformuotai praleistas užsiėmimas',
  97. 'Pakeistas statusas',
  98. 'Dabartinis statusas',
  99. 'Statusas pakeistas',
  100. 'Dienos nuo mokinio pirmos pamokos',
  101. ], ';');
  102. foreach ($incidents as $incident) {
  103. [$wasChanged, $currentStatus, $statusChangedAt] = $this->resolveIncidentStatus(
  104. $incident,
  105. $statusChangeLogs
  106. );
  107. fputcsv($f, [
  108. $incident->getTeacherName() ?? '',
  109. $incident->getTeacherIndividualNumber() ?? '',
  110. $incident->getChildName() ?? '',
  111. $incident->getLessonStartTime()->format(self::DATE_FORMAT),
  112. $incident->getRecordedAt()->format(self::DATE_FORMAT),
  113. $wasChanged ? 'Taip' : 'Ne',
  114. $currentStatus,
  115. $statusChangedAt ?? '-',
  116. $incident->getDaysSinceFirstLesson() ?? '-',
  117. ], ';');
  118. }
  119. fseek($f, 0);
  120. header('Content-Encoding: UTF-8');
  121. header('Content-Type: text/csv; charset=UTF-8');
  122. header('Content-Disposition: attachment; filename="tutor_no_show_incidents.csv";');
  123. echo "\xEF\xBB\xBF";
  124. fpassthru($f);
  125. exit();
  126. }
  127. private function buildKpis(
  128. TutorNoShowIncidentRepository $incidentRepository,
  129. ?string $dateFrom,
  130. ?string $dateTo
  131. ): array {
  132. $qb = $incidentRepository->createFilteredQueryBuilder($dateFrom, $dateTo);
  133. $total = (clone $qb)
  134. ->select('COUNT(i.id)')
  135. ->getQuery()
  136. ->getSingleScalarResult();
  137. $uniqueTutors = (clone $qb)
  138. ->select('COUNT(DISTINCT IDENTITY(i.teacher))')
  139. ->getQuery()
  140. ->getSingleScalarResult();
  141. $tutorCounts = (clone $qb)
  142. ->select('IDENTITY(i.teacher) as teacherId, COUNT(i.id) as cnt')
  143. ->groupBy('i.teacher')
  144. ->getQuery()
  145. ->getResult();
  146. $tutorsWith2Plus = 0;
  147. $tutorsWith3Plus = 0;
  148. foreach ($tutorCounts as $row) {
  149. if ($row['cnt'] >= 2) {
  150. $tutorsWith2Plus++;
  151. }
  152. if ($row['cnt'] >= 3) {
  153. $tutorsWith3Plus++;
  154. }
  155. }
  156. $avgDays = (clone $qb)
  157. ->select('AVG(i.daysSinceFirstLesson)')
  158. ->getQuery()
  159. ->getSingleScalarResult();
  160. return [
  161. 'total' => (int) $total,
  162. 'uniqueTutors' => (int) $uniqueTutors,
  163. 'tutorsWith2Plus' => $tutorsWith2Plus,
  164. 'tutorsWith3Plus' => $tutorsWith3Plus,
  165. 'avgDaysSinceFirstLesson' => $avgDays !== null ? round((float) $avgDays, 1) : null,
  166. ];
  167. }
  168. private function applyTableFilters(QueryBuilder $qb, array $filters): void
  169. {
  170. foreach ($filters as $filter) {
  171. $value = $filter['value'] ?? '';
  172. $field = $filter['field'] ?? '';
  173. if ($value === '' || $value === null) {
  174. continue;
  175. }
  176. if ($field === 'teacherFilter') {
  177. $qb->andWhere('(i.teacherName LIKE :teacherFilter OR i.teacherIndividualNumber LIKE :teacherFilter)')
  178. ->setParameter('teacherFilter', '%' . $value . '%');
  179. } elseif ($field === 'childName') {
  180. $qb->andWhere('i.childName LIKE :childName')
  181. ->setParameter('childName', '%' . $value . '%');
  182. } elseif ($field === 'wasStatusChanged') {
  183. $noShowStatus = Lesson::STATUS_TUTOR_UNINFORMED_MISSED['value'];
  184. if ($value === 'yes') {
  185. $qb->andWhere('i.lesson IS NULL OR l.status != :noShowStatus')
  186. ->setParameter('noShowStatus', $noShowStatus);
  187. } elseif ($value === 'no') {
  188. $qb->andWhere('i.lesson IS NOT NULL AND l.status = :noShowStatus')
  189. ->setParameter('noShowStatus', $noShowStatus);
  190. }
  191. } elseif ($field === 'daysSinceFirstLesson') {
  192. $qb->andWhere('i.daysSinceFirstLesson = :days')
  193. ->setParameter('days', (int) $value);
  194. }
  195. }
  196. }
  197. private function fetchStatusChangeLogs(array $lessonIds, LessonChangeLogRepository $changeLogRepository): array
  198. {
  199. if (!$lessonIds) {
  200. return [];
  201. }
  202. $logs = $changeLogRepository->createQueryBuilder('lcl')
  203. ->where('lcl.lesson IN (:lessonIds)')
  204. ->andWhere('lcl.changedField = :field')
  205. ->andWhere('lcl.changedFrom = :from')
  206. ->setParameter('lessonIds', array_values($lessonIds))
  207. ->setParameter('field', 'status')
  208. ->setParameter('from', Lesson::STATUS_TUTOR_UNINFORMED_MISSED['value'])
  209. ->orderBy('lcl.dateAdd', 'ASC')
  210. ->getQuery()
  211. ->getResult();
  212. $result = [];
  213. foreach ($logs as $log) {
  214. if (!$log->getLesson()) {
  215. continue;
  216. }
  217. $lid = $log->getLesson()->getId();
  218. if (!isset($result[$lid])) {
  219. $result[$lid] = $log;
  220. }
  221. }
  222. return $result;
  223. }
  224. private function resolveIncidentStatus(TutorNoShowIncident $incident, array $statusChangeLogs): array
  225. {
  226. $lesson = $incident->getLesson();
  227. $noShowStatusValue = Lesson::STATUS_TUTOR_UNINFORMED_MISSED['value'];
  228. if (!$lesson) {
  229. return [true, self::DELETED_LESSON_LABEL, null];
  230. }
  231. if ($lesson->getStatusValue() !== $noShowStatusValue) {
  232. $lessonId = $lesson->getId();
  233. $statusChangedAt = isset($statusChangeLogs[$lessonId])
  234. ? $statusChangeLogs[$lessonId]->getDateAdd()?->format(self::DATE_FORMAT)
  235. : null;
  236. return [true, $lesson->getStatus(), $statusChangedAt];
  237. }
  238. return [false, Lesson::STATUS_TUTOR_UNINFORMED_MISSED['text'], null];
  239. }
  240. private function formatIncidentRow(TutorNoShowIncident $incident, array $statusChangeLogs): array
  241. {
  242. [$wasChanged, $currentStatus, $statusChangedAt] = $this->resolveIncidentStatus(
  243. $incident,
  244. $statusChangeLogs
  245. );
  246. return [
  247. 'id' => $incident->getId(),
  248. 'teacherName' => $incident->getTeacherName(),
  249. 'teacherIndividualNumber' => $incident->getTeacherIndividualNumber(),
  250. 'teacherId' => $incident->getTeacher()?->getId(),
  251. 'childName' => $incident->getChildName(),
  252. 'childId' => $incident->getChild()?->getId(),
  253. 'lessonStartTime' => $incident->getLessonStartTime()->format(self::DATE_FORMAT),
  254. 'recordedAt' => $incident->getRecordedAt()->format(self::DATE_FORMAT),
  255. 'wasStatusChanged' => $wasChanged,
  256. 'currentStatus' => $currentStatus,
  257. 'statusChangedAt' => $statusChangedAt,
  258. 'daysSinceFirstLesson' => $incident->getDaysSinceFirstLesson(),
  259. ];
  260. }
  261. }