src/Controller/CandidateController.php line 335

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Candidate;
  4. use App\Enum\CalendarEvent\Type;
  5. use App\Enum\Payment\Type as PaymentType;
  6. use App\Form\TipSettingsType;
  7. use App\Library\Utils\StringUtils;
  8. use App\Service\CalendarEvent\CalendarEventService;
  9. use App\Service\Image\ImageService;
  10. use App\Service\Person\PersonService;
  11. use App\Service\ReviewerSettingsService;
  12. use App\Service\TelegramCampaignParticipant\TelegramCampaignParticipantService;
  13. use App\Service\TipSettingsService;
  14. use App\Service\User\UserService;
  15. use App\Service\UserSettingsService;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use App\Entity\User;
  19. use App\Enum\Candidate\Status;
  20. use App\Form\CandidateType;
  21. use App\Service\PaymentService;
  22. use App\Library\PyTg\PyTgUtils;
  23. use App\Library\Utils\ApiHandler;
  24. use App\Service\Candidate\CandidateService;
  25. use Doctrine\ORM\EntityManagerInterface;
  26. use Symfony\Component\HttpFoundation\JsonResponse;
  27. use App\Service\RouteService;
  28. use App\Service\ServiceRetriever;
  29. use App\Service\FileService;
  30. use App\Service\TelegramCampaign\TelegramCampaignService;
  31. use Symfony\Component\HttpFoundation\RequestStack;
  32. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  33. use Symfony\Component\Routing\RouterInterface;
  34. use Symfony\Component\Security\Core\Security;
  35. use Symfony\Component\Validator\Validator\ValidatorInterface;
  36. use Symfony\Component\HttpFoundation\RedirectResponse;
  37. class CandidateController extends BaseAbstractController
  38. {
  39.     const CANDIDATE_IMAGES_SUB_PATH "/candidate/images";
  40.     /*
  41.     * @var PyTgUtils
  42.     */
  43.     private $pyTgUtils;
  44.     /**
  45.      * @var TelegramCampaignService
  46.      */
  47.     private $telegramCampaignService;
  48.     public function __construct(Security                    $securityRequestStack $requestStack,
  49.                                 ValidatorInterface          $validatorRouteService $routeService,
  50.                                 RouterInterface             $routerSessionInterface $sessionEntityManagerInterface $em,
  51.                                 FileService $fileService,
  52.                                 ServiceRetriever $serviceRetrieverPyTgUtils $pyTgUtils,
  53.                                 TelegramCampaignService $telegramCampaignService)
  54.     {
  55.         parent::__construct($security$requestStack$validator$routeService$router$session,
  56.             $em$fileService$serviceRetriever);
  57.         $this->pyTgUtils $pyTgUtils;
  58.         $this->telegramCampaignService $telegramCampaignService;
  59.     }
  60.     public function review(Request $requestPaymentService $paymentServiceCandidateService $candidateService,
  61.                            PyTgUtils $pyTgUtilsReviewerSettingsService $reviewerSettingsServiceUserService $userService,
  62.                            TipSettingsService $tipSettingsServiceUserSettingsService $settingsService): Response
  63.     {
  64.         $candidates $candidateService->getForReview();
  65.         return $this->render('admin/candidate/candidates_review.html.twig', [
  66.             'candidates' => $candidates,
  67.         ]);
  68.     }
  69.     public function approve(Request $requestCandidateService $candidateService): JsonResponse
  70.     {
  71.         return ApiHandler::handleApiRequest($request, function (&$responseCode$content) use ($candidateService) {
  72.             $candidateId $content["candidate_id"];
  73.             $recommendations $content["recommendations"];
  74.             /**
  75.              * @var Candidate
  76.              */
  77.             $candidate $candidateService->getBaseService()->get($candidateId);
  78.             if (!$candidate) {
  79.                 $responseCode 400;
  80.                 return null;
  81.             }
  82.             $candidateService->setStatusApproved($candidate$recommendations);
  83.             // try {
  84. //                $this->notifyCandidateReviewResult($candidate);
  85.             // } catch (\Throwable $e) {
  86.             //     //ignore notification errors
  87.             // }
  88.             $responseCode 200;
  89.             return null;
  90.         }, true, ["candidate_id""recommendations"]);
  91.     }
  92.     private function setPyTgData(User $user)
  93.     {
  94.         $this->pyTgUtils->pyTg->setPhone($user->getTelegramPhone());
  95.         $this->pyTgUtils->pyTg->setAccountName($user->getTelegramPhone());
  96.         $this->pyTgUtils->pyTg->setPythonTelegramPath($_ENV['PYTHON_TELEGRAM_PATH']);
  97.     }
  98.     private function notifyCandidateReviewResult(Candidate $candidate)
  99.     {
  100. //        $this->setPyTgData($candidate->getAuthor());
  101. //        $this->telegramCampaignService->addCandidateToCampaign($candidate);
  102.         // $result = $this->pyTgUtils->pyTg->sendMessage($candidate->getPerson()->getName(), $message);
  103.     }
  104.     public function decline(Request $requestCandidateService $candidateService): JsonResponse
  105.     {
  106.         return ApiHandler::handleApiRequest($request, function (&$responseCode$content) use ($candidateService) {
  107.             $candidateId $content["candidate_id"];
  108.             $recommendations $content["recommendations"];
  109.             $declineComment $content["decline_comment"];
  110.             /** @var Candidate $candidate */
  111.             $candidate $candidateService->getBaseService()->get($candidateId);
  112.             if (!$candidate) {
  113.                 $responseCode 400;
  114.                 return null;
  115.             }
  116.             $candidateService->setStatusDeclined($candidate$recommendations$declineComment);
  117.             // try {
  118. //                $this->notifyCandidateReviewResult($candidate);
  119.             // } catch (\Throwable $e) {
  120.             //     //ignore notification errors
  121.             // }
  122.             $responseCode 200;
  123.             return null;
  124.         }, true, ["candidate_id""recommendations""decline_comment"]);
  125.     }
  126.     public function list(Request $requestCandidateService $candidateService,
  127.                          CalendarEventService $calendarEventService): Response
  128.     {
  129.         $userSettings $this->getLoggedInUser() ? $this->getLoggedInUser()->getSettings() : null;
  130.         $start $userSettings $userSettings->getStartCalendarEventsDate() : null;
  131.         $end $userSettings $userSettings->getEndCalendarEventsDate() : null;
  132.         $items $candidateService->getList(["createdAt""DESC"], null,
  133.             null$start$end);
  134.         $calendarEvents $calendarEventService->getAll(["startDate""ASC"],
  135.             Type::getListTypes(), $start$end);
  136.         // return $this->redirectToRoute("login");
  137.         return $this->render('admin/candidate/candidates.html.twig', [
  138.             'items' => $items,
  139.             "calendarEvents" => $calendarEvents,
  140.         ]);
  141.     }
  142.     public function innerPayments(Request $requestCandidateService $candidateService,
  143.                                   CalendarEventService $calendarEventServicePaymentService $paymentService): Response
  144.     {
  145.         $items $candidateService->getList(["createdAt""DESC"],
  146.             nullnull);
  147.         // Оставляем только кандидатов, у которых есть цена мероприятия
  148.         $items array_filter($items, function (Candidate $candidate) use ($paymentService) {
  149.             if ($candidate->getStatus() !== Status::STATUS_APPROVED) {
  150.                 return false;
  151.             }
  152.             $ce $candidate->getCalendarEvent();
  153.             if (!$ce || !$ce->getPrice()) {
  154.                 return false;
  155.             }
  156.             $person $candidate->getPerson();
  157.             if (!$person) {
  158.                 return false;
  159.             }
  160.             // Если у человека есть платежи по этому мероприятию с type != inner — исключаем кандидата
  161.             $payments $paymentService->getDefault([
  162.                 'person' => $person,
  163.                 'calendarEvent' => $ce,
  164.             ]);
  165.             foreach ($payments as $p) {
  166.                 if ($p->getType() !== PaymentType::TYPE_INNER) {
  167.                     return false;
  168.                 }
  169.             }
  170.             return true;
  171.         });
  172.         $calendarEvents $calendarEventService->getAll(nullType::getListTypes());
  173.         $hasInnerMap = [];
  174.         foreach ($items as $it) {
  175.             $person $it->getPerson();
  176.             $ce $it->getCalendarEvent();
  177.             $hasInner false;
  178.             if ($person && $ce) {
  179.                 // Используем метод сервиса для проверки наличия полной оплаты у человека по мероприятию
  180.                 $hasInner $paymentService->hasPersonFullPay($person$ce);
  181.             }
  182.             $hasInnerMap[$it->getId()] = $hasInner;
  183.         }
  184.         return $this->render('admin/candidate/inner_payments.html.twig', [
  185.             'items' => $items,
  186.             'calendarEvents' => $calendarEvents,
  187.             'hasInnerMap' => $hasInnerMap,
  188.         ]);
  189.     }
  190.     public function toggleInnerPaymentAction(Request $requestCandidateService $candidateService,
  191.                                              PaymentService $paymentService): JsonResponse
  192.     {
  193.         return ApiHandler::handleApiRequest($request, function (&$responseCode$content) use ($candidateService$paymentService) {
  194.             $candidateId $content['candidate_id'] ?? null;
  195.             $actionType $content['actionType'] ?? null;
  196.             if (!$candidateId) {
  197.                 $responseCode 400;
  198.                 return ["message" => "Не указан идентификатор кандидата."];
  199.             }
  200.             if (!in_array($actionType, ['delete_payment''add_payment'], true)) {
  201.                 $responseCode 400;
  202.                 return ["message" => "Не указан или некорректен параметр actionType. Ожидается 'delete_payment' или 'add_payment'."];
  203.             }
  204.             /** @var Candidate|null $candidate */
  205.             $candidate $candidateService->getBaseService()->get($candidateId);
  206.             if (!$candidate) {
  207.                 $responseCode 404;
  208.                 return ["message" => "Кандидат не найден."];
  209.             }
  210.             // Проверка статуса
  211.             if ($candidate->getStatus() !== Status::STATUS_APPROVED) {
  212.                 $responseCode 400;
  213.                 return ["message" => "Кандидат не в статусе Принят. Операция невозможна."];
  214.             }
  215.             // Поиск существующей внутренней оплаты (только type = inner)
  216.             $existingPayments $paymentService->getDefault([
  217.                 'person' => $candidate->getPerson(),
  218.                 'calendarEvent' => $candidate->getCalendarEvent(),
  219.                 'type' => PaymentType::TYPE_INNER,
  220.             ]);
  221.             if ($actionType === 'delete_payment') {
  222.                 if ($existingPayments) {
  223.                     try {
  224.                         foreach ($existingPayments as $p) {
  225.                             $paymentService->remove($p);
  226.                         }
  227.                         $responseCode 200;
  228.                         return ["success" => true"action" => "deleted""message" => "Внутренние оплаты удалены"];
  229.                     } catch (\Throwable $e) {
  230.                         $responseCode 500;
  231.                         return ["message" => "Ошибка при удалении платежей: " $e->getMessage()];
  232.                     }
  233.                 } else {
  234.                     $responseCode 200;
  235.                     return ["success" => true"action" => "deleted""message" => "Внутренние оплаты отсутствуют"];
  236.                 }
  237.             }
  238.             // actionType === 'add_payment'
  239.             try {
  240.                 // Если у человека уже есть полная оплата по этому мероприятию — не создаём внутреннюю оплату
  241.                 if ($paymentService->hasPersonFullPay($candidate->getPerson(), $candidate->getCalendarEvent())) {
  242.                     $responseCode 400;
  243.                     return ["message" => "У человека уже есть полная оплата"];
  244.                 }
  245.                 $paymentClass '\\App\\Entity\\Payment';
  246.                 /** @var \App\Entity\Payment $payment */
  247.                 $payment = new $paymentClass();
  248.                 $payment->setAmount($candidate->getCalendarEvent()->getPrice());
  249.                 $payment->setComment('Внутренняя оплата');
  250.                 $payment->setCalendarEvent($candidate->getCalendarEvent());
  251.                 $payment->setPerson($candidate->getPerson());
  252.                 $payment->setCreatedBy($this->getLoggedInUser());
  253.                 $payment->setDateTime(new \DateTime());
  254.                 $payment->setType(PaymentType::TYPE_INNER);
  255.                 $this->em->persist($payment);
  256.                 $this->em->flush();
  257.                 $responseCode 200;
  258.                 return ["success" => true"action" => "created""message" => "Внутренняя оплата создана"];
  259.             } catch (\Throwable $e) {
  260.                 $responseCode 500;
  261.                 return ["message" => "Ошибка при создании платежа: " $e->getMessage()];
  262.             }
  263.         }, true, ['candidate_id''actionType']);
  264.     }
  265.     public function toggleAccessByReceiptAjax(Request $requestCandidateService $candidateService): JsonResponse
  266.     {
  267.         return ApiHandler::handleApiRequest($request, function (&$responseCode$content) use ($candidateService) {
  268.             $candidateId $content['candidate_id'] ?? null;
  269.             if (!$candidateId) {
  270.                 $responseCode 400;
  271.                 return ["message" => "Не указан идентификатор кандидата."];
  272.             }
  273.             /** @var Candidate|null $candidate */
  274.             $candidate $candidateService->getBaseService()->get($candidateId);
  275.             if (!$candidate) {
  276.                 $responseCode 404;
  277.                 return ["message" => "Кандидат не найден."];
  278.             }
  279.             if ($candidate->getStatus() !== Status::STATUS_APPROVED) {
  280.                 $responseCode 400;
  281.                 return ["message" => "Кандидат не в статусе Принят. Операция невозможна."];
  282.             }
  283.             $newValue = !$candidate->isAccessByReceipt();
  284.             $candidate->setAccessByReceipt($newValue);
  285.             $this->em->flush();
  286.             $responseCode 200;
  287.             return ["success" => true"accessByReceipt" => $newValue"message" => $newValue "Доступ по квитанции включён." "Доступ по квитанции отключён."];
  288.         }, true, ['candidate_id']);
  289.     }
  290.     public function edit(Request $requestCandidateService $candidateServiceEntityManagerInterface $em,
  291.                          TelegramCampaignParticipantService $telegramCampaignParticipantServicePersonService $personService): Response
  292.     {
  293.         $id $request->get('id');
  294.         $isNew = !$id;
  295.         /**
  296.          * @var Candidate|null $item
  297.          */
  298.         $item $id $candidateService->getBaseService()->get($id) : null;
  299.         if (!$item) {
  300.             $item = (new Candidate())
  301.                 ->setStatus(Status::STATUS_DRAFT)
  302.             ;
  303.         }
  304.         $oldPerson $item->getPerson();
  305.         $form $this->createForm(CandidateType::class, $item);
  306.         $form->handleRequest($request);
  307.         if ($form->isSubmitted() && $form->isValid()) {
  308.             $hasErrors false;
  309.             if (!$isNew && $item->getStatus() === Status::STATUS_DRAFT) {
  310.                 $this->addFlash('errors''Нельзя установить статус черновика. Выберите другой статус.');
  311.                 $hasErrors true;
  312.             }
  313.             if ($candidateService->existsSame($item)) {
  314.                 $this->addFlash('errors''Кандидат с таким именем уже существует для данного мероприятия.');
  315.                 $hasErrors true;
  316.             }
  317.             if (!$hasErrors && $item->isStatusDraft()) {
  318.                 $confirmData $candidateService->confirmCandidate($item);
  319.                 if ($confirmData['result']) {
  320.                     if ($item->getImage()) {
  321.                         $personService->setPersonImage($item->getPerson(), $item->getImage());
  322.                     }
  323.                 } else {
  324.                     $hasErrors true;
  325.                     $needFieldsText array_reduce($confirmData['needFields'], function($carry$item) {
  326.                         return $carry . ($carry ", " "") . $item['text'];
  327.                     }, "");
  328.                     $needFieldsText StringUtils::addDot($needFieldsText);
  329.                     $confirmErrMsg "Невозможно создать"
  330.                         " анкету. Не все данные введены или не соответствуют требованиям: $needFieldsText";
  331.                     $this->addFlash('errors'$confirmErrMsg);
  332.                 }
  333.             }
  334.             if (!$hasErrors) {
  335.                 if ($oldPerson && $oldPerson->getId() != $item->getPerson()->getId()) {
  336.                     $this->addFlash('success''Имя кандидата "'
  337.                         $oldPerson->getName() . '" изменено на "' $item->getPerson()->getName() . '".');
  338.                 }
  339.                 if ($oldPerson) {
  340.                     $removedFromCampaigns $telegramCampaignParticipantService
  341.                         ->removeAllReviewCampaignsParticipantsWithCandidate($item$oldPerson);
  342.                     if ($this->isUserAdmin()) {
  343.                         foreach ($removedFromCampaigns as $campaign) {
  344.                             $this->addFlash('success''Кандидат исключён из рассылки "' $campaign->getTitle() . "\"");
  345.                         }
  346.                         if ($removedFromCampaigns) {
  347.                             $this->addFlash('success''Кандидат будет добавлен в необходиые рассылки при необходимости повторно.');
  348.                         }
  349.                     }
  350.                 }
  351.                 if (!$item->getAuthor()) {
  352.                     $item->setAuthor($this->getLoggedInUser());
  353.                 }
  354.                 $item->setUpdatedAt(new \DateTime());
  355.                 // Обработка возможной загрузки/удаления изображения из формы
  356.                 $this->setImage($form$item$request->get($form->getName()), 'image',
  357.                     self::CANDIDATE_IMAGES_SUB_PATH);
  358.                 $em->persist($item);
  359.                 $em->flush();
  360.                 return $this->redirectToRoute('moderator_candidates');
  361.             }
  362.         } else {
  363.             foreach ($form->getErrors(true) as $error) {
  364.                 $this->addFlash('errors'$error->getMessage());
  365.             }
  366.         }
  367.         return $this->render('admin/candidate/candidate_edit.html.twig', [
  368.             'form' => $form->createView(),
  369.             'item' => $item,
  370.         ]);
  371.     }
  372.     public function delete(Request $requestCandidateService $candidateService,
  373.                            TelegramCampaignParticipantService $campaignParticipantServiceImageService $imageService): RedirectResponse
  374.     {
  375.         $id $request->get('id');
  376.         if (!$id) {
  377.             $this->addFlash('errors''Не указан идентификатор кандидата.');
  378.             return $this->redirectToRoute('moderator_candidates');
  379.         }
  380.         /** @var Candidate|null $candidate */
  381.         $candidate $candidateService->getBaseService()->get($id);
  382.         if (!$candidate) {
  383.             $this->addFlash('errors''Кандидат не найден.');
  384.             return $this->redirectToRoute('moderator_candidates');
  385.         }
  386.         try {
  387.             $campaignParticipantService->removeAllParticipantsWithCandidate($candidate);
  388.         } catch (\Throwable $e) {
  389.             $this->addFlash('errors''Ошибка при удалении связанных участников рассылок. ' $e->getMessage() . '.');
  390.             return $this->redirectToRoute('moderator_candidates');
  391.         }
  392. //        $candidate->setImage(null);
  393. //        $this->em->flush();
  394.         try {
  395. //            $this->em->remove($candidate);
  396.             $candidate->setStatus(Status::STATUS_DELETED);
  397.             $this->em->flush();
  398.             $this->addFlash('success''Кандидат ' $candidate->getPerson()->getName() . ' удалён.');
  399.         } catch (\Throwable $e) {
  400.             $this->addFlash('errors''Ошибка при удалении кандидата. ' $e->getMessage() . '.');
  401.         }
  402.         return $this->redirectToRoute('moderator_candidates');
  403.     }
  404. }