src/Service/Candidate/CandidateService.php line 271

Open in your IDE?
  1. <?php
  2. namespace App\Service\Candidate;
  3. use App\Entity\CalendarEvent;
  4. use App\Entity\Candidate;
  5. use App\Entity\Image;
  6. use App\Entity\Invoice;
  7. use App\Entity\Person;
  8. use App\Entity\Student;
  9. use App\Entity\User;
  10. use App\Enum\CalendarEvent\Type;
  11. use App\Enum\Candidate\Status;
  12. use App\Library\Utils\Other\Other;
  13. use App\Service\BaseEntityService;
  14. use App\Service\CalendarEvent\CalendarEventService;
  15. use App\Service\PaymentService;
  16. use App\Service\Person\PersonService;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. class CandidateService extends BaseEntityService
  19. {
  20.     /**
  21.      * @var PaymentService
  22.      */
  23.     private $paymentService;
  24.     /**
  25.      * @var CalendarEventService
  26.      */
  27.     private $calendarEventService;
  28.     public function __construct(EntityManagerInterface $emPaymentService $paymentService,
  29.         CalendarEventService $calendarEventService)
  30.     {
  31.         parent::__construct($em);
  32.         $this->initialize(Candidate::class);
  33.         $this->paymentService $paymentService;
  34.         $this->calendarEventService $calendarEventService;
  35.     }
  36.     /**
  37.      * @return Candidate[]
  38.      */
  39.     public function getForReview(): array
  40.     {
  41.         return $this->getDefault([
  42.             "status" => Status::STATUS_NEW,
  43.             "calendarEvent.isCandidateReviewRequired" => true,
  44.         ], nullnull, [CalendarEvent::class]);
  45.     }
  46.     /**
  47.      * @return Candidate[]
  48.      */
  49.     public function getList(array $sortBy nullCalendarEvent $calendarEvent null,
  50.         ?bool $isCandidateReviewRequired true\DateTime $start null\DateTime $end null): array
  51.     {
  52.         $params = [
  53.             "status" => Status::getListStatuses(),
  54.         ];
  55.         if ($calendarEvent) {
  56.             $params["calendarEvent"] = $calendarEvent;
  57.         }
  58.         if ($isCandidateReviewRequired !== null) {
  59.             $params["calendarEvent.isCandidateReviewRequired"] = $isCandidateReviewRequired;
  60.         }
  61.         /** @var Candidate[] $items */
  62.         $items $this->getDefault($paramsnull,
  63.             null, [CalendarEvent::class], null$sortBy);
  64.         if ($start || $end) {
  65.             $items $this->filterCandidatesByCalendarEventEndDate($items$start$end);
  66.         }
  67.         return $items;
  68.     }
  69.     /**
  70.      * @param array|Candidate[] $items
  71.      * @return array|Candidate[]
  72.      */
  73.     public function filterCandidatesByCalendarEventEndDate(array     $items,
  74.                                                            \DateTime $start null\DateTime $end null): array
  75.     {
  76.         if ($start || $end) {
  77.             $items array_filter($items, function ($item) use ($start$end) {
  78.                 $ce $item->getCalendarEvent();
  79.                 if ($ce) {
  80.                     $ceEnd $ce->getEndDate();
  81.                     if ($start && $ceEnd $start) {
  82.                         return false;
  83.                     }
  84.                     if ($end && $ceEnd $end) {
  85.                         return false;
  86.                     }
  87.                     return true;
  88.                 }
  89.                 return false;
  90.             });
  91.         }
  92.         return $items;
  93.     }
  94.     /**
  95.      * @return array<Candidate>
  96.      */
  97.     public function getApprovedCandidates($sortByName falseCalendarEvent $calendarEvent null): array
  98.     {
  99.         $qb $this->em->getRepository(Candidate::class)->createQueryBuilder('c');
  100.         $qb->where('c.status = :status')
  101.             ->setParameter('status'Status::STATUS_APPROVED)
  102.             ->join('c.person''p');
  103.         if ($calendarEvent) {
  104.             $qb->andWhere('c.calendarEvent = :calendarEvent')
  105.                 ->setParameter('calendarEvent'$calendarEvent);
  106.         }
  107.         if ($sortByName) {
  108.             $qb
  109.                 ->orderBy('p.lastName''ASC');
  110.         }
  111.         return $qb->getQuery()->getResult();
  112.     }
  113.     /**
  114.      * @param array $statuses
  115.      * @return Candidate[]
  116.      */
  117.     public function getByStatuses(array $statuses): array
  118.     {
  119.         return $this->getDefault([
  120.             "status" => $statuses,
  121.         ]);
  122.     }
  123.     /**
  124.      * @return Candidate[]
  125.      */
  126.     public function getApprovedAndDeclined(): array
  127.     {
  128.         return $this->getDefault([
  129.             "status" => [Status::STATUS_APPROVEDStatus::STATUS_DECLINED],
  130.         ]);
  131.     }
  132.     /**
  133.      * @param Candidate[] $candidates
  134.      * @param CalendarEvent $calendarEvent
  135.      * @return array{summaryAmount: float, candidate: Candidate, isPayed: bool, debtAmount: float, lastPayment: ?\App\Entity\Payment}[]
  136.      */
  137.     public function getCandidatesPaymentsData($candidates$calendarEvent)
  138.     {
  139.         $candidatePaymentsData = [];
  140.         $candidatePaymentsDataNotPayed = [];
  141.         $candidatePaymentsDataPartial = [];
  142.         $candidatePaymentsDataFull = [];
  143.         $candidatePaymentsDataByReceipt = [];
  144.         /**
  145.          * @var Candidate $candidate
  146.          */
  147.         foreach ($candidates as $candidate) {
  148.             $payments $this->paymentService->getDefault([
  149.                 "person" => $candidate->getPerson(),
  150.                 "calendarEvent" => $calendarEvent,
  151.             ]);
  152.             $amount array_reduce($payments, function($carry$item) {
  153.                 return $carry $item->getAmount();
  154.             }, 0);
  155.             $candidatePaymentData = [
  156.                 'summaryAmount' => $amount,
  157.                 'candidate' => $candidate,
  158.                 'isPayed' => $amount >= $candidate->getCalendarEvent()->getPrice(),
  159.                 'hasAnyPayments' => $amount,
  160.                 'debtAmount' => max(0$candidate->getCalendarEvent()->getPrice() - $amount),
  161.                 'lastPayment' => count($payments) ? $payments[count($payments) - 1] : null,
  162.             ];
  163.             $payStatus $amount >= $candidate->getCalendarEvent()->getPrice() ? "full"
  164.                 : (($candidate->isAccessByReceipt() ? "by_receipt" : ($amount == "not_payed" "partial")));
  165.             switch ($payStatus) {
  166.                 case "full":
  167.                     $candidatePaymentsDataFull[] = $candidatePaymentData;
  168.                     break;
  169.                 case "not_payed":
  170.                     $candidatePaymentsDataNotPayed[] = $candidatePaymentData;
  171.                     break;
  172.                 case "partial":
  173.                     $candidatePaymentsDataPartial[] = $candidatePaymentData;
  174.                     break;
  175.                 case "by_receipt":
  176.                     $candidatePaymentsDataByReceipt[] = $candidatePaymentData;
  177.                     break;
  178.                 default:
  179.                     throw new \Exception("Unknown: "$payStatus);
  180.             }
  181.         }
  182.         $candidatePaymentsData array_merge($candidatePaymentsDataFull,
  183.             $candidatePaymentsDataPartial$candidatePaymentsDataNotPayed$candidatePaymentsDataByReceipt);
  184.         return $candidatePaymentsData;
  185.     }
  186.     /**
  187.      * @return Candidate[]
  188.      */
  189.     public function getZoomListCandidates(CalendarEvent $calendarEvent): array
  190.     {
  191.         $candidates $this->getDefault([
  192.             "calendarEvent" => $calendarEvent,
  193.             "status" => Status::STATUS_APPROVED,
  194.         ]);
  195.         $candidatesPaymentsData $this->getCandidatesPaymentsData($candidates$calendarEvent);
  196.         $candidatesPaymentsData array_filter($candidatesPaymentsData, function ($data) {
  197.             return $data['isPayed'];
  198.         });
  199.         $candidates array_map(function ($data) {
  200.             return $data['candidate'];
  201.         }, $candidatesPaymentsData);
  202.         return $candidates;
  203.     }
  204.     public function createOrGetApproved(Person $personCalendarEvent $eventUser $author,
  205.         \DateTime $createdAt null): Candidate
  206.     {
  207.         $candidate $this->getFirst([
  208.             "person" => $person,
  209.             "calendarEvent" => $event,
  210.         ]);
  211.         if (!$candidate) {
  212.             $candidate = (new Candidate())
  213.                 ->setPerson($person)
  214.                 ->setCalendarEvent($event)
  215.                 ->setStatus(Status::STATUS_APPROVED)
  216.                 ->setAuthor($author)
  217.             ;
  218.             if ($createdAt) {
  219.                 $candidate->setCreatedAt($createdAt);
  220.             }
  221.             $this->em->persist($candidate);
  222.             $this->em->flush();
  223.         }
  224.         return $candidate;
  225.     }
  226.     public function createDraft(User $author): Candidate
  227.     {
  228.         $candidate = (new Candidate())
  229.             ->setAuthor($author)
  230.             ->setStatus(Status::STATUS_DRAFT)
  231.         ;
  232.         $this->em->persist($candidate);
  233.         $this->em->flush();
  234.         return $candidate;
  235.     }
  236.     public function getLastDraft(User $author): ?Candidate
  237.     {
  238.         return $this->getBaseService()->getLast([
  239.             "author" => $author,
  240.             "status" => Status::STATUS_DRAFT,
  241.         ]);
  242.     }
  243.     public function getLastNonDraftCandidate(User $author): ?Candidate
  244.     {
  245.         return $this->getBaseService()->getLast([
  246.             "author" => $author,
  247.             "status" => Status::getForCloneCandidateStatuses()
  248.         ]);
  249.     }
  250.     public function copyCandidateDataToDraft(Candidate $fromCandidateCandidate $draft): void
  251.     {
  252.         $draft->setPerson($fromCandidate->getPerson());
  253.         $draft->setComment($fromCandidate->getComment());
  254.         $draft->setAuthor($fromCandidate->getAuthor());
  255.         $draft->setCalendarEvent($fromCandidate->getCalendarEvent());
  256.         $draft->setReviewApproveComment($fromCandidate->getReviewApproveComment());
  257.         $draft->setCurator($fromCandidate->getCurator());
  258.         $draft->setStatus(Status::STATUS_DRAFT);
  259.         $this->em->flush();
  260.     }
  261.     public function existsSame(Candidate $candidate): bool
  262.     {
  263.         if ($candidate->getPerson() && $candidate->getCalendarEvent()) {
  264.             $sameCandidates $this->getDefault([
  265.                 "person" => $candidate->getPerson(),
  266.                 "calendarEvent" => $candidate->getCalendarEvent(),
  267.                 "status" => [Status::STATUS_NEWStatus::STATUS_APPROVED],
  268.             ]);
  269.             return count($sameCandidates) > 0;
  270.         }
  271.         return false;
  272.     }
  273.     /**
  274.      * @param Candidate $candidate
  275.      * @param $status
  276.      * @param bool $checkPhoto
  277.      * @return array{result: bool, needFields: array, candidate: Candidate}
  278.      */
  279.     public function confirmCandidate(Candidate $candidate$status Status::STATUS_NEWbool $checkPhoto true): array
  280.     {
  281.         $result = [
  282.             "result" => false,
  283.             "needFields" => [],
  284.             "candidate" => $candidate,
  285.         ];
  286.         $ce $candidate->getCalendarEvent();
  287.         $person $candidate->getPerson();
  288.         $student $person $person->getStudent() : null;
  289.         $isCeFinished $ce
  290.             && $ce->isFinished();
  291.         $isCeCandidateReviewRequired $ce
  292.             && $ce->isIsCandidateReviewRequired();
  293.         $isCalendarEventsParticipationsRequired $ce
  294.             && $ce->getRequireCalendarEventsParticipations()->count();
  295.         $isDeniedCalendarEventParticipationCheckRequired $ce
  296.             && $ce->getDenyCalendarEventsParticipations()->count();
  297.         $isRegistrationOpen $ce
  298.             && $ce->isRegistrationOpen();
  299.         $isStatusApproved $status == Status::STATUS_APPROVED;
  300.         $isCalendarEventTypeRequiresApproveReviewComment $ce
  301.             && Type::isApproveReviewCommentRequireType($ce->getType());
  302.         /** @var CalendarEvent[] $missingRequiredCandidateCalendarEvents */
  303.         $missingRequiredCandidateCalendarEvents null;
  304.         /** @var CalendarEvent[] $deniedCalendarEventsParticipations */
  305.         $deniedCalendarEventsParticipations null;
  306.         $requiredKeys = ['person''calendarEvent'"comment""curator""levelCompliance",
  307.             "studentStatusCompliance"];
  308.         if ($person && (!$person->getStudent()
  309.                 || $person->getStudent()->getLevel() <= 1)) { //todo change this hardcode. Добавить 1-й так же без комментария
  310.             $requiredKeys array_filter($requiredKeys, function ($key) {
  311.                 return $key != "comment";
  312.             });
  313.         }
  314.         if ((!$isRegistrationOpen || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))
  315.             && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  316.             $requiredKeys[] = 'reviewApproveComment';
  317.         }
  318.         if (!$isRegistrationOpen && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  319.             $requiredKeys[] = 'statusApprovedForClosedCalendarEvent';
  320.         }
  321.         if ($checkPhoto && $ce && $ce->isIsCandidateReviewRequired()
  322.             && (!$student || !$student->isCandidateReviewIsNotRequired())) {
  323.             $requiredKeys[] = 'image';
  324.         }
  325.         if ($isCalendarEventsParticipationsRequired) {
  326.             $requiredKeys[] = 'calendarEventsParticipations';
  327.             if ($ce && $person) {
  328.                 $missingRequiredCandidateCalendarEvents =
  329.                     $this->calendarEventService->getMissingRequiredCandidateCalendarEvents(
  330.                         $person$ce
  331.                     );
  332.             }
  333.         }
  334.         if ($isDeniedCalendarEventParticipationCheckRequired) {
  335.             $requiredKeys[] = 'deniedCalendarEventsParticipations';
  336.             if ($ce && $person) {
  337.                 $deniedCalendarEventsParticipations =
  338.                     $this->calendarEventService->getDeniedCalendarEventsForPerson(
  339.                         $person$ce
  340.                     );
  341.             }
  342.         }
  343.         $isFieldNotRequired = [
  344.             "person" => !!$person,
  345.             "calendarEvent" => $ce true false,
  346.             "image" => !$checkPhoto || $status == Status::STATUS_APPROVED
  347.                 || ((($isCeFinished || $isCeCandidateReviewRequired)) && $candidate->getImage())
  348.                 || ($student && $student->isCandidateReviewIsNotRequired()) ? true false,
  349.             "reviewApproveComment" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS || !(!$candidate->getReviewApproveComment()
  350.                 && ($ce
  351.                     && (!$ce->isRegistrationOpen()
  352.                     || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))))
  353.                     || ($ce && !$ce->isIsCandidateReviewRequired())
  354.             ,
  355.             "statusApprovedForClosedCalendarEvent" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS
  356.                 || !$ce || ($student && $student->isCandidateReviewIsNotRequired())
  357.                 || $ce->isRegistrationOpen()
  358.                 || $status == Status::STATUS_APPROVED,
  359.             "comment" => !!$candidate->getComment() ||
  360.                 ($student && $student->isCandidateReviewIsNotRequired())
  361.                 || ($ce && !$ce->isIsCandidateReviewRequired())
  362.             ,
  363.             "curator" => !!$candidate->getCurator(),
  364.             "calendarEventsParticipations" => !$isCalendarEventsParticipationsRequired
  365.                 || ($missingRequiredCandidateCalendarEvents !== null
  366.                     && count($missingRequiredCandidateCalendarEvents) === 0),
  367.             "deniedCalendarEventsParticipations" => !$isDeniedCalendarEventParticipationCheckRequired
  368.                 || ($deniedCalendarEventsParticipations !== null
  369.                     && count($deniedCalendarEventsParticipations) === 0),
  370.             "levelCompliance" => !$ce || $ce->getFromLevel() === null || !$person
  371.                 || ($student && $student->getLevel() >= $ce->getFromLevel()
  372.                 && ($ce->getToLevel() === null || $student->getLevel() <= $ce->getToLevel())),
  373.             "studentStatusCompliance" => !$ce || $ce->getRequireStudentStatus() === null || !$person
  374.                 || ($person->getVirtualStudentStatus() == $ce->getRequireStudentStatus()),
  375.         ];
  376.         $fieldTexts = [
  377.             "person" => "ФИО",
  378.             "calendarEvent" => "мероприятие",
  379.             "image" => "фото",
  380.             "reviewApproveComment" => "комментарий согласования о пройденном отборе",
  381.             "statusApprovedForClosedCalendarEvent" =>
  382.                 "статус должен быть \"" Status::getText(Status::STATUS_APPROVED)
  383.                 . "\" для закрытой регистрации на мероприятие",
  384.             "comment" => "комментарий",
  385.             "curator" => "куратор",
  386.             "calendarEventsParticipations" => "отсуствует участие в обязательных мероприятиях: "
  387.                 implode(", "array_map(function ($e) {
  388.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  389.                 }, $missingRequiredCandidateCalendarEvents ?? [])),
  390.             "deniedCalendarEventsParticipations" => "есть участие в исключенных мероприятиях: "
  391.                 implode(", "array_map(function ($e) {
  392.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  393.                 }, $deniedCalendarEventsParticipations ?? [])),
  394.             "levelCompliance" => ($ce "несоответствие уровня участника: "
  395.                .  'необходим ' $ce->getFromLevel() . " ур." ""),
  396.             "studentStatusCompliance" => ($ce "несоответствие статуса участника: "
  397.                 .  'необходим ' $ce->getRequireStudentStatusText() . " ур." ""),
  398.         ];
  399.         foreach ($isFieldNotRequired as $fieldName => $isSet) {
  400.             if (!$isSet) {
  401.                 $result['needFields'][] = [
  402.                     "field" => $fieldName,
  403.                     "text" => $fieldTexts[$fieldName],
  404.                 ];
  405.             }
  406.         }
  407.         $allRequiredFieldsSet true;
  408.         foreach ($requiredKeys as $key) {
  409.             if (!$isFieldNotRequired[$key]) {
  410.                 $allRequiredFieldsSet false;
  411.                 break;
  412.             }
  413.         }
  414.         if ($allRequiredFieldsSet
  415. //                && $candidate->getCurator()
  416.             ) {
  417.             if ($ce &&
  418.                 (!$ce->isIsCandidateReviewRequired() || $ce->isFinished()
  419.                 || ($student && $student->isCandidateReviewIsNotRequired()))) {
  420.                 $this->setStatusApproved($candidate);
  421.             } else {
  422.                 $this->setStatus($candidate$status);
  423.             }
  424.             $result['result'] = true;
  425.         }
  426.         return $result;
  427.     }
  428.     public function setStatusNew(Candidate $candidate)
  429.     {
  430.         $candidate->setStatus(Status::STATUS_NEW);
  431.         $candidate->setCreatedAt(new \DateTime());
  432.         $candidate->setUpdatedAt(new \DateTime());
  433.         $this->em->flush();
  434.     }
  435.     /**
  436.      * @param Person $person
  437.      * @param CalendarEvent|CalendarEvent[] $calendarEvent
  438.      * @return Candidate[]
  439.      */
  440.     public function getApprovedByPersonAndCalendarEvent(Person $person$calendarEvent): array
  441.     {
  442.         return $this->getDefault([
  443.             "person" => $person,
  444.             "calendarEvent" => $calendarEvent,
  445.             "status" => [Status::STATUS_APPROVED],
  446.         ]);
  447.     }
  448.     public function getCandidatesForStudentLevelUpdate(): array
  449.     {
  450.         return [];
  451.     }
  452.     /**
  453.      * @param array|Candidate[] $candidates
  454.      * @return array
  455.      */
  456.     public function getCandidatesForPayments(array $candidates null,
  457.              \DateTime $start null\DateTime $end null): array
  458.     {
  459.         if (!$candidates) {
  460.             $candidates $this->getApprovedCandidates(true);
  461.             if ($start || $end) {
  462.                 $candidates $this->filterCandidatesByCalendarEventEndDate($candidates$start$end);
  463.             }
  464.         }
  465.         $candidates2 = [];
  466.         foreach ($candidates as $candidate) {
  467.             $candidates2[] = [
  468.                 "candidate" => $candidate,
  469.                 "text" => PersonService::getFirstLastNameWithOld($candidate->getPerson())
  470.                     . ($candidate->getPerson()->getLastName2() ? " [1]" ""),
  471.                 "isLastName2" => false,
  472.             ];
  473.             if ($candidate->getPerson()->getLastName2()) {
  474.                 $candidates2[] = [
  475.                     "candidate" => $candidate,
  476.                     "text" => PersonService::getFirstLastNameWithOld($candidate->getPerson(), true) . " [2]",
  477.                     "isLastName2" => true,
  478.                 ];
  479.             }
  480.         }
  481.         usort($candidates2, function ($a$b) {
  482.             return strcoll($a['text'], $b['text']);
  483.         });
  484.         return $candidates2;
  485.     }
  486.     public function toArray(array $candidates): array
  487.     {
  488.         $objToArrHandler = function ($currentObject$currentObjectData) use (&$objToArrHandler) {
  489.             if (is_object($currentObject)) {
  490.                 switch (str_replace("Proxies\__CG__\\"""get_class($currentObject))) {
  491.                     case Candidate::class:
  492.                         $currentObjectData['person'] = Other::objectToArray($currentObject->getPerson(),
  493.                             ["id""name""phone""email""city"], $objToArrHandler);
  494.                         $currentObjectData['calendarEvent'] = Other::objectToArray($currentObject->getCalendarEvent(),
  495.                             ["id""name""price"], $objToArrHandler);
  496.                         break;
  497.                     case Person::class:
  498.                         $currentObjectData['city'] = Other::objectToArray($currentObject->getCity(),
  499.                             ["id""name"]);
  500.                         break;
  501.                     case CalendarEvent::class:
  502.                         // no additional fields
  503.                         break;
  504.                     default:
  505.                         throw new \Exception("Unknown: " get_class($currentObject));
  506.                 }
  507.             }
  508.             return $currentObjectData;
  509.         };
  510.         $candidatesData Other::objectToArray($candidates, ["id""status""person""calendarEvent"],
  511.             $objToArrHandler);
  512.         return $candidatesData;
  513.     }
  514.     public function setStatusApproved(Candidate $candidatestring $recommendations null)
  515.     {
  516.         $candidate->setStatus(Status::STATUS_APPROVED);
  517.         $candidate->setRecommendations($recommendations);
  518.         $candidate->setDeclineComment(null);
  519.         $candidate->setUpdatedAt(new \DateTime());
  520.         $this->em->flush();
  521.     }
  522.     public function setStatus(Candidate $candidatestring $status null)
  523.     {
  524.         $candidate->setStatus($status);
  525.         $candidate->setUpdatedAt(new \DateTime());
  526.         $this->em->flush();
  527.     }
  528.     public function setStatusDeclined(Candidate $candidate$recommendations$declineComment)
  529.     {
  530.         $candidate->setStatus(Status::STATUS_DECLINED);
  531.         $candidate->setRecommendations($recommendations);
  532.         $candidate->setDeclineComment($declineComment);
  533.         $candidate->setUpdatedAt(new \DateTime());
  534.         $this->em->flush();
  535.     }
  536.     /**
  537.      * @return Candidate[]
  538.      */
  539.     public function getAll(): array
  540.     {
  541.         return $this->getBaseService()->getAll();
  542.     }
  543.     /**
  544.      * Вернуть количество кандидатов сгруппированное по студентам и по мероприятиям.
  545.      * Возвращаемая структура: [
  546.      *   studentId => [ calendarEventId|string('null') => count, ... ],
  547.      *   ...
  548.      * ]
  549.      *
  550.      * @return array<int, array<string,int>>
  551.      */
  552.     public function getCandidatesGroupedByStudentsAndCalendarEvents(): array
  553.     {
  554.         $qb $this->em->createQueryBuilder();
  555.         // left join candidates through student->person relationship (c.person = p)
  556.         // выбираем минимальный candidate id (если несколько) чтобы иметь единичный candidate_id на пару студент-событие
  557.         $qb->select('s.id AS student_id, ce.id AS calendar_event_id, MIN(c.id) AS candidate_id')
  558.             ->from(Student::class, 's')
  559.             ->leftJoin('s.person''p')
  560.             // join Candidate entity explicitly with ON (WITH) c.person = p to keep students without candidates
  561.             ->leftJoin('\App\\Entity\\Candidate''c''WITH''c.person = p')
  562.             ->leftJoin('c.calendarEvent''ce')
  563. //            ->where($qb->expr()->in('s.status', ':statuses'))
  564. //            ->setParameter('statuses', \App\Enum\Student\Status::getWorkingStatuses())
  565.             ->groupBy('s.id, ce.id');
  566.         // Используем getScalarResult для предсказуемых ключей в результирующих строках
  567.         $results $qb->getQuery()->getScalarResult();
  568.         $groupedData = [];
  569.         foreach ($results as $row) {
  570.             $studentId = (int)$row['student_id'];
  571.             $calendarEventId $row['calendar_event_id'] !== null ? (int)$row['calendar_event_id'] : null;
  572.             $candidateId $row['candidate_id'] !== null ? (int)$row['candidate_id'] : null;
  573.             if (!isset($groupedData[$studentId])) {
  574.                 $groupedData[$studentId] = [];
  575.             }
  576.             if ($calendarEventId === null) {
  577.                 continue;
  578.             }
  579.             $calendarKey $calendarEventId === null 'null' : (string)$calendarEventId;
  580.             $groupedData[$studentId][$calendarKey] = $candidateId;
  581.         }
  582.         return $groupedData;
  583.     }
  584.     /**
  585.      * @param Candidate $candidate
  586.      * @param $status
  587.      * @param bool $checkPhoto
  588.      * @return array{result: bool, needFields: array, candidate: Candidate}
  589.      */
  590.     public function isPersonCorrespondToCalendarEvent(Person $person,
  591.                                                       CalendarEvent $calendarEvent,
  592.                                                       Image $image null,
  593.         $status Status::STATUS_NEWbool $checkPhoto true): array
  594.     {
  595.         $result = [
  596.             "result" => false,
  597.             "needFields" => [],
  598.             "candidate" => $candidate,
  599.         ];
  600.         $ce $candidate->getCalendarEvent();
  601.         $person $candidate->getPerson();
  602.         $student $person $person->getStudent() : null;
  603.         $isCeFinished $ce
  604.             && $ce->isFinished();
  605.         $isCeCandidateReviewRequired $ce
  606.             && $ce->isIsCandidateReviewRequired();
  607.         $isCalendarEventsParticipationsRequired $ce
  608.             && $ce->getRequireCalendarEventsParticipations()->count();
  609.         $isDeniedCalendarEventParticipationCheckRequired $ce
  610.             && $ce->getDenyCalendarEventsParticipations()->count();
  611.         $isRegistrationOpen $ce
  612.             && $ce->isRegistrationOpen();
  613.         $isStatusApproved $status == Status::STATUS_APPROVED;
  614.         $isCalendarEventTypeRequiresApproveReviewComment $ce
  615.             && Type::isApproveReviewCommentRequireType($ce->getType());
  616.         /** @var CalendarEvent[] $missingRequiredCandidateCalendarEvents */
  617.         $missingRequiredCandidateCalendarEvents null;
  618.         /** @var CalendarEvent[] $deniedCalendarEventsParticipations */
  619.         $deniedCalendarEventsParticipations null;
  620.         $requiredKeys = ['person''calendarEvent'"comment""curator""levelCompliance",
  621.             "studentStatusCompliance"];
  622.         if ($person && (!$person->getStudent()
  623.                 || $person->getStudent()->getLevel() <= 1)) { //todo change this hardcode. Добавить 1-й так же без комментария
  624.             $requiredKeys array_filter($requiredKeys, function ($key) {
  625.                 return $key != "comment";
  626.             });
  627.         }
  628.         if ((!$isRegistrationOpen || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))
  629.             && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  630.             $requiredKeys[] = 'reviewApproveComment';
  631.         }
  632.         if (!$isRegistrationOpen && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  633.             $requiredKeys[] = 'statusApprovedForClosedCalendarEvent';
  634.         }
  635.         if ($checkPhoto && $ce && $ce->isIsCandidateReviewRequired()
  636.             && (!$student || !$student->isCandidateReviewIsNotRequired())) {
  637.             $requiredKeys[] = 'image';
  638.         }
  639.         if ($isCalendarEventsParticipationsRequired) {
  640.             $requiredKeys[] = 'calendarEventsParticipations';
  641.             if ($ce && $person) {
  642.                 $missingRequiredCandidateCalendarEvents =
  643.                     $this->calendarEventService->getMissingRequiredCandidateCalendarEvents(
  644.                         $person$ce
  645.                     );
  646.             }
  647.         }
  648.         if ($isDeniedCalendarEventParticipationCheckRequired) {
  649.             $requiredKeys[] = 'deniedCalendarEventsParticipations';
  650.             if ($ce && $person) {
  651.                 $deniedCalendarEventsParticipations =
  652.                     $this->calendarEventService->getDeniedCalendarEventsForPerson(
  653.                         $person$ce
  654.                     );
  655.             }
  656.         }
  657.         $isFieldNotRequired = [
  658.             "person" => !!$person,
  659.             "calendarEvent" => $ce true false,
  660.             "image" => !$checkPhoto || $status == Status::STATUS_APPROVED
  661.             || ((($isCeFinished || $isCeCandidateReviewRequired)) && $candidate->getImage())
  662.             || ($student && $student->isCandidateReviewIsNotRequired()) ? true false,
  663.             "reviewApproveComment" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS || !(!$candidate->getReviewApproveComment()
  664.                     && ($ce
  665.                         && (!$ce->isRegistrationOpen()
  666.                             || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))))
  667.                 || ($ce && !$ce->isIsCandidateReviewRequired())
  668.             ,
  669.             "statusApprovedForClosedCalendarEvent" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS
  670.                 || !$ce || ($student && $student->isCandidateReviewIsNotRequired())
  671.                 || $ce->isRegistrationOpen()
  672.                 || $status == Status::STATUS_APPROVED,
  673.             "comment" => !!$candidate->getComment() ||
  674.                 ($student && $student->isCandidateReviewIsNotRequired())
  675.                 || ($ce && !$ce->isIsCandidateReviewRequired())
  676.             ,
  677.             "curator" => !!$candidate->getCurator(),
  678.             "calendarEventsParticipations" => !$isCalendarEventsParticipationsRequired
  679.                 || ($missingRequiredCandidateCalendarEvents !== null
  680.                     && count($missingRequiredCandidateCalendarEvents) === 0),
  681.             "deniedCalendarEventsParticipations" => !$isDeniedCalendarEventParticipationCheckRequired
  682.                 || ($deniedCalendarEventsParticipations !== null
  683.                     && count($deniedCalendarEventsParticipations) === 0),
  684.             "levelCompliance" => !$ce || $ce->getFromLevel() === null || !$person
  685.                 || ($student && $student->getLevel() >= $ce->getFromLevel()
  686.                     && ($ce->getToLevel() === null || $student->getLevel() <= $ce->getToLevel())),
  687.             "studentStatusCompliance" => !$ce || $ce->getRequireStudentStatus() === null || !$person
  688.                 || ($person->getVirtualStudentStatus() == $ce->getRequireStudentStatus()),
  689.         ];
  690.         $fieldTexts = [
  691.             "person" => "ФИО",
  692.             "calendarEvent" => "мероприятие",
  693.             "image" => "фото",
  694.             "reviewApproveComment" => "комментарий согласования о пройденном отборе",
  695.             "statusApprovedForClosedCalendarEvent" =>
  696.                 "статус должен быть \"" Status::getText(Status::STATUS_APPROVED)
  697.                 . "\" для закрытой регистрации на мероприятие",
  698.             "comment" => "комментарий",
  699.             "curator" => "куратор",
  700.             "calendarEventsParticipations" => "отсуствует участие в обязательных мероприятиях: "
  701.                 implode(", "array_map(function ($e) {
  702.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  703.                 }, $missingRequiredCandidateCalendarEvents ?? [])),
  704.             "deniedCalendarEventsParticipations" => "есть участие в исключенных мероприятиях: "
  705.                 implode(", "array_map(function ($e) {
  706.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  707.                 }, $deniedCalendarEventsParticipations ?? [])),
  708.             "levelCompliance" => ($ce "несоответствие уровня участника: "
  709.                 .  'необходим ' $ce->getFromLevel() . " ур." ""),
  710.             "studentStatusCompliance" => ($ce "несоответствие статуса участника: "
  711.                 .  'необходим ' $ce->getRequireStudentStatusText() . " ур." ""),
  712.         ];
  713.         foreach ($isFieldNotRequired as $fieldName => $isSet) {
  714.             if (!$isSet) {
  715.                 $result['needFields'][] = [
  716.                     "field" => $fieldName,
  717.                     "text" => $fieldTexts[$fieldName],
  718.                 ];
  719.             }
  720.         }
  721.         $allRequiredFieldsSet true;
  722.         foreach ($requiredKeys as $key) {
  723.             if (!$isFieldNotRequired[$key]) {
  724.                 $allRequiredFieldsSet false;
  725.                 break;
  726.             }
  727.         }
  728.         if ($allRequiredFieldsSet
  729. //                && $candidate->getCurator()
  730.         ) {
  731.             if ($ce &&
  732.                 (!$ce->isIsCandidateReviewRequired() || $ce->isFinished()
  733.                     || ($student && $student->isCandidateReviewIsNotRequired()))) {
  734.                 $this->setStatusApproved($candidate);
  735.             } else {
  736.                 $this->setStatus($candidate$status);
  737.             }
  738.             $result['result'] = true;
  739.         }
  740.         return $result;
  741.     }
  742.     /**
  743.      * @param Candidate $candidate
  744.      * @param $status
  745.      * @param bool $checkPhoto
  746.      * @return array{result: bool, needFields: array, candidate: Candidate}
  747.      */
  748.     public function isCandidateCorrespondToCalendarEvent(Candidate $candidate,
  749.                                                          bool $checkPhoto true): array
  750.     {
  751.         $result = [
  752.             "result" => false,
  753.             "needFields" => [],
  754.             "candidate" => $candidate,
  755.         ];
  756.         $ce $candidate->getCalendarEvent();
  757.         $person $candidate->getPerson();
  758.         $student $person $person->getStudent() : null;
  759.         $isCeFinished $ce
  760.             && $ce->isFinished();
  761.         $isCeCandidateReviewRequired $ce
  762.             && $ce->isIsCandidateReviewRequired();
  763.         $isCalendarEventsParticipationsRequired $ce
  764.             && $ce->getRequireCalendarEventsParticipations()->count();
  765.         $isDeniedCalendarEventParticipationCheckRequired $ce
  766.             && $ce->getDenyCalendarEventsParticipations()->count();
  767.         $isRegistrationOpen $ce
  768.             && $ce->isRegistrationOpen();
  769.         $isStatusApproved $candidate->getStatus() == Status::STATUS_APPROVED;
  770.         $isCalendarEventTypeRequiresApproveReviewComment $ce
  771.             && Type::isApproveReviewCommentRequireType($ce->getType());
  772.         /** @var CalendarEvent[] $missingRequiredCandidateCalendarEvents */
  773.         $missingRequiredCandidateCalendarEvents null;
  774.         /** @var CalendarEvent[] $deniedCalendarEventsParticipations */
  775.         $deniedCalendarEventsParticipations null;
  776.         $requiredKeys = ['person''calendarEvent'"comment""curator""levelCompliance",
  777.             "studentStatusCompliance"];
  778.         if ($person && (!$person->getStudent()
  779.                 || $person->getStudent()->getLevel() <= 1)) { //todo change this hardcode. Добавить 1-й так же без комментария
  780.             $requiredKeys array_filter($requiredKeys, function ($key) {
  781.                 return $key != "comment";
  782.             });
  783.         }
  784.         if ((!$isRegistrationOpen || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))
  785.             && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  786.             $requiredKeys[] = 'reviewApproveComment';
  787.         }
  788.         if (!$isRegistrationOpen && !CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS) {
  789.             $requiredKeys[] = 'statusApprovedForClosedCalendarEvent';
  790.         }
  791.         if ($checkPhoto && $ce && $ce->isIsCandidateReviewRequired()
  792.             && (!$student || !$student->isCandidateReviewIsNotRequired())) {
  793.             $requiredKeys[] = 'image';
  794.         }
  795.         if ($isCalendarEventsParticipationsRequired) {
  796.             $requiredKeys[] = 'calendarEventsParticipations';
  797.             if ($ce && $person) {
  798.                 $missingRequiredCandidateCalendarEvents =
  799.                     $this->calendarEventService->getMissingRequiredCandidateCalendarEvents(
  800.                         $person$ce
  801.                     );
  802.             }
  803.         }
  804.         if ($isDeniedCalendarEventParticipationCheckRequired) {
  805.             $requiredKeys[] = 'deniedCalendarEventsParticipations';
  806.             if ($ce && $person) {
  807.                 $deniedCalendarEventsParticipations =
  808.                     $this->calendarEventService->getDeniedCalendarEventsForPerson(
  809.                         $person$ce
  810.                     );
  811.             }
  812.         }
  813.         $isFieldNotRequired = [
  814.             "person" => !!$person,
  815.             "calendarEvent" => $ce true false,
  816.             "image" => !$checkPhoto || $status == Status::STATUS_APPROVED
  817.             || ((($isCeFinished || $isCeCandidateReviewRequired)) && $candidate->getImage())
  818.             || ($student && $student->isCandidateReviewIsNotRequired()) ? true false,
  819.             "reviewApproveComment" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS || !(!$candidate->getReviewApproveComment()
  820.                     && ($ce
  821.                         && (!$ce->isRegistrationOpen()
  822.                             || ($isStatusApproved && $isCalendarEventTypeRequiresApproveReviewComment))))
  823.                 || ($ce && !$ce->isIsCandidateReviewRequired())
  824.             ,
  825.             "statusApprovedForClosedCalendarEvent" => CalendarEventService::FORCE_REGISTER_CLOSED_EVENTS
  826.                 || !$ce || ($student && $student->isCandidateReviewIsNotRequired())
  827.                 || $ce->isRegistrationOpen()
  828.                 || $status == Status::STATUS_APPROVED,
  829.             "comment" => !!$candidate->getComment() ||
  830.                 ($student && $student->isCandidateReviewIsNotRequired())
  831.                 || ($ce && !$ce->isIsCandidateReviewRequired())
  832.             ,
  833.             "curator" => !!$candidate->getCurator(),
  834.             "calendarEventsParticipations" => !$isCalendarEventsParticipationsRequired
  835.                 || ($missingRequiredCandidateCalendarEvents !== null
  836.                     && count($missingRequiredCandidateCalendarEvents) === 0),
  837.             "deniedCalendarEventsParticipations" => !$isDeniedCalendarEventParticipationCheckRequired
  838.                 || ($deniedCalendarEventsParticipations !== null
  839.                     && count($deniedCalendarEventsParticipations) === 0),
  840.             "levelCompliance" => !$ce || $ce->getFromLevel() === null || !$person
  841.                 || ($student && $student->getLevel() >= $ce->getFromLevel()
  842.                     && ($ce->getToLevel() === null || $student->getLevel() <= $ce->getToLevel())),
  843.             "studentStatusCompliance" => !$ce || $ce->getRequireStudentStatus() === null || !$person
  844.                 || ($person->getVirtualStudentStatus() == $ce->getRequireStudentStatus()),
  845.         ];
  846.         $fieldTexts = [
  847.             "person" => "ФИО",
  848.             "calendarEvent" => "мероприятие",
  849.             "image" => "фото",
  850.             "reviewApproveComment" => "комментарий согласования о пройденном отборе",
  851.             "statusApprovedForClosedCalendarEvent" =>
  852.                 "статус должен быть \"" Status::getText(Status::STATUS_APPROVED)
  853.                 . "\" для закрытой регистрации на мероприятие",
  854.             "comment" => "комментарий",
  855.             "curator" => "куратор",
  856.             "calendarEventsParticipations" => "отсуствует участие в обязательных мероприятиях: "
  857.                 implode(", "array_map(function ($e) {
  858.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  859.                 }, $missingRequiredCandidateCalendarEvents ?? [])),
  860.             "deniedCalendarEventsParticipations" => "есть участие в исключенных мероприятиях: "
  861.                 implode(", "array_map(function ($e) {
  862.                     return mb_lcfirst($e->getNameText(), 'utf-8') . " (" $e->getStartDate()->format("d.m.Y") . ")";
  863.                 }, $deniedCalendarEventsParticipations ?? [])),
  864.             "levelCompliance" => ($ce "несоответствие уровня участника: "
  865.                 .  'необходим ' $ce->getFromLevel() . " ур." ""),
  866.             "studentStatusCompliance" => ($ce "несоответствие статуса участника: "
  867.                 .  'необходим ' $ce->getRequireStudentStatusText() . " ур." ""),
  868.         ];
  869.         foreach ($isFieldNotRequired as $fieldName => $isSet) {
  870.             if (!$isSet) {
  871.                 $result['needFields'][] = [
  872.                     "field" => $fieldName,
  873.                     "text" => $fieldTexts[$fieldName],
  874.                 ];
  875.             }
  876.         }
  877.         $allRequiredFieldsSet true;
  878.         foreach ($requiredKeys as $key) {
  879.             if (!$isFieldNotRequired[$key]) {
  880.                 $allRequiredFieldsSet false;
  881.                 break;
  882.             }
  883.         }
  884.         if ($allRequiredFieldsSet
  885. //                && $candidate->getCurator()
  886.         ) {
  887.             if ($ce &&
  888.                 (!$ce->isIsCandidateReviewRequired() || $ce->isFinished()
  889.                     || ($student && $student->isCandidateReviewIsNotRequired()))) {
  890.                 $this->setStatusApproved($candidate);
  891.             } else {
  892.                 $this->setStatus($candidate$status);
  893.             }
  894.             $result['result'] = true;
  895.         }
  896.         return $result;
  897.     }
  898.     /**
  899.      * @param Person $person
  900.      * @param CalendarEvent $calendarEvent
  901.      * @param User $author
  902.      * @return array{result: bool, needFields: array, candidate: Candidate}
  903.      */
  904.     public function createApprovedCandidate(Person $personCalendarEvent $calendarEventUser $author): array
  905.     {
  906.         $candidate = (new Candidate())
  907.             ->setPerson($person)
  908.             ->setCalendarEvent($calendarEvent)
  909.             ->setStatus(Status::STATUS_DRAFT)
  910.             ->setAuthor($author);
  911.         if ($this->existsSame($candidate)) {
  912.             return [
  913.                 "result" => false,
  914.                 "needFields" => [],
  915.                 "candidate" => null,
  916.             ];
  917.         }
  918.         $result $this->confirmCandidate($candidateStatus::STATUS_APPROVEDfalse);
  919.         if ($result['result']) {
  920.             if ($candidate->getPerson()->getStudent() && $candidate->getPerson()->getStudent()->isCandidateReviewIsNotRequired()) {
  921.                 $candidate->setStatus(Status::STATUS_APPROVED);
  922.             }
  923.             $this->em->persist($candidate);
  924.             $this->em->flush();
  925.         }
  926.         return $result;
  927.     }
  928.     /**
  929.      * @param Person[] $persons
  930.      * @return array{int, Candidate|null}[]
  931.      */
  932.     public function getPersonsLastCandidateWithEarnLevelCalendarEvent(array $persons): array
  933.     {
  934.         $result = [];
  935.         foreach ($persons as $person) {
  936.             $qb $this->em->createQueryBuilder();
  937.             $qb->select('c')
  938.                 ->from(Candidate::class, 'c')
  939.                 ->join('c.calendarEvent''ce')
  940.                 ->where('c.person = :person')
  941.                 ->andWhere('ce.earnLevel IS NOT NULL')
  942.                 ->andWhere('ce.earnLevel >= 0')
  943.                 ->andWhere($qb->expr()->in('c.status'':statuses'))
  944.                 ->andWhere('ce.endDate <= :today')
  945.                 ->setParameter('today', (new \DateTime())->setTime(0,0,0))
  946.                 ->setParameter('statuses'Status::getForLastCandidateWithEarnLevelCalendarEventStatuses())
  947.                 ->setParameter('person'$person)
  948.                 ->orderBy('ce.startDate''DESC')
  949.                 ->setMaxResults(1);
  950.             $lastEvent $qb->getQuery()->getOneOrNullResult();
  951.             $result[$person->getId()] = $lastEvent;
  952.         }
  953.         return $result;
  954.     }
  955.     public function getPersonsLastCandidate(array $persons): array
  956.     {
  957.         $result = [];
  958.         foreach ($persons as $person) {
  959.             $qb $this->em->createQueryBuilder();
  960.             $qb->select('c')
  961.                 ->from(Candidate::class, 'c')
  962.                 ->where('c.person = :person')
  963.                 ->andWhere($qb->expr()->in('c.status'':statuses'))
  964.                 ->setParameter('statuses'Status::getForLastCandidateStatuses())
  965.                 ->setParameter('person'$person)
  966.                 ->orderBy('c.createdAt''DESC')
  967.                 ->setMaxResults(1);
  968.             $lastCandidate $qb->getQuery()->getOneOrNullResult();
  969.             $result[$person->getId()] = $lastCandidate;
  970.         }
  971.         return $result;
  972.     }
  973.     public function getCandidateByCalendarEvent(Person $person, ?int $calendarEventId,
  974.                                                        &$calendarEvent): ?Candidate
  975.     {
  976.         if ($calendarEventId !== null) {
  977.             $calendarEvent $this->calendarEventService->getBaseService()->get($calendarEventId);
  978.         }
  979.         if (!$calendarEvent) {
  980.             return null;
  981.         }
  982.         $candidate $this->getFirst([
  983.             'person' => $person,
  984.             'calendarEvent' => $calendarEvent,
  985.         ]);
  986.         return $candidate;
  987.     }
  988.     /**
  989.      * @param Person $person
  990.      * @return array|Candidate[]
  991.      */
  992.     public function getUpcomingCalendarEventCandidates(Person $person,
  993.                                                        array  $orderCalendarEventsBy = ['startDate''ASC']): array
  994.     {
  995.         $qb $this->em->createQueryBuilder();
  996.         $qb->select('c')
  997.             ->from(Candidate::class, 'c')
  998.             ->join('c.calendarEvent''ce')
  999.             ->where('c.person = :person')
  1000.             ->andWhere('ce.startDate > :today')
  1001.             ->setParameter('today', new \DateTime())
  1002.             ->setParameter('person'$person);
  1003.         if ($orderCalendarEventsBy) {
  1004.             $qb->orderBy('ce.' $orderCalendarEventsBy[0], $orderCalendarEventsBy[1]);
  1005.         }
  1006.         return $qb->getQuery()->getResult();
  1007.     }
  1008.     /**
  1009.      * @param array|Invoice[] $invoices
  1010.      * @return array|Candidate[]
  1011.      */
  1012.     public function getByInvoices(array $invoices): array
  1013.     {
  1014.         $candidates = [];
  1015.         foreach ($invoices as $invoice) {
  1016.             $candidate $invoice->getCandidate();
  1017.             $candidates[$invoice->getId()] = $candidate;
  1018.         }
  1019.         return $candidates;
  1020.     }
  1021.     public function sortByCalendarEventStartDate(array &$candidates): void
  1022.     {
  1023.         usort($candidates, function (Candidate $aCandidate $b) {
  1024.             $aStartDate $a->getCalendarEvent() ? $a->getCalendarEvent()->getStartDate() : null;
  1025.             $bStartDate $b->getCalendarEvent() ? $b->getCalendarEvent()->getStartDate() : null;
  1026.             if ($aStartDate == $bStartDate) {
  1027.                 return 0;
  1028.             }
  1029.             if ($aStartDate === null) {
  1030.                 return 1;
  1031.             }
  1032.             if ($bStartDate === null) {
  1033.                 return -1;
  1034.             }
  1035.             return $aStartDate $bStartDate ? -1;
  1036.         });
  1037.     }
  1038.     /**
  1039.      * @param array|Candidate[] $candidates
  1040.      * @return array{int, Candidate[]}
  1041.      */
  1042.     public function groupByFinanceCategoryId(array $candidates): array
  1043.     {
  1044.         $result = [];
  1045.         foreach ($candidates as $candidate) {
  1046.             $financeCategory $candidate->getCalendarEvent() ? $candidate->getCalendarEvent()->getFinanceCategory() : null;
  1047.             $financeCategory $financeCategory $financeCategory->getId() : -1;
  1048.             if (!isset($result[$financeCategory])) {
  1049.                 $result[$financeCategory] = [];
  1050.             }
  1051.             $result[$financeCategory][] = $candidate;
  1052.         }
  1053.         return $result;
  1054.     }
  1055.     /**
  1056.      * @param Person $person
  1057.      * @return array|Candidate[]
  1058.      */
  1059.     public function getCandidates(Person $person): array
  1060.     {
  1061.         return $this->getDefault([
  1062.             "person" => $person,
  1063.         ]);
  1064.     }
  1065. }