diff --git a/backend/app/Http/Controllers/ScheduleController.php b/backend/app/Http/Controllers/ScheduleController.php index fb0bb40..cc8cd60 100644 --- a/backend/app/Http/Controllers/ScheduleController.php +++ b/backend/app/Http/Controllers/ScheduleController.php @@ -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'), ), diff --git a/backend/app/Http/Controllers/SetController.php b/backend/app/Http/Controllers/SetController.php index 024e83c..fe09b72 100644 --- a/backend/app/Http/Controllers/SetController.php +++ b/backend/app/Http/Controllers/SetController.php @@ -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 * } */ @@ -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); diff --git a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php index 1759d73..fb8635e 100644 --- a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php +++ b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php @@ -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, diff --git a/backend/app/Schedule/UseCases/CreateSchedule/CreateScheduleRequest.php b/backend/app/Schedule/UseCases/CreateSchedule/CreateScheduleRequest.php index d75ab71..70ab5db 100644 --- a/backend/app/Schedule/UseCases/CreateSchedule/CreateScheduleRequest.php +++ b/backend/app/Schedule/UseCases/CreateSchedule/CreateScheduleRequest.php @@ -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, ) {} diff --git a/backend/app/Set/UseCases/GetSetLayout/GetSetLayout.php b/backend/app/Set/UseCases/GetSetLayout/GetSetLayout.php index 39085e1..12c59d9 100644 --- a/backend/app/Set/UseCases/GetSetLayout/GetSetLayout.php +++ b/backend/app/Set/UseCases/GetSetLayout/GetSetLayout.php @@ -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), ); } diff --git a/backend/app/Set/UseCases/GetSetLayout/SetLayout.php b/backend/app/Set/UseCases/GetSetLayout/SetLayout.php index c98974a..69cf008 100644 --- a/backend/app/Set/UseCases/GetSetLayout/SetLayout.php +++ b/backend/app/Set/UseCases/GetSetLayout/SetLayout.php @@ -7,10 +7,12 @@ use App\Set\Set; final readonly class SetLayout { /** + * @param list $levels * @param list $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 + */ + public function getLevels(): array + { + return $this->levels; + } + /** * @return list */ diff --git a/backend/app/Set/UseCases/GetSetLayout/SetLayoutLevel.php b/backend/app/Set/UseCases/GetSetLayout/SetLayoutLevel.php new file mode 100644 index 0000000..6ebac06 --- /dev/null +++ b/backend/app/Set/UseCases/GetSetLayout/SetLayoutLevel.php @@ -0,0 +1,23 @@ +level; + } + + public function getElementCount(): int + { + return $this->elementCount; + } +} diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index c90217c..afd1905 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder { $this->call(UserSeeder::class); $this->call(SetSeeder::class); + $this->call(SetLevelSeeder::class); $this->call(ElementSeeder::class); } } diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index bde3ed9..52fe294 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -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 * }> $definitions + * @param list $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; } diff --git a/backend/database/seeders/SetLevelSeeder.php b/backend/database/seeders/SetLevelSeeder.php new file mode 100644 index 0000000..72c239c --- /dev/null +++ b/backend/database/seeders/SetLevelSeeder.php @@ -0,0 +1,45 @@ + ['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; + } + } + } +}