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( new CreateScheduleRequest(
user: $user, user: $user,
setId: $input->integer('setId'), setId: $input->integer('setId'),
elementKind: $input->string('elementKind'), levelId: $input->integer('levelId'),
startDate: $input->string('startDate'), startDate: $input->string('startDate'),
targetDate: $input->string('targetDate'), 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\ElementLayoutNode;
use App\Set\UseCases\GetSetLayout\GetSetLayout; use App\Set\UseCases\GetSetLayout\GetSetLayout;
use App\Set\UseCases\GetSetLayout\GetSetLayoutRequest; use App\Set\UseCases\GetSetLayout\GetSetLayoutRequest;
use App\Set\UseCases\GetSetLayout\SetLayoutLevel;
use App\Set\UseCases\ListSets\ListSets; use App\Set\UseCases\ListSets\ListSets;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -49,6 +50,19 @@ class SetController extends Controller
'id' => $set->getId(), 'id' => $set->getId(),
'name' => $set->getName(), '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( 'elements' => array_map(
function (ElementLayoutNode $node): array { function (ElementLayoutNode $node): array {
return $this->elementPayload($node); return $this->elementPayload($node);
@ -63,6 +77,7 @@ class SetController extends Controller
* id: int, * id: int,
* name: string, * name: string,
* kind: string, * kind: string,
* levelId: int,
* children: list<mixed> * children: list<mixed>
* } * }
*/ */
@ -74,6 +89,7 @@ class SetController extends Controller
'id' => $element->getId(), 'id' => $element->getId(),
'name' => $element->getName(), 'name' => $element->getName(),
'kind' => $element->getKind(), 'kind' => $element->getKind(),
'levelId' => $element->getLevel()->getId(),
'children' => array_map( 'children' => array_map(
function (ElementLayoutNode $childNode): array { function (ElementLayoutNode $childNode): array {
return $this->elementPayload($childNode); return $this->elementPayload($childNode);

View file

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

View file

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

View file

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

View file

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

View file

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