snapshot schedule source data

This commit is contained in:
Yisroel Baum 2026-08-10 22:36:14 +03:00
parent 14fe2fcb46
commit f500bd6095
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
14 changed files with 84 additions and 87 deletions

View file

@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Element\Element;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException; use App\Exceptions\NotFoundException;
use App\Schedule\Schedule; use App\Schedule\Schedule;
@ -96,13 +95,10 @@ class ScheduleController extends Controller
*/ */
private function summaryPayload(Schedule $schedule): array private function summaryPayload(Schedule $schedule): array
{ {
$set = $schedule->getSet();
return [ return [
'id' => $schedule->getId(), 'id' => $schedule->getId(),
'set' => [ 'set' => [
'id' => $set->getId(), 'name' => $schedule->getSetName(),
'name' => $set->getName(),
], ],
'elementKind' => $schedule->getElementKind(), 'elementKind' => $schedule->getElementKind(),
'startDate' => $schedule->getStartDate()->format('Y-m-d'), 'startDate' => $schedule->getStartDate()->format('Y-m-d'),
@ -150,8 +146,7 @@ class ScheduleController extends Controller
} }
/** /**
* @return array{element: array{ * @return array{id: int, element: array{
* id: int,
* name: string, * name: string,
* kind: string, * kind: string,
* path: list<string> * path: list<string>
@ -160,34 +155,16 @@ class ScheduleController extends Controller
private function assignmentPayload( private function assignmentPayload(
ScheduleAssignment $assignment, ScheduleAssignment $assignment,
): array { ): array {
$element = $assignment->getElement();
return [ return [
'id' => $assignment->getId(),
'element' => [ 'element' => [
'id' => $element->getId(), 'name' => $assignment->getName(),
'name' => $element->getName(), 'kind' => $assignment->getKind(),
'kind' => $element->getKind(), 'path' => $assignment->getPath(),
'path' => $this->elementPath($element),
], ],
]; ];
} }
/**
* @return list<string>
*/
private function elementPath(Element $element): array
{
$path = [];
$currentElement = $element;
while ($currentElement !== null) {
array_unshift($path, $currentElement->getName());
$currentElement = $currentElement->getParentElement();
}
return $path;
}
private function user(Request $request): User private function user(Request $request): User
{ {
/** @var User $user */ /** @var User $user */

View file

@ -2,13 +2,17 @@
namespace App\Schedule; namespace App\Schedule;
use App\Element\Element;
use DateTimeImmutable; use DateTimeImmutable;
final readonly class CreateScheduleAssignmentDto final readonly class CreateScheduleAssignmentDto
{ {
/**
* @param list<string> $path
*/
public function __construct( public function __construct(
public Element $element, public string $name,
public string $kind,
public array $path,
public DateTimeImmutable $scheduledDate, public DateTimeImmutable $scheduledDate,
public int $position, public int $position,
) {} ) {}

View file

@ -2,7 +2,6 @@
namespace App\Schedule; namespace App\Schedule;
use App\Set\Set;
use App\User\User; use App\User\User;
use DateTimeImmutable; use DateTimeImmutable;
@ -13,7 +12,7 @@ final readonly class CreateScheduleDto
*/ */
public function __construct( public function __construct(
public User $user, public User $user,
public Set $set, public string $setName,
public string $elementKind, public string $elementKind,
public DateTimeImmutable $startDate, public DateTimeImmutable $startDate,
public DateTimeImmutable $targetDate, public DateTimeImmutable $targetDate,

View file

@ -2,27 +2,19 @@
namespace App\Schedule; namespace App\Schedule;
use App\Element\ElementRepository;
use App\Set\SetRepository;
use App\User\User; use App\User\User;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use RuntimeException;
class EloquentScheduleRepository implements ScheduleRepository class EloquentScheduleRepository implements ScheduleRepository
{ {
public function __construct(
private SetRepository $setRepository,
private ElementRepository $elementRepository,
) {}
public function create(CreateScheduleDto $dto): Schedule public function create(CreateScheduleDto $dto): Schedule
{ {
return DB::transaction(function () use ($dto): Schedule { return DB::transaction(function () use ($dto): Schedule {
$model = ScheduleModel::create([ $model = ScheduleModel::create([
'user_id' => $dto->user->getId(), 'user_id' => $dto->user->getId(),
'set_id' => $dto->set->getId(), 'set_name' => $dto->setName,
'element_kind' => $dto->elementKind, 'element_kind' => $dto->elementKind,
'start_date' => $dto->startDate->format('Y-m-d'), 'start_date' => $dto->startDate->format('Y-m-d'),
'target_date' => $dto->targetDate->format('Y-m-d'), 'target_date' => $dto->targetDate->format('Y-m-d'),
@ -31,7 +23,9 @@ class EloquentScheduleRepository implements ScheduleRepository
foreach ($dto->assignments as $assignmentDto) { foreach ($dto->assignments as $assignmentDto) {
ScheduleAssignmentModel::create([ ScheduleAssignmentModel::create([
'schedule_id' => $model->id, 'schedule_id' => $model->id,
'element_id' => $assignmentDto->element->getId(), 'element_name' => $assignmentDto->name,
'element_kind' => $assignmentDto->kind,
'element_path' => $assignmentDto->path,
'scheduled_date' => $assignmentDto->scheduledDate 'scheduled_date' => $assignmentDto->scheduledDate
->format('Y-m-d'), ->format('Y-m-d'),
'position' => $assignmentDto->position, 'position' => $assignmentDto->position,
@ -69,11 +63,6 @@ class EloquentScheduleRepository implements ScheduleRepository
private function toDomain(ScheduleModel $model, User $user): Schedule private function toDomain(ScheduleModel $model, User $user): Schedule
{ {
$set = $this->setRepository->find($model->set_id);
if ($set === null) {
throw new RuntimeException('schedule set not found');
}
$assignmentModels = ScheduleAssignmentModel::query() $assignmentModels = ScheduleAssignmentModel::query()
->where('schedule_id', $model->id) ->where('schedule_id', $model->id)
->orderBy('position') ->orderBy('position')
@ -82,16 +71,11 @@ class EloquentScheduleRepository implements ScheduleRepository
$assignments = []; $assignments = [];
foreach ($assignmentModels as $assignmentModel) { foreach ($assignmentModels as $assignmentModel) {
$element = $this->elementRepository->find(
$assignmentModel->element_id,
);
if ($element === null) {
throw new RuntimeException('schedule element not found');
}
$assignments[] = new ScheduleAssignment( $assignments[] = new ScheduleAssignment(
id: $assignmentModel->id, id: $assignmentModel->id,
element: $element, name: $assignmentModel->element_name,
kind: $assignmentModel->element_kind,
path: $assignmentModel->element_path,
scheduledDate: $this->date($assignmentModel->scheduled_date), scheduledDate: $this->date($assignmentModel->scheduled_date),
position: $assignmentModel->position, position: $assignmentModel->position,
); );
@ -100,7 +84,7 @@ class EloquentScheduleRepository implements ScheduleRepository
return new Schedule( return new Schedule(
id: $model->id, id: $model->id,
user: $user, user: $user,
set: $set, setName: $model->set_name,
elementKind: $model->element_kind, elementKind: $model->element_kind,
startDate: $this->date($model->start_date), startDate: $this->date($model->start_date),
targetDate: $this->date($model->target_date), targetDate: $this->date($model->target_date),

View file

@ -2,7 +2,6 @@
namespace App\Schedule; namespace App\Schedule;
use App\Set\Set;
use App\User\User; use App\User\User;
use DateTimeImmutable; use DateTimeImmutable;
@ -14,7 +13,7 @@ final readonly class Schedule
public function __construct( public function __construct(
private int $id, private int $id,
private User $user, private User $user,
private Set $set, private string $setName,
private string $elementKind, private string $elementKind,
private DateTimeImmutable $startDate, private DateTimeImmutable $startDate,
private DateTimeImmutable $targetDate, private DateTimeImmutable $targetDate,
@ -31,9 +30,9 @@ final readonly class Schedule
return $this->user; return $this->user;
} }
public function getSet(): Set public function getSetName(): string
{ {
return $this->set; return $this->setName;
} }
public function getElementKind(): string public function getElementKind(): string

View file

@ -2,14 +2,18 @@
namespace App\Schedule; namespace App\Schedule;
use App\Element\Element;
use DateTimeImmutable; use DateTimeImmutable;
final readonly class ScheduleAssignment final readonly class ScheduleAssignment
{ {
/**
* @param list<string> $path
*/
public function __construct( public function __construct(
private int $id, private int $id,
private Element $element, private string $name,
private string $kind,
private array $path,
private DateTimeImmutable $scheduledDate, private DateTimeImmutable $scheduledDate,
private int $position, private int $position,
) {} ) {}
@ -19,9 +23,22 @@ final readonly class ScheduleAssignment
return $this->id; return $this->id;
} }
public function getElement(): Element public function getName(): string
{ {
return $this->element; return $this->name;
}
public function getKind(): string
{
return $this->kind;
}
/**
* @return list<string>
*/
public function getPath(): array
{
return $this->path;
} }
public function getScheduledDate(): DateTimeImmutable public function getScheduledDate(): DateTimeImmutable

View file

@ -9,7 +9,9 @@ use Illuminate\Database\Eloquent\Model;
/** /**
* @property int $id * @property int $id
* @property int $schedule_id * @property int $schedule_id
* @property int $element_id * @property string $element_name
* @property string $element_kind
* @property list<string> $element_path
* @property string $scheduled_date * @property string $scheduled_date
* @property int $position * @property int $position
* *
@ -21,7 +23,9 @@ use Illuminate\Database\Eloquent\Model;
*/ */
#[Fillable([ #[Fillable([
'schedule_id', 'schedule_id',
'element_id', 'element_name',
'element_kind',
'element_path',
'scheduled_date', 'scheduled_date',
'position', 'position',
])] ])]
@ -38,7 +42,7 @@ class ScheduleAssignmentModel extends Model
{ {
return [ return [
'schedule_id' => 'integer', 'schedule_id' => 'integer',
'element_id' => 'integer', 'element_path' => 'array',
'position' => 'integer', 'position' => 'integer',
]; ];
} }

View file

@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Model;
/** /**
* @property int $id * @property int $id
* @property int $user_id * @property int $user_id
* @property int $set_id * @property string $set_name
* @property string $element_kind * @property string $element_kind
* @property string $start_date * @property string $start_date
* @property string $target_date * @property string $target_date
@ -22,7 +22,7 @@ use Illuminate\Database\Eloquent\Model;
*/ */
#[Fillable([ #[Fillable([
'user_id', 'user_id',
'set_id', 'set_name',
'element_kind', 'element_kind',
'start_date', 'start_date',
'target_date', 'target_date',
@ -40,7 +40,6 @@ class ScheduleModel extends Model
{ {
return [ return [
'user_id' => 'integer', 'user_id' => 'integer',
'set_id' => 'integer',
]; ];
} }
} }

View file

@ -70,7 +70,7 @@ class CreateSchedule
return $this->scheduleRepository->create(new CreateScheduleDto( return $this->scheduleRepository->create(new CreateScheduleDto(
user: $request->user, user: $request->user,
set: $set, setName: $set->getName(),
elementKind: $request->elementKind, elementKind: $request->elementKind,
startDate: $startDate, startDate: $startDate,
targetDate: $targetDate, targetDate: $targetDate,
@ -153,7 +153,9 @@ class CreateSchedule
dayCount: $dayCount, dayCount: $dayCount,
); );
$assignments[] = new CreateScheduleAssignmentDto( $assignments[] = new CreateScheduleAssignmentDto(
element: $element, name: $element->getName(),
kind: $element->getKind(),
path: $this->elementPath($element),
scheduledDate: $startDate->modify("+{$dayIndex} days"), scheduledDate: $startDate->modify("+{$dayIndex} days"),
position: $index + 1, position: $index + 1,
); );
@ -162,6 +164,22 @@ class CreateSchedule
return $assignments; return $assignments;
} }
/**
* @return list<string>
*/
private function elementPath(Element $element): array
{
$path = [];
$currentElement = $element;
while ($currentElement !== null) {
array_unshift($path, $currentElement->getName());
$currentElement = $currentElement->getParentElement();
}
return $path;
}
private function dayIndex( private function dayIndex(
int $elementIndex, int $elementIndex,
int $elementCount, int $elementCount,

View file

@ -13,9 +13,7 @@ return new class extends Migration
$table->foreignId('user_id') $table->foreignId('user_id')
->constrained('users') ->constrained('users')
->restrictOnDelete(); ->restrictOnDelete();
$table->foreignId('set_id') $table->string('set_name');
->constrained('sets')
->restrictOnDelete();
$table->string('element_kind'); $table->string('element_kind');
$table->date('start_date'); $table->date('start_date');
$table->date('target_date'); $table->date('target_date');

View file

@ -15,12 +15,11 @@ return new class extends Migration
$table->foreignId('schedule_id') $table->foreignId('schedule_id')
->constrained('schedules') ->constrained('schedules')
->cascadeOnDelete(); ->cascadeOnDelete();
$table->foreignId('element_id') $table->string('element_name');
->constrained('elements') $table->string('element_kind');
->restrictOnDelete(); $table->json('element_path');
$table->date('scheduled_date'); $table->date('scheduled_date');
$table->unsignedInteger('position'); $table->unsignedInteger('position');
$table->unique(['schedule_id', 'element_id']);
$table->unique(['schedule_id', 'position']); $table->unique(['schedule_id', 'position']);
$table->index([ $table->index([
'schedule_id', 'schedule_id',

View file

@ -45,7 +45,7 @@ const bibleLayout = {
const scheduleDetail = { const scheduleDetail = {
schedule: { schedule: {
id: 73, id: 73,
set: { id: 41, name: 'Bible' }, set: { name: 'Bible' },
elementKind: 'chapter', elementKind: 'chapter',
startDate: '2026-08-10', startDate: '2026-08-10',
targetDate: '2026-08-12', targetDate: '2026-08-12',
@ -55,8 +55,8 @@ const scheduleDetail = {
date: '2026-08-10', date: '2026-08-10',
assignments: [ assignments: [
{ {
id: 1,
element: { element: {
id: 4,
name: 'Chapter 1', name: 'Chapter 1',
kind: 'chapter', kind: 'chapter',
path: ['Genesis', 'Creation', 'Chapter 1'], path: ['Genesis', 'Creation', 'Chapter 1'],
@ -69,8 +69,8 @@ const scheduleDetail = {
date: '2026-08-12', date: '2026-08-12',
assignments: [ assignments: [
{ {
id: 2,
element: { element: {
id: 5,
name: 'Chapter 1', name: 'Chapter 1',
kind: 'chapter', kind: 'chapter',
path: ['Exodus', 'Chapter 1'], path: ['Exodus', 'Chapter 1'],
@ -213,7 +213,7 @@ describe('set scheduling', () => {
schedules: [ schedules: [
{ {
id: 73, id: 73,
set: { id: 41, name: 'Bible' }, set: { name: 'Bible' },
elementKind: 'chapter', elementKind: 'chapter',
startDate: '2026-08-10', startDate: '2026-08-10',
targetDate: '2026-08-12', targetDate: '2026-08-12',

View file

@ -9,7 +9,6 @@ const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
export const scheduleSummarySchema = z.object({ export const scheduleSummarySchema = z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
set: z.object({ set: z.object({
id: z.number().int().positive(),
name: z.string().min(1), name: z.string().min(1),
}), }),
elementKind: z.string().min(1), elementKind: z.string().min(1),
@ -19,8 +18,8 @@ export const scheduleSummarySchema = z.object({
}) })
const scheduleAssignmentSchema = z.object({ const scheduleAssignmentSchema = z.object({
element: z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
element: z.object({
name: z.string().min(1), name: z.string().min(1),
kind: z.string().min(1), kind: z.string().min(1),
path: z.array(z.string().min(1)).min(1), path: z.array(z.string().min(1)).min(1),

View file

@ -117,7 +117,7 @@ function pluralKind(kind: string, count: number): string {
<p v-if="day.assignments.length === 0" class="rest-day">Rest day</p> <p v-if="day.assignments.length === 0" class="rest-day">Rest day</p>
<ul v-else class="assignment-list"> <ul v-else class="assignment-list">
<li v-for="assignment in day.assignments" :key="assignment.element.id"> <li v-for="assignment in day.assignments" :key="assignment.id">
<span class="assignment-path">{{ assignment.element.path.join(' / ') }}</span> <span class="assignment-path">{{ assignment.element.path.join(' / ') }}</span>
<span class="assignment-kind">{{ humanizeKind(assignment.element.kind) }}</span> <span class="assignment-kind">{{ humanizeKind(assignment.element.kind) }}</span>
</li> </li>