setTime(0, 0); $violations = []; foreach ($this->seasonAssignmentTaskRepository->findOpen() as $task) { if (null === $level = $this->levelDueOn($task, $today)) { continue; } $teamer = (string) $task->getSeasonAssignment()?->getTeamer(); $violations[$teamer][] = [ 'task' => $task, 'label' => $task->getTask()?->label() ?? $task->getTaskKey(), 'level' => $level, ]; } if (0 === $count = count($violations)) { $message = 'No season task deadlines reached today'; $this->logger->info($message); return $message; } // the two roles that carry ROLE_ADMINISTRATIVE: the repository matches roles as they are // stored on the user, and nobody is ever stored with the hierarchy-granted one foreach ($this->userRepository->getUsersByRoles(['ROLE_TEAM_ADMIN', 'ROLE_MANAGER']) as $user) { if (true === $user->isMuteNotifications()) { continue; } $this->mailer->createAndSendEmail([ 'violations' => $violations, ], [ 'to' => $user->getEmail(), 'subject' => 'Fristen für Saisonunterlagen erreicht', 'template' => 'email/season_deadlines.html.twig', ]); } $message = 'Sent the season task deadline summary for '.$count.' teamers'; $this->logger->info($message); return $message; } /** * One mail per teamer, every Monday, listing what they still have to send us. It repeats * until an administrative user ticks the entry off; it is not tied to a deadline at all. */ public function sendTeamerDigest(?\DateTimeImmutable $today = null): string { $today = ($today ?? new \DateTimeImmutable('today'))->setTime(0, 0); if (self::DIGEST_WEEKDAY !== (int) $today->format('N')) { $message = 'Season task digests are only sent on mondays'; $this->logger->info($message); return $message; } // Scoped to the season being prepared, so a teamer is never chased about a season that // is long over. [$windowStart, $windowEnd] = $this->seasonCalendar->windowFor( $this->seasonCalendar->currentOrUpcomingSeasonYear($today) ); $open = $this->seasonAssignmentTaskRepository->findOpen( $windowStart->toDateTimeImmutable(), $windowEnd->toDateTimeImmutable() ); $perTeamer = []; foreach ($open as $task) { $seasonAssignment = $task->getSeasonAssignment(); if (null === $seasonAssignment) { continue; } // office-internal entries never reach the teamer if (true !== $task->getTask()?->requiresReturn()) { continue; } $perTeamer[$seasonAssignment->getId()]['assignment'] = $seasonAssignment; $perTeamer[$seasonAssignment->getId()]['labels'][] = $task->getTask()->label(); } $count = 0; foreach ($perTeamer as $entry) { $teamer = $entry['assignment']->getTeamer(); $email = $teamer?->getCommunication()?->getEmail(); if (null === $email) { continue; } $this->mailer->createAndSendText(EmailTextKey::REMINDER_SEASON_TASKS, [ // the placeholder keeps its name: admins can have edited this text in the // database, and renaming it would blank it out in those stored versions 'destination' => $this->labeler->label($entry['assignment']), 'tasks' => implode(', ', $entry['labels']), ], [ 'to' => $email, ]); ++$count; } $message = 'Sent '.$count.' season task digests to teamers'; $this->logger->info($message); return $message; } /** * Which deadline of this entry falls on $day, or null for none. Frist 1 wins when both do. */ private function levelDueOn(SeasonAssignmentTask $task, \DateTimeImmutable $day): ?int { if ($this->isSameDay($this->deadlineResolver->resolveFirst($task), $day)) { return 1; } if ($this->isSameDay($this->deadlineResolver->resolveSecond($task), $day)) { return 2; } return null; } private function isSameDay(?\DateTimeImmutable $deadline, \DateTimeImmutable $day): bool { return null !== $deadline && $deadline->format('Y-m-d') === $day->format('Y-m-d'); } }