269 lines
8.1 KiB
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;
|
|
}
|
|
}
|