<?php
namespace App\Service\CalendarEvent;
use App\Entity\CalendarEvent;
use App\Entity\Payment;
use App\Entity\Person;
use App\Enum\CalendarEvent\Type;
use App\Service\CalendarEventKind\CalendarEventKindService;
use App\Service\Candidate\CandidateService;
use App\Service\ServiceRetriever;
use App\Service\User\UserService;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\BaseEntityService;
class CalendarEventService extends BaseEntityService
{
const FORCE_REGISTER_CLOSED_EVENTS = true;
/**
* @var ServiceRetriever
*/
protected $serviceRetriever;
/**
* @var CalendarEventKindService
*/
private $calendarEventKindService;
/**
* @var UserService
*/
private $userService;
public function __construct(EntityManagerInterface $em, ServiceRetriever $serviceRetriever,
CalendarEventKindService $calendarEventKindService, UserService $userService)
{
parent::__construct($em);
$this->initialize(CalendarEvent::class);
$this->serviceRetriever = $serviceRetriever;
$this->calendarEventKindService = $calendarEventKindService;
$this->userService = $userService;
}
public function getPaymentChoices(Payment $payment): array
{
$userSettings = $this->userService->getLoggedInUserSettings();
$start = $userSettings ? $userSettings->getStartCalendarEventsDate() : null;
$end = $userSettings ? $userSettings->getEndCalendarEventsDate() : null;
$items = $this->getForAccountantCalendarEvents();
if ($start && $end) {
$items = $this->filterByEndDate($items, $start, $end);
}
return $items;
}
public function getChoiceLabel(CalendarEvent $calendarEvent, $entity): string
{
$class = get_class($entity);
switch ($class) {
case Payment::class:
return $calendarEvent->getNameWithCode();
case CalendarEvent::class:
return $calendarEvent->getNameText() . ' (' . $calendarEvent->getStartDate()->format('d.m.Y') . ')';
default:
return $calendarEvent->getStartDate()->format("d.m.Y") . " – " . $calendarEvent->getNameText();
}
}
/**
* @return CalendarEvent[]
*/
public function getByPrice(int $price): array
{
return $this->getDefault([
"price" => $price,
]);
}
public function getCandidateChoices(): array
{
return $this->getAll();
}
/**
* Возвращает возможные варианты CalendarEvent для поля requireCalendarEventsParticipations
* Исключаем текущее редактируемое событие из списка, если оно уже сохранено (имеет id)
* @param CalendarEvent|null $excludeCalendarEvent
* @return CalendarEvent[]
*/
public function getCalendarEventChoices(?CalendarEvent $excludeCalendarEvent = null): array
{
$all = $this->getDefault(null, null,
null, null,
null, ["startDate", "DESC"]);
if (!$excludeCalendarEvent || !$excludeCalendarEvent->getId()) {
return $all;
}
return array_values(array_filter($all, function (CalendarEvent $event) use ($excludeCalendarEvent) {
return $event->getId() !== $excludeCalendarEvent->getId();
}));
}
public function getCalendarEventByDate(\DateTime $now): ?CalendarEvent
{
// 1) Попробуем найти событие, которое покрывает текущую дату (startDate <= today <= endDate)
$today = (clone $now)->setTime(0, 0, 0);
// 2) Если не найдено, ищем все события, которые в рамках даты today (startDate<=today<=endDate)
// и у которых время начала/окончания попадает в интервал now-2h .. now+2h. Выберем ближайшее по времени к now.
$from = (clone $now)->modify('-1 hours');
$to = (clone $now)->modify('+1 hours');
$fromTime = $from->format('H:i:s');
$toTime = $to->format('H:i:s');
$q2 = $this->em->getRepository(CalendarEvent::class)->createQueryBuilder('e');
$q2->andWhere('e.startDate <= :today')
->andWhere('e.endDate >= :today')
->andWhere('(' .
"(e.startTime BETWEEN :fromTime AND :toTime) OR " .
"(e.endTime BETWEEN :fromTime AND :toTime) OR " .
"(e.startTime <= :fromTime AND e.endTime >= :toTime)" .
')')
->setParameter('today', $today->format('Y-m-d'))
->setParameter('fromTime', $fromTime)
->setParameter('toTime', $toTime);
$events = $q2->getQuery()->getResult();
if (count($events) === 0) {
return null;
}
$closest = null;
$closestDiff = null;
foreach ($events as $event) {
/** @var CalendarEvent $event */
$eventStartTime = $event->getStartTime();
if (!$eventStartTime) {
continue;
}
// Создаём datetime для сегодняшнего события, комбинируя сегодняшнюю дату и время начала события
$eventStart = new \DateTime($now->format('Y-m-d') . ' ' . $eventStartTime->format('H:i:s'));
$diff = abs($eventStart->getTimestamp() - $now->getTimestamp());
if ($closest === null || $diff < $closestDiff) {
$closest = $event;
$closestDiff = $diff;
}
}
return $closest;
}
/**
* @param mixed $telegramCampaign
* @return CalendarEvent[]
*/
public function getTelegramCampaignChoices($telegramCampaign = null): array
{
$this->calendarEventService = $this->serviceRetriever->getService(CalendarEventService::class);
return $this->calendarEventService->getAll();
}
public function getByStartEndDate(\DateTime $start = null, \DateTime $end = null,
$order = ["startDate", "DESC"]): array
{
$qb = $this->em->getRepository(CalendarEvent::class)->createQueryBuilder('e');
if ($start) {
$qb->andWhere('e.startDate >= :start')
->setParameter('start', $start->format('Y-m-d'));
}
if ($end !== null) {
$qb->andWhere('e.endDate <= :end')
->setParameter('end', $end->format('Y-m-d'));
}
if ($order) {
$qb->orderBy('e.' . $order[0], $order[1]);
}
return $qb->getQuery()->getResult();
}
/**
* @return array|CalendarEvent[]
*/
public function getCalendarEventsForMakeCandidates($type = null, $order = null): array
{
return $this->getCalendarEventsForRegister($type,
$order, -7);
}
/**
* @return array|CalendarEvent[]
*/
public function getCalendarEventsForRegister($type = null, $order = null,
$addDaysToStartDate = null): array
{
if ($type && !is_array($type)) {
$type = [$type];
}
$start = new \DateTime();
if ($addDaysToStartDate) {
$start = $start->modify("$addDaysToStartDate days");
}
$result = $this->getByStartEndDate($start, null, $order);
$result = array_filter($result, function (CalendarEvent $event) use ($start, $type, $addDaysToStartDate) {
return !$event->isStarted(($addDaysToStartDate ?? 0) * -1) &&
($type === null || in_array($event->getType(), $type));
});
if ($type && in_array(Type::TYPE_PERSONAL_CONSULTATION, $type)) {
$result2 = $this->getDefault([
"endDate" => ["startDate" => $start->format("Y-m-d")],
]);
$result2 = array_filter($result2, function (CalendarEvent $event) use ($start, $type, $addDaysToStartDate) {
return ($type === null || in_array($event->getType(), [Type::TYPE_PERSONAL_CONSULTATION]));
});
$result = array_merge($result, $result2);
$result = array_unique($result);
}
return $result;
}
public function getCalendarEventsForChannelApprove($type = null, $order = ["startDate", "ASC"]): array
{
if ($type && !is_array($type)) {
$type = [$type];
}
$start = new \DateTime();
$result = $this->getByStartEndDate($start, null, $order);
return $result;
}
public function getAll(array $orderBy = null, $type = null, \DateTime $start = null,
\DateTime $end = null, $status = null): array
{
if ($orderBy === null) {
$orderBy = ["startDate", "DESC"];
}
$params = [];
if ($type !== null) {
$params['type'] = $type;
}
if ($start !== null) {
$params['startDate'] = ["startDate" => $start->format("Y-m-d"),
"endDate" => $end->format("Y-m-d")];
}
if ($status !== null) {
$params['status'] = $status;
}
return $this->getDefault($params, null, null,
null, null, $orderBy);
}
/**
* @return array|CalendarEvent[]
*/
public function getForDropDown($start, $end): array
{
return $this->getAll(["startDate", "ASC"],
Type::getDropDownTypes(), $start, $end,
\App\Enum\CalendarEvent\Status::getForListStatuses());
}
public function getClosestCalendarEvent(): ?CalendarEvent
{
$now = new \DateTime();
// current
$qb = $this->em->getRepository(CalendarEvent::class)->createQueryBuilder('e');
$qb->andWhere('e.startDate >= :now')
->andWhere('e.endDate >= :now')
->setParameter('now', $now->format('Y-m-d'))
->orderBy('e.startDate', 'ASC')
->setMaxResults(1);
$currentEvents = $qb->getQuery()->getResult();
if (count($currentEvents) > 0) {
return $currentEvents[0];
}
// ищем ближайшее будущее событие
$qb = $this->em->getRepository(CalendarEvent::class)->createQueryBuilder('e');
$qb->andWhere('e.startDate > :now')
->setParameter('now', $now->format('Y-m-d'))
->orderBy('e.startDate', 'ASC')
->setMaxResults(1);
$futureEvents = $qb->getQuery()->getResult();
if (count($futureEvents) > 0) {
return $futureEvents[0];
}
//or get last past event
$last = $this->getFirst([], null, null,
null, ["startDate", "DESC"]);
if ($last) {
return $last;
}
return null;
}
public function getMissingRequiredCandidateCalendarEvents(Person $person,
CalendarEvent $calendarEvent): array
{
if (!$calendarEvent->getRequireCalendarEventsParticipations()->count()) {
return [];
}
$candidateService = $this->serviceRetriever->getService(CandidateService::class);
/** @var \App\Entity\Candidate[] $candidates */
$candidates = $candidateService->getDefault([
'person' => $person,
'status' => \App\Enum\Candidate\Status::STATUS_APPROVED,
]);
$missingEvents = [];
foreach ($calendarEvent->getRequireCalendarEventsParticipations() as $requiredEvent) {
$found = false;
foreach ($candidates as $candidate) {
if ($candidate->getCalendarEvent()->getId() === $requiredEvent->getId()) {
$found = true;
break;
}
}
if (!$found) {
$missingEvents[] = $requiredEvent;
}
}
return $missingEvents;
}
public function getDeniedCalendarEventsForPerson(Person $person, CalendarEvent $calendarEvent): array
{
if (!$calendarEvent->getDenyCalendarEventsParticipations()->count()) {
return [];
}
$candidateService = $this->serviceRetriever->getService(CandidateService::class);
/** @var \App\Entity\Candidate[] $candidates */
$candidates = $candidateService->getDefault([
'person' => $person,
'status' => \App\Enum\Candidate\Status::STATUS_APPROVED,
]);
$deniedEvents = [];
foreach ($calendarEvent->getDenyCalendarEventsParticipations() as $deniedEvent) {
foreach ($candidates as $candidate) {
if ($candidate->getCalendarEvent()->getId() === $deniedEvent->getId()) {
$deniedEvents[] = $deniedEvent;
break;
}
}
}
return $deniedEvents;
}
/**
* @param array $ids
* @return CalendarEvent[]
*/
public function getByIds(array $ids): array
{
return $this->getDefault([
'id' => $ids,
]);
}
/**
* @return array|CalendarEvent[]
*/
public function getForAccountantCalendarEvents(): array
{
/** @var CalendarEvent[] $calendarEvents */
$calendarEvents = $this->getAll(["startDate", "ASC"]);
$calendarEvents = array_filter($calendarEvents, function ($event) {
return $event->getPrice();
});
$minEndDate = (new \DateTime())->modify("-2 month");
$calendarEvents = array_filter($calendarEvents, function (CalendarEvent $ce) use ($minEndDate) {
if ($ce->getConnectingCalendarEvent() && $ce->getConnectingCalendarEvent()->isConnectingEventForAccountant()) {
return false;
}
if ($ce->getEndDate() < $minEndDate) {
return false;
}
return true;
});
return $calendarEvents;
}
public function filterByEndDate(array $items, \DateTime $start, \DateTime $end): array
{
return array_filter($items, function (CalendarEvent $item) use ($start, $end) {
return $item->getEndDate() >= $start && $item->getEndDate() <= $end;
});
}
/**
* @param array $row
* @param CalendarEvent[] $calendarEvents
* @param array $purposeRegexes
* @param int $amountColIndex
* @param string|null $firstName
* @param string|null $lastName
*/
public function getCalendarEventFromBankStatementRow(array $row,
array $calendarEvents, array $purposeRegexes,
int $amountColIndex,
string $firstName = null, string $lastName = null,
array $requiredDataMatchNames = null): array
{
$result = [
"calendarEvent" => null,
"row" => null,
];
$findName = $firstName || $lastName;
$requiredDataMatchNames = array_merge([
"name" => $findName,
"amount" => false,
"purpose" => false,
], $requiredDataMatchNames ?? []);
$isMatchesFound = [];
foreach ($requiredDataMatchNames as $name => $isRequired) {
$isMatchesFound[$name] = false;
}
if ($findName) {
$firstName = mb_strtolower(mb_trim($firstName, null, 'UTF-8'), 'UTF-8');
$lastName = mb_strtolower(mb_trim($lastName, null, 'UTF-8'), 'UTF-8');
$nameVars = [
$firstName . " " . $lastName,
$lastName . " " . $firstName,
];
$rowText = implode("|||", $row);
$rowText = mb_strtolower($rowText, 'UTF-8');
foreach ($nameVars as $nameVar) {
if (mb_strpos($rowText, $nameVar) !== false) {
$isMatchesFound["name"] = true;
break;
}
}
if (!$isMatchesFound["name"] && $requiredDataMatchNames["name"]) {
return $result;
}
}
$amount = (int)$row[$amountColIndex];
foreach ($calendarEvents as $calendarEvent) {
$isMatchesFound["amount"] = $calendarEvent->getPrice() === $amount;
if (!isset($purposeRegexes["calendar_event_" . $calendarEvent->getId()])) {
throw new \Exception("No purpose regex found for calendar event: " . $calendarEvent->getName());
}
$purposeRegex = $purposeRegexes["calendar_event_" . $calendarEvent->getId()];
$isPurposeFound = false;
foreach ($row as $cell) {
$cell = mb_strtolower($cell, 'UTF-8');
if (preg_match($purposeRegex, $cell)) {
$isPurposeFound = true;
break;
}
}
$isMatchesFound["purpose"] = $isPurposeFound;
$isAllRequiredMatchesFound = true;
foreach ($requiredDataMatchNames as $name => $isRequired) {
if ($isRequired && !$isMatchesFound[$name]) {
$isAllRequiredMatchesFound = false;
break;
}
}
if ($isAllRequiredMatchesFound) {
$result["calendarEvent"] = $calendarEvent;
$result["row"] = $row;
return $result;
}
}
return $result;
}
public function checkCalendarEventNameRegex(string $regex): bool
{
$calendarEvents = $this->getDefault();
$calendarEvents = array_filter($calendarEvents, function (CalendarEvent $calendarEvent) {
return !!$calendarEvent->getPrice();
});
$calendarEvents = array_filter($calendarEvents, function (CalendarEvent $calendarEvent) use ($regex) {
$calendarEventName = $calendarEvent->getName();
$calendarEventName = mb_strtolower($calendarEventName, 'UTF-8');
$result = preg_match($regex, $calendarEventName);
return $result;
});
$calendarEvents = array_values($calendarEvents);
$matchedCalendarEventNamesCount = count($calendarEvents);
if ($matchedCalendarEventNamesCount > 1) {
$isAllEventsSameKind = $this->calendarEventKindService
->isAllCalendarEventsOfSameKind($calendarEvents);
if (!$isAllEventsSameKind) {
throw new \Exception("Multiple calendar events matched regex \"$regex\" and they are of different kinds");
}
return true;
} elseif ($matchedCalendarEventNamesCount == 0) {
throw new \Exception("No calendar events matched regex \"$regex\"");
} elseif ($matchedCalendarEventNamesCount == 1) {
if (!$calendarEvents[0]->getKind()) {
throw new \Exception("Matched calendar event does not have a kind");
}
return true;
}
return false;
}
public function checkCalendarEventsReadyForBankStatementWorkWith(array $calendarEvents)
{
foreach ($calendarEvents as $calendarEvent) {
foreach ($calendarEvents as $calendarEvent2) {
if ($calendarEvent->getId() !== $calendarEvent2->getId()
&& $calendarEvent->getPrice() === $calendarEvent2->getPrice()) {
throw new \Exception("Calendar events \"{$calendarEvent->getName()}\" and \"{$calendarEvent2->getName()}\" have the same price {$calendarEvent->getPrice()}. All calendar events should have different price for bank statement work");
}
}
}
}
/**
* @param string $names
* @return CalendarEvent[]
*/
public function getByNames(array $names): array
{
$q = $this->em->getRepository(CalendarEvent::class)->createQueryBuilder('item');
$q
->andWhere('item.name in (:name)')
->setParameter('name', $names)
;
$ces = $q->getQuery()->getResult();
return $ces;
}
public function sortItemsByCalendarEvent(array &$items, bool $asc = true)
{
usort($items, function ($a, $b) use ($asc) {
$aDate = $a->getCalendarEvent() ? $a->getCalendarEvent()->getStartDate() : null;
$bDate = $b->getCalendarEvent() ? $b->getCalendarEvent()->getStartDate() : null;
if ($aDate === null && $bDate === null) {
return 0;
} elseif ($aDate === null) {
return 1;
} elseif ($bDate === null) {
return -1;
} else {
return $asc ? $aDate <=> $bDate : $bDate <=> $aDate;
}
});
}
/**
* @return array|CalendarEvent[]
*/
public function getForBankStatementDataImport(): array
{
/** @var CalendarEvent[] $calendarEventsForRegister */
$calendarEventsForRegister = $this->getAll(
["startDate", "DESC"], Type::getForRegisterTypes()
);
$calendarEventsForRegister = array_filter($calendarEventsForRegister, function (CalendarEvent $ce) {
return !$ce->getConnectingCalendarEvent() && $ce->getPrice();
});
return array_values($calendarEventsForRegister);
}
public function hasVTPaymentRequisites(?CalendarEvent $calendarEvent): bool
{
if (!$calendarEvent || !$calendarEvent->getPaymentRequisites()) {
return false;
}
return $calendarEvent->getPaymentRequisites()->getTitle() == "ИП Вычужанина Татьяна";
}
public function isFutureCalendarEvent(?CalendarEvent $calendarEvent): bool
{
if (!$calendarEvent) {
return false;
}
$now = new \DateTime();
return $now < $calendarEvent->getStartDate();
}
}