release/v0.1.0 #24
16 changed files with 697 additions and 0 deletions
149
.claude/11/plan-of-attack.md
Normal file
149
.claude/11/plan-of-attack.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Plannable Items Feature - Plan of Attack
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implement a plannable items feature for trips, with a simplified 3-table structure that allows users to create items and organize them into calendar slots.
|
||||||
|
|
||||||
|
## Database Structure (3 tables)
|
||||||
|
|
||||||
|
### 1. `plannable_items` table
|
||||||
|
- `id` (primary key)
|
||||||
|
- `trip_id` (foreign key to trips)
|
||||||
|
- `name` (string)
|
||||||
|
- `type` (enum: hotel/restaurant/attraction/transport/activity)
|
||||||
|
- `address` (string, nullable)
|
||||||
|
- `notes` (text, nullable)
|
||||||
|
- `metadata` (JSON for type-specific fields)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
### 2. `calendar_slots` table
|
||||||
|
- `id` (primary key)
|
||||||
|
- `trip_id` (foreign key to trips)
|
||||||
|
- `name` (string, e.g., "Monday", "Day 1", "Morning of Day 2")
|
||||||
|
- `datetime_start`, `datetime_end`
|
||||||
|
- `slot_date` (date)
|
||||||
|
- `slot_order` (integer, for sorting)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
### 3. `planned_items` table (junction)
|
||||||
|
- `id` (primary key)
|
||||||
|
- `plannable_item_id` (foreign key)
|
||||||
|
- `calendar_slot_id` (foreign key)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
## Slot Management Strategy
|
||||||
|
- **Auto-create day-based slots** when trip is created or dates are updated
|
||||||
|
- Generate one slot per day between start_date and end_date
|
||||||
|
- Items can exist in "unplanned" state (no slot assignment) or be assigned to a slot
|
||||||
|
|
||||||
|
## Backend Implementation
|
||||||
|
|
||||||
|
### 1. Database Migrations
|
||||||
|
- Create migration for `plannable_items` table
|
||||||
|
- Create migration for `calendar_slots` table
|
||||||
|
- Create migration for `planned_items` junction table
|
||||||
|
|
||||||
|
### 2. Models and Relationships
|
||||||
|
- `PlannableItem` model (belongs to Trip, belongs to many CalendarSlots through PlannedItems)
|
||||||
|
- `CalendarSlot` model (belongs to Trip, has many PlannableItems through PlannedItems)
|
||||||
|
- `PlannedItem` model (junction model with additional attributes)
|
||||||
|
- Update `Trip` model to include relationships
|
||||||
|
|
||||||
|
### 3. Business Logic
|
||||||
|
- Auto-create/update slots when trip dates change
|
||||||
|
- Handle slot regeneration when dates are modified
|
||||||
|
- Preserve existing planned items when possible
|
||||||
|
|
||||||
|
### 4. API Endpoints
|
||||||
|
```
|
||||||
|
GET /api/trips/{id}/plannables - List all plannable items for a trip
|
||||||
|
POST /api/trips/{id}/plannables - Create new plannable item
|
||||||
|
PUT /api/plannables/{id} - Update plannable item
|
||||||
|
DELETE /api/plannables/{id} - Delete plannable item
|
||||||
|
|
||||||
|
GET /api/trips/{id}/calendar-slots - Get all slots for a trip
|
||||||
|
PUT /api/calendar-slots/{id} - Update slot (rename)
|
||||||
|
|
||||||
|
POST /api/planned-items - Assign item to slot
|
||||||
|
PUT /api/planned-items/{id} - Update assignment (change slot, time, order)
|
||||||
|
DELETE /api/planned-items/{id} - Unassign item from slot
|
||||||
|
PUT /api/calendar-slots/{id}/reorder - Reorder items within a slot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Implementation
|
||||||
|
|
||||||
|
### 1. Routing Setup
|
||||||
|
- Install `react-router-dom` package
|
||||||
|
- Configure routes in App.jsx
|
||||||
|
- Add route for trip detail page: `/trip/:id`
|
||||||
|
|
||||||
|
### 2. Trip Detail Page (`TripDetail.jsx`)
|
||||||
|
- Header section: Display trip name, dates, description
|
||||||
|
- Two-column layout:
|
||||||
|
- Left: Plannable items sidebar
|
||||||
|
- Right: Calendar view (future implementation)
|
||||||
|
|
||||||
|
### 3. Plannable Items Sidebar Components
|
||||||
|
|
||||||
|
#### `PlannablesList.jsx`
|
||||||
|
- "+" button at top to add new items
|
||||||
|
- "Unplanned Items" section
|
||||||
|
- Day slot sections with assigned items
|
||||||
|
- Drag & drop functionality between sections
|
||||||
|
|
||||||
|
#### `PlannableItem.jsx`
|
||||||
|
- Display item with type icon
|
||||||
|
- Show name and key details
|
||||||
|
- Edit/Delete actions
|
||||||
|
- Draggable wrapper
|
||||||
|
|
||||||
|
#### `PlannableForm.jsx`
|
||||||
|
- Modal/slide-out form
|
||||||
|
- Dynamic fields based on item type
|
||||||
|
- Validation
|
||||||
|
|
||||||
|
### 4. Update Existing Components
|
||||||
|
- Make `TripCard` clickable
|
||||||
|
- Add navigation to trip detail on click
|
||||||
|
- Update Dashboard to handle navigation
|
||||||
|
|
||||||
|
### 5. State Management
|
||||||
|
- Consider using Context or simple state management for:
|
||||||
|
- Plannable items list
|
||||||
|
- Calendar slots
|
||||||
|
- Drag & drop state
|
||||||
|
|
||||||
|
## UI/UX Considerations
|
||||||
|
- Type-specific icons (🏨 hotel, 🍽️ restaurant, 🎯 attraction, ✈️ transport, 🎭 activity)
|
||||||
|
- Color coding for different types
|
||||||
|
- Smooth drag & drop animations
|
||||||
|
- Loading states during API calls
|
||||||
|
- Confirmation dialogs for deletions
|
||||||
|
- Toast notifications for actions
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
1. ✅ Backend migrations and models - COMPLETED
|
||||||
|
- Created migrations for plannable_items, calendar_slots, planned_items tables
|
||||||
|
- Created models with relationships
|
||||||
|
2. ✅ Backend API endpoints - COMPLETED
|
||||||
|
- Created controllers for PlannableItem, CalendarSlot, PlannedItem
|
||||||
|
- Added all routes to api.php
|
||||||
|
- Organized controllers in DDD structure (Infrastructure/Domain)
|
||||||
|
3. ✅ Auto-slot creation - COMPLETED
|
||||||
|
- Created CalendarSlotService
|
||||||
|
- Created TripObserver to auto-create slots on trip creation/update
|
||||||
|
- Registered service provider
|
||||||
|
4. ⏳ Frontend routing setup - TODO
|
||||||
|
5. ⏳ Trip detail page structure - TODO
|
||||||
|
6. ⏳ Plannable items CRUD - TODO
|
||||||
|
7. ⏳ Calendar slots display - TODO
|
||||||
|
8. ⏳ Drag & drop functionality - TODO
|
||||||
|
9. ⏳ Polish and edge cases - TODO
|
||||||
|
|
||||||
|
## Future Enhancements (Not in MVP)
|
||||||
|
- Calendar view in right column
|
||||||
|
- Time-based slots (morning/afternoon/evening)
|
||||||
|
- Collaborative features (share with travel companions)
|
||||||
|
- Import from external sources (booking confirmations)
|
||||||
|
- Export to PDF/calendar formats
|
||||||
|
- Cost tracking and budgeting
|
||||||
|
- Map view of planned items
|
||||||
43
backend/app/Domain/Trip/Observers/TripObserver.php
Normal file
43
backend/app/Domain/Trip/Observers/TripObserver.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Trip\Observers;
|
||||||
|
|
||||||
|
use App\Models\Trip;
|
||||||
|
use App\Domain\Trip\Services\CalendarSlotService;
|
||||||
|
|
||||||
|
class TripObserver
|
||||||
|
{
|
||||||
|
protected CalendarSlotService $calendarSlotService;
|
||||||
|
|
||||||
|
public function __construct(CalendarSlotService $calendarSlotService)
|
||||||
|
{
|
||||||
|
$this->calendarSlotService = $calendarSlotService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function created(Trip $trip): void
|
||||||
|
{
|
||||||
|
$this->calendarSlotService->createOrUpdateSlotsForTrip($trip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(Trip $trip): void
|
||||||
|
{
|
||||||
|
if ($trip->isDirty(['start_date', 'end_date'])) {
|
||||||
|
$this->calendarSlotService->createOrUpdateSlotsForTrip($trip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(Trip $trip): void
|
||||||
|
{
|
||||||
|
$this->calendarSlotService->deleteSlotsForTrip($trip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restored(Trip $trip): void
|
||||||
|
{
|
||||||
|
$this->calendarSlotService->createOrUpdateSlotsForTrip($trip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forceDeleted(Trip $trip): void
|
||||||
|
{
|
||||||
|
$this->calendarSlotService->deleteSlotsForTrip($trip);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
backend/app/Domain/Trip/Providers/TripServiceProvider.php
Normal file
20
backend/app/Domain/Trip/Providers/TripServiceProvider.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Trip\Providers;
|
||||||
|
|
||||||
|
use App\Models\Trip;
|
||||||
|
use App\Domain\Trip\Observers\TripObserver;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class TripServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Trip::observe(TripObserver::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
backend/app/Domain/Trip/Services/CalendarSlotService.php
Normal file
66
backend/app/Domain/Trip/Services/CalendarSlotService.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingSlots = $trip->calendarSlots;
|
||||||
|
$existingSlotsMap = $existingSlots->keyBy('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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Http\Controllers\API\CalendarSlot;
|
||||||
|
|
||||||
|
use App\Infrastructure\Http\Controllers\Controller;
|
||||||
|
use App\Models\CalendarSlot;
|
||||||
|
use App\Models\Trip;
|
||||||
|
use App\Models\PlannedItem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class CalendarSlotController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Trip $trip): JsonResponse
|
||||||
|
{
|
||||||
|
$calendarSlots = $trip->calendarSlots()
|
||||||
|
->with(['plannableItems'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json($calendarSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, CalendarSlot $calendarSlot): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'sometimes|required|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$calendarSlot->update($validated);
|
||||||
|
|
||||||
|
return response()->json($calendarSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reorder(Request $request, CalendarSlot $calendarSlot): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'items' => 'required|array',
|
||||||
|
'items.*.plannable_item_id' => 'required|exists:plannable_items,id',
|
||||||
|
'items.*.sort_order' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($validated['items'] as $item) {
|
||||||
|
PlannedItem::where('calendar_slot_id', $calendarSlot->id)
|
||||||
|
->where('plannable_item_id', $item['plannable_item_id'])
|
||||||
|
->update(['sort_order' => $item['sort_order']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Items reordered successfully']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Http\Controllers\API\PlannableItem;
|
||||||
|
|
||||||
|
use App\Infrastructure\Http\Controllers\Controller;
|
||||||
|
use App\Models\PlannableItem;
|
||||||
|
use App\Models\Trip;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class PlannableItemController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Trip $trip): JsonResponse
|
||||||
|
{
|
||||||
|
$plannableItems = $trip->plannableItems()
|
||||||
|
->with(['calendarSlots'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json($plannableItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Trip $trip): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'type' => 'required|in:hotel,restaurant,attraction,transport,activity',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'metadata' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$plannableItem = $trip->plannableItems()->create($validated);
|
||||||
|
|
||||||
|
return response()->json($plannableItem, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(PlannableItem $plannableItem): JsonResponse
|
||||||
|
{
|
||||||
|
$plannableItem->load(['calendarSlots', 'trip']);
|
||||||
|
return response()->json($plannableItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, PlannableItem $plannableItem): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'sometimes|required|string|max:255',
|
||||||
|
'type' => 'sometimes|required|in:hotel,restaurant,attraction,transport,activity',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'metadata' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$plannableItem->update($validated);
|
||||||
|
|
||||||
|
return response()->json($plannableItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(PlannableItem $plannableItem): JsonResponse
|
||||||
|
{
|
||||||
|
$plannableItem->delete();
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Http\Controllers\API\PlannedItem;
|
||||||
|
|
||||||
|
use App\Infrastructure\Http\Controllers\Controller;
|
||||||
|
use App\Models\PlannedItem;
|
||||||
|
use App\Models\PlannableItem;
|
||||||
|
use App\Models\CalendarSlot;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class PlannedItemController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'plannable_item_id' => 'required|exists:plannable_items,id',
|
||||||
|
'calendar_slot_id' => 'required|exists:calendar_slots,id',
|
||||||
|
'sort_order' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$plannedItem = PlannedItem::updateOrCreate(
|
||||||
|
[
|
||||||
|
'plannable_item_id' => $validated['plannable_item_id'],
|
||||||
|
'calendar_slot_id' => $validated['calendar_slot_id'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'sort_order' => $validated['sort_order'] ?? 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json($plannedItem->load(['plannableItem', 'calendarSlot']), 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, PlannedItem $plannedItem): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'calendar_slot_id' => 'sometimes|required|exists:calendar_slots,id',
|
||||||
|
'sort_order' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$plannedItem->update($validated);
|
||||||
|
|
||||||
|
return response()->json($plannedItem->load(['plannableItem', 'calendarSlot']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(PlannedItem $plannedItem): JsonResponse
|
||||||
|
{
|
||||||
|
$plannedItem->delete();
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/app/Models/CalendarSlot.php
Normal file
46
backend/app/Models/CalendarSlot.php
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class CalendarSlot extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'trip_id',
|
||||||
|
'name',
|
||||||
|
'datetime_start',
|
||||||
|
'datetime_end',
|
||||||
|
'slot_date',
|
||||||
|
'slot_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'datetime_start' => 'datetime',
|
||||||
|
'datetime_end' => 'datetime',
|
||||||
|
'slot_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function trip(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Trip::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function plannableItems(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(PlannableItem::class, 'planned_items')
|
||||||
|
->withPivot('sort_order')
|
||||||
|
->withTimestamps()
|
||||||
|
->orderBy('planned_items.sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function plannedItems()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PlannedItem::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/app/Models/PlannableItem.php
Normal file
44
backend/app/Models/PlannableItem.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class PlannableItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'trip_id',
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'address',
|
||||||
|
'notes',
|
||||||
|
'metadata',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function trip(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Trip::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarSlots(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(CalendarSlot::class, 'planned_items')
|
||||||
|
->withPivot('sort_order')
|
||||||
|
->withTimestamps()
|
||||||
|
->orderBy('planned_items.sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function plannedItems()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PlannedItem::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
backend/app/Models/PlannedItem.php
Normal file
28
backend/app/Models/PlannedItem.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class PlannedItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'plannable_item_id',
|
||||||
|
'calendar_slot_id',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function plannableItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PlannableItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarSlot(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CalendarSlot::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Trip extends Model
|
class Trip extends Model
|
||||||
{
|
{
|
||||||
|
|
@ -26,4 +27,14 @@ public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function plannableItems(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PlannableItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarSlots(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CalendarSlot::class)->orderBy('slot_date')->orderBy('slot_order');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,5 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Domain\Trip\Providers\TripServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('plannable_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('trip_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('name');
|
||||||
|
$table->enum('type', ['hotel', 'restaurant', 'attraction', 'transport', 'activity']);
|
||||||
|
$table->string('address')->nullable();
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('plannable_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('calendar_slots', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('trip_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('name');
|
||||||
|
$table->dateTime('datetime_start')->nullable();
|
||||||
|
$table->dateTime('datetime_end')->nullable();
|
||||||
|
$table->date('slot_date');
|
||||||
|
$table->integer('slot_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['trip_id', 'slot_date']);
|
||||||
|
$table->index(['trip_id', 'slot_order']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('calendar_slots');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('planned_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('plannable_item_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('calendar_slot_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['plannable_item_id', 'calendar_slot_id']);
|
||||||
|
$table->index(['calendar_slot_id', 'sort_order']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('planned_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
use App\Infrastructure\Http\Controllers\API\User\Auth\AuthController;
|
use App\Infrastructure\Http\Controllers\API\User\Auth\AuthController;
|
||||||
use App\Infrastructure\Http\Controllers\API\Trip\TripController;
|
use App\Infrastructure\Http\Controllers\API\Trip\TripController;
|
||||||
use App\Infrastructure\Http\Controllers\API\E2e\TestSetupController;
|
use App\Infrastructure\Http\Controllers\API\E2e\TestSetupController;
|
||||||
|
use App\Infrastructure\Http\Controllers\API\PlannableItem\PlannableItemController;
|
||||||
|
use App\Infrastructure\Http\Controllers\API\CalendarSlot\CalendarSlotController;
|
||||||
|
use App\Infrastructure\Http\Controllers\API\PlannedItem\PlannedItemController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
@ -31,4 +34,21 @@
|
||||||
|
|
||||||
// Trip routes
|
// Trip routes
|
||||||
Route::apiResource('trips', TripController::class);
|
Route::apiResource('trips', TripController::class);
|
||||||
|
|
||||||
|
// Plannable items routes
|
||||||
|
Route::get('trips/{trip}/plannables', [PlannableItemController::class, 'index']);
|
||||||
|
Route::post('trips/{trip}/plannables', [PlannableItemController::class, 'store']);
|
||||||
|
Route::get('plannables/{plannableItem}', [PlannableItemController::class, 'show']);
|
||||||
|
Route::put('plannables/{plannableItem}', [PlannableItemController::class, 'update']);
|
||||||
|
Route::delete('plannables/{plannableItem}', [PlannableItemController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Calendar slots routes
|
||||||
|
Route::get('trips/{trip}/calendar-slots', [CalendarSlotController::class, 'index']);
|
||||||
|
Route::put('calendar-slots/{calendarSlot}', [CalendarSlotController::class, 'update']);
|
||||||
|
Route::put('calendar-slots/{calendarSlot}/reorder', [CalendarSlotController::class, 'reorder']);
|
||||||
|
|
||||||
|
// Planned items routes
|
||||||
|
Route::post('planned-items', [PlannedItemController::class, 'store']);
|
||||||
|
Route::put('planned-items/{plannedItem}', [PlannedItemController::class, 'update']);
|
||||||
|
Route::delete('planned-items/{plannedItem}', [PlannedItemController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue