src/Controller/StudentController.php line 320

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\CalendarEvent;
  4. use App\Entity\Candidate;
  5. use App\Entity\Person;
  6. use App\Entity\Student;
  7. use App\Entity\StudentLevel;
  8. use App\Enum\CalendarEvent\Type;
  9. use App\Enum\Candidate\RegistrationType;
  10. use App\Enum\Student\Status;
  11. use App\Enum\Student\VirtualStatus;
  12. use App\Form\PersonType;
  13. use App\Form\StudentType;
  14. use App\Library\PyTg\PyTgUtils;
  15. use App\Library\TdLib\TdLibObject\Model\Message\Message;
  16. use App\Library\Utils\ApiHandler;
  17. use App\Library\Utils\Other\Other;
  18. use App\Service\CalendarEvent\CalendarEventService;
  19. use App\Service\Candidate\CandidateService;
  20. use App\Service\City\CityService;
  21. use App\Service\FileService;
  22. use App\Service\Image\ImageService;
  23. use App\Service\Job\JobService;
  24. use App\Service\Person\PersonService;
  25. use App\Service\Queue\ErrorLogCreateQueueService;
  26. use App\Service\RouteService;
  27. use App\Service\ServiceRetriever;
  28. use App\Service\Student\StudentService;
  29. use App\Service\StudentLevel\StudentLevelService;
  30. use App\Service\User\UserService;
  31. use Doctrine\ORM\EntityManagerInterface;
  32. use Symfony\Component\HttpFoundation\JsonResponse;
  33. use Symfony\Component\HttpFoundation\RedirectResponse;
  34. use Symfony\Component\HttpFoundation\Request;
  35. use Symfony\Component\HttpFoundation\RequestStack;
  36. use Symfony\Component\HttpFoundation\Response;
  37. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  38. use Symfony\Component\Routing\RouterInterface;
  39. use Symfony\Component\Security\Core\Security;
  40. use Symfony\Component\Validator\Validator\ValidatorInterface;
  41. class StudentController extends BaseAbstractController
  42. {
  43.     /**
  44.      * @var PersonService
  45.      */
  46.     private $personService;
  47.     /**
  48.      * @var CalendarEventService
  49.      */
  50.     private $calendarEventService;
  51.     /**
  52.      * @var CandidateService
  53.      */
  54.     private $candidateService;
  55.     /**
  56.      * @var UserService
  57.      */
  58.     private $userService;
  59.     /**
  60.      * @var JobService
  61.      */
  62.     private $jobService;
  63.     /**
  64.      * @var CityService
  65.      */
  66.     private $cityService;
  67.     /**
  68.      * @var StudentLevelService
  69.      */
  70.     private $studentLevelService;
  71.     public function __construct(Security                   $securityRequestStack $requestStack,
  72.                                 ValidatorInterface         $validatorRouteService $routeService,
  73.                                 RouterInterface            $routerSessionInterface $session,
  74.                                 EntityManagerInterface     $emFileService $fileService,
  75.                                 ServiceRetriever           $serviceRetrieverPersonService $personServiceCalendarEventService $calendarEventServiceCandidateService $candidateServiceUserService $userServiceJobService $jobServiceCityService $cityServiceStudentLevelService $studentLevelServiceImageService $imageService null,
  76.                                 ErrorLogCreateQueueService $errorLogCreateQueueService null)
  77.     {
  78.         parent::__construct($security$requestStack$validator$routeService,
  79.             $router$session$em$fileService$serviceRetriever,
  80.             $imageService$errorLogCreateQueueService);
  81.         $this->personService $personService;
  82.         $this->calendarEventService $calendarEventService;
  83.         $this->candidateService $candidateService;
  84.         $this->userService $userService;
  85.         $this->jobService $jobService;
  86.         $this->cityService $cityService;
  87.         $this->studentLevelService $studentLevelService;
  88.     }
  89.     public function campaignsSubscription(Request $requestPersonService $personService): JsonResponse
  90.     {
  91.         $personId $request->get('subscriberId');
  92.         $status $request->get('status');
  93.         /** @var Person|null $person */
  94.         $person $personService->getBaseService()->get($personId);
  95.         if (!$person) {
  96.             return new JsonResponse([], 400);
  97.         }
  98.         if ($status) {
  99.             if (!in_array($status, ["subscribed""unsubscribed"])) {
  100.                 return new JsonResponse([], 400);
  101.             }
  102.             $unsubscribed $status == "unsubscribed";
  103.             $person->setIsUnsubscribed($unsubscribed);
  104.             $isSubscriberAnyStatus in_array($person->getVirtualStudentStatus(), [
  105.                 Status::STATUS_SUBSCRIBER_PREPARING_STUDENT,
  106.                 Status::STATUS_SUBSCRIBER_RIPENING,
  107.                 VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER,
  108.                 Status::STATUS_SUBSCRIBER_NON_ACTIVE,
  109.             ]);
  110.             if ($isSubscriberAnyStatus) { //пока только для подписчиков
  111.                 $isNew = !$person->getStudent();
  112.                 if (!$person->getStudent() || !$unsubscribed) {
  113.                     $this->changeLearnerData($person$person->getStudent(), VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER,
  114.                         $isNewnulltrue);
  115.                 } elseif ($unsubscribed) {
  116.                     $this->changeLearnerData($person$person->getStudent(), Status::STATUS_SUBSCRIBER_NON_ACTIVE,
  117.                         $isNewnulltrue);
  118.                 }
  119.             }
  120.             $person->setCanSendTelegramCampaigns(!$unsubscribed);
  121.             $this->em->flush();
  122.             return new JsonResponse([], 200);
  123.         } else {
  124.             return new JsonResponse([], $person->isIsUnsubscribed() ? 404 200);
  125.         }
  126.     }
  127.     public function list(Request $requestCandidateService $candidateService,
  128.                          StudentService $studentServiceCalendarEventService $calendarEventService): Response
  129.     {
  130.         $students $studentService->getBaseService()->getAll();
  131.         $persons array_map(function (Student $student) {
  132.             return $student->getPerson();
  133.         }, $students);
  134.         $candidatesByStudentAndCalendarEvent =
  135.             $candidateService->getCandidatesGroupedByPersonsAndCalendarEvents();
  136.         $calendarEvents $calendarEventService->getAll();
  137.         $calendarEvents Other::getIdIndexedEntityArray($calendarEvents);
  138.         $earnLevelCandidates $candidateService
  139.             ->getPersonsLastCandidateWithEarnLevelCalendarEvent($persons);
  140.         return $this->render('admin/student/students.html.twig', [
  141.             'students' => $students,
  142.             "candidatesByStudentAndCalendarEvent" => $candidatesByStudentAndCalendarEvent,
  143.             "calendarEvents" => $calendarEvents,
  144.             "earnLevelCandidates" => $earnLevelCandidates,
  145.         ]);
  146.     }
  147.     public function learners(Request $requestCandidateService $candidateService,
  148.                              StudentService $studentServiceCalendarEventService $calendarEventService,
  149.                              PersonService $personService): Response
  150.     {
  151.         $items $personService->getLearners();
  152.         $candidatesByPersonsAndCalendarEvent =
  153.             $candidateService->getCandidatesGroupedByPersonsAndCalendarEvents();
  154.         $calendarEvents $calendarEventService->getAll();
  155.         $calendarEvents Other::getIdIndexedEntityArray($calendarEvents);
  156.         $earnLevelCandidates $candidateService
  157.             ->getPersonsLastCandidateWithEarnLevelCalendarEvent($items);
  158.         $itemsByStatusCount = [];
  159.         foreach (Status::getAsArray() as $status) {
  160.             $itemsByStatusCount[$status] = [
  161.                 "status" => $status,
  162.                 "statusText" => Status::getText($status),
  163.                 "count" => 0,
  164.             ];
  165.         }
  166.         $itemsByStatusCount["subscriber_virtual_status"] = [
  167.             "status" => "subscriber_virtual_status",
  168.             "statusText" => "Подписчик",
  169.             "count" => 0,
  170.         ];
  171.         foreach ($items as $item) {
  172.             $status $item->isIsStudent() ? $item->getStudent()->getStatus() : "subscriber_virtual_status";
  173.             $itemsByStatusCount[$status]["count"]++;
  174.         }
  175.         $itemsByStudentLevelCount = [];
  176.         for ($i 0$i <= 6$i++) {
  177.             $itemsByStudentLevelCount[$i] = [
  178.                 "level" => $i,
  179.                 "count" => 0,
  180.             ];
  181.         }
  182.         foreach ($items as $item) {
  183.             if ($item->isIsStudent() && Status::isActiveStatus($item->getStudent()->getStatus())) {
  184.                 $level $item->getStudent()->getLevel();
  185.                 //redo
  186.                 if ($level == -100) {
  187.                     continue;
  188.                 }
  189.                 if ($level !== null) {
  190.                     if ($level 0) {
  191.                         dd($item);
  192.                     }
  193.                     $itemsByStudentLevelCount[$level]["count"]++;
  194.                 }
  195.             }
  196.         }
  197.         //sort by key, Status::canSendCampaignStatuses(), and rest statuses
  198.         $sorted = [];
  199.         foreach (Status::getCanSendCampaignStatuses() as $status) {
  200.             if (isset($itemsByStatusCount[$status])) {
  201.                 $sorted[$status] = $itemsByStatusCount[$status];
  202.             }
  203.         }
  204.         foreach ($itemsByStatusCount as $status => $itemByStatusCount) {
  205.             if (!isset($sorted[$status])) {
  206.                 $sorted[$status] = $itemByStatusCount;
  207.             }
  208.         }
  209.         $itemsByStatusCount $sorted;
  210.         $itemsWithCanSendCampaignStatusCount 0;
  211.         foreach ($items as $item) {
  212.             $status $item->isIsStudent() ? $item->getStudent()->getStatus() : null;
  213.             if ($status && Status::isStudentStatus($status)) {
  214.                 $itemsWithCanSendCampaignStatusCount++;
  215.             }
  216.         }
  217.         $allLeavedStudentsCount 0;
  218.         foreach ($items as $item) {
  219.             $status $item->isIsStudent() ? $item->getStudent()->getStatus() : null;
  220.             if ($status && Status::isLeavedStatus($status)) {
  221.                 $allLeavedStudentsCount++;
  222.             }
  223.         }
  224.         $allStudentsCount 0;
  225.         foreach ($items as $item) {
  226.             if ($item->isIsStudent()) {
  227.                 $allStudentsCount++;
  228.             }
  229.         }
  230.         return $this->render('admin/student/learners.html.twig', [
  231.             'items' => $items,
  232.             "candidatesByPersonsAndCalendarEvent" => $candidatesByPersonsAndCalendarEvent,
  233.             "calendarEvents" => $calendarEvents,
  234.             "earnLevelCandidates" => $earnLevelCandidates,
  235.             "itemsByStatusCount" => $itemsByStatusCount,
  236.             "itemsWithCanSendCampaignStatusCount" => $itemsWithCanSendCampaignStatusCount,
  237.             "allStudentsCount" => $allStudentsCount,
  238.             "allLeavedStudentsCount" => $allLeavedStudentsCount,
  239.             "itemsByStudentLevelCount" => $itemsByStudentLevelCount,
  240.         ]);
  241.     }
  242.     public function edit(Request $requestStudentService $studentService): Response
  243.     {
  244.         $id $request->get('id');
  245.         /**
  246.          * @var Student $item
  247.          */
  248.         $item $id $studentService->getBaseService()->get($id) : null;
  249.         $person $item $item->getPerson() : null;
  250.         $isNew false;
  251.         if (!$item) {
  252.             $person = new Person();
  253.             $item = (new Student());
  254.             $isNew true;
  255.         }
  256.         $form $this->createForm(StudentType::class, $item);
  257.         $form->handleRequest($request);
  258.         $personForm $this->createForm(PersonType::class, $person);
  259.         $personForm->handleRequest($request);
  260.         if ($form->isSubmitted() && $form->isValid()) {
  261.             $this->em->persist($person);
  262.             $this->em->flush();
  263.             $item->setPerson($person);
  264.             $this->em->persist($item);
  265.             $this->em->flush();
  266.             if ($isNew) {
  267.                 $studentLevel = (new StudentLevel())
  268.                     ->setStudent($item)
  269.                     ->setValue((int)$request->get("student")['level'])
  270.                     ->setComment('Уровень при добавлении ученика')
  271.                     ->setAuthor($this->getUser());
  272.                 $this->em->persist($studentLevel);
  273.                 $this->em->flush();
  274.             }
  275.             return $this->redirectToRoute('moderator_students');
  276.         }
  277.         return $this->render('admin/student/student_edit.html.twig', [
  278.             'form' => $form->createView(),
  279.             'personForm' => $personForm->createView(),
  280.             'item' => $item,
  281.         ]);
  282.     }
  283.     private function changeLearnerData($person$student null$virtualStatus,
  284.                                        bool &$isNew null,
  285.                                        Request $request nullbool $isFormDataValid null)
  286.     {
  287.         if (!$student) {
  288.             $student = (new Student());
  289.             $isNew true;
  290.         }
  291.         $studentLevelInt $request ? (int)$request->get("student")['level'] : null;
  292.         $oldStatus $student->getStatus();
  293.         $oldPhone $person->getPhone();
  294.         if ($virtualStatus === VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER) {
  295.             $person->setIsSubscriber(true);
  296.             $person->setIsStudent(false);
  297.         } elseif (Status::isCorrect($virtualStatus)) {
  298.             $person->setIsStudent(true);
  299.             $person->setIsSubscriber(false);
  300.             $student->setStatus($virtualStatus);
  301.         } else {
  302.             throw new \Exception("Unknown: " $virtualStatus);
  303.         }
  304.         if (in_array($student->getStatus(), [Status::STATUS_SUBSCRIBER_NON_ACTIVE,
  305. //                    Status::STATUS_SUBSCRIBER_LISTENER,
  306.             Status::STATUS_SUBSCRIBER_RIPENING,
  307.             Status::STATUS_SUBSCRIBER_PREPARING_STUDENT,])) {
  308.             $student->setLevel(-100);
  309.         }
  310.         if ($isFormDataValid) {
  311.             //задание старого статуса студента при переводе студента в
  312.             //подписчики или после редактирования подписчика,
  313.             //который ранее был студентом
  314.             if ($student && $oldStatus && !$person->isIsStudent() && !$isNew) {
  315.                 $student->setStatus($oldStatus);
  316.             }
  317.             //todo redo
  318.             if ($request) {
  319.                 $newJobName $request->get("newJobName");
  320.                 $jobId = (int)($request->get("person")['job'] ?? null);
  321.                 $addNewJob $jobId === -100;
  322.                 if ($addNewJob) {
  323.                     if (!$newJobName) {
  324.                         throw new \Exception('Название новой деятельности не может быть пустым.');
  325.                     }
  326.                     $job $this->jobService->createOrGetDefault(["name" => $newJobName]);
  327.                 } else {
  328.                     $job $this->jobService->getBaseService()->get($jobId);
  329.                 }
  330.                 $person->setJob($job);
  331.                 $newCityName $request->get("newCityName");
  332.                 $cityId = (int)($request->get("person")['city'] ?? null);
  333.                 $addNewCity $cityId === -100;
  334.                 if ($addNewCity) {
  335.                     if (!$newCityName) {
  336.                         throw new \Exception('Название нового города не может быть пустым.');
  337.                     }
  338.                     $city $this->cityService->createOrGetDefault(["name" => $newCityName]);
  339.                 } else {
  340.                     $city $this->cityService->getBaseService()->get($cityId);
  341.                 }
  342.                 $person->setCity($city);
  343.             }
  344.             if ($person->isIsStudent() && $this->studentLevelService->calcStudentLevel($student) == -1) {
  345.                 throw new \Exception('Нельзя через редактор восстановить статус ученика подписчику.');
  346.             }
  347.             //обнуение студента при создании подписчика
  348.             if ($person->isIsSubscriber() && $isNew) {
  349.                 $student null;
  350. //                $person->setStudent(null);
  351.             }
  352.             if ($this->personService->hasSameDefault($person)) {
  353.                 throw new \Exception('Учащийся с такими данными уже существует.');
  354.             }
  355.             $this->em->persist($person);
  356.             $this->em->flush();
  357.             if ($isNew && $student && !$student->getId() && $student->getLevel() === null) {
  358.                 $student->setPerson($person)
  359.                     ->setLevel($studentLevelInt);
  360.             }
  361.             if ($student && $student->getLevel() === null) {
  362.                 $student->setLevel(0);
  363.             }
  364.             //уровень студента -1 при переводе из студента в подписчики
  365.             if ($person->isIsSubscriber() && $student && $student->getLevels()->count()) {
  366.                 $level $this->studentLevelService->calcStudentLevel($student);
  367.                 if ($level > -1) {
  368.                     $studentLevel = (new StudentLevel())
  369.                         ->setStudent($student)
  370.                         ->setValue($this->studentLevelService->calcStudentLevel($student) * -1)
  371.                         ->setComment('Перевод в подписчики')
  372.                         ->setAuthor($this->getUser());
  373.                     $this->em->persist($studentLevel);
  374.                 }
  375.                 //если по какой-то причине уровень неверно рассчитан
  376.                 if ($student && $student->getLevel() != -1) {
  377.                     $student->setLevel($this->studentLevelService->calcStudentLevel($student));
  378.                     $this->em->flush();
  379.                 }
  380.             }
  381.             if ($oldPhone && $oldPhone != $person->getPhone()) {
  382.                 $person->setTelegramUserId(null);
  383.             }
  384.             if ($person->isIsStudent()) {
  385.                 $student->setPerson($person);
  386.                 if ($student->getLevel() === null) {
  387.                     $student->setLevel(0);
  388.                 }
  389.                 $this->em->persist($student);
  390.                 $this->em->flush();
  391.             }
  392.             if ($isNew) {
  393.                 if ($person->isIsStudent()) {
  394.                     $studentLevel = (new StudentLevel())
  395.                         ->setStudent($student)
  396.                         ->setValue($student->getLevel() !== null $student->getLevel() : $studentLevelInt)
  397.                         ->setComment('Уровень при добавлении ученика')
  398.                         ->setAuthor($this->getUser());
  399.                     $this->em->persist($studentLevel);
  400.                     $this->em->flush();
  401.                 }
  402.             }
  403.         }
  404.     }
  405.     public function learnerEdit(Request $request,
  406.                                 PersonService $personServiceStudentLevelService $studentLevelService,
  407.                                 JobService $jobServiceCityService $cityService): Response
  408.     {
  409.         $id $request->get('id');
  410.         /**
  411.          * @var Person $person
  412.          */
  413.         $person $id $personService->getBaseService()->get($id) : null;
  414.         /**
  415.          * @var Student $student
  416.          */
  417.         $student $person $person->getStudent() : null;
  418.         $isNew false;
  419.         if (!$person) {
  420.             $person = new Person();
  421.             $isNew true;
  422.         }
  423.         if (!$student) {
  424.             $student = (new Student());
  425.             $isNew true;
  426.         }
  427.         $form $this->createForm(StudentType::class, $student);
  428.         $form->handleRequest($request);
  429.         $personForm $this->createForm(PersonType::class, $person);
  430.         $personForm->handleRequest($request);
  431.         try {
  432.             if ($form->isSubmitted()) {
  433.                 $virtualStatus $request->get("person")['virtualStudentStatus'] ?? null;
  434.                 $isFormDataValid $form->isValid() && $personForm->isValid();
  435.                 $this->changeLearnerData($person$student$virtualStatus$isNew$request,
  436.                     $isFormDataValid);
  437.                 if ($isFormDataValid) {
  438.                     return $this->redirectToRoute('moderator_learners');
  439.                 }
  440.             }
  441.         } catch (\Throwable $e) {
  442.             $this->addExceptionFlash($e);
  443.         }
  444. //        if (!$form->isSubmitted()) {
  445. //            $pyTgUtils->setTechAdminAccount();
  446. //        }
  447.         return $this->render('admin/student/learner_edit.html.twig', [
  448.             'form' => $form->createView(),
  449.             'personForm' => $personForm->createView(),
  450.             'item' => $student,
  451.             'person' => $person,
  452.         ]);
  453.     }
  454.     public function delete(Request $requestStudentService $studentService,
  455.         PersonService $personService): RedirectResponse
  456.     {
  457.         $id $request->get('id');
  458.         if (!$id) {
  459.             $this->addFlash('errors''Не указан идентификатор учащегося.');
  460.             return $this->redirectToRoute('moderator_learners');
  461.         }
  462.         /** @var Person|null $item */
  463.         $item $personService->getBaseService()->get($id);
  464.         if (!$item) {
  465.             $this->addFlash('errors''Учащийся не найден.');
  466.             return $this->redirectToRoute('moderator_learners');
  467.         }
  468.         try {
  469.             $item->setStatus(Status::STATUS_DELETED);
  470.             if ($item->getStudent()) {
  471.                 $item->getStudent()->setStatus(Status::STATUS_DELETED);
  472.             }
  473.             $this->em->flush();
  474.             $studentName $item->getName();
  475.             $this->addFlash('success''Учащийся ' $studentName ' помечен как удалённый.');
  476.         } catch (\Throwable $e) {
  477.             $this->addExceptionFlash($enull'Ошибка при удалении учащегося. ');
  478.         }
  479.         return $this->redirectToRoute('moderator_learners');
  480.     }
  481.     public function registration(Request $requestPersonService $personService,
  482.                                  CalendarEventService $calendarEventServiceCandidateService $candidateService): Response
  483.     {
  484.         $calendarEventId $request->query->get('calendarEventId');
  485.         $selectedCalendarEvent null;
  486.         $persons = [];
  487.         // Get all calendar events without review requirement
  488.         $allCalendarEvents $calendarEventService->getCalendarEventsForMakeCandidates(
  489.             Type::getForRegisterWithoutReviewTypes()
  490.         );
  491.         $calendarEvents array_filter($allCalendarEvents, function($event) {
  492.             return !$event->isIsCandidateReviewRequired();
  493.         });
  494.         if ($calendarEventId) {
  495.             /** @var CalendarEvent $selectedCalendarEvent */
  496.             $selectedCalendarEvent $calendarEventService->getBaseService()->get($calendarEventId);
  497.             if ($selectedCalendarEvent) {
  498.                 // Get existing candidates for this event
  499.                 $existingCandidates $candidateService->getDefault([
  500.                     'calendarEvent' => $selectedCalendarEvent,
  501.                     'status' => [
  502.                         \App\Enum\Candidate\Status::STATUS_NEW,
  503.                         \App\Enum\Candidate\Status::STATUS_APPROVED,
  504.                         \App\Enum\Candidate\Status::STATUS_DRAFT,
  505.                     ]
  506.                 ]);
  507.                 $candidatesByPerson = [];
  508.                 foreach ($existingCandidates as $candidate) {
  509.                     if ($candidate->getPerson()) {
  510.                         $candidatesByPerson[$candidate->getPerson()->getId()] = $candidate;
  511.                     }
  512.                 }
  513.                 // Get students with matching level
  514.                 $fromLevel $selectedCalendarEvent->getFromLevel();
  515.                 $allPersons $personService->getLearners();
  516.                 $persons array_filter($allPersons, function(Person $person) use ($fromLevel$selectedCalendarEvent$candidatesByPerson) {
  517.                     if (isset($candidatesByPerson[$person->getId()])) {
  518.                         return true;
  519.                     }
  520.                     if (!Status::isCanSendCampaignStatus($person->getVirtualStudentStatus())) {
  521.                         return false;
  522.                     }
  523.                     //redo
  524.                     $isRequiredStatusSubscriber $selectedCalendarEvent->getRequireStudentStatus() == VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER;
  525.                     $isSubscriberAnyStatus in_array($person->getVirtualStudentStatus(), [
  526.                         Status::STATUS_SUBSCRIBER_PREPARING_STUDENT,
  527.                         Status::STATUS_SUBSCRIBER_RIPENING,
  528.                         VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER,
  529.                     ]);
  530.                     if ($selectedCalendarEvent->getRequireStudentStatus()
  531.                         && !($isRequiredStatusSubscriber && $isSubscriberAnyStatus true :
  532.                             $person->getVirtualStudentStatus() == $selectedCalendarEvent->getRequireStudentStatus())) {
  533.                         return false;
  534.                     }
  535.                     if ($selectedCalendarEvent->getFromLevel() === null) {
  536.                         return true;
  537.                     }
  538.                     if ($person->isIsStudent() && $person->getStudent()) {
  539.                         $level $person->getStudent()->getLevel();
  540.                         return $level !== null && $level >= $fromLevel
  541.                             && ($selectedCalendarEvent->getToLevel() === null || $level <= $selectedCalendarEvent->getToLevel())
  542.                             && $person->isCalendarEventStatusAllowed($selectedCalendarEvent);
  543.                     }
  544.                     return false;
  545.                 });
  546.             }
  547.         }
  548.         // Prepare dropdown data for calendar events
  549.         $routeParamDropdownsData = [
  550.             'calendarEvent' => array_map(function($event) {
  551.                 return [
  552.                     'listItemId' => $event->getId(),
  553.                     'text' => $event->getText(['quotes' => true])
  554.                 ];
  555.             }, array_values($calendarEvents))
  556.         ];
  557.         return $this->render('admin/student/registration.html.twig', [
  558.             'persons' => $persons,
  559.             'calendarEvents' => $calendarEvents,
  560.             'selectedCalendarEvent' => $selectedCalendarEvent,
  561.             'routeParamDropdownsData' => $routeParamDropdownsData,
  562.             'candidatesByPerson' => $candidatesByPerson ?? []
  563.         ]);
  564.     }
  565.     public function toggleRegistration(Request $requestCandidateService $candidateService,
  566.                                        PersonService $personServiceCalendarEventService $calendarEventService,
  567.                                        UserService $userServiceApiHandler $apiHandler): JsonResponse
  568.     {
  569.         return $apiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  570.             $candidateService$personService$calendarEventService$userService
  571.         ) {
  572.             $personId $content['person_id'] ?? null;
  573.             $calendarEventId $content['calendar_event_id'] ?? null;
  574.             /** @var Person $person */
  575.             $person $this->personService->getBaseService()->get($personId);
  576.             /** @var CalendarEvent $calendarEvent */
  577.             $calendarEvent $this->calendarEventService->getBaseService()->get($calendarEventId);
  578.             return $this->doToggleRegistration($person$calendarEvent$responseCode);
  579.         }, true, ['person_id''calendar_event_id']);
  580.     }
  581.     public function toggleRegistrationByStudent(Request $requestCandidateService $candidateService,
  582.                                        PersonService $personServiceCalendarEventService $calendarEventService,
  583.                                        UserService $userServiceApiHandler $apiHandler): JsonResponse
  584.     {
  585.         return $apiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  586.             $candidateService$personService$calendarEventService$userService$request
  587.         ) {
  588.             $personId $request->get("registrantId");
  589.             $calendarEventId $request->get("eventId");
  590.             $status $request->get("status");
  591.             $create $status == "registered";
  592.             /** @var Person $person */
  593.             $person $this->personService->getBaseService()->get($personId);
  594.             /** @var CalendarEvent $calendarEvent */
  595.             $calendarEvent $this->calendarEventService->getBaseService()->get($calendarEventId);
  596.             if ($status) {
  597.                 return $this->doToggleRegistration($person$calendarEvent$responseCode$create,
  598.                     RegistrationType::REGISTRATION_TYPE_LEARNER);
  599.             } else {
  600.                 /** @var Candidate|null $existingCandidate */
  601.                 $existingCandidate null;
  602.                 try {
  603.                     $existingCandidate $this->getExistingCandidate($person$calendarEvent$existsWithStatusIsNotNew);
  604.                     if ($existingCandidate) {
  605.                         $responseCode 200;
  606.                         return [];
  607.                     } else {
  608.                         $responseCode 404;
  609.                         return [];
  610.                     }
  611.                 } catch (\Throwable $exception) {
  612.                     if ($existsWithStatusIsNotNew) {
  613.                         $responseCode 200;
  614.                         return [];
  615.                     }
  616.                     $responseCode 400;
  617.                     return ['error' => $exception->getMessage()];
  618.                 }
  619.             }
  620.         }, false, []);
  621.     }
  622.     private function doToggleRegistration(?Person $person, ?CalendarEvent $calendarEvent,
  623.                                           &$responseCodebool $create null$registrationType null): array
  624.     {
  625.         if (!$person || !$calendarEvent) {
  626.             $responseCode 404;
  627.             return ['error' => 'Person or CalendarEvent not found'];
  628.         }
  629.         if ($calendarEvent->isIsCandidateReviewRequired()) {
  630.             throw new \Exception("Candidate review is required");
  631.         }
  632.         // Check if candidate already exists
  633.         /** @var Candidate|null $existingCandidate */
  634.         $existingCandidate null;
  635.         try {
  636.             $existingCandidate $this->getExistingCandidate($person$calendarEvent);
  637.         } catch (\Throwable $exception) {
  638.             $responseCode 400;
  639.             return ['error' => $exception->getMessage()];
  640.         }
  641.         if ($existingCandidate && ($create === null || !$create)) {
  642.             // Delete candidate
  643.             $this->candidateService->removeCandidate($existingCandidate);
  644.             $responseCode 200;
  645.             return ['action' => 'deleted''candidateId' => null];
  646.         } elseif ($create === null || $create) {
  647.             // Create new candidate
  648.             $params = [
  649.                 "person" => $person,
  650.                 "calendarEvent" => $calendarEvent,
  651.                 "status" => \App\Enum\Candidate\Status::STATUS_APPROVED,
  652.             ];
  653.             if ($registrationType) {
  654.                 $params['registrationType'] = $registrationType;
  655.             }
  656.             /** @var Candidate $candidate */
  657.             $candidate $this->candidateService->createOrGetDefault($params);
  658.             if (!$candidate->getAuthor() && $this->getUser()) {
  659.                 $candidate->setAuthor($this->getUser());
  660.             }
  661.             if (!$candidate->getCurator() && $this->userService->getStarCurator()) {
  662.                 $candidate->setCurator($this->userService->getStarCurator());
  663.             }
  664.             $this->em->flush();
  665.             $responseCode 200;
  666.             return ['action' => 'created''candidateId' => $candidate->getId()];
  667.         } else {
  668.             $responseCode 200;
  669.             return [];
  670.         }
  671.     }
  672.     private function getExistingCandidate($person$calendarEventbool &$existsWithStatusIsNotNew null): ?Candidate
  673.     {
  674.         $existingCandidates $this->candidateService->getDefault([
  675.             'person' => $person,
  676.             'calendarEvent' => $calendarEvent,
  677.         ]);
  678.         $existingCandidates array_filter($existingCandidates, function (Candidate $candidate) {
  679.             return $candidate->getStatus() != \App\Enum\Candidate\Status::STATUS_DELETED;
  680.         });
  681.         /** @var Candidate $existingCandidate */
  682.         $existingCandidate count($existingCandidates) > current($existingCandidates) : null;
  683.         $existsWithStatusIsNotNew $existingCandidate && $existingCandidate->getStatus() !== \App\Enum\Candidate\Status::STATUS_NEW;
  684.         if ($existsWithStatusIsNotNew && $calendarEvent->isIsCandidateReviewRequired()) {
  685.             throw new \Exception('Статус кандидата: ' $existingCandidate->getStatusText());
  686.         }
  687.         return $existingCandidate;
  688.     }
  689.     public function loadTelegramHistory(Request $requestPersonService $personService,
  690.                         PyTgUtils $pyTgUtilsApiHandler $apiHandler): JsonResponse
  691.     {
  692.         return $apiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  693.             $personService$pyTgUtils
  694.         ) {
  695.             $personId $content['person_id'] ?? null;
  696.             if (!$personId) {
  697.                 $responseCode 400;
  698.                 return ['error' => 'Missing person_id'];
  699.             }
  700.             /** @var Person $person */
  701.             $person $personService->getBaseService()->get($personId);
  702.             if (!$person) {
  703.                 $responseCode 404;
  704.                 return ['error' => 'Person not found'];
  705.             }
  706.             $telegramUserId $person->getTelegramUserId();
  707.             if (!$telegramUserId) {
  708.                 $responseCode 400;
  709.                 return ['error' => 'Telegram user ID not set'];
  710.             }
  711.             try {
  712.                 // Устанавливаем технический аккаунт администратора
  713.                 $pyTgUtils->setTechAdminAccount();
  714.                 // Создаем приватный чат
  715.                 $chat $pyTgUtils->pyTg->createPrivateChat($telegramUserId);
  716.                 $chatId $chat['id'];
  717.                 // Получаем последние 5 сообщений
  718.                 $messages $pyTgUtils->pyTg->getChatHistory($chatId10);
  719.                 $messages array_map(function (Message $message) {
  720.                     return $message->getData();
  721.                 }, $messages);
  722.                 // Обрабатываем сообщения
  723.                 $processedMessages = [];
  724.                 $debugInfo = [];
  725.                 foreach ($messages as $messageIndex => $message) {
  726.                     $processedMessage = [
  727.                         'id' => $message['id'] ?? null,
  728.                         'date' => $message['date'] ?? null,
  729.                         'is_outgoing' => $message['is_outgoing'] ?? false,
  730.                         'text' => '',
  731.                         'images' => [],
  732.                         'debug' => [] // Отладочная информация (будет удалена перед отправкой)
  733.                     ];
  734.                     $messageDebug = [
  735.                         'message_index' => $messageIndex,
  736.                         'message_id' => $message['id'] ?? null,
  737.                         'content_type' => $message['content']['@type'] ?? 'unknown',
  738.                     ];
  739.                     // Получаем текст сообщения
  740.                     if (isset($message['content'])) {
  741.                         if (isset($message['content']['text']) && isset($message['content']['text']['text'])) {
  742.                             $processedMessage['text'] = $message['content']['text']['text'];
  743.                         } elseif (isset($message['content']['caption']) && isset($message['content']['caption']['text'])) {
  744.                             $processedMessage['text'] = $message['content']['caption']['text'];
  745.                         }
  746.                         // Обрабатываем фото
  747.                         if (isset($message['content']['@type']) && $message['content']['@type'] === 'messagePhoto') {
  748.                             $messageDebug['has_photo'] = true;
  749.                             if (isset($message['content']['photo']['sizes'])) {
  750.                                 $sizes $message['content']['photo']['sizes'];
  751.                                 $messageDebug['sizes_count'] = count($sizes);
  752.                                 // Берем изображение среднего размера (для оптимизации)
  753.                                 // Если размеров меньше 3, берем последнее
  754.                                 $photoIndex count($sizes) > count($sizes) - count($sizes) - 1;
  755.                                 $selectedPhoto $sizes[$photoIndex];
  756.                                 $messageDebug['selected_photo_index'] = $photoIndex;
  757.                                 $messageDebug['selected_photo_type'] = $selectedPhoto['type'] ?? 'unknown';
  758.                                 $imageLoaded false;
  759.                                 $imagePath null;
  760.                                 // Проверяем, есть ли локальный путь к файлу
  761.                                 if (isset($selectedPhoto['photo']['local']['path']) &&
  762.                                     file_exists($selectedPhoto['photo']['local']['path'])) {
  763.                                     $imagePath $selectedPhoto['photo']['local']['path'];
  764.                                     $messageDebug['source'] = 'local_path';
  765.                                     $messageDebug['path'] = $imagePath;
  766.                                     $imageLoaded true;
  767.                                 } elseif (isset($selectedPhoto['photo']['id'])) {
  768.                                     // Если файл не загружен локально, загружаем через API
  769.                                     $messageDebug['source'] = 'api_download';
  770.                                     $messageDebug['file_id'] = $selectedPhoto['photo']['id'];
  771.                                     try {
  772.                                         // downloadFile возвращает base64 данные напрямую
  773.                                         $base64Data $pyTgUtils->pyTg->downloadFile($selectedPhoto['photo']['id']);
  774.                                         $messageDebug['download_response'] = [
  775.                                             'is_string' => is_string($base64Data),
  776.                                             'data_length' => is_string($base64Data) ? strlen($base64Data) : 0,
  777.                                         ];
  778.                                         if (is_string($base64Data) && !empty($base64Data)) {
  779.                                             // Данные уже в base64, используем их напрямую
  780.                                             $processedMessage['images'][] = "data:image/jpeg;base64," $base64Data;
  781.                                             $messageDebug['base64_length'] = strlen($base64Data);
  782.                                             $messageDebug['success'] = true;
  783.                                             $imageLoaded true// Помечаем, что изображение загружено
  784.                                         } else {
  785.                                             $messageDebug['error'] = 'Invalid base64 data received';
  786.                                         }
  787.                                     } catch (\Exception $e) {
  788.                                         $messageDebug['error'] = 'Download failed: ' $e->getMessage();
  789.                                     }
  790.                                 }
  791.                                 // Конвертируем изображение в base64 (только для локальных файлов)
  792.                                 if ($imageLoaded && $imagePath) {
  793.                                     try {
  794.                                         $fileSize filesize($imagePath);
  795.                                         $messageDebug['file_size'] = $fileSize;
  796.                                         // Ограничение размера файла (5 МБ)
  797.                                         if ($fileSize 1024 1024) {
  798.                                             $messageDebug['error'] = 'File too large: ' $fileSize ' bytes';
  799.                                         } else {
  800.                                             $imageData file_get_contents($imagePath);
  801.                                             if ($imageData !== false) {
  802.                                                 $base64 base64_encode($imageData);
  803.                                                 $mimeType mime_content_type($imagePath);
  804.                                                 $processedMessage['images'][] = "data:$mimeType;base64,$base64";
  805.                                                 $messageDebug['mime_type'] = $mimeType;
  806.                                                 $messageDebug['base64_length'] = strlen($base64);
  807.                                                 $messageDebug['success'] = true;
  808.                                             } else {
  809.                                                 $messageDebug['error'] = 'Failed to read file contents';
  810.                                             }
  811.                                         }
  812.                                     } catch (\Exception $e) {
  813.                                         $messageDebug['error'] = 'Base64 conversion failed: ' $e->getMessage();
  814.                                     }
  815.                                 }
  816.                             } else {
  817.                                 $messageDebug['error'] = 'No photo sizes found';
  818.                             }
  819.                         }
  820.                     }
  821.                     $debugInfo[] = $messageDebug;
  822.                     // Удаляем debug перед добавлением в результат
  823.                     unset($processedMessage['debug']);
  824.                     $processedMessages[] = $processedMessage;
  825.                 }
  826.                 // Сортируем сообщения по дате (от старых к новым)
  827.                 usort($processedMessages, function($a$b) {
  828.                     return ($a['date'] ?? 0) - ($b['date'] ?? 0);
  829.                 });
  830.                 $responseCode 200;
  831.                 return [
  832.                     'success' => true,
  833.                     'messages' => $processedMessages,
  834.                     'person_name' => $person->getName(),
  835.                     'debug' => $debugInfo// Отладочная информация
  836.                     'total_messages' => count($processedMessages),
  837.                     'messages_with_images' => count(array_filter($processedMessages, function($m) {
  838.                         return !empty($m['images']);
  839.                     }))
  840.                 ];
  841.             } catch (\Exception $e) {
  842.                 $responseCode 500;
  843.                 return ['error' => 'Failed to load Telegram history: ' $e->getMessage()];
  844.             }
  845.         }, true, ['person_id']);
  846.     }
  847. }