- Add CalendarSlotService methods for slot order calculation - Update PlannedItemController to create slots from datetime - Update CalendarSlotController with proper eager loading - Create timeline UI components (TripTimeline, DaySection, HourRow, ScheduleItemModal) - Add comprehensive PHPUnit tests (PlannedItemTest, CalendarSlotServiceTest) - Add comprehensive Selenium E2E tests (timeline-scheduling.test.js) - Update PlannablesList to support onItemsChange callback
104 lines
3.6 KiB
PHP
104 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Domain\PlannedItem\Actions;
|
|
|
|
use App\Domain\Trip\Services\CalendarSlotService;
|
|
use App\Models\PlannableItem;
|
|
use App\Models\CalendarSlot;
|
|
use App\Models\PlannedItem;
|
|
use Carbon\Carbon;
|
|
|
|
class CreatePlannedItemAction
|
|
{
|
|
public function __construct(
|
|
private CalendarSlotService $calendarSlotService
|
|
) {}
|
|
|
|
/**
|
|
* Create a new planned item from datetime parameters
|
|
*
|
|
* @param array $data ['plannable_item_id', 'trip_id', 'start_datetime', 'end_datetime', 'sort_order'?]
|
|
* @return CalendarSlot The calendar slot with loaded planned items
|
|
* @throws \Exception
|
|
*/
|
|
public function execute(array $data): CalendarSlot
|
|
{
|
|
$startDatetime = Carbon::parse($data['start_datetime']);
|
|
$endDatetime = Carbon::parse($data['end_datetime']);
|
|
$slotDate = $startDatetime->toDateString();
|
|
|
|
// Verify the plannable item belongs to the trip
|
|
$plannableItem = PlannableItem::findOrFail($data['plannable_item_id']);
|
|
if ($plannableItem->trip_id !== $data['trip_id']) {
|
|
throw new \Exception('Plannable item does not belong to this trip');
|
|
}
|
|
|
|
// Use database transaction to prevent race conditions
|
|
$calendarSlot = \DB::transaction(function () use ($data, $plannableItem, $startDatetime, $endDatetime, $slotDate) {
|
|
// Use firstOrCreate to prevent duplicate slots with same times
|
|
$calendarSlot = CalendarSlot::firstOrCreate(
|
|
[
|
|
'trip_id' => $data['trip_id'],
|
|
'datetime_start' => $startDatetime,
|
|
'datetime_end' => $endDatetime,
|
|
'slot_date' => $slotDate,
|
|
],
|
|
[
|
|
'name' => $plannableItem->name,
|
|
'slot_order' => 0, // Will be recalculated
|
|
]
|
|
);
|
|
|
|
// Recalculate slot orders to ensure consistency
|
|
$this->calendarSlotService->recalculateSlotOrdersForDate(
|
|
$data['trip_id'],
|
|
$slotDate
|
|
);
|
|
|
|
// Create PlannedItem link (use firstOrCreate to prevent race condition duplicates)
|
|
PlannedItem::firstOrCreate(
|
|
[
|
|
'plannable_item_id' => $data['plannable_item_id'],
|
|
'calendar_slot_id' => $calendarSlot->id,
|
|
],
|
|
[
|
|
'sort_order' => $data['sort_order'] ?? 0,
|
|
]
|
|
);
|
|
|
|
return $calendarSlot;
|
|
});
|
|
|
|
return $calendarSlot->load(['plannedItems.plannableItem']);
|
|
}
|
|
|
|
/**
|
|
* Create planned item from existing calendar slot (legacy flow)
|
|
*
|
|
* @param array $data ['plannable_item_id', 'calendar_slot_id', 'sort_order'?]
|
|
* @return PlannedItem
|
|
* @throws \Exception
|
|
*/
|
|
public function executeFromSlot(array $data): PlannedItem
|
|
{
|
|
// Validate that calendar slot and plannable item belong to the same trip
|
|
$plannableItem = PlannableItem::findOrFail($data['plannable_item_id']);
|
|
$calendarSlot = CalendarSlot::findOrFail($data['calendar_slot_id']);
|
|
|
|
if ($plannableItem->trip_id !== $calendarSlot->trip_id) {
|
|
throw new \Exception('Calendar slot and plannable item must belong to the same trip');
|
|
}
|
|
|
|
$plannedItem = PlannedItem::updateOrCreate(
|
|
[
|
|
'plannable_item_id' => $data['plannable_item_id'],
|
|
'calendar_slot_id' => $data['calendar_slot_id'],
|
|
],
|
|
[
|
|
'sort_order' => $data['sort_order'] ?? 0,
|
|
]
|
|
);
|
|
|
|
return $plannedItem->load(['plannableItem', 'calendarSlot']);
|
|
}
|
|
}
|