add schedule rescheduling
This commit is contained in:
parent
b8eeb093d3
commit
7576ccaf09
9 changed files with 314 additions and 5 deletions
|
|
@ -11,9 +11,11 @@ use App\Schedule\UseCases\CreateSchedule\CreateSchedule;
|
|||
use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest;
|
||||
use App\Schedule\UseCases\GetSchedule\GetSchedule;
|
||||
use App\Schedule\UseCases\GetSchedule\GetScheduleRequest;
|
||||
use App\Schedule\UseCases\ListSchedules\ListSchedules;
|
||||
use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDate;
|
||||
use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDateRequest;
|
||||
use App\Schedule\UseCases\ListSchedules\ListSchedules;
|
||||
use App\Schedule\UseCases\RescheduleSchedule\RescheduleSchedule;
|
||||
use App\Schedule\UseCases\RescheduleSchedule\RescheduleScheduleRequest;
|
||||
use App\Schedule\UseCases\SetAssignmentCompletion\SetAssignmentCompletion;
|
||||
use App\Schedule\UseCases\SetAssignmentCompletion\SetAssignmentCompletionRequest;
|
||||
use App\Shared\Http\RequestInput;
|
||||
|
|
@ -27,6 +29,7 @@ class ScheduleController extends Controller
|
|||
private CreateSchedule $createSchedule,
|
||||
private ListSchedules $listSchedules,
|
||||
private GetSchedule $getSchedule,
|
||||
private RescheduleSchedule $rescheduleSchedule,
|
||||
private ListAssignmentsForDate $listAssignmentsForDate,
|
||||
private SetAssignmentCompletion $setAssignmentCompletion,
|
||||
) {}
|
||||
|
|
@ -100,6 +103,39 @@ class ScheduleController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $scheduleId): JsonResponse
|
||||
{
|
||||
$input = new RequestInput($request);
|
||||
|
||||
try {
|
||||
$schedule = $this->rescheduleSchedule->execute(
|
||||
new RescheduleScheduleRequest(
|
||||
scheduleId: $scheduleId,
|
||||
user: $this->user($request),
|
||||
startDate: $input->string('startDate'),
|
||||
targetDate: $input->string('targetDate'),
|
||||
workloadPlacement: $input->string(
|
||||
'workloadPlacement',
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (BadRequestException $exception) {
|
||||
return new JsonResponse(
|
||||
['error' => $exception->getMessage()],
|
||||
400,
|
||||
);
|
||||
} catch (NotFoundException $exception) {
|
||||
return new JsonResponse(
|
||||
['error' => $exception->getMessage()],
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'schedule' => $this->detailPayload($schedule),
|
||||
]);
|
||||
}
|
||||
|
||||
public function assignments(Request $request): JsonResponse
|
||||
{
|
||||
$input = new RequestInput($request);
|
||||
|
|
@ -206,15 +242,23 @@ class ScheduleController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
$days = [];
|
||||
$includedDates = [];
|
||||
$date = $schedule->getStartDate();
|
||||
while ($date <= $schedule->getTargetDate()) {
|
||||
$formattedDate = $date->format('Y-m-d');
|
||||
$includedDates[$date->format('Y-m-d')] = true;
|
||||
$date = $date->modify('+1 day');
|
||||
}
|
||||
foreach (array_keys($assignmentsByDate) as $assignmentDate) {
|
||||
$includedDates[$assignmentDate] = true;
|
||||
}
|
||||
ksort($includedDates);
|
||||
|
||||
$days = [];
|
||||
foreach (array_keys($includedDates) as $formattedDate) {
|
||||
$days[] = [
|
||||
'date' => $formattedDate,
|
||||
'assignments' => $assignmentsByDate[$formattedDate] ?? [],
|
||||
];
|
||||
$date = $date->modify('+1 day');
|
||||
}
|
||||
|
||||
return $days;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,39 @@ class EloquentScheduleRepository implements ScheduleRepository
|
|||
});
|
||||
}
|
||||
|
||||
public function reschedule(RescheduleScheduleDto $dto): Schedule
|
||||
{
|
||||
return DB::transaction(function () use ($dto): Schedule {
|
||||
$model = ScheduleModel::query()
|
||||
->where('id', $dto->scheduleId)
|
||||
->where('user_id', $dto->user->getId())
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
if ($model === null) {
|
||||
throw new DomainException(
|
||||
"Schedule with id {$dto->scheduleId} not found",
|
||||
);
|
||||
}
|
||||
|
||||
$model->start_date = $dto->startDate->format('Y-m-d');
|
||||
$model->target_date = $dto->targetDate->format('Y-m-d');
|
||||
$model->save();
|
||||
|
||||
foreach ($dto->assignments as $assignmentDto) {
|
||||
ScheduleAssignmentModel::query()
|
||||
->where('id', $assignmentDto->id)
|
||||
->where('schedule_id', $dto->scheduleId)
|
||||
->whereNull('completed_at')
|
||||
->update([
|
||||
'scheduled_date' => $assignmentDto->scheduledDate
|
||||
->format('Y-m-d'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->toDomain($model, $dto->user);
|
||||
});
|
||||
}
|
||||
|
||||
public function findForUser(int $id, User $user): ?Schedule
|
||||
{
|
||||
$model = ScheduleModel::query()
|
||||
|
|
|
|||
13
backend/app/Schedule/RescheduleScheduleAssignmentDto.php
Normal file
13
backend/app/Schedule/RescheduleScheduleAssignmentDto.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class RescheduleScheduleAssignmentDto
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public DateTimeImmutable $scheduledDate,
|
||||
) {}
|
||||
}
|
||||
20
backend/app/Schedule/RescheduleScheduleDto.php
Normal file
20
backend/app/Schedule/RescheduleScheduleDto.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule;
|
||||
|
||||
use App\User\User;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class RescheduleScheduleDto
|
||||
{
|
||||
/**
|
||||
* @param list<RescheduleScheduleAssignmentDto> $assignments
|
||||
*/
|
||||
public function __construct(
|
||||
public int $scheduleId,
|
||||
public User $user,
|
||||
public DateTimeImmutable $startDate,
|
||||
public DateTimeImmutable $targetDate,
|
||||
public array $assignments,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ interface ScheduleRepository
|
|||
{
|
||||
public function create(CreateScheduleDto $dto): Schedule;
|
||||
|
||||
public function reschedule(RescheduleScheduleDto $dto): Schedule;
|
||||
|
||||
public function findForUser(int $id, User $user): ?Schedule;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\RescheduleSchedule;
|
||||
|
||||
use App\Exceptions\BadRequestException;
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Schedule\EvenDistributionScheduler;
|
||||
use App\Schedule\RescheduleScheduleAssignmentDto;
|
||||
use App\Schedule\RescheduleScheduleDto;
|
||||
use App\Schedule\Schedule;
|
||||
use App\Schedule\ScheduleAssignment;
|
||||
use App\Schedule\ScheduleRepository;
|
||||
use App\Schedule\WorkloadPlacement;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
class RescheduleSchedule
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleRepository $scheduleRepository,
|
||||
private EvenDistributionScheduler $evenDistributionScheduler,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequestException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function execute(RescheduleScheduleRequest $request): Schedule
|
||||
{
|
||||
$schedule = $this->scheduleRepository->findForUser(
|
||||
$request->scheduleId,
|
||||
$request->user,
|
||||
);
|
||||
if ($schedule === null) {
|
||||
throw new NotFoundException('schedule not found');
|
||||
}
|
||||
|
||||
$startDate = $this->parseDate($request->startDate, 'startDate');
|
||||
$targetDate = $this->parseDate($request->targetDate, 'targetDate');
|
||||
if ($targetDate < $startDate) {
|
||||
throw new BadRequestException(
|
||||
'targetDate must not be before startDate',
|
||||
);
|
||||
}
|
||||
$workloadPlacement = $this->workloadPlacement(
|
||||
$request->workloadPlacement,
|
||||
);
|
||||
$unfinishedAssignments = array_values(array_filter(
|
||||
$schedule->getAssignments(),
|
||||
function (ScheduleAssignment $assignment): bool {
|
||||
return $assignment->getCompletedAt() === null;
|
||||
},
|
||||
));
|
||||
if ($unfinishedAssignments === []) {
|
||||
throw new BadRequestException(
|
||||
'schedule has no unfinished assignments',
|
||||
);
|
||||
}
|
||||
|
||||
$scheduledDates = $this->evenDistributionScheduler->scheduledDates(
|
||||
assignmentCount: count($unfinishedAssignments),
|
||||
startDate: $startDate,
|
||||
targetDate: $targetDate,
|
||||
workloadPlacement: $workloadPlacement,
|
||||
);
|
||||
$assignmentDtos = [];
|
||||
foreach ($unfinishedAssignments as $index => $assignment) {
|
||||
$assignmentDtos[] = new RescheduleScheduleAssignmentDto(
|
||||
id: $assignment->getId(),
|
||||
scheduledDate: $scheduledDates[$index],
|
||||
);
|
||||
}
|
||||
|
||||
return $this->scheduleRepository->reschedule(
|
||||
new RescheduleScheduleDto(
|
||||
scheduleId: $schedule->getId(),
|
||||
user: $request->user,
|
||||
startDate: $startDate,
|
||||
targetDate: $targetDate,
|
||||
assignments: $assignmentDtos,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 WorkloadPlacement::Middle;
|
||||
}
|
||||
|
||||
$workloadPlacement = WorkloadPlacement::tryFrom($value);
|
||||
if ($workloadPlacement === null) {
|
||||
throw new BadRequestException(
|
||||
'workloadPlacement must be start, middle, or end',
|
||||
);
|
||||
}
|
||||
|
||||
return $workloadPlacement;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Schedule\UseCases\RescheduleSchedule;
|
||||
|
||||
use App\User\User;
|
||||
|
||||
final readonly class RescheduleScheduleRequest
|
||||
{
|
||||
public function __construct(
|
||||
public int $scheduleId,
|
||||
public User $user,
|
||||
public ?string $startDate,
|
||||
public ?string $targetDate,
|
||||
public ?string $workloadPlacement,
|
||||
) {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue