add set scheduling api
This commit is contained in:
parent
bdb266746d
commit
98ae9bf088
20 changed files with 935 additions and 1 deletions
212
backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php
Normal file
212
backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<?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\Schedule;
|
||||
use App\Schedule\ScheduleRepository;
|
||||
use App\Set\Set;
|
||||
use App\Set\SetRepository;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
class CreateSchedule
|
||||
{
|
||||
public function __construct(
|
||||
private SetRepository $setRepository,
|
||||
private ElementRepository $elementRepository,
|
||||
private ScheduleRepository $scheduleRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @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->elementKind === null || $request->elementKind === '') {
|
||||
throw new BadRequestException('elementKind is required');
|
||||
}
|
||||
|
||||
$startDate = $this->parseDate($request->startDate, 'startDate');
|
||||
$targetDate = $this->parseDate($request->targetDate, 'targetDate');
|
||||
if ($targetDate < $startDate) {
|
||||
throw new BadRequestException(
|
||||
'targetDate must not be before startDate',
|
||||
);
|
||||
}
|
||||
|
||||
$elements = array_values(array_filter(
|
||||
$this->orderedElements($set),
|
||||
function (Element $element) use ($request): bool {
|
||||
return $element->getKind() === $request->elementKind;
|
||||
},
|
||||
));
|
||||
if ($elements === []) {
|
||||
throw new BadRequestException(
|
||||
'elementKind is not available for set',
|
||||
);
|
||||
}
|
||||
|
||||
$assignments = $this->assignments(
|
||||
elements: $elements,
|
||||
startDate: $startDate,
|
||||
targetDate: $targetDate,
|
||||
);
|
||||
|
||||
return $this->scheduleRepository->create(new CreateScheduleDto(
|
||||
user: $request->user,
|
||||
set: $set,
|
||||
elementKind: $request->elementKind,
|
||||
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
|
||||
* @return list<CreateScheduleAssignmentDto>
|
||||
*/
|
||||
private function assignments(
|
||||
array $elements,
|
||||
DateTimeImmutable $startDate,
|
||||
DateTimeImmutable $targetDate,
|
||||
): array {
|
||||
$differenceInDays = $startDate->diff($targetDate)->days;
|
||||
$dayCount = $differenceInDays + 1;
|
||||
$elementCount = count($elements);
|
||||
$assignments = [];
|
||||
|
||||
foreach ($elements as $index => $element) {
|
||||
$dayIndex = $this->dayIndex(
|
||||
elementIndex: $index,
|
||||
elementCount: $elementCount,
|
||||
dayCount: $dayCount,
|
||||
);
|
||||
$assignments[] = new CreateScheduleAssignmentDto(
|
||||
element: $element,
|
||||
scheduledDate: $startDate->modify("+{$dayIndex} days"),
|
||||
position: $index + 1,
|
||||
);
|
||||
}
|
||||
|
||||
return $assignments;
|
||||
}
|
||||
|
||||
private function dayIndex(
|
||||
int $elementIndex,
|
||||
int $elementCount,
|
||||
int $dayCount,
|
||||
): int {
|
||||
if ($elementCount === 1 || $dayCount === 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($elementCount < $dayCount) {
|
||||
$scaledIndex = $elementIndex * ($dayCount - 1)
|
||||
/ ($elementCount - 1);
|
||||
|
||||
return (int) floor($scaledIndex + 0.5);
|
||||
}
|
||||
|
||||
return intdiv($elementIndex * $dayCount, $elementCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\CreateSchedule;
|
||||
|
||||
use App\User\User;
|
||||
|
||||
final readonly class CreateScheduleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public ?int $setId,
|
||||
public ?string $elementKind,
|
||||
public ?string $startDate,
|
||||
public ?string $targetDate,
|
||||
) {}
|
||||
}
|
||||
30
backend/app/Schedule/UseCases/GetSchedule/GetSchedule.php
Normal file
30
backend/app/Schedule/UseCases/GetSchedule/GetSchedule.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\GetSchedule;
|
||||
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Schedule\Schedule;
|
||||
use App\Schedule\ScheduleRepository;
|
||||
|
||||
class GetSchedule
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleRepository $scheduleRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function execute(GetScheduleRequest $request): Schedule
|
||||
{
|
||||
$schedule = $this->scheduleRepository->findForUser(
|
||||
$request->scheduleId,
|
||||
$request->user,
|
||||
);
|
||||
if ($schedule === null) {
|
||||
throw new NotFoundException('schedule not found');
|
||||
}
|
||||
|
||||
return $schedule;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\GetSchedule;
|
||||
|
||||
use App\User\User;
|
||||
|
||||
final readonly class GetScheduleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public int $scheduleId,
|
||||
public User $user,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\ListSchedules;
|
||||
|
||||
use App\Schedule\Schedule;
|
||||
use App\Schedule\ScheduleRepository;
|
||||
use App\User\User;
|
||||
|
||||
class ListSchedules
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleRepository $scheduleRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return list<Schedule>
|
||||
*/
|
||||
public function execute(User $user): array
|
||||
{
|
||||
return $this->scheduleRepository->findAllForUser($user);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue