trip-planner/backend/app/Domain/Trip/Services/CalendarSlotService.php
myrmidex 935162ea70 Implement timeline scheduling feature with hour-based calendar slots
- 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
2025-10-07 22:54:09 +02:00

111 lines
No EOL
3.4 KiB
PHP

<?php
namespace App\Domain\Trip\Services;
use App\Models\Trip;
use App\Models\CalendarSlot;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class CalendarSlotService
{
public function createOrUpdateSlotsForTrip(Trip $trip): Collection
{
if (!$trip->start_date || !$trip->end_date) {
return collect();
}
// Fresh load to avoid stale relationship data
$trip->refresh();
$existingSlots = $trip->calendarSlots;
$existingSlotsMap = $existingSlots->keyBy(function ($slot) {
return $slot->slot_date instanceof \Carbon\Carbon
? $slot->slot_date->toDateString()
: $slot->slot_date;
});
$startDate = Carbon::parse($trip->start_date);
$endDate = Carbon::parse($trip->end_date);
$newSlots = collect();
$currentDate = $startDate->copy();
$dayNumber = 1;
while ($currentDate->lte($endDate)) {
$slotDate = $currentDate->toDateString();
if (!$existingSlotsMap->has($slotDate)) {
$slot = CalendarSlot::create([
'trip_id' => $trip->id,
'name' => 'Day ' . $dayNumber,
'slot_date' => $slotDate,
'datetime_start' => $currentDate->copy()->startOfDay(),
'datetime_end' => $currentDate->copy()->endOfDay(),
'slot_order' => $dayNumber,
]);
$newSlots->push($slot);
} else {
$existingSlot = $existingSlotsMap->get($slotDate);
$existingSlot->update([
'slot_order' => $dayNumber,
'datetime_start' => $currentDate->copy()->startOfDay(),
'datetime_end' => $currentDate->copy()->endOfDay(),
]);
$newSlots->push($existingSlot);
}
$currentDate->addDay();
$dayNumber++;
}
$trip->calendarSlots()
->whereNotIn('slot_date', $newSlots->pluck('slot_date'))
->delete();
return $newSlots;
}
public function deleteSlotsForTrip(Trip $trip): void
{
$trip->calendarSlots()->delete();
}
/**
* Calculate the slot_order for a new slot based on datetime_start
* Orders chronologically by start time within the same day
*/
public function calculateSlotOrder(int $tripId, string $slotDate, Carbon $datetimeStart): int
{
$maxOrder = CalendarSlot::where('trip_id', $tripId)
->where('slot_date', $slotDate)
->where('datetime_start', '<', $datetimeStart)
->max('slot_order');
return ($maxOrder ?? -1) + 1;
}
/**
* Recalculate slot_order for all slots on a given date
* Orders by datetime_start ASC
* Uses database transaction with individual updates for safety
*/
public function recalculateSlotOrdersForDate(int $tripId, string $slotDate): void
{
$slots = CalendarSlot::where('trip_id', $tripId)
->where('slot_date', $slotDate)
->orderBy('datetime_start')
->get();
if ($slots->isEmpty()) {
return;
}
// Update each slot's order within a transaction
\DB::transaction(function () use ($slots) {
foreach ($slots as $index => $slot) {
$slot->slot_order = $index;
$slot->save();
}
});
}
}