src/Controller/StudentController.php line 171

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Core\Controller\BaseAbstractController;
  4. use App\Entity\CalendarEvent;
  5. use App\Entity\Candidate;
  6. use App\Entity\Person;
  7. use App\Entity\Student;
  8. use App\Enum\CalendarEvent\Type;
  9. use App\Enum\Candidate\RegistrationType;
  10. use App\Enum\Student\Status;
  11. use App\Exception\Student\StudentToSubscriberStatusException;
  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.             $isSubscribed = !$unsubscribed;
  110.             $person->setIsUnsubscribed($unsubscribed);
  111.             $isSubscriberAnyStatus in_array($person->getStudentStatus(), Status::getSubscriberStatuses());
  112.             if ($isSubscriberAnyStatus) { //статус учащегося пока только для подписчиков
  113.                 if ($isSubscribed) {
  114.                     $person->getStudent()->setStatus(Status::STATUS_SUBSCRIBER_LISTENER);
  115.                 } elseif (!$isSubscribed) {
  116.                     $person->getStudent()->setStatus(Status::STATUS_SUBSCRIBER_NON_ACTIVE);
  117.                 }
  118.             }
  119.             $person->setCanSendTelegramCampaigns($isSubscribed);
  120.             $person->setIsUnsubscribed($isSubscribed);
  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.         foreach ($items as $item) {
  167.             $status $item->getStudent()->getStatus();
  168.             $itemsByStatusCount[$status]["count"]++;
  169.         }
  170.         $itemsByStudentLevelCount = [];
  171.         for ($i 0$i <= 6$i++) {
  172.             $itemsByStudentLevelCount[$i] = [
  173.                 "level" => $i,
  174.                 "count" => 0,
  175.             ];
  176.         }
  177.         foreach ($items as $item) {
  178.             if ($item->isStudent() && Status::isActiveStatus($item->getStudent()->getStatus())) {
  179.                 $level $item->getStudent()->getLevel();
  180.                 //redo
  181.                 if ($level === null) {
  182.                     continue;
  183.                 }
  184.                 if ($level !== null) {
  185.                     if ($level 0) {
  186.                         dd($item);
  187.                     }
  188.                     $itemsByStudentLevelCount[$level]["count"]++;
  189.                 }
  190.             }
  191.         }
  192.         //sort by key, Status::canSendCampaignStatuses(), and rest statuses
  193.         $sorted = [];
  194.         foreach (Status::getCanSendCampaignStatuses() as $status) {
  195.             if (isset($itemsByStatusCount[$status])) {
  196.                 $sorted[$status] = $itemsByStatusCount[$status];
  197.             }
  198.         }
  199.         foreach ($itemsByStatusCount as $status => $itemByStatusCount) {
  200.             if (!isset($sorted[$status])) {
  201.                 $sorted[$status] = $itemByStatusCount;
  202.             }
  203.         }
  204.         $itemsByStatusCount $sorted;
  205.         $itemsWithCanSendCampaignStatusCount 0;
  206.         foreach ($items as $item) {
  207.             $status $item->isStudent() ? $item->getStudent()->getStatus() : null;
  208.             if ($status && Status::isStudentStatus($status)) {
  209.                 $itemsWithCanSendCampaignStatusCount++;
  210.             }
  211.         }
  212.         $allLeavedStudentsCount 0;
  213.         foreach ($items as $item) {
  214.             if ($status && Status::isLeavedStatus($item->getStudent()->getStatus())) {
  215.                 $allLeavedStudentsCount++;
  216.             }
  217.         }
  218.         $allStudentsCount 0;
  219.         foreach ($items as $item) {
  220.             if ($item->isStudent()) {
  221.                 $allStudentsCount++;
  222.             }
  223.         }
  224.         $allSubscribersCount 0;
  225.         foreach ($items as $item) {
  226.             if ($item->isActiveSubscriberStatus()) {
  227.                 $allSubscribersCount++;
  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.             "allSubscribersCount" => $allSubscribersCount,
  239.             "allLeavedStudentsCount" => $allLeavedStudentsCount,
  240.             "itemsByStudentLevelCount" => $itemsByStudentLevelCount,
  241.         ]);
  242.     }
  243. //    public function edit(Request $request, StudentService $studentService): Response
  244. //    {
  245. //        $id = $request->get('id');
  246. //        /**
  247. //         * @var Student $item
  248. //         */
  249. //        $item = $id ? $studentService->getBaseService()->get($id) : null;
  250. //        $person = $item ? $item->getPerson() : null;
  251. //
  252. //        $isNew = false;
  253. //        if (!$item) {
  254. //            $person = new Person();
  255. //            $item = (new Student());
  256. //            $isNew = true;
  257. //        }
  258. //
  259. //        $form = $this->createForm(StudentType::class, $item);
  260. //        $form->handleRequest($request);
  261. //
  262. //        $personForm = $this->createForm(PersonType::class, $person);
  263. //        $personForm->handleRequest($request);
  264. //
  265. //        if ($form->isSubmitted() && $form->isValid()) {
  266. //            $this->em->persist($person);
  267. //            $this->em->flush();
  268. //
  269. //            $item->setPerson($person);
  270. //            $this->em->persist($item);
  271. //            $this->em->flush();
  272. //
  273. //            return $this->redirectToRoute('moderator_students');
  274. //        }
  275. //
  276. //        return $this->render('admin/student/student_edit.html.twig', [
  277. //            'form' => $form->createView(),
  278. //            'personForm' => $personForm->createView(),
  279. //            'item' => $item,
  280. //        ]);
  281. //    }
  282.     private function changeLearnerData(Person $person$student null$virtualStatus,
  283.                                        bool &$isNew null,
  284.                                        Request $request nullbool $isFormDataValid null)
  285.     {
  286.         if (!$student) {
  287.             $student = (new Student());
  288.             $isNew true;
  289.         }
  290.         $oldPhone $person->getPhone();
  291.         if ($isFormDataValid) {
  292.             //todo redo
  293.             if ($request) {
  294.                 $newJobName $request->get("newJobName");
  295.                 $jobId = (int)($request->get("person")['job'] ?? null);
  296.                 $addNewJob $jobId === -100;
  297.                 if ($addNewJob) {
  298.                     if (!$newJobName) {
  299.                         throw new \Exception('Название новой деятельности не может быть пустым.');
  300.                     }
  301.                     $job $this->jobService->createOrGetDefault(["name" => $newJobName]);
  302.                 } else {
  303.                     $job $this->jobService->getBaseService()->get($jobId);
  304.                 }
  305.                 $person->setJob($job);
  306.                 $newCityName $request->get("newCityName");
  307.                 $cityId = (int)($request->get("person")['city'] ?? null);
  308.                 $addNewCity $cityId === -100;
  309.                 if ($addNewCity) {
  310.                     if (!$newCityName) {
  311.                         throw new \Exception('Название нового города не может быть пустым.');
  312.                     }
  313.                     $city $this->cityService->createOrGetDefault(["name" => $newCityName]);
  314.                 } else {
  315.                     $city $this->cityService->getBaseService()->get($cityId);
  316.                 }
  317.                 $person->setCity($city);
  318.             }
  319.             if ($this->personService->hasSameDefault($person)) {
  320.                 throw new \Exception('Учащийся с такими данными уже существует.');
  321.             }
  322.             $this->em->persist($person);
  323.             $this->em->flush();
  324.             if ($oldPhone && $oldPhone != $person->getPhone()) {
  325.                 $person->setTelegramUserId(null);
  326.             }
  327. //            if ($person->isStudent()) {
  328.             $student->setPerson($person);
  329.             $this->em->persist($student);
  330.             $this->em->flush();
  331. //            }
  332.         }
  333.     }
  334.     public function learnerEdit(Request $request,
  335.                                 PersonService $personServiceStudentLevelService $studentLevelService,
  336.                                 JobService $jobServiceCityService $cityService): Response
  337.     {
  338.         $id $request->get('id');
  339.         /**
  340.          * @var Person $person
  341.          */
  342.         $person $id $personService->getBaseService()->get($id) : null;
  343.         /**
  344.          * @var Student $student
  345.          */
  346.         $student $person $person->getStudent() : null;
  347.         $isNew false;
  348.         if (!$person) {
  349.             $person = new Person();
  350.             $isNew true;
  351.         }
  352.         if (!$student) {
  353.             $student = (new Student());
  354.             $isNew true;
  355.         }
  356.         $form $this->createForm(StudentType::class, $student);
  357.         $form->handleRequest($request);
  358.         $personForm $this->createForm(PersonType::class, $person);
  359.         $personForm->handleRequest($request);
  360.         try {
  361.             if ($form->isSubmitted()) {
  362.                 $isFormDataValid $form->isValid() && $personForm->isValid();
  363.                 $this->changeLearnerData($person$studentnull$isNew$request,
  364.                     $isFormDataValid);
  365.                 if ($isFormDataValid) {
  366.                     return $this->redirectToRoute('moderator_learners');
  367.                 }
  368.             }
  369.         } catch (\Throwable $e) {
  370.             $this->addExceptionFlash($e);
  371.         }
  372. //        if (!$form->isSubmitted()) {
  373. //            $pyTgUtils->setTechAdminAccount();
  374. //        }
  375.         return $this->render('admin/student/learner_edit.html.twig', [
  376.             'form' => $form->createView(),
  377.             'personForm' => $personForm->createView(),
  378.             'item' => $student,
  379.             'person' => $person,
  380.         ]);
  381.     }
  382.     public function delete(Request $requestStudentService $studentService,
  383.         PersonService $personService): RedirectResponse
  384.     {
  385.         $id $request->get('id');
  386.         if (!$id) {
  387.             $this->addFlash('errors''Не указан идентификатор учащегося.');
  388.             return $this->redirectToRoute('moderator_learners');
  389.         }
  390.         /** @var Person|null $item */
  391.         $item $personService->getBaseService()->get($id);
  392.         if (!$item) {
  393.             $this->addFlash('errors''Учащийся не найден.');
  394.             return $this->redirectToRoute('moderator_learners');
  395.         }
  396.         try {
  397.             if ($this->candidateService->getCandidates($item)) {
  398.                 throw new \Exception(
  399.             "Нельзя удалять учащихся, проходивших обучение"
  400.                     " во избежание потери истории обучения и при случае необходимости"
  401.                     " восстановления учащегося!"
  402.                     " Если учащийся в ближайшее время не планирует обучение"
  403.                     " – вы можете вместо этого установить статус: \"Выбыл\""
  404.                     "."
  405.                 );
  406.             }
  407.             $item->setStatus(\App\Enum\Person\Status::STATUS_DELETED);
  408.             $item->archiveTelegramUserId();
  409.             if ($item->getStudent()) {
  410.                 $item->getStudent()->setStatus(Status::STATUS_DELETED);
  411.             }
  412.             $this->em->flush();
  413.             $studentName $item->getName();
  414.             $this->addFlash('success''Учащийся ' $studentName ' помечен как удалённый.');
  415.         } catch (\Throwable $e) {
  416.             $this->addExceptionFlash($enull'Ошибка при удалении учащегося. ');
  417.         }
  418.         return $this->redirectToRoute('moderator_learners');
  419.     }
  420.     public function registration(Request $requestPersonService $personService,
  421.                                  CalendarEventService $calendarEventServiceCandidateService $candidateService): Response
  422.     {
  423.         $calendarEventId $request->query->get('calendarEventId');
  424.         $selectedCalendarEvent null;
  425.         $persons = [];
  426.         // Get all calendar events without review requirement
  427.         $allCalendarEvents $calendarEventService->getCalendarEventsForMakeCandidates(
  428.             Type::getForRegisterWithoutReviewTypes()
  429.         );
  430.         $calendarEvents array_filter($allCalendarEvents, function($event) {
  431.             return !$event->isIsCandidateReviewRequired();
  432.         });
  433.         if ($calendarEventId) {
  434.             /** @var CalendarEvent $selectedCalendarEvent */
  435.             $selectedCalendarEvent $calendarEventService->getBaseService()->get($calendarEventId);
  436.             if ($selectedCalendarEvent) {
  437.                 // Get existing candidates for this event
  438.                 $existingCandidates $candidateService->getDefault([
  439.                     'calendarEvent' => $selectedCalendarEvent,
  440.                     'status' => [
  441.                         \App\Enum\Candidate\Status::STATUS_NEW,
  442.                         \App\Enum\Candidate\Status::STATUS_APPROVED,
  443.                         \App\Enum\Candidate\Status::STATUS_DRAFT,
  444.                     ]
  445.                 ]);
  446.                 $candidatesByPerson = [];
  447.                 foreach ($existingCandidates as $candidate) {
  448.                     if ($candidate->getPerson()) {
  449.                         $candidatesByPerson[$candidate->getPerson()->getId()] = $candidate;
  450.                     }
  451.                 }
  452.                 // Get students with matching level
  453.                 $fromLevel $selectedCalendarEvent->getFromLevel();
  454.                 $allPersons $personService->getLearners();
  455.                 $persons array_filter($allPersons, function(Person $person) use ($fromLevel$selectedCalendarEvent$candidatesByPerson) {
  456.                     if (isset($candidatesByPerson[$person->getId()])) {
  457.                         return true;
  458.                     }
  459.                     if (!Status::isCanSendCampaignStatus($person->getStudentStatus())) {
  460.                         return false;
  461.                     }
  462.                     //redo
  463.                     $isRequiredStatusSubscriber $selectedCalendarEvent->getRequireStudentStatus() == Status::STATUS_SUBSCRIBER_LISTENER;
  464.                     $isSubscriberAnyStatus in_array($person->getStudentStatus(), Status::getSubscriberActiveStatuses());
  465.                     if ($selectedCalendarEvent->getRequireStudentStatus()
  466.                         && !($isRequiredStatusSubscriber && $isSubscriberAnyStatus true :
  467.                             $person->getStudentStatus() == $selectedCalendarEvent->getRequireStudentStatus())) {
  468.                         return false;
  469.                     }
  470.                     if ($selectedCalendarEvent->getFromLevel() === null) {
  471.                         return true;
  472.                     }
  473.                     if ($person->isStudent() && $person->getStudent()) {
  474.                         $level $person->getStudent()->getLevel();
  475.                         return $level !== null && $level >= $fromLevel
  476.                             && ($selectedCalendarEvent->getToLevel() === null || $level <= $selectedCalendarEvent->getToLevel())
  477.                             && $person->isCalendarEventStatusAllowed($selectedCalendarEvent);
  478.                     }
  479.                     return false;
  480.                 });
  481.             }
  482.         }
  483.         // Prepare dropdown data for calendar events
  484.         $routeParamDropdownsData = [
  485.             'calendarEvent' => array_map(function($event) {
  486.                 return [
  487.                     'listItemId' => $event->getId(),
  488.                     'text' => $event->getText(['quotes' => true])
  489.                 ];
  490.             }, array_values($calendarEvents))
  491.         ];
  492.         return $this->render('admin/student/registration.html.twig', [
  493.             'persons' => $persons,
  494.             'calendarEvents' => $calendarEvents,
  495.             'selectedCalendarEvent' => $selectedCalendarEvent,
  496.             'routeParamDropdownsData' => $routeParamDropdownsData,
  497.             'candidatesByPerson' => $candidatesByPerson ?? []
  498.         ]);
  499.     }
  500.     public function toggleRegistration(Request $requestCandidateService $candidateService,
  501.                                        PersonService $personServiceCalendarEventService $calendarEventService,
  502.                                        UserService $userService): JsonResponse
  503.     {
  504.         return $this->defaultApiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  505.             $candidateService$personService$calendarEventService$userService
  506.         ) {
  507.             $personId $content['person_id'] ?? null;
  508.             $calendarEventId $content['calendar_event_id'] ?? null;
  509.             /** @var Person $person */
  510.             $person $this->personService->getBaseService()->get($personId);
  511.             /** @var CalendarEvent $calendarEvent */
  512.             $calendarEvent $this->calendarEventService->getBaseService()->get($calendarEventId);
  513.             return $this->doToggleRegistration($person$calendarEvent$responseCode);
  514.         }, true, ['person_id''calendar_event_id']);
  515.     }
  516.     public function toggleRegistrationByStudent(Request $requestCandidateService $candidateService,
  517.                                        PersonService $personServiceCalendarEventService $calendarEventService,
  518.                                        UserService $userService): JsonResponse
  519.     {
  520.         return $this->defaultApiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  521.             $candidateService$personService$calendarEventService$userService$request
  522.         ) {
  523.             $personId $request->get("registrantId");
  524.             $calendarEventId $request->get("eventId");
  525.             $status $request->get("status");
  526.             $create $status == "registered";
  527.             /** @var Person $person */
  528.             $person $this->personService->getBaseService()->get($personId);
  529.             /** @var CalendarEvent $calendarEvent */
  530.             $calendarEvent $this->calendarEventService->getBaseService()->get($calendarEventId);
  531.             if ($status) {
  532.                 return $this->doToggleRegistration($person$calendarEvent$responseCode$create,
  533.                     RegistrationType::REGISTRATION_TYPE_LEARNER);
  534.             } else {
  535.                 /** @var Candidate|null $existingCandidate */
  536.                 $existingCandidate null;
  537.                 try {
  538.                     $existingCandidate $this->getExistingCandidate($person$calendarEvent$existsWithStatusIsNotNew);
  539.                     if ($existingCandidate) {
  540.                         $responseCode 200;
  541.                         return [];
  542.                     } else {
  543.                         $responseCode 404;
  544.                         return [];
  545.                     }
  546.                 } catch (\Throwable $exception) {
  547.                     if ($existsWithStatusIsNotNew) {
  548.                         $responseCode 200;
  549.                         return [];
  550.                     }
  551.                     $responseCode 400;
  552.                     return ['error' => $exception->getMessage()];
  553.                 }
  554.             }
  555.         }, false, []);
  556.     }
  557.     private function doToggleRegistration(?Person $person, ?CalendarEvent $calendarEvent,
  558.                                           &$responseCodebool $create null$registrationType null): array
  559.     {
  560.         if (!$person || !$calendarEvent) {
  561.             $responseCode 404;
  562.             return ['error' => 'Person or CalendarEvent not found'];
  563.         }
  564.         if ($calendarEvent->isIsCandidateReviewRequired()) {
  565.             throw new \Exception("Candidate review is required");
  566.         }
  567.         // Check if candidate already exists
  568.         /** @var Candidate|null $existingCandidate */
  569.         $existingCandidate null;
  570.         try {
  571.             $existingCandidate $this->getExistingCandidate($person$calendarEvent);
  572.         } catch (\Throwable $exception) {
  573.             $responseCode 400;
  574.             return ['error' => $exception->getMessage()];
  575.         }
  576.         if ($existingCandidate && ($create === null || !$create)) {
  577.             // Delete candidate
  578.             $this->candidateService->removeCandidate($existingCandidate);
  579.             $responseCode 200;
  580.             return ['action' => 'deleted''candidateId' => null];
  581.         } elseif ($create === null || $create) {
  582.             // Create new candidate
  583.             $params = [
  584.                 "person" => $person,
  585.                 "calendarEvent" => $calendarEvent,
  586.                 "status" => \App\Enum\Candidate\Status::STATUS_APPROVED,
  587.             ];
  588.             if ($registrationType) {
  589.                 $params['registrationType'] = $registrationType;
  590.             }
  591.             /** @var Candidate $candidate */
  592.             $candidate $this->candidateService->createOrGetDefault($params);
  593.             if (!$candidate->getAuthor() && $this->getUser()) {
  594.                 $candidate->setAuthor($this->getUser());
  595.             }
  596.             if (!$candidate->getCurator() && $this->userService->getStarCurator()) {
  597.                 $candidate->setCurator($this->userService->getStarCurator());
  598.             }
  599.             $this->em->flush();
  600.             $responseCode 200;
  601.             return ['action' => 'created''candidateId' => $candidate->getId()];
  602.         } else {
  603.             $responseCode 200;
  604.             return [];
  605.         }
  606.     }
  607.     private function getExistingCandidate($person$calendarEventbool &$existsWithStatusIsNotNew null): ?Candidate
  608.     {
  609.         $existingCandidates $this->candidateService->getDefault([
  610.             'person' => $person,
  611.             'calendarEvent' => $calendarEvent,
  612.         ]);
  613.         $existingCandidates array_filter($existingCandidates, function (Candidate $candidate) {
  614.             return $candidate->getStatus() != \App\Enum\Candidate\Status::STATUS_DELETED;
  615.         });
  616.         /** @var Candidate $existingCandidate */
  617.         $existingCandidate count($existingCandidates) > current($existingCandidates) : null;
  618.         $existsWithStatusIsNotNew $existingCandidate && $existingCandidate->getStatus() !== \App\Enum\Candidate\Status::STATUS_NEW;
  619.         if ($existsWithStatusIsNotNew && $calendarEvent->isIsCandidateReviewRequired()) {
  620.             throw new \Exception('Статус кандидата: ' $existingCandidate->getStatusText());
  621.         }
  622.         return $existingCandidate;
  623.     }
  624.     public function loadTelegramHistory(Request $requestPersonService $personService,
  625.                         PyTgUtils $pyTgUtils): JsonResponse
  626.     {
  627.         return $this->defaultApiHandler->handleApiRequest($request, function (&$responseCode$content) use (
  628.             $personService$pyTgUtils
  629.         ) {
  630.             $personId $content['person_id'] ?? null;
  631.             if (!$personId) {
  632.                 $responseCode 400;
  633.                 return ['error' => 'Missing person_id'];
  634.             }
  635.             /** @var Person $person */
  636.             $person $personService->getBaseService()->get($personId);
  637.             if (!$person) {
  638.                 $responseCode 404;
  639.                 return ['error' => 'Person not found'];
  640.             }
  641.             $telegramUserId $person->getTelegramUserId();
  642.             if (!$telegramUserId) {
  643.                 $responseCode 400;
  644.                 return ['error' => 'Telegram user ID not set'];
  645.             }
  646.             try {
  647.                 // Устанавливаем технический аккаунт администратора
  648.                 $pyTgUtils->setTechAdminAccount();
  649.                 // Создаем приватный чат
  650.                 $chat $pyTgUtils->pyTg->createPrivateChat($telegramUserId);
  651.                 $chatId $chat['id'];
  652.                 // Получаем последние 5 сообщений
  653.                 $messages $pyTgUtils->pyTg->getChatHistory($chatId10);
  654.                 $messages array_map(function (Message $message) {
  655.                     return $message->getData();
  656.                 }, $messages);
  657.                 // Обрабатываем сообщения
  658.                 $processedMessages = [];
  659.                 $debugInfo = [];
  660.                 foreach ($messages as $messageIndex => $message) {
  661.                     $processedMessage = [
  662.                         'id' => $message['id'] ?? null,
  663.                         'date' => $message['date'] ?? null,
  664.                         'is_outgoing' => $message['is_outgoing'] ?? false,
  665.                         'text' => '',
  666.                         'images' => [],
  667.                         'debug' => [] // Отладочная информация (будет удалена перед отправкой)
  668.                     ];
  669.                     $messageDebug = [
  670.                         'message_index' => $messageIndex,
  671.                         'message_id' => $message['id'] ?? null,
  672.                         'content_type' => $message['content']['@type'] ?? 'unknown',
  673.                     ];
  674.                     // Получаем текст сообщения
  675.                     if (isset($message['content'])) {
  676.                         if (isset($message['content']['text']) && isset($message['content']['text']['text'])) {
  677.                             $processedMessage['text'] = $message['content']['text']['text'];
  678.                         } elseif (isset($message['content']['caption']) && isset($message['content']['caption']['text'])) {
  679.                             $processedMessage['text'] = $message['content']['caption']['text'];
  680.                         }
  681.                         // Обрабатываем фото
  682.                         if (isset($message['content']['@type']) && $message['content']['@type'] === 'messagePhoto') {
  683.                             $messageDebug['has_photo'] = true;
  684.                             if (isset($message['content']['photo']['sizes'])) {
  685.                                 $sizes $message['content']['photo']['sizes'];
  686.                                 $messageDebug['sizes_count'] = count($sizes);
  687.                                 // Берем изображение среднего размера (для оптимизации)
  688.                                 // Если размеров меньше 3, берем последнее
  689.                                 $photoIndex count($sizes) > count($sizes) - count($sizes) - 1;
  690.                                 $selectedPhoto $sizes[$photoIndex];
  691.                                 $messageDebug['selected_photo_index'] = $photoIndex;
  692.                                 $messageDebug['selected_photo_type'] = $selectedPhoto['type'] ?? 'unknown';
  693.                                 $imageLoaded false;
  694.                                 $imagePath null;
  695.                                 // Проверяем, есть ли локальный путь к файлу
  696.                                 if (isset($selectedPhoto['photo']['local']['path']) &&
  697.                                     file_exists($selectedPhoto['photo']['local']['path'])) {
  698.                                     $imagePath $selectedPhoto['photo']['local']['path'];
  699.                                     $messageDebug['source'] = 'local_path';
  700.                                     $messageDebug['path'] = $imagePath;
  701.                                     $imageLoaded true;
  702.                                 } elseif (isset($selectedPhoto['photo']['id'])) {
  703.                                     // Если файл не загружен локально, загружаем через API
  704.                                     $messageDebug['source'] = 'api_download';
  705.                                     $messageDebug['file_id'] = $selectedPhoto['photo']['id'];
  706.                                     try {
  707.                                         // downloadFile возвращает base64 данные напрямую
  708.                                         $base64Data $pyTgUtils->pyTg->downloadFile($selectedPhoto['photo']['id']);
  709.                                         $messageDebug['download_response'] = [
  710.                                             'is_string' => is_string($base64Data),
  711.                                             'data_length' => is_string($base64Data) ? strlen($base64Data) : 0,
  712.                                         ];
  713.                                         if (is_string($base64Data) && !empty($base64Data)) {
  714.                                             // Данные уже в base64, используем их напрямую
  715.                                             $processedMessage['images'][] = "data:image/jpeg;base64," $base64Data;
  716.                                             $messageDebug['base64_length'] = strlen($base64Data);
  717.                                             $messageDebug['success'] = true;
  718.                                             $imageLoaded true// Помечаем, что изображение загружено
  719.                                         } else {
  720.                                             $messageDebug['error'] = 'Invalid base64 data received';
  721.                                         }
  722.                                     } catch (\Exception $e) {
  723.                                         $messageDebug['error'] = 'Download failed: ' $e->getMessage();
  724.                                     }
  725.                                 }
  726.                                 // Конвертируем изображение в base64 (только для локальных файлов)
  727.                                 if ($imageLoaded && $imagePath) {
  728.                                     try {
  729.                                         $fileSize filesize($imagePath);
  730.                                         $messageDebug['file_size'] = $fileSize;
  731.                                         // Ограничение размера файла (5 МБ)
  732.                                         if ($fileSize 1024 1024) {
  733.                                             $messageDebug['error'] = 'File too large: ' $fileSize ' bytes';
  734.                                         } else {
  735.                                             $imageData file_get_contents($imagePath);
  736.                                             if ($imageData !== false) {
  737.                                                 $base64 base64_encode($imageData);
  738.                                                 $mimeType mime_content_type($imagePath);
  739.                                                 $processedMessage['images'][] = "data:$mimeType;base64,$base64";
  740.                                                 $messageDebug['mime_type'] = $mimeType;
  741.                                                 $messageDebug['base64_length'] = strlen($base64);
  742.                                                 $messageDebug['success'] = true;
  743.                                             } else {
  744.                                                 $messageDebug['error'] = 'Failed to read file contents';
  745.                                             }
  746.                                         }
  747.                                     } catch (\Exception $e) {
  748.                                         $messageDebug['error'] = 'Base64 conversion failed: ' $e->getMessage();
  749.                                     }
  750.                                 }
  751.                             } else {
  752.                                 $messageDebug['error'] = 'No photo sizes found';
  753.                             }
  754.                         }
  755.                     }
  756.                     $debugInfo[] = $messageDebug;
  757.                     // Удаляем debug перед добавлением в результат
  758.                     unset($processedMessage['debug']);
  759.                     $processedMessages[] = $processedMessage;
  760.                 }
  761.                 // Сортируем сообщения по дате (от старых к новым)
  762.                 usort($processedMessages, function($a$b) {
  763.                     return ($a['date'] ?? 0) - ($b['date'] ?? 0);
  764.                 });
  765.                 $responseCode 200;
  766.                 return [
  767.                     'success' => true,
  768.                     'messages' => $processedMessages,
  769.                     'person_name' => $person->getName(),
  770.                     'debug' => $debugInfo// Отладочная информация
  771.                     'total_messages' => count($processedMessages),
  772.                     'messages_with_images' => count(array_filter($processedMessages, function($m) {
  773.                         return !empty($m['images']);
  774.                     }))
  775.                 ];
  776.             } catch (\Exception $e) {
  777.                 $responseCode 500;
  778.                 return ['error' => 'Failed to load Telegram history: ' $e->getMessage()];
  779.             }
  780.         }, true, ['person_id']);
  781.     }
  782. }