src/Controller/StudentController.php line 326

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