Attainly/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php

269 lines
8.1 KiB
PHP

<?php
namespace App\Schedule\UseCases\CreateSchedule;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\CreateScheduleAssignmentDto;
use App\Schedule\CreateScheduleDto;
use App\Schedule\EvenDistributionScheduler;
use App\Schedule\Schedule;
use App\Schedule\ScheduleRepository;
use App\Schedule\WorkloadPlacement;
use App\Set\Set;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use DateTimeImmutable;
use DateTimeZone;
class CreateSchedule
{
public function __construct(
private SetRepository $setRepository,
private SetLevelRepository $setLevelRepository,
private ElementRepository $elementRepository,
private ScheduleRepository $scheduleRepository,
private EvenDistributionScheduler $evenDistributionScheduler,
) {}
/**
* @throws BadRequestException
* @throws NotFoundException
*/
public function execute(CreateScheduleRequest $request): Schedule
{
if ($request->setId === null || $request->setId < 1) {
throw new BadRequestException('setId is required');
}
$set = $this->setRepository->find($request->setId);
if ($set === null) {
throw new NotFoundException('set not found');
}
if ($request->levelId === null || $request->levelId < 1) {
throw new BadRequestException('levelId is required');
}
$level = $this->setLevelRepository->find($request->levelId);
if (
$level === null
|| $level->getSet()->getId() !== $set->getId()
) {
throw new BadRequestException(
'level is not available for set',
);
}
$startDate = $this->parseDate($request->startDate, 'startDate');
$targetDate = $this->parseDate($request->targetDate, 'targetDate');
if ($targetDate < $startDate) {
throw new BadRequestException(
'targetDate must not be before startDate',
);
}
$requestedWorkloadPlacement = $this->workloadPlacement(
$request->workloadPlacement,
);
$elements = array_values(array_filter(
$this->orderedElements($set),
function (Element $element) use ($level): bool {
return $element->getLevel()->getId() === $level->getId();
},
));
if ($elements === []) {
throw new BadRequestException('level has no elements');
}
$workloadPlacement = $this->resolvedWorkloadPlacement(
requestedWorkloadPlacement: $requestedWorkloadPlacement,
assignmentCount: count($elements),
startDate: $startDate,
targetDate: $targetDate,
);
$scheduledDates = $this->evenDistributionScheduler->scheduledDates(
assignmentCount: count($elements),
startDate: $startDate,
targetDate: $targetDate,
workloadPlacement: $workloadPlacement,
);
$assignments = $this->assignments(
elements: $elements,
scheduledDates: $scheduledDates,
);
return $this->scheduleRepository->create(new CreateScheduleDto(
user: $request->user,
setName: $set->getName(),
elementKind: $level->getKind(),
startDate: $startDate,
targetDate: $targetDate,
assignments: $assignments,
));
}
/**
* @return list<Element>
*/
private function orderedElements(Set $set): array
{
$childrenByParentId = [];
foreach ($this->elementRepository->findBySet($set) as $element) {
$parentId = $element->getParentElement()?->getId() ?? 0;
$childrenByParentId[$parentId][] = $element;
}
foreach ($childrenByParentId as &$children) {
usort($children, function (Element $first, Element $second): int {
$positionComparison = $first->getPosition()
<=> $second->getPosition();
if ($positionComparison !== 0) {
return $positionComparison;
}
return $first->getId() <=> $second->getId();
});
}
unset($children);
$orderedElements = [];
$this->appendChildren(
parentId: 0,
childrenByParentId: $childrenByParentId,
orderedElements: $orderedElements,
);
return $orderedElements;
}
/**
* @param array<int, list<Element>> $childrenByParentId
* @param list<Element> $orderedElements
*/
private function appendChildren(
int $parentId,
array $childrenByParentId,
array &$orderedElements,
): void {
foreach ($childrenByParentId[$parentId] ?? [] as $element) {
$orderedElements[] = $element;
$this->appendChildren(
parentId: $element->getId(),
childrenByParentId: $childrenByParentId,
orderedElements: $orderedElements,
);
}
}
/**
* @param list<Element> $elements
* @param list<DateTimeImmutable> $scheduledDates
* @return list<CreateScheduleAssignmentDto>
*/
private function assignments(
array $elements,
array $scheduledDates,
): array {
$assignments = [];
foreach ($elements as $index => $element) {
$assignments[] = new CreateScheduleAssignmentDto(
name: $element->getName(),
kind: $element->getKind(),
path: $this->elementPath($element),
scheduledDate: $scheduledDates[$index],
position: $index + 1,
);
}
return $assignments;
}
/**
* @return list<string>
*/
private function elementPath(Element $element): array
{
$path = [];
$currentElement = $element;
while ($currentElement !== null) {
array_unshift($path, $currentElement->getName());
$currentElement = $currentElement->getParentElement();
}
return $path;
}
/**
* @throws BadRequestException
*/
private function parseDate(?string $value, string $field): DateTimeImmutable
{
if ($value === null || $value === '') {
throw new BadRequestException("{$field} is required");
}
$date = DateTimeImmutable::createFromFormat(
'!Y-m-d',
$value,
new DateTimeZone('UTC'),
);
$errors = DateTimeImmutable::getLastErrors();
if (
$date === false
|| $date->format('Y-m-d') !== $value
|| ($errors !== false
&& ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
) {
throw new BadRequestException(
"{$field} must be a valid date in YYYY-MM-DD format",
);
}
return $date;
}
/**
* @throws BadRequestException
*/
private function workloadPlacement(?string $value): ?WorkloadPlacement
{
if ($value === null) {
return null;
}
$workloadPlacement = WorkloadPlacement::tryFrom($value);
if ($workloadPlacement === null) {
throw new BadRequestException(
'workloadPlacement must be spread, start, middle, or end',
);
}
return $workloadPlacement;
}
private function resolvedWorkloadPlacement(
?WorkloadPlacement $requestedWorkloadPlacement,
int $assignmentCount,
DateTimeImmutable $startDate,
DateTimeImmutable $targetDate,
): WorkloadPlacement {
$differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1;
$isSparse = $assignmentCount < $dayCount;
if ($requestedWorkloadPlacement === null) {
return $isSparse
? WorkloadPlacement::Spread
: WorkloadPlacement::Middle;
}
return $requestedWorkloadPlacement;
}
}