schedule explicit set levels

This commit is contained in:
Yisroel Baum 2026-08-11 22:57:15 +03:00
parent a64df64b76
commit 3cf1aff6a9
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
10 changed files with 156 additions and 38 deletions

View file

@ -34,7 +34,7 @@ class ScheduleController extends Controller
new CreateScheduleRequest(
user: $user,
setId: $input->integer('setId'),
elementKind: $input->string('elementKind'),
levelId: $input->integer('levelId'),
startDate: $input->string('startDate'),
targetDate: $input->string('targetDate'),
),

View file

@ -7,6 +7,7 @@ use App\Set\Set;
use App\Set\UseCases\GetSetLayout\ElementLayoutNode;
use App\Set\UseCases\GetSetLayout\GetSetLayout;
use App\Set\UseCases\GetSetLayout\GetSetLayoutRequest;
use App\Set\UseCases\GetSetLayout\SetLayoutLevel;
use App\Set\UseCases\ListSets\ListSets;
use Illuminate\Http\JsonResponse;
@ -49,6 +50,19 @@ class SetController extends Controller
'id' => $set->getId(),
'name' => $set->getName(),
],
'levels' => array_map(
function (SetLayoutLevel $layoutLevel): array {
$level = $layoutLevel->getLevel();
return [
'id' => $level->getId(),
'kind' => $level->getKind(),
'depth' => $level->getDepth(),
'elementCount' => $layoutLevel->getElementCount(),
];
},
$layout->getLevels(),
),
'elements' => array_map(
function (ElementLayoutNode $node): array {
return $this->elementPayload($node);
@ -63,6 +77,7 @@ class SetController extends Controller
* id: int,
* name: string,
* kind: string,
* levelId: int,
* children: list<mixed>
* }
*/
@ -74,6 +89,7 @@ class SetController extends Controller
'id' => $element->getId(),
'name' => $element->getName(),
'kind' => $element->getKind(),
'levelId' => $element->getLevel()->getId(),
'children' => array_map(
function (ElementLayoutNode $childNode): array {
return $this->elementPayload($childNode);

View file

@ -11,6 +11,7 @@ use App\Schedule\CreateScheduleDto;
use App\Schedule\Schedule;
use App\Schedule\ScheduleRepository;
use App\Set\Set;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use DateTimeImmutable;
use DateTimeZone;
@ -19,6 +20,7 @@ class CreateSchedule
{
public function __construct(
private SetRepository $setRepository,
private SetLevelRepository $setLevelRepository,
private ElementRepository $elementRepository,
private ScheduleRepository $scheduleRepository,
) {}
@ -38,8 +40,18 @@ class CreateSchedule
throw new NotFoundException('set not found');
}
if ($request->elementKind === null || $request->elementKind === '') {
throw new BadRequestException('elementKind is required');
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');
@ -52,14 +64,12 @@ class CreateSchedule
$elements = array_values(array_filter(
$this->orderedElements($set),
function (Element $element) use ($request): bool {
return $element->getKind() === $request->elementKind;
function (Element $element) use ($level): bool {
return $element->getLevel()->getId() === $level->getId();
},
));
if ($elements === []) {
throw new BadRequestException(
'elementKind is not available for set',
);
throw new BadRequestException('level has no elements');
}
$assignments = $this->assignments(
@ -71,7 +81,7 @@ class CreateSchedule
return $this->scheduleRepository->create(new CreateScheduleDto(
user: $request->user,
setName: $set->getName(),
elementKind: $request->elementKind,
elementKind: $level->getKind(),
startDate: $startDate,
targetDate: $targetDate,
assignments: $assignments,

View file

@ -9,7 +9,7 @@ final readonly class CreateScheduleRequest
public function __construct(
public User $user,
public ?int $setId,
public ?string $elementKind,
public ?int $levelId,
public ?string $startDate,
public ?string $targetDate,
) {}

View file

@ -6,11 +6,13 @@ use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\NotFoundException;
use App\Set\SetRepository;
use App\Set\SetLevelRepository;
class GetSetLayout
{
public function __construct(
private SetRepository $setRepository,
private SetLevelRepository $setLevelRepository,
private ElementRepository $elementRepository,
) {}
@ -24,10 +26,15 @@ class GetSetLayout
throw new NotFoundException('set not found');
}
$elements = $this->elementRepository->findBySet($set);
$elementsByParentId = [];
foreach ($this->elementRepository->findBySet($set) as $element) {
$elementCountsByLevelId = [];
foreach ($elements as $element) {
$parentId = $element->getParentElement()?->getId() ?? 0;
$elementsByParentId[$parentId][] = $element;
$levelId = $element->getLevel()->getId();
$elementCountsByLevelId[$levelId]
= ($elementCountsByLevelId[$levelId] ?? 0) + 1;
}
foreach ($elementsByParentId as &$siblings) {
@ -46,8 +53,17 @@ class GetSetLayout
}
unset($siblings);
$layoutLevels = [];
foreach ($this->setLevelRepository->findBySet($set) as $level) {
$layoutLevels[] = new SetLayoutLevel(
level: $level,
elementCount: $elementCountsByLevelId[$level->getId()] ?? 0,
);
}
return new SetLayout(
set: $set,
levels: $layoutLevels,
elements: $this->buildNodes(0, $elementsByParentId),
);
}

View file

@ -7,10 +7,12 @@ use App\Set\Set;
final readonly class SetLayout
{
/**
* @param list<SetLayoutLevel> $levels
* @param list<ElementLayoutNode> $elements
*/
public function __construct(
private Set $set,
private array $levels,
private array $elements,
) {}
@ -19,6 +21,14 @@ final readonly class SetLayout
return $this->set;
}
/**
* @return list<SetLayoutLevel>
*/
public function getLevels(): array
{
return $this->levels;
}
/**
* @return list<ElementLayoutNode>
*/

View file

@ -0,0 +1,23 @@
<?php
namespace App\Set\UseCases\GetSetLayout;
use App\Set\SetLevel;
final readonly class SetLayoutLevel
{
public function __construct(
private SetLevel $level,
private int $elementCount,
) {}
public function getLevel(): SetLevel
{
return $this->level;
}
public function getElementCount(): int
{
return $this->elementCount;
}
}

View file

@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder
{
$this->call(UserSeeder::class);
$this->call(SetSeeder::class);
$this->call(SetLevelSeeder::class);
$this->call(ElementSeeder::class);
}
}

View file

@ -6,8 +6,11 @@ use App\Element\CreateElementDto;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Set\Set;
use App\Set\SetLevel;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use Illuminate\Database\Seeder;
use RuntimeException;
class ElementSeeder extends Seeder
{
@ -15,38 +18,31 @@ class ElementSeeder extends Seeder
'Bible' => [
[
'name' => 'Genesis',
'kind' => 'book',
'children' => [
[
'name' => 'Creation',
'kind' => 'portion',
'children' => [
[
'name' => 'Chapter 1',
'kind' => 'chapter',
'children' => [],
],
[
'name' => 'Chapter 2',
'kind' => 'chapter',
'children' => [],
],
],
],
[
'name' => 'Noah',
'kind' => 'portion',
'children' => [],
],
],
],
[
'name' => 'Exodus',
'kind' => 'book',
'children' => [
[
'name' => 'Shemot',
'kind' => 'portion',
'children' => [],
],
],
@ -55,32 +51,26 @@ class ElementSeeder extends Seeder
'Course' => [
[
'name' => 'Foundations',
'kind' => 'module',
'children' => [
[
'name' => 'Welcome',
'kind' => 'lesson',
'children' => [],
],
[
'name' => 'Core Concepts',
'kind' => 'lesson',
'children' => [],
],
],
],
[
'name' => 'Applied Practice',
'kind' => 'module',
'children' => [
[
'name' => 'Guided Exercise',
'kind' => 'lesson',
'children' => [],
],
[
'name' => 'Final Review',
'kind' => 'lesson',
'children' => [],
],
],
@ -89,31 +79,25 @@ class ElementSeeder extends Seeder
'Fitness Program' => [
[
'name' => 'Foundation Phase',
'kind' => 'phase',
'children' => [
[
'name' => 'Strength Day',
'kind' => 'workout',
'children' => [
[
'name' => 'Squat',
'kind' => 'exercise',
'children' => [],
],
[
'name' => 'Push-up',
'kind' => 'exercise',
'children' => [],
],
],
],
[
'name' => 'Mobility Day',
'kind' => 'workout',
'children' => [
[
'name' => 'Hip Flow',
'kind' => 'exercise',
'children' => [],
],
],
@ -122,11 +106,9 @@ class ElementSeeder extends Seeder
],
[
'name' => 'Build Phase',
'kind' => 'phase',
'children' => [
[
'name' => 'Full Body Circuit',
'kind' => 'workout',
'children' => [],
],
],
@ -137,6 +119,7 @@ class ElementSeeder extends Seeder
public function run(): void
{
$elementRepository = app(ElementRepository::class);
$setLevelRepository = app(SetLevelRepository::class);
foreach (app(SetRepository::class)->all() as $set) {
$definitions = self::ELEMENTS_BY_SET[$set->getName()] ?? null;
@ -147,6 +130,8 @@ class ElementSeeder extends Seeder
$this->seedChildren(
repository: $elementRepository,
set: $set,
levels: $setLevelRepository->findBySet($set),
depth: 0,
parentElement: null,
definitions: $definitions,
);
@ -156,16 +141,27 @@ class ElementSeeder extends Seeder
/**
* @param list<array{
* name: string,
* kind: string,
* children: list<mixed>
* }> $definitions
* @param list<SetLevel> $levels
*/
private function seedChildren(
ElementRepository $repository,
Set $set,
array $levels,
int $depth,
?Element $parentElement,
array $definitions,
): void {
if ($definitions === []) {
return;
}
$level = $levels[$depth] ?? null;
if ($level === null) {
throw new RuntimeException('element level not defined');
}
$siblings = $parentElement === null
? $repository->findTopLevelBySet($set)
: $repository->findByParentElement($parentElement);
@ -174,13 +170,12 @@ class ElementSeeder extends Seeder
$element = $this->findSibling(
siblings: $siblings,
name: $definition['name'],
kind: $definition['kind'],
level: $level,
);
if ($element === null) {
$element = $repository->create(new CreateElementDto(
set: $set,
name: $definition['name'],
kind: $definition['kind'],
level: $level,
parentElement: $parentElement,
));
$siblings[] = $element;
@ -189,6 +184,8 @@ class ElementSeeder extends Seeder
$this->seedChildren(
repository: $repository,
set: $set,
levels: $levels,
depth: $depth + 1,
parentElement: $element,
definitions: $definition['children'],
);
@ -201,12 +198,12 @@ class ElementSeeder extends Seeder
private function findSibling(
array $siblings,
string $name,
string $kind,
SetLevel $level,
): ?Element {
foreach ($siblings as $sibling) {
if (
$sibling->getName() === $name
&& $sibling->getKind() === $kind
&& $sibling->getLevel()->getId() === $level->getId()
) {
return $sibling;
}

View file

@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use App\Set\CreateSetLevelDto;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
use Illuminate\Database\Seeder;
class SetLevelSeeder extends Seeder
{
private const array KINDS_BY_SET = [
'Bible' => ['book', 'portion', 'chapter'],
'Course' => ['module', 'lesson'],
'Fitness Program' => ['phase', 'workout', 'exercise'],
];
public function run(): void
{
$repository = app(SetLevelRepository::class);
foreach (app(SetRepository::class)->all() as $set) {
$kinds = self::KINDS_BY_SET[$set->getName()] ?? null;
if ($kinds === null) {
continue;
}
$existingKinds = array_map(function ($level): string {
return $level->getKind();
}, $repository->findBySet($set));
foreach ($kinds as $kind) {
if (in_array($kind, $existingKinds, true)) {
continue;
}
$repository->create(new CreateSetLevelDto(
set: $set,
kind: $kind,
));
$existingKinds[] = $kind;
}
}
}
}