src/Controller/CandidateController.php line 157

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