add dated assignment endpoint

This commit is contained in:
Yisroel Baum 2026-08-15 21:50:37 +03:00
parent 6afe7864be
commit 483ca6fc1d
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
9 changed files with 252 additions and 8 deletions

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException; use App\Exceptions\NotFoundException;
use App\Schedule\AssignmentForDate;
use App\Schedule\Schedule; use App\Schedule\Schedule;
use App\Schedule\ScheduleAssignment; use App\Schedule\ScheduleAssignment;
use App\Schedule\UseCases\CreateSchedule\CreateSchedule; use App\Schedule\UseCases\CreateSchedule\CreateSchedule;
@ -11,6 +12,8 @@ use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest;
use App\Schedule\UseCases\GetSchedule\GetSchedule; use App\Schedule\UseCases\GetSchedule\GetSchedule;
use App\Schedule\UseCases\GetSchedule\GetScheduleRequest; use App\Schedule\UseCases\GetSchedule\GetScheduleRequest;
use App\Schedule\UseCases\ListSchedules\ListSchedules; use App\Schedule\UseCases\ListSchedules\ListSchedules;
use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDate;
use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDateRequest;
use App\Shared\Http\RequestInput; use App\Shared\Http\RequestInput;
use App\User\User; use App\User\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -22,6 +25,7 @@ class ScheduleController extends Controller
private CreateSchedule $createSchedule, private CreateSchedule $createSchedule,
private ListSchedules $listSchedules, private ListSchedules $listSchedules,
private GetSchedule $getSchedule, private GetSchedule $getSchedule,
private ListAssignmentsForDate $listAssignmentsForDate,
) {} ) {}
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
@ -90,6 +94,36 @@ class ScheduleController extends Controller
]); ]);
} }
public function assignments(Request $request): JsonResponse
{
$input = new RequestInput($request);
$date = $input->string('date');
try {
$assignments = $this->listAssignmentsForDate->execute(
new ListAssignmentsForDateRequest(
user: $this->user($request),
date: $date,
),
);
} catch (BadRequestException $exception) {
return new JsonResponse(
['error' => $exception->getMessage()],
400,
);
}
return new JsonResponse([
'date' => $date,
'assignments' => array_map(
function (AssignmentForDate $assignment): array {
return $this->assignmentForDatePayload($assignment);
},
$assignments,
),
]);
}
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
@ -165,6 +199,30 @@ class ScheduleController extends Controller
]; ];
} }
/**
* @return array<string, mixed>
*/
private function assignmentForDatePayload(
AssignmentForDate $assignmentForDate,
): array {
$assignment = $assignmentForDate->getAssignment();
return [
'id' => $assignment->getId(),
'schedule' => [
'id' => $assignmentForDate->getScheduleId(),
'set' => [
'name' => $assignmentForDate->getSetName(),
],
],
'element' => [
'name' => $assignment->getName(),
'kind' => $assignment->getKind(),
'path' => $assignment->getPath(),
],
];
}
private function user(Request $request): User private function user(Request $request): User
{ {
/** @var User $user */ /** @var User $user */

View file

@ -0,0 +1,27 @@
<?php
namespace App\Schedule;
final readonly class AssignmentForDate
{
public function __construct(
private int $scheduleId,
private string $setName,
private ScheduleAssignment $assignment,
) {}
public function getScheduleId(): int
{
return $this->scheduleId;
}
public function getSetName(): string
{
return $this->setName;
}
public function getAssignment(): ScheduleAssignment
{
return $this->assignment;
}
}

View file

@ -61,6 +61,40 @@ class EloquentScheduleRepository implements ScheduleRepository
return $schedules; return $schedules;
} }
public function findAssignmentsForUserOnDate(
User $user,
DateTimeImmutable $date,
): array {
$assignmentModels = ScheduleAssignmentModel::query()
->select('schedule_assignments.*')
->join(
'schedules',
'schedules.id',
'=',
'schedule_assignments.schedule_id',
)
->where('schedules.user_id', $user->getId())
->where('schedule_assignments.scheduled_date', $date->format(
'Y-m-d',
))
->with('schedule')
->orderByDesc('schedules.id')
->orderBy('schedule_assignments.position')
->orderBy('schedule_assignments.id')
->get();
$assignments = [];
foreach ($assignmentModels as $assignmentModel) {
$assignments[] = new AssignmentForDate(
scheduleId: $assignmentModel->schedule->id,
setName: $assignmentModel->schedule->set_name,
assignment: $this->assignmentToDomain($assignmentModel),
);
}
return $assignments;
}
private function toDomain(ScheduleModel $model, User $user): Schedule private function toDomain(ScheduleModel $model, User $user): Schedule
{ {
$assignmentModels = ScheduleAssignmentModel::query() $assignmentModels = ScheduleAssignmentModel::query()
@ -71,14 +105,7 @@ class EloquentScheduleRepository implements ScheduleRepository
$assignments = []; $assignments = [];
foreach ($assignmentModels as $assignmentModel) { foreach ($assignmentModels as $assignmentModel) {
$assignments[] = new ScheduleAssignment( $assignments[] = $this->assignmentToDomain($assignmentModel);
id: $assignmentModel->id,
name: $assignmentModel->element_name,
kind: $assignmentModel->element_kind,
path: $assignmentModel->element_path,
scheduledDate: $this->date($assignmentModel->scheduled_date),
position: $assignmentModel->position,
);
} }
return new Schedule( return new Schedule(
@ -92,6 +119,19 @@ class EloquentScheduleRepository implements ScheduleRepository
); );
} }
private function assignmentToDomain(
ScheduleAssignmentModel $model,
): ScheduleAssignment {
return new ScheduleAssignment(
id: $model->id,
name: $model->element_name,
kind: $model->element_kind,
path: $model->element_path,
scheduledDate: $this->date($model->scheduled_date),
position: $model->position,
);
}
private function date(string $value): DateTimeImmutable private function date(string $value): DateTimeImmutable
{ {
return new DateTimeImmutable($value, new DateTimeZone('UTC')); return new DateTimeImmutable($value, new DateTimeZone('UTC'));

View file

@ -5,6 +5,7 @@ namespace App\Schedule;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property int $id * @property int $id
@ -14,6 +15,7 @@ use Illuminate\Database\Eloquent\Model;
* @property list<string> $element_path * @property list<string> $element_path
* @property string $scheduled_date * @property string $scheduled_date
* @property int $position * @property int $position
* @property-read ScheduleModel $schedule
* *
* @method static Builder<static>|ScheduleAssignmentModel newModelQuery() * @method static Builder<static>|ScheduleAssignmentModel newModelQuery()
* @method static Builder<static>|ScheduleAssignmentModel newQuery() * @method static Builder<static>|ScheduleAssignmentModel newQuery()
@ -46,4 +48,12 @@ class ScheduleAssignmentModel extends Model
'position' => 'integer', 'position' => 'integer',
]; ];
} }
/**
* @return BelongsTo<ScheduleModel, $this>
*/
public function schedule(): BelongsTo
{
return $this->belongsTo(ScheduleModel::class, 'schedule_id');
}
} }

View file

@ -3,6 +3,7 @@
namespace App\Schedule; namespace App\Schedule;
use App\User\User; use App\User\User;
use DateTimeImmutable;
interface ScheduleRepository interface ScheduleRepository
{ {
@ -14,4 +15,12 @@ interface ScheduleRepository
* @return list<Schedule> * @return list<Schedule>
*/ */
public function findAllForUser(User $user): array; public function findAllForUser(User $user): array;
/**
* @return list<AssignmentForDate>
*/
public function findAssignmentsForUserOnDate(
User $user,
DateTimeImmutable $date,
): array;
} }

View file

@ -0,0 +1,59 @@
<?php
namespace App\Schedule\UseCases\ListAssignmentsForDate;
use App\Exceptions\BadRequestException;
use App\Schedule\AssignmentForDate;
use App\Schedule\ScheduleRepository;
use DateTimeImmutable;
use DateTimeZone;
class ListAssignmentsForDate
{
public function __construct(
private ScheduleRepository $scheduleRepository,
) {}
/**
* @return list<AssignmentForDate>
* @throws BadRequestException
*/
public function execute(ListAssignmentsForDateRequest $request): array
{
$date = $this->parseDate($request->date);
return $this->scheduleRepository->findAssignmentsForUserOnDate(
$request->user,
$date,
);
}
/**
* @throws BadRequestException
*/
private function parseDate(?string $value): DateTimeImmutable
{
if ($value === null || $value === '') {
throw new BadRequestException('date 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(
'date must be a valid date in YYYY-MM-DD format',
);
}
return $date;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Schedule\UseCases\ListAssignmentsForDate;
use App\User\User;
final readonly class ListAssignmentsForDateRequest
{
public function __construct(
public User $user,
public ?string $date,
) {}
}

View file

@ -15,6 +15,7 @@ Route::middleware(AuthMiddleware::class)->group(function (): void {
Route::get('/sets', [SetController::class, 'index']); Route::get('/sets', [SetController::class, 'index']);
Route::get('/sets/{setId}', [SetController::class, 'show']) Route::get('/sets/{setId}', [SetController::class, 'show'])
->whereNumber('setId'); ->whereNumber('setId');
Route::get('/assignments', [ScheduleController::class, 'assignments']);
Route::post('/schedules', [ScheduleController::class, 'store']); Route::post('/schedules', [ScheduleController::class, 'store']);
Route::get('/schedules', [ScheduleController::class, 'index']); Route::get('/schedules', [ScheduleController::class, 'index']);
Route::get('/schedules/{scheduleId}', [ScheduleController::class, 'show']) Route::get('/schedules/{scheduleId}', [ScheduleController::class, 'show'])

View file

@ -3,10 +3,12 @@
namespace Tests\Fakes; namespace Tests\Fakes;
use App\Schedule\CreateScheduleDto; use App\Schedule\CreateScheduleDto;
use App\Schedule\AssignmentForDate;
use App\Schedule\Schedule; use App\Schedule\Schedule;
use App\Schedule\ScheduleAssignment; use App\Schedule\ScheduleAssignment;
use App\Schedule\ScheduleRepository; use App\Schedule\ScheduleRepository;
use App\User\User; use App\User\User;
use DateTimeImmutable;
class FakeScheduleRepository implements ScheduleRepository class FakeScheduleRepository implements ScheduleRepository
{ {
@ -72,6 +74,31 @@ class FakeScheduleRepository implements ScheduleRepository
}, array_values($schedules)); }, array_values($schedules));
} }
public function findAssignmentsForUserOnDate(
User $user,
DateTimeImmutable $date,
): array {
$assignments = [];
foreach ($this->findAllForUser($user) as $schedule) {
foreach ($schedule->getAssignments() as $assignment) {
if ($assignment->getScheduledDate()->format('Y-m-d')
!== $date->format('Y-m-d')
) {
continue;
}
$assignments[] = new AssignmentForDate(
scheduleId: $schedule->getId(),
setName: $schedule->getSetName(),
assignment: $assignment,
);
}
}
return $assignments;
}
private function copy(Schedule $schedule): Schedule private function copy(Schedule $schedule): Schedule
{ {
$assignments = array_map( $assignments = array_map(