diff --git a/.gitignore b/.gitignore index c09ab17..70ea49a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ /.idea -/docker/data -/.claude -CLAUDE.md -/coverage \ No newline at end of file +/docker/data \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 4cbe1a5..b71b1ea 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -12,7 +12,6 @@ /.vscode /.zed /auth.json -/coverage /node_modules /public/build /public/hot diff --git a/backend/app/Domain/CalendarSlot/Policies/CalendarSlotPolicy.php b/backend/app/Domain/CalendarSlot/Policies/CalendarSlotPolicy.php deleted file mode 100644 index a23d08b..0000000 --- a/backend/app/Domain/CalendarSlot/Policies/CalendarSlotPolicy.php +++ /dev/null @@ -1,34 +0,0 @@ -created_by_user_id === $user->id; - } - - /** - * Determine if the user can update the calendar slot - */ - public function update(User $user, CalendarSlot $calendarSlot): bool - { - return $calendarSlot->trip->created_by_user_id === $user->id; - } - - /** - * Determine if the user can reorder items in the calendar slot - */ - public function reorder(User $user, CalendarSlot $calendarSlot): bool - { - return $calendarSlot->trip->created_by_user_id === $user->id; - } -} diff --git a/backend/app/Domain/PlannableItem/Policies/PlannableItemPolicy.php b/backend/app/Domain/PlannableItem/Policies/PlannableItemPolicy.php deleted file mode 100644 index 0bb267b..0000000 --- a/backend/app/Domain/PlannableItem/Policies/PlannableItemPolicy.php +++ /dev/null @@ -1,42 +0,0 @@ -created_by_user_id === $user->id; - } - - /** - * Determine if the user can view the plannable item - */ - public function view(User $user, PlannableItem $plannableItem): bool - { - return $plannableItem->trip->created_by_user_id === $user->id; - } - - /** - * Determine if the user can update the plannable item - */ - public function update(User $user, PlannableItem $plannableItem): bool - { - return $plannableItem->trip->created_by_user_id === $user->id; - } - - /** - * Determine if the user can delete the plannable item - */ - public function delete(User $user, PlannableItem $plannableItem): bool - { - return $plannableItem->trip->created_by_user_id === $user->id; - } -} diff --git a/backend/app/Domain/PlannedItem/Actions/CreatePlannedItemAction.php b/backend/app/Domain/PlannedItem/Actions/CreatePlannedItemAction.php deleted file mode 100644 index ed2523a..0000000 --- a/backend/app/Domain/PlannedItem/Actions/CreatePlannedItemAction.php +++ /dev/null @@ -1,104 +0,0 @@ -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']); - } -} diff --git a/backend/app/Domain/PlannedItem/Actions/DeletePlannedItemAction.php b/backend/app/Domain/PlannedItem/Actions/DeletePlannedItemAction.php deleted file mode 100644 index cfc43bf..0000000 --- a/backend/app/Domain/PlannedItem/Actions/DeletePlannedItemAction.php +++ /dev/null @@ -1,19 +0,0 @@ -delete(); - } -} diff --git a/backend/app/Domain/PlannedItem/Actions/UpdatePlannedItemAction.php b/backend/app/Domain/PlannedItem/Actions/UpdatePlannedItemAction.php deleted file mode 100644 index 88b89c8..0000000 --- a/backend/app/Domain/PlannedItem/Actions/UpdatePlannedItemAction.php +++ /dev/null @@ -1,22 +0,0 @@ -update($data); - - return $plannedItem->load(['plannableItem', 'calendarSlot']); - } -} diff --git a/backend/app/Domain/PlannedItem/Policies/PlannedItemPolicy.php b/backend/app/Domain/PlannedItem/Policies/PlannedItemPolicy.php deleted file mode 100644 index fec6fc2..0000000 --- a/backend/app/Domain/PlannedItem/Policies/PlannedItemPolicy.php +++ /dev/null @@ -1,54 +0,0 @@ -where('id', $tripId) - ->where('created_by_user_id', $user->id) - ->exists(); - } - - /** - * Determine if the user can update the planned item - */ - public function update(User $user, PlannedItem $plannedItem): bool - { - return \DB::table('planned_items') - ->join('calendar_slots', 'planned_items.calendar_slot_id', '=', 'calendar_slots.id') - ->join('trips', 'calendar_slots.trip_id', '=', 'trips.id') - ->where('planned_items.id', $plannedItem->id) - ->where('trips.created_by_user_id', $user->id) - ->exists(); - } - - /** - * Determine if the user can delete the planned item - */ - public function delete(User $user, PlannedItem $plannedItem): bool - { - return $this->update($user, $plannedItem); - } - - /** - * Determine if the user can move a planned item to a new calendar slot - */ - public function moveToSlot(User $user, int $calendarSlotId): bool - { - return \DB::table('calendar_slots') - ->join('trips', 'calendar_slots.trip_id', '=', 'trips.id') - ->where('calendar_slots.id', $calendarSlotId) - ->where('trips.created_by_user_id', $user->id) - ->exists(); - } -} diff --git a/backend/app/Domain/Trip/Observers/TripObserver.php b/backend/app/Domain/Trip/Observers/TripObserver.php deleted file mode 100644 index f0217f6..0000000 --- a/backend/app/Domain/Trip/Observers/TripObserver.php +++ /dev/null @@ -1,45 +0,0 @@ -calendarSlotService = $calendarSlotService; - } - - public function created(Trip $trip): void - { - if ($trip->start_date && $trip->end_date) { - $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); - } -} \ No newline at end of file diff --git a/backend/app/Domain/Trip/Policies/TripPolicy.php b/backend/app/Domain/Trip/Policies/TripPolicy.php deleted file mode 100644 index 5b7ea87..0000000 --- a/backend/app/Domain/Trip/Policies/TripPolicy.php +++ /dev/null @@ -1,33 +0,0 @@ -created_by_user_id === $user->id; - } - - /** - * Determine if the user can update the trip - */ - public function update(User $user, Trip $trip): bool - { - return $trip->created_by_user_id === $user->id; - } - - /** - * Determine if the user can delete the trip - */ - public function delete(User $user, Trip $trip): bool - { - return $trip->created_by_user_id === $user->id; - } -} diff --git a/backend/app/Domain/Trip/Providers/TripServiceProvider.php b/backend/app/Domain/Trip/Providers/TripServiceProvider.php deleted file mode 100644 index 929e0b1..0000000 --- a/backend/app/Domain/Trip/Providers/TripServiceProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -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(); - } - }); - } -} \ No newline at end of file diff --git a/backend/app/Infrastructure/Http/Controllers/API/User/Auth/AuthController.php b/backend/app/Http/Controllers/API/AuthController.php similarity index 96% rename from backend/app/Infrastructure/Http/Controllers/API/User/Auth/AuthController.php rename to backend/app/Http/Controllers/API/AuthController.php index bb9e583..455393a 100644 --- a/backend/app/Infrastructure/Http/Controllers/API/User/Auth/AuthController.php +++ b/backend/app/Http/Controllers/API/AuthController.php @@ -1,8 +1,8 @@ headers->get('Origin'); - $allowedOrigins = config('cors.allowed_origins', ['http://localhost:5173']); - $allowedOrigin = $allowedOrigins[0] ?? 'http://localhost:5173'; + $allowedOrigin = env('FRONTEND_URL', 'http://localhost:5173'); // Only set CORS headers if the origin matches our frontend if ($origin === $allowedOrigin) { diff --git a/backend/app/Infrastructure/Http/Controllers/API/CalendarSlot/CalendarSlotController.php b/backend/app/Infrastructure/Http/Controllers/API/CalendarSlot/CalendarSlotController.php deleted file mode 100644 index 58b1d76..0000000 --- a/backend/app/Infrastructure/Http/Controllers/API/CalendarSlot/CalendarSlotController.php +++ /dev/null @@ -1,81 +0,0 @@ -policy->viewAny(auth()->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $calendarSlots = $trip->calendarSlots() - ->with(['plannedItems.plannableItem']) - ->orderBy('slot_date') - ->orderBy('datetime_start') - ->get(); - - return response()->json(['data' => $calendarSlots]); - } - - public function update(Request $request, CalendarSlot $calendarSlot): JsonResponse - { - if (!$this->policy->update(auth()->user(), $calendarSlot)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $validated = $request->validate([ - 'name' => 'sometimes|required|string|max:255', - ]); - - $calendarSlot->update($validated); - - return response()->json(['data' => $calendarSlot]); - } - - public function reorder(Request $request, CalendarSlot $calendarSlot): JsonResponse - { - if (!$this->policy->reorder(auth()->user(), $calendarSlot)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $validated = $request->validate([ - 'items' => 'required|array', - 'items.*.plannable_item_id' => 'required|exists:plannable_items,id', - 'items.*.sort_order' => 'required|integer', - ]); - - // Validate all plannable items belong to the same trip - $trip = $calendarSlot->trip; - $validPlannableItemIds = $trip->plannableItems()->pluck('id')->toArray(); - - foreach ($validated['items'] as $item) { - if (!in_array($item['plannable_item_id'], $validPlannableItemIds)) { - return response()->json(['message' => 'Invalid plannable item for this trip'], 422); - } - } - - // Update sort orders in transaction - \DB::transaction(function () use ($validated, $calendarSlot) { - 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']); - } -} \ No newline at end of file diff --git a/backend/app/Infrastructure/Http/Controllers/API/E2e/TestSetupController.php b/backend/app/Infrastructure/Http/Controllers/API/E2e/TestSetupController.php deleted file mode 100644 index 1443ec8..0000000 --- a/backend/app/Infrastructure/Http/Controllers/API/E2e/TestSetupController.php +++ /dev/null @@ -1,73 +0,0 @@ -environment('production')) { - return response()->json(['error' => 'Not available in production'], 403); - } - - $validated = $request->validate([ - 'email' => 'required|email', - 'password' => 'required|min:8', - 'name' => 'required|string' - ]); - - $user = User::firstOrCreate( - ['email' => $validated['email']], - [ - 'name' => $validated['name'], - 'password' => Hash::make($validated['password']), - 'email_verified_at' => now(), - ] - ); - - // Ensure email_verified_at is set even for existing users - if (!$user->email_verified_at) { - $user->email_verified_at = now(); - $user->save(); - } - - return response()->json([ - 'success' => true, - 'message' => $user->wasRecentlyCreated ? 'Test user created' : 'Test user already exists', - 'data' => [ - 'id' => $user->id, - 'email' => $user->email, - 'name' => $user->name - ] - ]); - } - - /** - * Clean up test data - */ - public function cleanup(Request $request) - { - if (app()->environment('production')) { - return response()->json(['error' => 'Not available in production'], 403); - } - - // Delete test users (those with specific test email patterns) - $deleted = User::where('email', 'LIKE', 'test%@example.com') - ->orWhere('email', 'LIKE', 'test.user.%@example.com') - ->delete(); - - return response()->json([ - 'success' => true, - 'message' => "Deleted $deleted test users" - ]); - } -} \ No newline at end of file diff --git a/backend/app/Infrastructure/Http/Controllers/API/PlannableItem/PlannableItemController.php b/backend/app/Infrastructure/Http/Controllers/API/PlannableItem/PlannableItemController.php deleted file mode 100644 index 5ed4cb3..0000000 --- a/backend/app/Infrastructure/Http/Controllers/API/PlannableItem/PlannableItemController.php +++ /dev/null @@ -1,88 +0,0 @@ -policy->create(auth()->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $plannableItems = $trip->plannableItems() - ->with(['calendarSlots']) - ->get(); - - return response()->json(['data' => $plannableItems]); - } - - public function store(Request $request, Trip $trip): JsonResponse - { - if (!$this->policy->create(auth()->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $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(['data' => $plannableItem], 201); - } - - public function show(PlannableItem $plannableItem): JsonResponse - { - if (!$this->policy->view(auth()->user(), $plannableItem)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $plannableItem->load(['calendarSlots', 'trip']); - return response()->json($plannableItem); - } - - public function update(Request $request, PlannableItem $plannableItem): JsonResponse - { - if (!$this->policy->update(auth()->user(), $plannableItem)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $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(['data' => $plannableItem]); - } - - public function destroy(PlannableItem $plannableItem): JsonResponse - { - if (!$this->policy->delete(auth()->user(), $plannableItem)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $plannableItem->delete(); - - return response()->json(null, 204); - } -} \ No newline at end of file diff --git a/backend/app/Infrastructure/Http/Controllers/API/PlannedItem/PlannedItemController.php b/backend/app/Infrastructure/Http/Controllers/API/PlannedItem/PlannedItemController.php deleted file mode 100644 index 07b894a..0000000 --- a/backend/app/Infrastructure/Http/Controllers/API/PlannedItem/PlannedItemController.php +++ /dev/null @@ -1,92 +0,0 @@ -validate([ - 'plannable_item_id' => 'required|exists:plannable_items,id', - 'trip_id' => 'required|exists:trips,id', - 'start_datetime' => 'required|date', - 'end_datetime' => 'required|date|after:start_datetime', - 'calendar_slot_id' => 'sometimes|exists:calendar_slots,id', - 'sort_order' => 'nullable|integer', - ]); - - // Check authorization using Policy - if (!$this->policy->create(auth()->user(), $validated['trip_id'])) { - return response()->json(['message' => 'Forbidden'], 403); - } - - // If calendar_slot_id is provided, use existing flow (backward compatibility) - if (isset($validated['calendar_slot_id'])) { - $plannedItem = $this->createAction->executeFromSlot($validated); - return response()->json($plannedItem, 201); - } - - // New flow: Create CalendarSlot from datetime using Action - try { - $calendarSlot = $this->createAction->execute($validated); - return response()->json($calendarSlot, 201); - } catch (\Exception $e) { - return response()->json(['message' => $e->getMessage()], 422); - } - } - - public function update(Request $request, PlannedItem $plannedItem): JsonResponse - { - // Check authorization using Policy - if (!$this->policy->update(auth()->user(), $plannedItem)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $validated = $request->validate([ - 'calendar_slot_id' => 'sometimes|required|exists:calendar_slots,id', - 'sort_order' => 'nullable|integer', - ]); - - // If changing calendar slot, verify user owns the new slot's trip - if (isset($validated['calendar_slot_id'])) { - if (!$this->policy->moveToSlot(auth()->user(), $validated['calendar_slot_id'])) { - return response()->json(['message' => 'Forbidden'], 403); - } - } - - // Execute update using Action - $updatedItem = $this->updateAction->execute($plannedItem, $validated); - - return response()->json($updatedItem); - } - - public function destroy(PlannedItem $plannedItem): JsonResponse - { - // Check authorization using Policy - if (!$this->policy->delete(auth()->user(), $plannedItem)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - // Execute delete using Action - $this->deleteAction->execute($plannedItem); - - return response()->json(null, 204); - } -} \ No newline at end of file diff --git a/backend/app/Infrastructure/Http/Controllers/API/Trip/TripController.php b/backend/app/Infrastructure/Http/Controllers/API/Trip/TripController.php deleted file mode 100644 index 19bf89b..0000000 --- a/backend/app/Infrastructure/Http/Controllers/API/Trip/TripController.php +++ /dev/null @@ -1,93 +0,0 @@ -user()->id) - ->orderBy('created_at', 'desc') - ->get(); - - return response()->json(['data' => $trips]); - } - - /** - * Store a newly created resource in storage. - */ - public function store(Request $request): JsonResponse - { - $validated = $request->validate([ - 'name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'start_date' => 'nullable|date', - 'end_date' => 'nullable|date|after_or_equal:start_date', - ]); - - $validated['created_by_user_id'] = $request->user()->id; - - $trip = Trip::create($validated); - - return response()->json(['data' => $trip], 201); - } - - /** - * Display the specified resource. - */ - public function show(Request $request, Trip $trip): JsonResponse - { - if (!$this->policy->view($request->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - return response()->json(['data' => $trip]); - } - - /** - * Update the specified resource in storage. - */ - public function update(Request $request, Trip $trip): JsonResponse - { - if (!$this->policy->update($request->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $validated = $request->validate([ - 'name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'start_date' => 'nullable|date', - 'end_date' => 'nullable|date|after_or_equal:start_date', - ]); - - $trip->update($validated); - - return response()->json(['data' => $trip]); - } - - /** - * Remove the specified resource from storage. - */ - public function destroy(Request $request, Trip $trip): JsonResponse - { - if (!$this->policy->delete($request->user(), $trip)) { - return response()->json(['message' => 'Forbidden'], 403); - } - - $trip->delete(); - - return response()->json(['message' => 'Trip deleted successfully']); - } -} diff --git a/backend/app/Infrastructure/Http/Controllers/Controller.php b/backend/app/Infrastructure/Http/Controllers/Controller.php deleted file mode 100644 index c1cebc7..0000000 --- a/backend/app/Infrastructure/Http/Controllers/Controller.php +++ /dev/null @@ -1,8 +0,0 @@ - '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); - } -} diff --git a/backend/app/Models/PlannableItem.php b/backend/app/Models/PlannableItem.php deleted file mode 100644 index f6a1886..0000000 --- a/backend/app/Models/PlannableItem.php +++ /dev/null @@ -1,44 +0,0 @@ - '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); - } -} diff --git a/backend/app/Models/PlannedItem.php b/backend/app/Models/PlannedItem.php deleted file mode 100644 index 52b83ab..0000000 --- a/backend/app/Models/PlannedItem.php +++ /dev/null @@ -1,28 +0,0 @@ -belongsTo(PlannableItem::class); - } - - public function calendarSlot(): BelongsTo - { - return $this->belongsTo(CalendarSlot::class); - } -} diff --git a/backend/app/Models/Trip.php b/backend/app/Models/Trip.php deleted file mode 100644 index ea11dc8..0000000 --- a/backend/app/Models/Trip.php +++ /dev/null @@ -1,40 +0,0 @@ - 'date', - 'end_date' => 'date', - ]; - - public function user(): BelongsTo - { - 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'); - } -} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 22e8508..91135d7 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -46,12 +46,4 @@ protected function casts(): array 'password' => 'hashed', ]; } - - /** - * Get the trips created by this user. - */ - public function trips() - { - return $this->hasMany(Trip::class, 'created_by_user_id'); - } } diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php index 222bdb1..452e6b6 100644 --- a/backend/app/Providers/AppServiceProvider.php +++ b/backend/app/Providers/AppServiceProvider.php @@ -2,8 +2,6 @@ namespace App\Providers; -use App\Models\Trip; -use App\Domain\Trip\Observers\TripObserver; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -21,6 +19,6 @@ public function register(): void */ public function boot(): void { - Trip::observe(TripObserver::class); + // } } diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 9dec942..3e248e3 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -13,7 +13,7 @@ ) ->withMiddleware(function (Middleware $middleware): void { $middleware->api(prepend: [ - \App\Infrastructure\Http\Middleware\Cors::class, + \App\Http\Middleware\Cors::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php index 1823eaf..38b258d 100644 --- a/backend/bootstrap/providers.php +++ b/backend/bootstrap/providers.php @@ -2,5 +2,4 @@ return [ App\Providers\AppServiceProvider::class, - App\Domain\Trip\Providers\TripServiceProvider::class, ]; diff --git a/backend/composer.json b/backend/composer.json index a1cb6bb..40d10fc 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -16,15 +16,11 @@ }, "require-dev": { "fakerphp/faker": "^1.23", - "larastan/larastan": "^3.7", "laravel/pail": "^1.2.2", "laravel/pint": "^1.24", "laravel/sail": "^1.41", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-deprecation-rules": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^11.5.3" }, "autoload": { diff --git a/backend/composer.lock b/backend/composer.lock index 884931e..7077164 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9656361ea974cbb5fad3b98127519a54", + "content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42", "packages": [ { "name": "brick/math", @@ -6348,136 +6348,6 @@ }, "time": "2025-04-30T06:54:44+00:00" }, - { - "name": "iamcal/sql-parser", - "version": "v0.6", - "source": { - "type": "git", - "url": "https://github.com/iamcal/SQLParser.git", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62", - "shasum": "" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^1.0", - "phpunit/phpunit": "^5|^6|^7|^8|^9" - }, - "type": "library", - "autoload": { - "psr-4": { - "iamcal\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Cal Henderson", - "email": "cal@iamcal.com" - } - ], - "description": "MySQL schema parser", - "support": { - "issues": "https://github.com/iamcal/SQLParser/issues", - "source": "https://github.com/iamcal/SQLParser/tree/v0.6" - }, - "time": "2025-03-17T16:59:46+00:00" - }, - { - "name": "larastan/larastan", - "version": "v3.7.2", - "source": { - "type": "git", - "url": "https://github.com/larastan/larastan.git", - "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae", - "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae", - "shasum": "" - }, - "require": { - "ext-json": "*", - "iamcal/sql-parser": "^0.6.0", - "illuminate/console": "^11.44.2 || ^12.4.1", - "illuminate/container": "^11.44.2 || ^12.4.1", - "illuminate/contracts": "^11.44.2 || ^12.4.1", - "illuminate/database": "^11.44.2 || ^12.4.1", - "illuminate/http": "^11.44.2 || ^12.4.1", - "illuminate/pipeline": "^11.44.2 || ^12.4.1", - "illuminate/support": "^11.44.2 || ^12.4.1", - "php": "^8.2", - "phpstan/phpstan": "^2.1.28" - }, - "require-dev": { - "doctrine/coding-standard": "^13", - "laravel/framework": "^11.44.2 || ^12.7.2", - "mockery/mockery": "^1.6.12", - "nikic/php-parser": "^5.4", - "orchestra/canvas": "^v9.2.2 || ^10.0.1", - "orchestra/testbench-core": "^9.12.0 || ^10.1", - "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpunit/phpunit": "^10.5.35 || ^11.5.15" - }, - "suggest": { - "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Larastan\\Larastan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Can Vural", - "email": "can9119@gmail.com" - } - ], - "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", - "keywords": [ - "PHPStan", - "code analyse", - "code analysis", - "larastan", - "laravel", - "package", - "php", - "static analysis" - ], - "support": { - "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.7.2" - }, - "funding": [ - { - "url": "https://github.com/canvural", - "type": "github" - } - ], - "time": "2025-09-19T09:03:05+00:00" - }, { "name": "laravel/pail", "version": "v1.2.3", @@ -7046,159 +6916,6 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpstan/phpstan", - "version": "2.1.29", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-phar-composer-source.git", - "reference": "git" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e", - "reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e", - "shasum": "" - }, - "require": { - "php": "^7.4|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" - }, - "bin": [ - "phpstan", - "phpstan.phar" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" - ], - "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" - }, - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" - } - ], - "time": "2025-09-25T06:58:18+00:00" - }, - { - "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", - "reference": "468e02c9176891cc901143da118f09dc9505fc2f", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.15" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.6" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "rules.neon" - ] - } - }, - "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", - "support": { - "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" - }, - "time": "2025-05-14T10:56:57+00:00" - }, - { - "name": "phpstan/phpstan-strict-rules", - "version": "2.0.7", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538", - "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.29" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.6" - }, - "type": "phpstan-extension", - "extra": { - "phpstan": { - "includes": [ - "rules.neon" - ] - } - }, - "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Extra strict and opinionated rules for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7" - }, - "time": "2025-09-26T11:19:08+00:00" - }, { "name": "phpunit/php-code-coverage", "version": "11.0.11", diff --git a/backend/database/factories/PlannableItemFactory.php b/backend/database/factories/PlannableItemFactory.php deleted file mode 100644 index 8a83d1e..0000000 --- a/backend/database/factories/PlannableItemFactory.php +++ /dev/null @@ -1,62 +0,0 @@ -faker->randomElement($types); - - return [ - 'trip_id' => Trip::factory(), - 'name' => $this->faker->company(), - 'type' => $type, - 'address' => $this->faker->address(), - 'notes' => $this->faker->sentence(), - 'metadata' => $this->getMetadataForType($type), - ]; - } - - private function getMetadataForType($type) - { - switch ($type) { - case 'hotel': - return [ - 'checkin_time' => '15:00', - 'checkout_time' => '11:00', - 'confirmation_number' => $this->faker->uuid() - ]; - case 'restaurant': - return [ - 'reservation_time' => '19:00', - 'party_size' => $this->faker->numberBetween(2, 6) - ]; - case 'transport': - return [ - 'departure_time' => '10:00', - 'arrival_time' => '14:00', - 'transport_type' => $this->faker->randomElement(['flight', 'train', 'bus']) - ]; - case 'attraction': - return [ - 'opening_hours' => '9:00 AM - 5:00 PM', - 'ticket_price' => '$' . $this->faker->numberBetween(10, 50) - ]; - case 'activity': - return [ - 'duration' => $this->faker->numberBetween(1, 4) . ' hours', - 'meeting_point' => $this->faker->streetAddress() - ]; - default: - return []; - } - } -} \ No newline at end of file diff --git a/backend/database/factories/TripFactory.php b/backend/database/factories/TripFactory.php deleted file mode 100644 index 6391d18..0000000 --- a/backend/database/factories/TripFactory.php +++ /dev/null @@ -1,72 +0,0 @@ - - */ -class TripFactory extends Factory -{ - /** - * The name of the factory's corresponding model. - * - * @var string - */ - protected $model = Trip::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - $startDate = $this->faker->dateTimeBetween('now', '+1 year'); - $endDate = $this->faker->dateTimeBetween($startDate, '+1 year'); - - return [ - 'name' => $this->faker->sentence(3), - 'description' => $this->faker->optional()->paragraph(), - 'start_date' => $this->faker->optional()->date('Y-m-d', $startDate), - 'end_date' => $this->faker->optional()->date('Y-m-d', $endDate), - 'created_by_user_id' => User::factory(), - ]; - } - - /** - * Indicate that the trip has no dates. - */ - public function withoutDates(): static - { - return $this->state(fn (array $attributes) => [ - 'start_date' => null, - 'end_date' => null, - ]); - } - - /** - * Indicate that the trip is upcoming. - */ - public function upcoming(): static - { - return $this->state(fn (array $attributes) => [ - 'start_date' => now()->addDays(30)->format('Y-m-d'), - 'end_date' => now()->addDays(37)->format('Y-m-d'), - ]); - } - - /** - * Indicate that the trip is past. - */ - public function past(): static - { - return $this->state(fn (array $attributes) => [ - 'start_date' => now()->subDays(37)->format('Y-m-d'), - 'end_date' => now()->subDays(30)->format('Y-m-d'), - ]); - } -} \ No newline at end of file diff --git a/backend/database/migrations/2025_09_27_004838_create_trips_table.php b/backend/database/migrations/2025_09_27_004838_create_trips_table.php deleted file mode 100644 index 8cb2d53..0000000 --- a/backend/database/migrations/2025_09_27_004838_create_trips_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('name'); - $table->text('description')->nullable(); - $table->date('start_date')->nullable(); - $table->date('end_date')->nullable(); - $table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('trips'); - } -}; diff --git a/backend/database/migrations/2025_09_28_064348_create_plannable_items_table.php b/backend/database/migrations/2025_09_28_064348_create_plannable_items_table.php deleted file mode 100644 index fd012c7..0000000 --- a/backend/database/migrations/2025_09_28_064348_create_plannable_items_table.php +++ /dev/null @@ -1,33 +0,0 @@ -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'); - } -}; diff --git a/backend/database/migrations/2025_09_28_064407_create_calendar_slots_table.php b/backend/database/migrations/2025_09_28_064407_create_calendar_slots_table.php deleted file mode 100644 index 940a299..0000000 --- a/backend/database/migrations/2025_09_28_064407_create_calendar_slots_table.php +++ /dev/null @@ -1,37 +0,0 @@ -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(); - - // Composite indexes for performance - $table->index(['trip_id', 'slot_date', 'datetime_start']); - $table->index(['trip_id', 'slot_date', 'slot_order']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('calendar_slots'); - } -}; diff --git a/backend/database/migrations/2025_09_28_064428_create_planned_items_table.php b/backend/database/migrations/2025_09_28_064428_create_planned_items_table.php deleted file mode 100644 index 4c6d6fa..0000000 --- a/backend/database/migrations/2025_09_28_064428_create_planned_items_table.php +++ /dev/null @@ -1,33 +0,0 @@ -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'); - } -}; diff --git a/backend/database/seeders/TestUserSeeder.php b/backend/database/seeders/TestUserSeeder.php deleted file mode 100644 index 36d4d6b..0000000 --- a/backend/database/seeders/TestUserSeeder.php +++ /dev/null @@ -1,36 +0,0 @@ - 'test@example.com'], - [ - 'name' => 'Test User', - 'password' => Hash::make('password123'), - 'email_verified_at' => now(), - ] - ); - - // Create additional test users if needed - User::firstOrCreate( - ['email' => 'admin@example.com'], - [ - 'name' => 'Admin User', - 'password' => Hash::make('admin123'), - 'email_verified_at' => now(), - ] - ); - } -} \ No newline at end of file diff --git a/backend/phpstan.neon b/backend/phpstan.neon deleted file mode 100644 index ed67d73..0000000 --- a/backend/phpstan.neon +++ /dev/null @@ -1,24 +0,0 @@ -includes: - - vendor/larastan/larastan/extension.neon - -parameters: - level: 0 - - paths: - - app/ - - config/ - - database/ - - routes/ - - tests/ - - excludePaths: - - database/migrations/* - - reportUnmatchedIgnoredErrors: false - - ignoreErrors: - # Ignore errors for facades which are resolved at runtime - - '#Call to an undefined static method#' - - # Laravel specific settings - checkModelProperties: true \ No newline at end of file diff --git a/backend/phpunit.xml b/backend/phpunit.xml index dbeea8e..5fd5bcf 100644 --- a/backend/phpunit.xml +++ b/backend/phpunit.xml @@ -16,20 +16,7 @@ app - - app/Console - app/Exceptions - app/Http/Kernel.php - app/Providers/BroadcastServiceProvider.php - - - - - - - - diff --git a/backend/routes/api.php b/backend/routes/api.php index 72b8a0f..946570d 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,11 +1,6 @@ group(function () { - Route::post('/setup/user', [TestSetupController::class, 'createTestUser']); - Route::post('/cleanup', [TestSetupController::class, 'cleanup']); -}); - // Protected routes Route::middleware('auth:sanctum')->group(function () { Route::get('/user', function (Request $request) { @@ -31,24 +20,4 @@ }); Route::get('/profile', [AuthController::class, 'profile']); Route::post('/logout', [AuthController::class, 'logout']); - - // Trip routes - 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']); }); diff --git a/backend/tests/Feature/AuthTest.php b/backend/tests/Feature/AuthTest.php deleted file mode 100644 index 1f7b860..0000000 --- a/backend/tests/Feature/AuthTest.php +++ /dev/null @@ -1,457 +0,0 @@ - 'John Doe', - 'email' => 'john@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]; - - $response = $this->postJson('/api/register', $userData); - - $response->assertStatus(201) - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'user' => [ - 'id', - 'name', - 'email', - 'created_at', - 'updated_at', - ], - 'access_token', - 'token_type', - ] - ]) - ->assertJson([ - 'success' => true, - 'message' => 'User registered successfully', - 'data' => [ - 'user' => [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - ], - 'token_type' => 'Bearer', - ] - ]); - - // Verify user was created in database - $this->assertDatabaseHas('users', [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - ]); - - // Verify password was hashed - $user = User::where('email', 'john@example.com')->first(); - $this->assertTrue(Hash::check('password123', $user->password)); - } - - /** - * Test registration validation errors. - */ - public function test_registration_validates_required_fields() - { - // Test without any data - $response = $this->postJson('/api/register', []); - - $response->assertStatus(422) - ->assertJson([ - 'success' => false, - 'message' => 'Validation errors', - ]) - ->assertJsonPath('data.name', fn($value) => !empty($value)) - ->assertJsonPath('data.email', fn($value) => !empty($value)) - ->assertJsonPath('data.password', fn($value) => !empty($value)); - - // Test with invalid email - $response = $this->postJson('/api/register', [ - 'name' => 'John Doe', - 'email' => 'invalid-email', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]); - - $response->assertStatus(422) - ->assertJsonPath('data.email', fn($value) => !empty($value)); - - // Test with password too short - $response = $this->postJson('/api/register', [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'password' => '123', - 'password_confirmation' => '123', - ]); - - $response->assertStatus(422) - ->assertJsonPath('data.password', fn($value) => !empty($value)); - - // Test with password confirmation mismatch - $response = $this->postJson('/api/register', [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'password' => 'password123', - 'password_confirmation' => 'different123', - ]); - - $response->assertStatus(422) - ->assertJsonPath('data.password', fn($value) => !empty($value)); - } - - /** - * Test registration with duplicate email. - */ - public function test_registration_prevents_duplicate_email() - { - // Create an existing user - User::factory()->create([ - 'email' => 'existing@example.com', - ]); - - $userData = [ - 'name' => 'John Doe', - 'email' => 'existing@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]; - - $response = $this->postJson('/api/register', $userData); - - $response->assertStatus(422) - ->assertJsonPath('data.email', fn($value) => !empty($value)); - } - - /** - * Test user login with valid credentials. - */ - public function test_user_can_login_with_valid_credentials() - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => Hash::make('password123'), - ]); - - $loginData = [ - 'email' => 'john@example.com', - 'password' => 'password123', - ]; - - $response = $this->postJson('/api/login', $loginData); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'user' => [ - 'id', - 'name', - 'email', - 'created_at', - 'updated_at', - ], - 'access_token', - 'token_type', - ] - ]) - ->assertJson([ - 'success' => true, - 'message' => 'Login successful', - 'data' => [ - 'user' => [ - 'id' => $user->id, - 'email' => 'john@example.com', - ], - 'token_type' => 'Bearer', - ] - ]); - - // Verify token is valid - $this->assertNotEmpty($response->json('data.access_token')); - } - - /** - * Test login with invalid credentials. - */ - public function test_login_fails_with_invalid_credentials() - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => Hash::make('correct-password'), - ]); - - // Test with wrong password - This should return 422 with validation exception - $response = $this->postJson('/api/login', [ - 'email' => 'john@example.com', - 'password' => 'wrong-password', - ]); - - $response->assertStatus(422); - - // Test with non-existent email - $response = $this->postJson('/api/login', [ - 'email' => 'nonexistent@example.com', - 'password' => 'password123', - ]); - - $response->assertStatus(422); - } - - /** - * Test login validation errors. - */ - public function test_login_validates_required_fields() - { - // Test without any data - $response = $this->postJson('/api/login', []); - - $response->assertStatus(422) - ->assertJson([ - 'success' => false, - 'message' => 'Validation errors', - ]) - ->assertJsonPath('data.email', fn($value) => !empty($value)) - ->assertJsonPath('data.password', fn($value) => !empty($value)); - - // Test with invalid email format - $response = $this->postJson('/api/login', [ - 'email' => 'invalid-email', - 'password' => 'password123', - ]); - - $response->assertStatus(422) - ->assertJsonPath('data.email', fn($value) => !empty($value)); - } - - /** - * Test authenticated user can access profile. - */ - public function test_authenticated_user_can_access_profile() - { - $user = User::factory()->create([ - 'name' => 'John Doe', - 'email' => 'john@example.com', - ]); - - Sanctum::actingAs($user); - - $response = $this->getJson('/api/profile'); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'Profile retrieved successfully', - 'data' => [ - 'id' => $user->id, - 'name' => 'John Doe', - 'email' => 'john@example.com', - ] - ]); - } - - /** - * Test unauthenticated user cannot access profile. - */ - public function test_unauthenticated_user_cannot_access_profile() - { - $response = $this->getJson('/api/profile'); - - $response->assertStatus(401); - } - - /** - * Test authenticated user can logout. - */ - public function test_authenticated_user_can_logout() - { - $user = User::factory()->create(); - $token = $user->createToken('test-token'); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token->plainTextToken, - ])->postJson('/api/logout'); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'Logout successful', - ]); - - // Verify token was deleted - $this->assertDatabaseMissing('personal_access_tokens', [ - 'id' => $token->accessToken->id, - ]); - } - - /** - * Test unauthenticated user cannot logout. - */ - public function test_unauthenticated_user_cannot_logout() - { - $response = $this->postJson('/api/logout'); - - $response->assertStatus(401); - } - - /** - * Test token authentication works. - */ - public function test_token_authentication_works() - { - $user = User::factory()->create(); - $token = $user->createToken('test-token'); - - // Test that the token can be used to access protected routes - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token->plainTextToken, - ])->getJson('/api/user'); - - $response->assertStatus(200) - ->assertJson([ - 'id' => $user->id, - 'email' => $user->email, - ]); - } - - /** - * Test invalid token is rejected. - */ - public function test_invalid_token_is_rejected() - { - $response = $this->withHeaders([ - 'Authorization' => 'Bearer invalid-token', - ])->getJson('/api/user'); - - $response->assertStatus(401); - } - - /** - * Test registration creates valid tokens. - */ - public function test_registration_creates_valid_token() - { - $userData = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]; - - $response = $this->postJson('/api/register', $userData); - $token = $response->json('data.access_token'); - - // Use the token to access a protected route - $profileResponse = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token, - ])->getJson('/api/profile'); - - $profileResponse->assertStatus(200) - ->assertJson([ - 'success' => true, - 'data' => [ - 'email' => 'john@example.com', - ] - ]); - } - - /** - * Test login creates valid tokens. - */ - public function test_login_creates_valid_token() - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => Hash::make('password123'), - ]); - - $loginResponse = $this->postJson('/api/login', [ - 'email' => 'john@example.com', - 'password' => 'password123', - ]); - - $token = $loginResponse->json('data.access_token'); - - // Use the token to access a protected route - $profileResponse = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token, - ])->getJson('/api/profile'); - - $profileResponse->assertStatus(200) - ->assertJson([ - 'success' => true, - 'data' => [ - 'id' => $user->id, - ] - ]); - } - - /** - * Test registration returns user without password. - */ - public function test_registration_does_not_return_password() - { - $userData = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'password' => 'password123', - 'password_confirmation' => 'password123', - ]; - - $response = $this->postJson('/api/register', $userData); - - $response->assertStatus(201) - ->assertJsonMissing(['data.user.password']); - } - - /** - * Test login returns user without password. - */ - public function test_login_does_not_return_password() - { - $user = User::factory()->create([ - 'email' => 'john@example.com', - 'password' => Hash::make('password123'), - ]); - - $response = $this->postJson('/api/login', [ - 'email' => 'john@example.com', - 'password' => 'password123', - ]); - - $response->assertStatus(200) - ->assertJsonMissing(['data.user.password']); - } - - /** - * Test profile returns user without password. - */ - public function test_profile_does_not_return_password() - { - $user = User::factory()->create(); - Sanctum::actingAs($user); - - $response = $this->getJson('/api/profile'); - - $response->assertStatus(200) - ->assertJsonMissing(['data.password']); - } -} \ No newline at end of file diff --git a/backend/tests/Feature/CalendarSlotTest.php b/backend/tests/Feature/CalendarSlotTest.php deleted file mode 100644 index 4efb070..0000000 --- a/backend/tests/Feature/CalendarSlotTest.php +++ /dev/null @@ -1,336 +0,0 @@ -user = User::factory()->create(); - $this->token = $this->user->createToken('test-token')->plainTextToken; - } - - public function test_calendar_slots_are_auto_created_when_trip_is_created() - { - $tripData = [ - 'name' => 'Test Trip', - 'description' => 'Test Description', - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-03' - ]; - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson('/api/trips', $tripData); - - $response->assertStatus(201); - $tripId = $response->json('data.id'); - - // Check that 3 calendar slots were created for this trip (Jan 1, 2, 3) - $this->assertEquals(3, CalendarSlot::where('trip_id', $tripId)->count()); - - $slots = CalendarSlot::where('trip_id', $tripId) - ->orderBy('slot_order') - ->get(); - - $this->assertEquals('Day 1', $slots[0]->name); - $this->assertEquals('2024-01-01', $slots[0]->slot_date->format('Y-m-d')); - $this->assertEquals(1, $slots[0]->slot_order); - - $this->assertEquals('Day 2', $slots[1]->name); - $this->assertEquals('2024-01-02', $slots[1]->slot_date->format('Y-m-d')); - $this->assertEquals(2, $slots[1]->slot_order); - - $this->assertEquals('Day 3', $slots[2]->name); - $this->assertEquals('2024-01-03', $slots[2]->slot_date->format('Y-m-d')); - $this->assertEquals(3, $slots[2]->slot_order); - } - - public function test_calendar_slots_are_updated_when_trip_dates_change() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-02' - ]); - - // Initially should have 2 slots - $this->assertCount(2, $trip->calendarSlots); - - // Update trip dates - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/trips/{$trip->id}", [ - 'name' => $trip->name, - 'start_date' => '2024-02-01', - 'end_date' => '2024-02-04' - ]); - - $response->assertStatus(200); - - // Should now have 4 slots with new dates - $trip->refresh(); - $slots = $trip->calendarSlots()->orderBy('slot_order')->get(); - - $this->assertCount(4, $slots); - $this->assertEquals('2024-02-01', $slots[0]->slot_date->format('Y-m-d')); - $this->assertEquals('2024-02-02', $slots[1]->slot_date->format('Y-m-d')); - $this->assertEquals('2024-02-03', $slots[2]->slot_date->format('Y-m-d')); - $this->assertEquals('2024-02-04', $slots[3]->slot_date->format('Y-m-d')); - } - - public function test_can_list_calendar_slots_for_trip() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-03' - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$trip->id}/calendar-slots"); - - $response->assertStatus(200) - ->assertJsonCount(3, 'data') - ->assertJsonPath('data.0.name', 'Day 1') - ->assertJsonPath('data.1.name', 'Day 2') - ->assertJsonPath('data.2.name', 'Day 3'); - } - - public function test_can_update_calendar_slot_name() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-01' - ]); - - $slot = $trip->calendarSlots()->first(); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/calendar-slots/{$slot->id}", [ - 'name' => 'Arrival Day' - ]); - - $response->assertStatus(200) - ->assertJsonPath('data.name', 'Arrival Day'); - - $this->assertDatabaseHas('calendar_slots', [ - 'id' => $slot->id, - 'name' => 'Arrival Day' - ]); - } - - public function test_cannot_access_calendar_slots_of_other_users_trip() - { - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-02' - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$otherTrip->id}/calendar-slots"); - - $response->assertStatus(403); - } - - public function test_cannot_update_calendar_slot_of_other_users_trip() - { - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-01' - ]); - - $slot = $otherTrip->calendarSlots()->first(); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/calendar-slots/{$slot->id}", [ - 'name' => 'Hacked Name' - ]); - - $response->assertStatus(403); - } - - public function test_no_slots_created_for_trip_without_dates() - { - $tripData = [ - 'name' => 'Trip without dates', - 'description' => 'No dates set' - ]; - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson('/api/trips', $tripData); - - $response->assertStatus(201); - $tripId = $response->json('data.id'); - - // No slots should be created - $this->assertDatabaseCount('calendar_slots', 0); - - $slots = CalendarSlot::where('trip_id', $tripId)->get(); - $this->assertCount(0, $slots); - } - - public function test_slots_created_when_dates_added_to_trip() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => null, - 'end_date' => null - ]); - - // Initially no slots - $this->assertCount(0, $trip->calendarSlots); - - // Add dates to trip - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/trips/{$trip->id}", [ - 'name' => $trip->name, - 'start_date' => '2024-03-01', - 'end_date' => '2024-03-02' - ]); - - $response->assertStatus(200); - - // Now should have 2 slots - $trip->refresh(); - $this->assertCount(2, $trip->calendarSlots); - } - - public function test_calendar_slots_include_planned_items_with_plannable_item() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-01' - ]); - - // Create a plannable item and schedule it - $plannableItem = \App\Models\PlannableItem::factory()->create([ - 'trip_id' => $trip->id, - 'name' => 'Eiffel Tower' - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson('/api/planned-items', [ - 'plannable_item_id' => $plannableItem->id, - 'trip_id' => $trip->id, - 'start_datetime' => '2024-01-01 14:00:00', - 'end_datetime' => '2024-01-01 16:00:00', - ]); - - $response->assertStatus(201); - - // Fetch calendar slots and verify relationships are loaded - $slotsResponse = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$trip->id}/calendar-slots"); - - $slotsResponse->assertStatus(200); - - // Find the slot we just created - $slots = $slotsResponse->json('data'); - $scheduledSlot = collect($slots)->firstWhere('name', 'Eiffel Tower'); - - $this->assertNotNull($scheduledSlot); - $this->assertArrayHasKey('planned_items', $scheduledSlot); - $this->assertCount(1, $scheduledSlot['planned_items']); - $this->assertArrayHasKey('plannable_item', $scheduledSlot['planned_items'][0]); - $this->assertEquals('Eiffel Tower', $scheduledSlot['planned_items'][0]['plannable_item']['name']); - } - - public function test_calendar_slots_ordered_by_date_and_time() - { - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-02' - ]); - - // Create plannable items - $item1 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Breakfast']); - $item2 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Lunch']); - $item3 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Dinner Day 1']); - $item4 = \App\Models\PlannableItem::factory()->create(['trip_id' => $trip->id, 'name' => 'Breakfast Day 2']); - - // Schedule in non-chronological order - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $item3->id, - 'trip_id' => $trip->id, - 'start_datetime' => '2024-01-01 19:00:00', - 'end_datetime' => '2024-01-01 21:00:00', - ]); - - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $item1->id, - 'trip_id' => $trip->id, - 'start_datetime' => '2024-01-01 08:00:00', - 'end_datetime' => '2024-01-01 09:00:00', - ]); - - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $item4->id, - 'trip_id' => $trip->id, - 'start_datetime' => '2024-01-02 08:00:00', - 'end_datetime' => '2024-01-02 09:00:00', - ]); - - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $item2->id, - 'trip_id' => $trip->id, - 'start_datetime' => '2024-01-01 12:00:00', - 'end_datetime' => '2024-01-01 13:00:00', - ]); - - // Fetch slots and verify they're ordered correctly - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$trip->id}/calendar-slots"); - - $response->assertStatus(200); - - $slots = $response->json('data'); - $scheduledSlots = collect($slots)->filter(fn($slot) => $slot['datetime_start'] !== null)->values(); - - // Should be ordered: Breakfast (Day 1, 8am), Lunch (Day 1, 12pm), Dinner (Day 1, 7pm), Breakfast (Day 2, 8am) - $this->assertEquals('Breakfast', $scheduledSlots[0]['name']); - $this->assertEquals('Lunch', $scheduledSlots[1]['name']); - $this->assertEquals('Dinner Day 1', $scheduledSlots[2]['name']); - $this->assertEquals('Breakfast Day 2', $scheduledSlots[3]['name']); - } -} \ No newline at end of file diff --git a/backend/tests/Feature/ExampleTest.php b/backend/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/backend/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/backend/tests/Feature/PlannableItemTest.php b/backend/tests/Feature/PlannableItemTest.php deleted file mode 100644 index 6300edf..0000000 --- a/backend/tests/Feature/PlannableItemTest.php +++ /dev/null @@ -1,188 +0,0 @@ -user = User::factory()->create(); - $this->trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-03' - ]); - - $this->token = $this->user->createToken('test-token')->plainTextToken; - } - - public function test_can_create_plannable_item() - { - $data = [ - 'name' => 'Eiffel Tower', - 'type' => 'attraction', - 'address' => 'Champ de Mars, Paris', - 'notes' => 'Visit in the morning', - 'metadata' => [ - 'opening_hours' => '9:00 AM - 11:00 PM', - 'ticket_price' => '25 EUR' - ] - ]; - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson("/api/trips/{$this->trip->id}/plannables", $data); - - $response->assertStatus(201) - ->assertJsonPath('data.name', 'Eiffel Tower') - ->assertJsonPath('data.type', 'attraction') - ->assertJsonPath('data.address', 'Champ de Mars, Paris') - ->assertJsonPath('data.metadata.opening_hours', '9:00 AM - 11:00 PM'); - - $this->assertDatabaseHas('plannable_items', [ - 'trip_id' => $this->trip->id, - 'name' => 'Eiffel Tower', - 'type' => 'attraction' - ]); - } - - public function test_can_list_plannable_items_for_trip() - { - PlannableItem::factory()->count(3)->create([ - 'trip_id' => $this->trip->id - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$this->trip->id}/plannables"); - - $response->assertStatus(200) - ->assertJsonCount(3, 'data'); - } - - public function test_can_update_plannable_item() - { - $item = PlannableItem::factory()->create([ - 'trip_id' => $this->trip->id, - 'name' => 'Old Name' - ]); - - $updateData = [ - 'name' => 'Updated Name', - 'type' => 'restaurant', - 'notes' => 'Updated notes' - ]; - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/plannables/{$item->id}", $updateData); - - $response->assertStatus(200) - ->assertJsonPath('data.name', 'Updated Name') - ->assertJsonPath('data.type', 'restaurant'); - - $this->assertDatabaseHas('plannable_items', [ - 'id' => $item->id, - 'name' => 'Updated Name', - 'type' => 'restaurant' - ]); - } - - public function test_can_delete_plannable_item() - { - $item = PlannableItem::factory()->create([ - 'trip_id' => $this->trip->id - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->deleteJson("/api/plannables/{$item->id}"); - - $response->assertStatus(204); - - $this->assertDatabaseMissing('plannable_items', [ - 'id' => $item->id - ]); - } - - public function test_cannot_access_plannable_items_of_other_users_trip() - { - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->getJson("/api/trips/{$otherTrip->id}/plannables"); - - $response->assertStatus(403); - } - - public function test_cannot_update_plannable_item_of_other_users_trip() - { - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id - ]); - $item = PlannableItem::factory()->create([ - 'trip_id' => $otherTrip->id - ]); - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->putJson("/api/plannables/{$item->id}", [ - 'name' => 'Hacked Name' - ]); - - $response->assertStatus(403); - } - - public function test_validates_required_fields_when_creating_plannable_item() - { - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson("/api/trips/{$this->trip->id}/plannables", []); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['name', 'type']); - } - - public function test_validates_type_enum_values() - { - $data = [ - 'name' => 'Test Item', - 'type' => 'invalid_type' - ]; - - $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, - 'Accept' => 'application/json' - ])->postJson("/api/trips/{$this->trip->id}/plannables", $data); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['type']); - } -} \ No newline at end of file diff --git a/backend/tests/Feature/PlannedItemTest.php b/backend/tests/Feature/PlannedItemTest.php deleted file mode 100644 index b5820ef..0000000 --- a/backend/tests/Feature/PlannedItemTest.php +++ /dev/null @@ -1,321 +0,0 @@ -user = User::factory()->create(); - $this->trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'start_date' => '2025-01-15', - 'end_date' => '2025-01-17', - ]); - $this->plannableItem = PlannableItem::factory()->create([ - 'trip_id' => $this->trip->id, - 'name' => 'Eiffel Tower', - ]); - } - - /** @test */ - public function it_creates_calendar_slot_when_scheduling_item_with_datetime() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(201); - $this->assertDatabaseHas('calendar_slots', [ - 'trip_id' => $this->trip->id, - 'name' => 'Eiffel Tower', - 'slot_date' => '2025-01-15', - ]); - $this->assertDatabaseHas('planned_items', [ - 'plannable_item_id' => $this->plannableItem->id, - ]); - } - - /** @test */ - public function it_calculates_slot_order_correctly() - { - // Create first slot at 10:00 - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 10:00:00', - 'end_datetime' => '2025-01-15 11:00:00', - ]); - - // Create another plannable item - $secondItem = PlannableItem::factory()->create([ - 'trip_id' => $this->trip->id, - 'name' => 'Louvre', - ]); - - // Create second slot at 14:00 - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $secondItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - // Create third slot at 08:00 (should have slot_order 0 after recalculation) - $thirdItem = PlannableItem::factory()->create([ - 'trip_id' => $this->trip->id, - 'name' => 'Breakfast', - ]); - - $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $thirdItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 08:00:00', - 'end_datetime' => '2025-01-15 09:00:00', - ]); - - $slots = CalendarSlot::where('trip_id', $this->trip->id) - ->where('slot_date', '2025-01-15') - ->orderBy('slot_order') - ->get(); - - // After recalculation, order should be by datetime_start - $this->assertEquals('Breakfast', $slots[0]->name); - $this->assertEquals(0, $slots[0]->slot_order); - $this->assertEquals('Eiffel Tower', $slots[1]->name); - $this->assertEquals(1, $slots[1]->slot_order); - $this->assertEquals('Louvre', $slots[2]->name); - $this->assertEquals(2, $slots[2]->slot_order); - } - - /** @test */ - public function it_validates_end_time_is_after_start_time() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 16:00:00', - 'end_datetime' => '2025-01-15 14:00:00', - ]); - - $response->assertStatus(422); - } - - /** @test */ - public function it_requires_plannable_item_to_belong_to_trip() - { - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - ]); - $otherItem = PlannableItem::factory()->create([ - 'trip_id' => $otherTrip->id, - ]); - - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $otherItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(422); - $response->assertJson([ - 'error' => 'Plannable item does not belong to this trip' - ]); - } - - /** @test */ - public function it_supports_backward_compatible_calendar_slot_id_flow() - { - $calendarSlot = CalendarSlot::factory()->create([ - 'trip_id' => $this->trip->id, - ]); - - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot->id, - ]); - - $response->assertStatus(201); - $this->assertDatabaseHas('planned_items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot->id, - ]); - } - - /** @test */ - public function it_requires_authentication_to_create_planned_item() - { - $response = $this->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(401); - } - - /** @test */ - public function it_validates_required_fields() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', []); - - $response->assertStatus(422); - $response->assertJsonValidationErrors(['plannable_item_id', 'trip_id', 'start_datetime', 'end_datetime']); - } - - /** @test */ - public function it_validates_plannable_item_exists() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => 99999, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(422); - $response->assertJsonValidationErrors(['plannable_item_id']); - } - - /** @test */ - public function it_validates_trip_exists() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => 99999, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(422); - $response->assertJsonValidationErrors(['trip_id']); - } - - /** @test */ - public function it_returns_calendar_slot_with_relationships() - { - $response = $this->actingAs($this->user)->postJson('/api/planned-items', [ - 'plannable_item_id' => $this->plannableItem->id, - 'trip_id' => $this->trip->id, - 'start_datetime' => '2025-01-15 14:00:00', - 'end_datetime' => '2025-01-15 16:00:00', - ]); - - $response->assertStatus(201); - $response->assertJsonStructure([ - 'id', - 'trip_id', - 'name', - 'datetime_start', - 'datetime_end', - 'slot_date', - 'slot_order', - 'planned_items' => [ - '*' => [ - 'id', - 'plannable_item_id', - 'calendar_slot_id', - 'plannable_item' => [ - 'id', - 'name', - 'trip_id', - ] - ] - ] - ]); - } - - /** @test */ - public function it_can_update_planned_item_slot() - { - $calendarSlot1 = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); - $calendarSlot2 = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); - - $plannedItem = PlannedItem::create([ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot1->id, - 'sort_order' => 0, - ]); - - $response = $this->actingAs($this->user)->putJson("/api/planned-items/{$plannedItem->id}", [ - 'calendar_slot_id' => $calendarSlot2->id, - 'sort_order' => 5, - ]); - - $response->assertStatus(200); - $this->assertDatabaseHas('planned_items', [ - 'id' => $plannedItem->id, - 'calendar_slot_id' => $calendarSlot2->id, - 'sort_order' => 5, - ]); - } - - /** @test */ - public function it_can_delete_planned_item() - { - $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); - $plannedItem = PlannedItem::create([ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot->id, - 'sort_order' => 0, - ]); - - $response = $this->actingAs($this->user)->deleteJson("/api/planned-items/{$plannedItem->id}"); - - $response->assertStatus(204); - $this->assertDatabaseMissing('planned_items', [ - 'id' => $plannedItem->id, - ]); - } - - /** @test */ - public function it_requires_authentication_to_update_planned_item() - { - $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); - $plannedItem = PlannedItem::create([ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot->id, - ]); - - $response = $this->putJson("/api/planned-items/{$plannedItem->id}", [ - 'sort_order' => 1, - ]); - - $response->assertStatus(401); - } - - /** @test */ - public function it_requires_authentication_to_delete_planned_item() - { - $calendarSlot = CalendarSlot::factory()->create(['trip_id' => $this->trip->id]); - $plannedItem = PlannedItem::create([ - 'plannable_item_id' => $this->plannableItem->id, - 'calendar_slot_id' => $calendarSlot->id, - ]); - - $response = $this->deleteJson("/api/planned-items/{$plannedItem->id}"); - - $response->assertStatus(401); - } -} diff --git a/backend/tests/Feature/TestSetupControllerTest.php b/backend/tests/Feature/TestSetupControllerTest.php deleted file mode 100644 index a80e941..0000000 --- a/backend/tests/Feature/TestSetupControllerTest.php +++ /dev/null @@ -1,267 +0,0 @@ - 'Test User', - 'email' => 'test@example.com', - 'password' => 'password123', - ]; - - $response = $this->postJson('/api/e2e/test/setup/user', $userData); - - $response->assertStatus(200); - $response->assertJson([ - 'success' => true, - 'message' => 'Test user created', - 'data' => [ - 'email' => 'test@example.com', - 'name' => 'Test User' - ] - ]); - - $this->assertDatabaseHas('users', [ - 'email' => 'test@example.com', - 'name' => 'Test User', - ]); - - $user = User::where('email', 'test@example.com')->first(); - $this->assertNotNull($user->email_verified_at); - $this->assertTrue(\Hash::check('password123', $user->password)); - } - - /** - * Test returning existing test user if already exists. - */ - public function test_returns_existing_test_user_if_already_exists() - { - // Create a user first - $existingUser = User::factory()->create([ - 'email' => 'existing@example.com', - 'name' => 'Existing User', - ]); - - $userData = [ - 'name' => 'Different Name', - 'email' => 'existing@example.com', - 'password' => 'password123', - ]; - - $response = $this->postJson('/api/e2e/test/setup/user', $userData); - - $response->assertStatus(200); - $response->assertJson([ - 'success' => true, - 'message' => 'Test user already exists', - 'data' => [ - 'id' => $existingUser->id, - 'email' => 'existing@example.com', - 'name' => 'Existing User' // Should keep original name - ] - ]); - - // Should not create a new user - $this->assertCount(1, User::where('email', 'existing@example.com')->get()); - } - - /** - * Test validation for required fields. - */ - public function test_validates_required_fields_for_create_user() - { - $response = $this->postJson('/api/e2e/test/setup/user', []); - - $response->assertStatus(422); - $response->assertJsonPath('errors.name', ['The name field is required.']); - $response->assertJsonPath('errors.email', ['The email field is required.']); - $response->assertJsonPath('errors.password', ['The password field is required.']); - } - - /** - * Test email validation. - */ - public function test_validates_email_format() - { - $userData = [ - 'name' => 'Test User', - 'email' => 'invalid-email', - 'password' => 'password123', - ]; - - $response = $this->postJson('/api/e2e/test/setup/user', $userData); - - $response->assertStatus(422); - $response->assertJsonPath('errors.email', ['The email field must be a valid email address.']); - } - - /** - * Test password length validation. - */ - public function test_validates_password_length() - { - $userData = [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'short', - ]; - - $response = $this->postJson('/api/e2e/test/setup/user', $userData); - - $response->assertStatus(422); - $response->assertJsonPath('errors.password', ['The password field must be at least 8 characters.']); - } - - /** - * Test cleanup removes test users with test email patterns. - */ - public function test_cleanup_removes_test_users() - { - // Create test users with test email patterns - User::factory()->create(['email' => 'test.user.1@example.com']); - User::factory()->create(['email' => 'test.user.2@example.com']); - User::factory()->create(['email' => 'test123@example.com']); - - // Create a regular user that should not be deleted - User::factory()->create(['email' => 'regular@example.com']); - - $response = $this->postJson('/api/e2e/test/cleanup'); - - $response->assertStatus(200); - $response->assertJson([ - 'success' => true, - 'message' => 'Deleted 3 test users' - ]); - - // Test users should be deleted - $this->assertDatabaseMissing('users', ['email' => 'test.user.1@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'test.user.2@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'test123@example.com']); - - // Regular user should still exist - $this->assertDatabaseHas('users', ['email' => 'regular@example.com']); - } - - /** - * Test cleanup when no test users exist. - */ - public function test_cleanup_when_no_test_users_exist() - { - // Create only regular users - User::factory()->create(['email' => 'regular1@example.com']); - User::factory()->create(['email' => 'regular2@example.com']); - - $response = $this->postJson('/api/e2e/test/cleanup'); - - $response->assertStatus(200); - $response->assertJson([ - 'success' => true, - 'message' => 'Deleted 0 test users' - ]); - - // Regular users should still exist - $this->assertDatabaseHas('users', ['email' => 'regular1@example.com']); - $this->assertDatabaseHas('users', ['email' => 'regular2@example.com']); - } - - /** - * Test cleanup removes only users matching test patterns. - */ - public function test_cleanup_only_removes_matching_patterns() - { - // Create users with various email patterns - // Patterns that should be deleted: 'test%@example.com' OR 'test.user.%@example.com' - User::factory()->create(['email' => 'test@example.com']); // Should be deleted (matches test%@example.com) - User::factory()->create(['email' => 'test.user.123@example.com']); // Should be deleted (matches test.user.%@example.com) - User::factory()->create(['email' => 'testABC@example.com']); // Should be deleted (matches test%@example.com) - User::factory()->create(['email' => 'test.user.xyz@example.com']); // Should be deleted (matches test.user.%@example.com) - User::factory()->create(['email' => 'test999@example.com']); // Should be deleted (matches test%@example.com) - User::factory()->create(['email' => 'mytesting@example.com']); // Should NOT be deleted - User::factory()->create(['email' => 'test@gmail.com']); // Should NOT be deleted - User::factory()->create(['email' => 'mytest@example.com']); // Should NOT be deleted - - $response = $this->postJson('/api/e2e/test/cleanup'); - - $response->assertStatus(200); - $response->assertJsonPath('success', true); - // Just verify the message contains "Deleted" and "test users" - $this->assertStringContainsString('Deleted', $response->json('message')); - $this->assertStringContainsString('test users', $response->json('message')); - - // Only specific patterns should be deleted - $this->assertDatabaseMissing('users', ['email' => 'test@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'test.user.123@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'testABC@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'test.user.xyz@example.com']); - $this->assertDatabaseMissing('users', ['email' => 'test999@example.com']); - - // Others should remain - $this->assertDatabaseHas('users', ['email' => 'mytesting@example.com']); - $this->assertDatabaseHas('users', ['email' => 'test@gmail.com']); - $this->assertDatabaseHas('users', ['email' => 'mytest@example.com']); - } - - - /** - * Test create user endpoint works in non-production environment. - */ - public function test_endpoints_work_in_non_production_environment() - { - // Ensure we're not in production environment - $this->assertNotEquals('production', app()->environment()); - - $response = $this->postJson('/api/e2e/test/setup/user', [ - 'name' => 'Test User', - 'email' => 'test@example.com', - 'password' => 'password123', - ]); - - $response->assertStatus(200); - $response->assertJson(['success' => true]); - } - - /** - * Test user data is properly formatted in response. - */ - public function test_user_data_properly_formatted_in_response() - { - $userData = [ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', - 'password' => 'securepassword123', - ]; - - $response = $this->postJson('/api/e2e/test/setup/user', $userData); - - $response->assertStatus(200); - $response->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'id', - 'email', - 'name' - ] - ]); - - // Ensure password is not included in response - $response->assertJsonMissing(['password']); - - $data = $response->json('data'); - $this->assertIsInt($data['id']); - $this->assertEquals('john.doe@example.com', $data['email']); - $this->assertEquals('John Doe', $data['name']); - } -} \ No newline at end of file diff --git a/backend/tests/Feature/TripTest.php b/backend/tests/Feature/TripTest.php deleted file mode 100644 index 23e8394..0000000 --- a/backend/tests/Feature/TripTest.php +++ /dev/null @@ -1,420 +0,0 @@ -user = User::factory()->create(); - } - - /** - * Test that unauthenticated users cannot access trip endpoints. - */ - public function test_unauthenticated_user_cannot_access_trips() - { - $response = $this->getJson('/api/trips'); - $response->assertStatus(401); - - $response = $this->postJson('/api/trips', []); - $response->assertStatus(401); - - $response = $this->putJson('/api/trips/1', []); - $response->assertStatus(401); - - $response = $this->deleteJson('/api/trips/1'); - $response->assertStatus(401); - } - - /** - * Test user can create a trip. - */ - public function test_user_can_create_trip() - { - Sanctum::actingAs($this->user); - - $tripData = [ - 'name' => 'Summer Vacation 2025', - 'description' => 'A wonderful trip to Europe', - 'start_date' => '2025-06-01', - 'end_date' => '2025-06-15', - ]; - - $response = $this->postJson('/api/trips', $tripData); - - $response->assertStatus(201) - ->assertJsonStructure([ - 'data' => [ - 'id', - 'name', - 'description', - 'start_date', - 'end_date', - 'created_by_user_id', - 'created_at', - 'updated_at', - ] - ]) - ->assertJsonPath('data.name', 'Summer Vacation 2025') - ->assertJsonPath('data.description', 'A wonderful trip to Europe') - ->assertJsonPath('data.created_by_user_id', $this->user->id); - - $this->assertDatabaseHas('trips', [ - 'name' => 'Summer Vacation 2025', - 'created_by_user_id' => $this->user->id, - ]); - } - - /** - * Test trip creation validation. - */ - public function test_trip_creation_validates_required_fields() - { - Sanctum::actingAs($this->user); - - // Test without name - $response = $this->postJson('/api/trips', [ - 'description' => 'A trip without a name', - ]); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['name']); - - // Test with empty name - $response = $this->postJson('/api/trips', [ - 'name' => '', - 'description' => 'A trip with empty name', - ]); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['name']); - - // Test with invalid dates - $response = $this->postJson('/api/trips', [ - 'name' => 'Trip with invalid dates', - 'start_date' => 'not-a-date', - 'end_date' => '2025-13-45', - ]); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['start_date', 'end_date']); - - // Test with end date before start date - $response = $this->postJson('/api/trips', [ - 'name' => 'Trip with reversed dates', - 'start_date' => '2025-06-15', - 'end_date' => '2025-06-01', - ]); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['end_date']); - } - - /** - * Test user can list their trips. - */ - public function test_user_can_list_their_own_trips() - { - Sanctum::actingAs($this->user); - - // Create some trips for this user - $trips = Trip::factory()->count(3)->create([ - 'created_by_user_id' => $this->user->id, - ]); - - // Create trips for another user (should not be visible) - $otherUser = User::factory()->create(); - Trip::factory()->count(2)->create([ - 'created_by_user_id' => $otherUser->id, - ]); - - $response = $this->getJson('/api/trips'); - - $response->assertStatus(200) - ->assertJsonCount(3, 'data') - ->assertJsonStructure([ - 'data' => [ - '*' => [ - 'id', - 'name', - 'description', - 'start_date', - 'end_date', - 'created_by_user_id', - 'created_at', - 'updated_at', - ] - ] - ]); - - // Verify all returned trips belong to the authenticated user - foreach ($response->json('data') as $trip) { - $this->assertEquals($this->user->id, $trip['created_by_user_id']); - } - } - - /** - * Test user can view a specific trip. - */ - public function test_user_can_view_their_own_trip() - { - Sanctum::actingAs($this->user); - - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'name' => 'My Special Trip', - ]); - - $response = $this->getJson("/api/trips/{$trip->id}"); - - $response->assertStatus(200) - ->assertJsonPath('data.id', $trip->id) - ->assertJsonPath('data.name', 'My Special Trip') - ->assertJsonPath('data.created_by_user_id', $this->user->id); - } - - /** - * Test user cannot view another user's trip. - */ - public function test_user_cannot_view_another_users_trip() - { - Sanctum::actingAs($this->user); - - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - ]); - - $response = $this->getJson("/api/trips/{$otherTrip->id}"); - - // Controller returns 404 when trip doesn't belong to user - $response->assertStatus(404); - } - - /** - * Test user can update their trip. - */ - public function test_user_can_update_their_own_trip() - { - Sanctum::actingAs($this->user); - - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'name' => 'Original Name', - 'description' => 'Original Description', - ]); - - $updateData = [ - 'name' => 'Updated Trip Name', - 'description' => 'Updated Description', - 'start_date' => '2025-07-01', - 'end_date' => '2025-07-15', - ]; - - $response = $this->putJson("/api/trips/{$trip->id}", $updateData); - - $response->assertStatus(200) - ->assertJsonPath('data.id', $trip->id) - ->assertJsonPath('data.name', 'Updated Trip Name') - ->assertJsonPath('data.description', 'Updated Description'); - - $this->assertDatabaseHas('trips', [ - 'id' => $trip->id, - 'name' => 'Updated Trip Name', - 'description' => 'Updated Description', - ]); - } - - /** - * Test user cannot update another user's trip. - */ - public function test_user_cannot_update_another_users_trip() - { - Sanctum::actingAs($this->user); - - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - 'name' => 'Other User Trip', - ]); - - $response = $this->putJson("/api/trips/{$otherTrip->id}", [ - 'name' => 'Trying to Update', - ]); - - // Controller returns 404 when trip doesn't belong to user - $response->assertStatus(404); - - // Verify the trip wasn't updated - $this->assertDatabaseHas('trips', [ - 'id' => $otherTrip->id, - 'name' => 'Other User Trip', - ]); - } - - /** - * Test user can delete their trip. - */ - public function test_user_can_delete_their_own_trip() - { - Sanctum::actingAs($this->user); - - $trip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - ]); - - $response = $this->deleteJson("/api/trips/{$trip->id}"); - - // Controller returns 200 with a message - $response->assertStatus(200) - ->assertJson([ - 'message' => 'Trip deleted successfully' - ]); - - $this->assertDatabaseMissing('trips', [ - 'id' => $trip->id, - ]); - } - - /** - * Test user cannot delete another user's trip. - */ - public function test_user_cannot_delete_another_users_trip() - { - Sanctum::actingAs($this->user); - - $otherUser = User::factory()->create(); - $otherTrip = Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - ]); - - $response = $this->deleteJson("/api/trips/{$otherTrip->id}"); - - // Controller returns 404 when trip doesn't belong to user - $response->assertStatus(404); - - // Verify the trip still exists - $this->assertDatabaseHas('trips', [ - 'id' => $otherTrip->id, - ]); - } - - /** - * Test handling non-existent trip. - */ - public function test_returns_404_for_non_existent_trip() - { - Sanctum::actingAs($this->user); - - $response = $this->getJson('/api/trips/99999'); - $response->assertStatus(404); - - $response = $this->putJson('/api/trips/99999', ['name' => 'Updated']); - $response->assertStatus(404); - - $response = $this->deleteJson('/api/trips/99999'); - $response->assertStatus(404); - } - - /** - * Test trip creation with minimal data. - */ - public function test_user_can_create_trip_with_minimal_data() - { - Sanctum::actingAs($this->user); - - $tripData = [ - 'name' => 'Minimal Trip', - ]; - - $response = $this->postJson('/api/trips', $tripData); - - $response->assertStatus(201) - ->assertJsonPath('data.name', 'Minimal Trip') - ->assertJsonPath('data.created_by_user_id', $this->user->id) - ->assertJsonStructure([ - 'data' => [ - 'id', - 'name', - 'created_by_user_id', - 'created_at', - 'updated_at', - ] - ]); - - $this->assertDatabaseHas('trips', [ - 'name' => 'Minimal Trip', - 'created_by_user_id' => $this->user->id, - 'description' => null, - 'start_date' => null, - 'end_date' => null, - ]); - } - - /** - * Test trip name length validation. - */ - public function test_trip_name_length_validation() - { - Sanctum::actingAs($this->user); - - // Test with too long name (assuming max is 255) - $response = $this->postJson('/api/trips', [ - 'name' => str_repeat('a', 256), - ]); - - $response->assertStatus(422) - ->assertJsonValidationErrors(['name']); - } - - /** - * Test trips are returned in correct order. - */ - public function test_trips_are_returned_in_descending_order() - { - Sanctum::actingAs($this->user); - - // Create trips with specific timestamps - $oldTrip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'name' => 'Old Trip', - 'created_at' => now()->subDays(2), - ]); - - $newTrip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'name' => 'New Trip', - 'created_at' => now(), - ]); - - $middleTrip = Trip::factory()->create([ - 'created_by_user_id' => $this->user->id, - 'name' => 'Middle Trip', - 'created_at' => now()->subDay(), - ]); - - $response = $this->getJson('/api/trips'); - - $response->assertStatus(200); - - $trips = $response->json('data'); - - // Verify trips are in descending order (newest first) - $this->assertEquals('New Trip', $trips[0]['name']); - $this->assertEquals('Middle Trip', $trips[1]['name']); - $this->assertEquals('Old Trip', $trips[2]['name']); - } -} \ No newline at end of file diff --git a/backend/tests/Unit/CalendarSlotServiceTest.php b/backend/tests/Unit/CalendarSlotServiceTest.php deleted file mode 100644 index 68b4ad3..0000000 --- a/backend/tests/Unit/CalendarSlotServiceTest.php +++ /dev/null @@ -1,235 +0,0 @@ -service = new CalendarSlotService(); - $this->trip = Trip::factory()->create([ - 'start_date' => '2024-01-01', - 'end_date' => '2024-01-03', - ]); - } - - /** @test */ - public function it_calculates_slot_order_for_first_slot_on_date() - { - $order = $this->service->calculateSlotOrder( - $this->trip->id, - '2024-01-01', - Carbon::parse('2024-01-01 10:00:00') - ); - - $this->assertEquals(0, $order); - } - - /** @test */ - public function it_calculates_slot_order_based_on_chronological_position() - { - // Create existing slots - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Morning', - 'datetime_start' => '2024-01-01 08:00:00', - 'datetime_end' => '2024-01-01 09:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 0, - ]); - - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Afternoon', - 'datetime_start' => '2024-01-01 14:00:00', - 'datetime_end' => '2024-01-01 15:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 1, - ]); - - // Calculate order for noon slot (should be between morning and afternoon) - $order = $this->service->calculateSlotOrder( - $this->trip->id, - '2024-01-01', - Carbon::parse('2024-01-01 12:00:00') - ); - - $this->assertEquals(1, $order); - } - - /** @test */ - public function it_calculates_slot_order_for_earliest_time() - { - // Create existing slot - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Late Morning', - 'datetime_start' => '2024-01-01 10:00:00', - 'datetime_end' => '2024-01-01 11:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 0, - ]); - - // Calculate order for earlier time - $order = $this->service->calculateSlotOrder( - $this->trip->id, - '2024-01-01', - Carbon::parse('2024-01-01 08:00:00') - ); - - $this->assertEquals(0, $order); - } - - /** @test */ - public function it_calculates_slot_order_for_latest_time() - { - // Create existing slots - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Morning', - 'datetime_start' => '2024-01-01 08:00:00', - 'datetime_end' => '2024-01-01 09:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 0, - ]); - - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Noon', - 'datetime_start' => '2024-01-01 12:00:00', - 'datetime_end' => '2024-01-01 13:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 1, - ]); - - // Calculate order for later time - $order = $this->service->calculateSlotOrder( - $this->trip->id, - '2024-01-01', - Carbon::parse('2024-01-01 20:00:00') - ); - - $this->assertEquals(2, $order); - } - - /** @test */ - public function it_only_considers_slots_from_same_date() - { - // Create slot on different date - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Other Day', - 'datetime_start' => '2024-01-02 08:00:00', - 'datetime_end' => '2024-01-02 09:00:00', - 'slot_date' => '2024-01-02', - 'slot_order' => 0, - ]); - - // Calculate order for Jan 1 (should start at 0, not affected by Jan 2) - $order = $this->service->calculateSlotOrder( - $this->trip->id, - '2024-01-01', - Carbon::parse('2024-01-01 10:00:00') - ); - - $this->assertEquals(0, $order); - } - - /** @test */ - public function it_recalculates_slot_orders_for_date() - { - // Create slots in non-chronological slot_order - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Evening', - 'datetime_start' => '2024-01-01 20:00:00', - 'datetime_end' => '2024-01-01 21:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 5, // Wrong order - ]); - - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Morning', - 'datetime_start' => '2024-01-01 08:00:00', - 'datetime_end' => '2024-01-01 09:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 3, // Wrong order - ]); - - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Afternoon', - 'datetime_start' => '2024-01-01 14:00:00', - 'datetime_end' => '2024-01-01 15:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 1, // Wrong order - ]); - - // Recalculate - $this->service->recalculateSlotOrdersForDate($this->trip->id, '2024-01-01'); - - // Verify correct chronological order - $slots = CalendarSlot::where('trip_id', $this->trip->id) - ->where('slot_date', '2024-01-01') - ->orderBy('slot_order') - ->get(); - - $this->assertEquals('Morning', $slots[0]->name); - $this->assertEquals(0, $slots[0]->slot_order); - - $this->assertEquals('Afternoon', $slots[1]->name); - $this->assertEquals(1, $slots[1]->slot_order); - - $this->assertEquals('Evening', $slots[2]->name); - $this->assertEquals(2, $slots[2]->slot_order); - } - - /** @test */ - public function it_only_recalculates_slots_for_specified_date() - { - // Create slots on Jan 1 - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Jan 1 Slot', - 'datetime_start' => '2024-01-01 10:00:00', - 'datetime_end' => '2024-01-01 11:00:00', - 'slot_date' => '2024-01-01', - 'slot_order' => 5, - ]); - - // Create slots on Jan 2 - CalendarSlot::create([ - 'trip_id' => $this->trip->id, - 'name' => 'Jan 2 Slot', - 'datetime_start' => '2024-01-02 10:00:00', - 'datetime_end' => '2024-01-02 11:00:00', - 'slot_date' => '2024-01-02', - 'slot_order' => 7, - ]); - - // Recalculate only Jan 1 - $this->service->recalculateSlotOrdersForDate($this->trip->id, '2024-01-01'); - - // Jan 1 should be recalculated to 0 - $jan1Slot = CalendarSlot::where('slot_date', '2024-01-01')->first(); - $this->assertEquals(0, $jan1Slot->slot_order); - - // Jan 2 should remain unchanged - $jan2Slot = CalendarSlot::where('slot_date', '2024-01-02')->first(); - $this->assertEquals(7, $jan2Slot->slot_order); - } -} diff --git a/backend/tests/Unit/ExampleTest.php b/backend/tests/Unit/ExampleTest.php new file mode 100644 index 0000000..5773b0c --- /dev/null +++ b/backend/tests/Unit/ExampleTest.php @@ -0,0 +1,16 @@ +assertTrue(true); + } +} diff --git a/backend/tests/Unit/TripTest.php b/backend/tests/Unit/TripTest.php deleted file mode 100644 index 483e3bc..0000000 --- a/backend/tests/Unit/TripTest.php +++ /dev/null @@ -1,248 +0,0 @@ -create(); - $trip = Trip::factory()->create([ - 'name' => 'Paris Adventure', - 'description' => 'A wonderful trip to Paris', - 'created_by_user_id' => $user->id, - ]); - - $this->assertInstanceOf(Trip::class, $trip); - $this->assertEquals('Paris Adventure', $trip->name); - $this->assertEquals('A wonderful trip to Paris', $trip->description); - $this->assertEquals($user->id, $trip->created_by_user_id); - $this->assertNotNull($trip->id); - $this->assertNotNull($trip->created_at); - $this->assertNotNull($trip->updated_at); - } - - /** - * Test trip fillable attributes. - */ - public function test_trip_fillable_attributes() - { - $user = User::factory()->create(); - $tripData = [ - 'name' => 'Tokyo Trip', - 'description' => 'Exploring Japan', - 'start_date' => '2025-06-01', - 'end_date' => '2025-06-15', - 'created_by_user_id' => $user->id, - ]; - - $trip = Trip::create($tripData); - - $this->assertEquals('Tokyo Trip', $trip->name); - $this->assertEquals('Exploring Japan', $trip->description); - $this->assertEquals('2025-06-01', $trip->start_date->format('Y-m-d')); - $this->assertEquals('2025-06-15', $trip->end_date->format('Y-m-d')); - $this->assertEquals($user->id, $trip->created_by_user_id); - } - - /** - * Test trip date casting. - */ - public function test_trip_date_casting() - { - $trip = Trip::factory()->create([ - 'start_date' => '2025-07-01', - 'end_date' => '2025-07-10', - ]); - - $this->assertInstanceOf(\Illuminate\Support\Carbon::class, $trip->start_date); - $this->assertInstanceOf(\Illuminate\Support\Carbon::class, $trip->end_date); - $this->assertEquals('2025-07-01', $trip->start_date->format('Y-m-d')); - $this->assertEquals('2025-07-10', $trip->end_date->format('Y-m-d')); - } - - /** - * Test trip can have null dates. - */ - public function test_trip_can_have_null_dates() - { - $trip = Trip::factory()->withoutDates()->create([ - 'name' => 'Flexible Trip', - ]); - - $this->assertNull($trip->start_date); - $this->assertNull($trip->end_date); - $this->assertEquals('Flexible Trip', $trip->name); - } - - /** - * Test trip belongs to user relationship. - */ - public function test_trip_belongs_to_user() - { - $user = User::factory()->create(['name' => 'John Doe']); - $trip = Trip::factory()->create([ - 'created_by_user_id' => $user->id, - ]); - - $this->assertInstanceOf(User::class, $trip->user); - $this->assertEquals($user->id, $trip->user->id); - $this->assertEquals('John Doe', $trip->user->name); - } - - /** - * Test trip factory creates valid trips. - */ - public function test_trip_factory_creates_valid_trips() - { - $trip = Trip::factory()->create(); - - $this->assertNotEmpty($trip->name); - $this->assertNotNull($trip->created_by_user_id); - $this->assertInstanceOf(User::class, $trip->user); - } - - /** - * Test trip factory upcoming state. - */ - public function test_trip_factory_upcoming_state() - { - $trip = Trip::factory()->upcoming()->create(); - - $this->assertNotNull($trip->start_date); - $this->assertNotNull($trip->end_date); - $this->assertTrue($trip->start_date->isFuture()); - $this->assertTrue($trip->end_date->isAfter($trip->start_date)); - } - - /** - * Test trip factory past state. - */ - public function test_trip_factory_past_state() - { - $trip = Trip::factory()->past()->create(); - - $this->assertNotNull($trip->start_date); - $this->assertNotNull($trip->end_date); - $this->assertTrue($trip->start_date->isPast()); - $this->assertTrue($trip->end_date->isPast()); - $this->assertTrue($trip->end_date->isAfter($trip->start_date)); - } - - /** - * Test trip model uses correct table. - */ - public function test_trip_uses_correct_table() - { - $trip = new Trip(); - - $this->assertEquals('trips', $trip->getTable()); - } - - /** - * Test trip model has correct primary key. - */ - public function test_trip_has_correct_primary_key() - { - $trip = new Trip(); - - $this->assertEquals('id', $trip->getKeyName()); - $this->assertTrue($trip->getIncrementing()); - } - - /** - * Test trip model uses timestamps. - */ - public function test_trip_uses_timestamps() - { - $trip = new Trip(); - - $this->assertTrue($trip->usesTimestamps()); - } - - /** - * Test trip can be created with minimal data. - */ - public function test_trip_can_be_created_with_minimal_data() - { - $user = User::factory()->create(); - $trip = Trip::create([ - 'name' => 'Minimal Trip', - 'created_by_user_id' => $user->id, - ]); - - $this->assertEquals('Minimal Trip', $trip->name); - $this->assertNull($trip->description); - $this->assertNull($trip->start_date); - $this->assertNull($trip->end_date); - $this->assertEquals($user->id, $trip->created_by_user_id); - } - - /** - * Test trip name is required. - */ - public function test_trip_name_is_required() - { - $user = User::factory()->create(); - - $this->expectException(\Illuminate\Database\QueryException::class); - - Trip::create([ - 'description' => 'A trip without a name', - 'created_by_user_id' => $user->id, - ]); - } - - /** - * Test trip requires a user. - */ - public function test_trip_requires_user() - { - $this->expectException(\Illuminate\Database\QueryException::class); - - Trip::create([ - 'name' => 'Orphaned Trip', - 'description' => 'A trip without a user', - ]); - } - - /** - * Test trip dates can be formatted. - */ - public function test_trip_dates_can_be_formatted() - { - $trip = Trip::factory()->create([ - 'start_date' => '2025-12-25', - 'end_date' => '2025-12-31', - ]); - - $this->assertEquals('2025-12-25', $trip->start_date->format('Y-m-d')); - $this->assertEquals('2025-12-31', $trip->end_date->format('Y-m-d')); - $this->assertEquals('December 25, 2025', $trip->start_date->format('F j, Y')); - $this->assertEquals('December 31, 2025', $trip->end_date->format('F j, Y')); - } - - /** - * Test trip can calculate duration. - */ - public function test_trip_can_calculate_duration() - { - $trip = Trip::factory()->create([ - 'start_date' => '2025-06-01', - 'end_date' => '2025-06-07', - ]); - - $duration = $trip->start_date->diffInDays($trip->end_date) + 1; // Include both start and end day - $this->assertEquals(7, $duration); - } -} \ No newline at end of file diff --git a/backend/tests/Unit/UserTest.php b/backend/tests/Unit/UserTest.php deleted file mode 100644 index 7ddb95e..0000000 --- a/backend/tests/Unit/UserTest.php +++ /dev/null @@ -1,224 +0,0 @@ -create([ - 'name' => 'John Doe', - 'email' => 'john@example.com', - ]); - - $this->assertInstanceOf(User::class, $user); - $this->assertEquals('John Doe', $user->name); - $this->assertEquals('john@example.com', $user->email); - $this->assertNotNull($user->id); - $this->assertNotNull($user->created_at); - $this->assertNotNull($user->updated_at); - } - - /** - * Test user fillable attributes. - */ - public function test_user_fillable_attributes() - { - $userData = [ - 'name' => 'Jane Doe', - 'email' => 'jane@example.com', - 'password' => 'password123', - ]; - - $user = User::create($userData); - - $this->assertEquals('Jane Doe', $user->name); - $this->assertEquals('jane@example.com', $user->email); - $this->assertTrue(Hash::check('password123', $user->password)); - } - - /** - * Test user hidden attributes. - */ - public function test_user_hidden_attributes() - { - $user = User::factory()->create([ - 'password' => Hash::make('secret123'), - ]); - - $userArray = $user->toArray(); - - $this->assertArrayNotHasKey('password', $userArray); - $this->assertArrayNotHasKey('remember_token', $userArray); - } - - /** - * Test user casts. - */ - public function test_user_casts() - { - $user = User::factory()->create([ - 'email_verified_at' => now(), - ]); - - $this->assertInstanceOf(\Illuminate\Support\Carbon::class, $user->email_verified_at); - } - - /** - * Test password is automatically hashed. - */ - public function test_password_is_automatically_hashed() - { - $user = User::factory()->create([ - 'password' => 'plaintext-password', - ]); - - $this->assertNotEquals('plaintext-password', $user->password); - $this->assertTrue(Hash::check('plaintext-password', $user->password)); - } - - /** - * Test user has API tokens trait. - */ - public function test_user_can_create_api_tokens() - { - $user = User::factory()->create(); - - $token = $user->createToken('test-token'); - - $this->assertInstanceOf(PersonalAccessToken::class, $token->accessToken); - $this->assertIsString($token->plainTextToken); - $this->assertEquals('test-token', $token->accessToken->name); - } - - /** - * Test user can have multiple tokens. - */ - public function test_user_can_have_multiple_tokens() - { - $user = User::factory()->create(); - - $token1 = $user->createToken('token-1'); - $token2 = $user->createToken('token-2'); - - $this->assertCount(2, $user->tokens); - $this->assertNotEquals($token1->plainTextToken, $token2->plainTextToken); - } - - /** - * Test user can delete tokens. - */ - public function test_user_can_delete_tokens() - { - $user = User::factory()->create(); - $token = $user->createToken('test-token'); - - $this->assertCount(1, $user->tokens); - - $token->accessToken->delete(); - $user->refresh(); - - $this->assertCount(0, $user->tokens); - } - - /** - * Test user has trips relationship. - */ - public function test_user_has_trips_relationship() - { - $user = User::factory()->create(); - - // Create some trips for this user - Trip::factory()->count(3)->create([ - 'created_by_user_id' => $user->id, - ]); - - // Create a trip for another user - $otherUser = User::factory()->create(); - Trip::factory()->create([ - 'created_by_user_id' => $otherUser->id, - ]); - - $this->assertCount(3, $user->trips); - $this->assertInstanceOf(Trip::class, $user->trips->first()); - } - - /** - * Test user factory creates valid users. - */ - public function test_user_factory_creates_valid_users() - { - $user = User::factory()->create(); - - $this->assertNotEmpty($user->name); - $this->assertNotEmpty($user->email); - $this->assertNotEmpty($user->password); - $this->assertNotNull($user->email_verified_at); - $this->assertTrue(filter_var($user->email, FILTER_VALIDATE_EMAIL) !== false); - } - - /** - * Test user factory can create unverified users. - */ - public function test_user_factory_can_create_unverified_users() - { - $user = User::factory()->unverified()->create(); - - $this->assertNull($user->email_verified_at); - } - - /** - * Test user email must be unique. - */ - public function test_user_email_must_be_unique() - { - User::factory()->create(['email' => 'test@example.com']); - - $this->expectException(\Illuminate\Database\QueryException::class); - - User::factory()->create(['email' => 'test@example.com']); - } - - /** - * Test user model uses correct table. - */ - public function test_user_uses_correct_table() - { - $user = new User(); - - $this->assertEquals('users', $user->getTable()); - } - - /** - * Test user model has correct primary key. - */ - public function test_user_has_correct_primary_key() - { - $user = new User(); - - $this->assertEquals('id', $user->getKeyName()); - $this->assertTrue($user->getIncrementing()); - } - - /** - * Test user model uses timestamps. - */ - public function test_user_uses_timestamps() - { - $user = new User(); - - $this->assertTrue($user->usesTimestamps()); - } -} \ No newline at end of file diff --git a/bin/phpstan b/bin/phpstan deleted file mode 100755 index 187078a..0000000 --- a/bin/phpstan +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# PHPStan runner script for Docker environment -# Usage: ./bin/phpstan [options] - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get the directory of this script -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" - -# Change to project root -cd "$PROJECT_ROOT" - -# Check if docker compose is running -if ! docker compose -f docker-compose.dev.yml ps --services --filter "status=running" | grep -q "backend"; then - echo -e "${RED}Error: Backend container is not running${NC}" - echo -e "${YELLOW}Starting containers...${NC}" - docker compose -f docker-compose.dev.yml up -d - - # Wait a moment for containers to be ready - sleep 3 -fi - -# Run PHPStan with default memory limit if not specified -echo -e "${GREEN}Running PHPStan analysis...${NC}" - -# Pass all arguments to phpstan, with default memory limit -if [[ "$*" == *"--memory-limit"* ]]; then - # User specified memory limit, use their arguments - docker compose -f docker-compose.dev.yml exec backend vendor/bin/phpstan analyse "$@" -else - # Add default memory limit - docker compose -f docker-compose.dev.yml exec backend vendor/bin/phpstan analyse --memory-limit=256M "$@" -fi - -# Capture exit code -EXIT_CODE=$? - -# Display result -if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}✓ PHPStan analysis completed successfully${NC}" -else - echo -e "${RED}✗ PHPStan found issues${NC}" -fi - -exit $EXIT_CODE \ No newline at end of file diff --git a/bin/phpunit b/bin/phpunit deleted file mode 100755 index a021856..0000000 --- a/bin/phpunit +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -# PHPUnit test runner script for Docker environment -# Usage: ./bin/phpunit [options] - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Get the directory of this script -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" - -# Change to project root -cd "$PROJECT_ROOT" - -# Check if backend container is running -if ! podman-compose -f docker-compose.dev.yml ps | grep "trip-planner-backend-dev" | grep -q "Up"; then - echo -e "${RED}Error: Backend container is not running${NC}" - echo -e "${YELLOW}Please start the containers first with: podman-compose -f docker-compose.dev.yml up -d${NC}" - exit 1 -fi - -# Run PHPUnit tests -echo -e "${GREEN}Running PHPUnit tests...${NC}" - -# If no arguments provided, run all tests -if [ $# -eq 0 ]; then - echo -e "${BLUE}Running all tests...${NC}" - podman-compose -f docker-compose.dev.yml exec backend php -d memory_limit=512M artisan test -else - # Pass all arguments to phpunit - echo -e "${BLUE}Running tests with options: $*${NC}" - podman-compose -f docker-compose.dev.yml exec backend php -d memory_limit=512M artisan test "$@" -fi - -# Capture exit code -EXIT_CODE=$? - -# Display result -if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}✓ All tests passed successfully${NC}" -else - echo -e "${RED}✗ Some tests failed${NC}" -fi - -echo -e "${YELLOW}Tip: You can also run specific tests:${NC}" -echo -e " ${BLUE}./bin/phpunit --filter=PlannableItemTest${NC}" -echo -e " ${BLUE}./bin/phpunit tests/Feature/PlannableItemTest.php${NC}" -echo -e " ${BLUE}./bin/phpunit --coverage-html=coverage${NC}" - -exit $EXIT_CODE \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9478523..7e4f380 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -72,7 +72,6 @@ services: networks: - trip-planner-network - networks: trip-planner-network: driver: bridge diff --git a/docker/backend/Dockerfile.dev b/docker/backend/Dockerfile.dev index 0974091..4891c28 100644 --- a/docker/backend/Dockerfile.dev +++ b/docker/backend/Dockerfile.dev @@ -11,21 +11,11 @@ RUN apk add --no-cache \ unzip \ nodejs \ npm \ - shadow \ - linux-headers \ - $PHPIZE_DEPS + shadow # Install PHP extensions RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd -# Install Xdebug for code coverage -RUN pecl install xdebug && \ - docker-php-ext-enable xdebug - -# Configure Xdebug for coverage (not debugging) -RUN echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \ - echo "xdebug.start_with_request=no" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - # Install Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer @@ -41,4 +31,4 @@ RUN mkdir -p storage/app/public storage/framework/cache storage/framework/sessio EXPOSE 8000 # Start Laravel development server with composer install -CMD sh -c "composer install && php artisan key:generate --force && php artisan migrate --force && php artisan serve --host=0.0.0.0 --port=8000" \ No newline at end of file +CMD sh -c "composer install && php artisan key:generate --force && php artisan serve --host=0.0.0.0 --port=8000" \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 467839b..c20fbd3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - TripPlanner + frontend
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f7c639a..6d9cf70 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@heroicons/react": "^2.2.0", "axios": "^1.12.2", "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-router-dom": "^7.9.3" + "react-dom": "^19.1.1" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -904,15 +902,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1679,15 +1668,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2800,44 +2780,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", - "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz", - "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==", - "license": "MIT", - "dependencies": { - "react-router": "7.9.3" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2906,12 +2848,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index fdb911e..e82bc18 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "@heroicons/react": "^2.2.0", "axios": "^1.12.2", "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-router-dom": "^7.9.3" + "react-dom": "^19.1.1" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/frontend/src/App.css b/frontend/src/App.css index 4b38420..1532ab9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,48 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&display=swap'); -@import './styles/variables.css'; - -:root { - /* Color Palette from Coolors.co */ - --color-navy: #03071e; - --color-dark-red: #370617; - --color-burgundy: #6a040f; - --color-crimson: #9d0208; - --color-red: #d00000; - --color-red-orange: #dc2f02; - --color-orange: #e85d04; - --color-orange-yellow: #f48c06; - --color-yellow-orange: #faa307; - --color-yellow: #ffba08; - - /* Semantic colors */ - --primary-color: var(--color-orange); - --primary-hover: var(--color-red-orange); - --secondary-color: var(--color-burgundy); - --accent-color: var(--color-yellow-orange); - --danger-color: var(--color-crimson); - --danger-hover: var(--color-red); - --text-primary: var(--color-navy); - --text-secondary: var(--color-dark-red); - --text-muted: #666; - - /* Background colors */ - --bg-primary: #faf8f5; - --bg-secondary: #f5f1eb; - --bg-light: #fdf9f4; - --bg-card: #fbf7f2; - --bg-gradient: linear-gradient(135deg, #faf8f5 0%, #f5f1eb 100%); - - /* Typography */ - --font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; - --font-secondary: 'Playfair Display', Georgia, 'Times New Roman', serif; - - /* Border radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; -} - * { box-sizing: border-box; } @@ -50,18 +5,14 @@ body { margin: 0; padding: 0; - background: var(--bg-gradient); - min-height: 100vh; - font-family: var(--font-primary); - line-height: 1.6; } .App { min-height: 100vh; display: flex; - align-items: flex-start; + align-items: center; justify-content: center; - padding: 0; + padding: 2rem; width: 100vw; box-sizing: border-box; } @@ -75,8 +26,8 @@ body { .login-form { background: #f8f9fa; padding: 2rem; - border-radius: var(--border-radius-md); - box-shadow: var(--shadow-md); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; margin: 0 auto; @@ -96,29 +47,23 @@ body { .form-group label { display: block; margin-bottom: 0.5rem; - font-weight: 600; - color: var(--color-dark-red); - background: rgba(255, 244, 230, 0.3); - padding: 0.25rem 0.5rem; - border-radius: var(--border-radius-sm); - display: inline-block; + font-weight: 500; + color: #555; } .form-group input { width: 100%; padding: 0.75rem; - border: 1px solid var(--primary-color); - border-radius: var(--border-radius-md); + border: 1px solid #ddd; + border-radius: 4px; font-size: 1rem; box-sizing: border-box; - background: #fff4e6; - color: #000; } .form-group input:focus { outline: none; - border-color: var(--color-red-orange); - box-shadow: 0 0 0 2px rgba(220, 47, 2, 0.25); + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); } .form-group input.error { @@ -135,7 +80,7 @@ body { .alert { padding: 0.75rem 1rem; margin-bottom: 1rem; - border-radius: var(--border-radius-sm); + border-radius: 4px; } .alert-success { @@ -153,21 +98,21 @@ body { button[type="submit"] { width: 100%; padding: 0.75rem; - background-color: var(--primary-color); + background-color: #007bff; color: white; border: none; - border-radius: var(--border-radius-sm); + border-radius: 4px; font-size: 1rem; cursor: pointer; - transition: background-color var(--transition-normal); + transition: background-color 0.2s; } button[type="submit"]:hover:not(:disabled) { - background-color: var(--primary-hover); + background-color: #0056b3; } button[type="submit"]:disabled { - background-color: var(--text-muted); + background-color: #6c757d; cursor: not-allowed; } @@ -191,7 +136,7 @@ button[type="submit"]:disabled { .auth-toggle { display: flex; margin-bottom: 2rem; - border-radius: var(--border-radius-md); + border-radius: 8px; overflow: hidden; border: 1px solid #ddd; } @@ -202,11 +147,11 @@ button[type="submit"]:disabled { background: #f8f9fa; border: none; cursor: pointer; - transition: all var(--transition-normal); + transition: all 0.2s; } .auth-toggle button.active { - background: var(--primary-color); + background: #007bff; color: white; } @@ -222,7 +167,7 @@ button[type="submit"]:disabled { .link-button { background: none; border: none; - color: var(--primary-color); + color: #007bff; cursor: pointer; text-decoration: underline; padding: 0; @@ -230,29 +175,14 @@ button[type="submit"]:disabled { } .link-button:hover { - color: var(--primary-hover); + color: #0056b3; } /* Dashboard Styles */ .dashboard { - width: 1200px; max-width: 1200px; margin: 0 auto; padding: 2rem; - position: relative; -} - -.dashboard::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 200px; - background: linear-gradient(135deg, var(--color-yellow) 0%, var(--color-orange) 50%, var(--color-red-orange) 100%); - opacity: 0.03; - border-radius: 0 0 50px 50px; - z-index: -1; } .dashboard-header { @@ -264,74 +194,9 @@ button[type="submit"]:disabled { border-bottom: 1px solid #eee; } -.dashboard-title { - display: flex; - align-items: center; -} - .dashboard-header h1 { margin: 0; - color: var(--text-primary); - background: linear-gradient(135deg, var(--color-yellow) 0%, var(--color-orange) 25%, var(--color-red-orange) 50%, var(--color-crimson) 75%, var(--color-dark-red) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - font-weight: 700; - font-size: 2rem; - font-family: var(--font-secondary); -} - -/* User menu styles */ -.user-dropdown { - position: relative; -} - -.user-menu-trigger { - background: var(--bg-card); - border: 1px solid var(--primary-color); - font-size: 0.875rem; - cursor: pointer; - padding: 0.5rem 0.75rem; - border-radius: 20px; - transition: all var(--transition-normal); - display: flex; - align-items: center; - gap: 0.5rem; - color: var(--text-primary); - font-weight: 500; -} - -.user-menu-trigger:hover { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--color-red-orange) 100%); - color: white; - border-color: var(--primary-color); -} - -.user-menu-trigger:focus { - outline: 2px solid var(--primary-color); - outline-offset: 2px; -} - -.dropdown-arrow { - font-size: 0.7rem; - transition: transform 0.2s; -} - -.user-dropdown[data-open="true"] .dropdown-arrow { - transform: rotate(180deg); -} - -.user-dropdown-menu { - position: absolute; - top: 100%; - right: 0; - background: var(--bg-card); - border: 1px solid rgba(228, 93, 4, 0.2); - border-radius: var(--border-radius-md); - box-shadow: var(--shadow-lg); - z-index: 1000; - min-width: 120px; - margin-top: 0.5rem; + color: #333; } .user-info { @@ -340,30 +205,18 @@ button[type="submit"]:disabled { gap: 1rem; } -/* Trips section header */ -.trips-section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0; -} - -.create-trip-btn-small { +.logout-btn { padding: 0.5rem 1rem; - background: var(--primary-color); + background: #dc3545; color: white; border: none; - border-radius: 6px; + border-radius: 4px; cursor: pointer; - font-weight: 500; - font-size: 0.875rem; - transition: all var(--transition-normal); - margin-top: 0.2rem; + transition: background-color 0.2s; } -.create-trip-btn-small:hover { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--color-red-orange) 100%); - transform: translateY(-1px); +.logout-btn:hover { + background: #c82333; } .welcome-section { @@ -386,8 +239,8 @@ button[type="submit"]:disabled { .feature-card { background: #f8f9fa; padding: 2rem; - border-radius: var(--border-radius-md); - box-shadow: var(--shadow-md); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); text-align: center; } @@ -418,7 +271,7 @@ button[type="submit"]:disabled { text-align: center; padding: 3rem; background: #f8f9fa; - border-radius: var(--border-radius-md); + border-radius: 8px; margin: 2rem 0; } @@ -430,529 +283,3 @@ button[type="submit"]:disabled { .unauthorized-container p { color: #666; } - -/* Trip Styles */ -.trips-section { - margin-top: 4rem; - position: relative; -} - -.trips-section::before { - content: ''; - position: absolute; - top: -3.5rem; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent 0%, var(--color-yellow) 20%, var(--color-orange) 40%, var(--color-red-orange) 60%, var(--color-crimson) 80%, transparent 100%); - opacity: 0.6; -} - -.trips-section-title { - background: linear-gradient(135deg, #6a040f 0%, #370617 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 1.5rem; - font-size: 2rem; - font-weight: 700; - font-family: var(--font-secondary); -} - -.trips-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1.5rem; - margin-top: 0; -} - -.trip-card { - background: var(--bg-card); - border-radius: 12px; - padding: 1.5rem; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - transition: all 0.3s ease; - position: relative; - border: 1px solid rgba(228, 93, 4, 0.1); - overflow: hidden; -} - -.trip-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--color-orange) 0%, var(--color-yellow-orange) 50%, var(--color-yellow) 100%); -} - -.trip-card:hover { - box-shadow: 0 8px 30px rgba(228, 93, 4, 0.15); - transform: translateY(-4px) scale(1.02); -} - -.add-trip-card { - background: var(--bg-card); - border: 4px dashed var(--primary-color); - border-radius: 12px; - padding: 1.5rem; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - min-height: 200px; - opacity: 0.8; -} - -.add-trip-card:hover { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--color-red-orange) 100%); - border-color: var(--primary-hover); - transform: translateY(-2px); - opacity: 1; -} - -.add-trip-card:hover .add-trip-content { - color: white; -} - -.add-trip-card:hover .add-trip-icon { - color: white; -} - -.add-trip-content { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - color: var(--primary-color); - transition: color 0.3s ease; -} - -.add-trip-icon { - width: 4rem; - height: 4rem; - color: var(--primary-color); -} - -.add-trip-text { - font-size: 1.3rem; - font-weight: 600; -} - -.trip-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1rem; -} - -.trip-card-title { - margin: 0; - color: var(--primary-color); - font-size: 1.25rem; - line-height: 1.3; - flex: 1; - margin-right: 1rem; - position: relative; -} - -.trip-card-title::before { - content: '🗺️'; - font-size: 0.9rem; - margin-right: 0.5rem; - opacity: 0.7; -} - -.trip-card-menu { - position: relative; -} - -.trip-menu-trigger { - background: none; - border: none; - font-size: 1.25rem; - cursor: pointer; - padding: 0.25rem; - color: #666; - border-radius: var(--border-radius-sm); - transition: background-color var(--transition-normal); -} - -.trip-menu-trigger:hover { - background: #e9ecef; -} - -.trip-dropdown { - position: absolute; - top: 100%; - right: 0; - background: white; - border: 1px solid #ddd; - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-lg); - z-index: 1000; - min-width: 120px; -} - -.dropdown-item { - display: block; - width: 100%; - padding: 0.5rem 1rem; - border: none; - background: none; - text-align: left; - cursor: pointer; - transition: background-color var(--transition-normal); - color: #333; -} - -.dropdown-item:hover { - background: #f8f9fa; -} - -.dropdown-item-danger { - color: var(--danger-color); -} - -.dropdown-item-danger:hover { - background: #f8d7da; - color: var(--danger-hover); -} - -.trip-card-description { - color: #666; - margin: 0 0 1rem 0; - line-height: 1.5; -} - -.trip-card-dates { - margin-bottom: 1rem; -} - -.trip-date-range { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.trip-dates { - font-weight: 500; - color: var(--text-primary); -} - -.trip-duration { - font-size: 0.875rem; - color: #666; -} - -.trip-dates-placeholder { - color: #999; - font-style: italic; -} - -.trip-card-footer { - border-top: 1px solid #e9ecef; - padding-top: 0.75rem; - margin-top: 1rem; -} - -.trip-created { - font-size: 0.875rem; - color: #999; -} - -.empty-state { - text-align: center; - padding: 3rem 2rem; - background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-light) 100%); - border-radius: 16px; - margin-top: 2rem; - border: 2px dashed rgba(228, 93, 4, 0.2); - position: relative; -} - -.empty-state::before { - content: '✈️'; - font-size: 3rem; - display: block; - margin-bottom: 1rem; - opacity: 0.6; -} - -.empty-state h3 { - color: #666; - margin-bottom: 0.5rem; -} - -.empty-state p { - color: #999; - margin: 0; -} - -/* Modal Styles */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(3, 7, 30, 0.8); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; - animation: modalOverlayFadeIn 0.2s ease-out; -} - -@keyframes modalOverlayFadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes modalSlideIn { - from { - opacity: 0; - transform: translateY(-20px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.modal-content { - background: var(--bg-card); - border-radius: var(--border-radius-md); - width: 100%; - max-width: 500px; - max-height: 90vh; - overflow: hidden; - box-shadow: - 0 25px 80px rgba(3, 7, 30, 0.4), - 0 0 0 1px rgba(228, 93, 4, 0.1); - animation: modalSlideIn 0.3s ease-out; - position: relative; -} - -.modal-content::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient(90deg, var(--color-orange) 0%, var(--color-red-orange) 50%, var(--color-crimson) 100%); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 2rem 2rem 1rem; - background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-light) 100%); -} - -.modal-header h2 { - margin: 0; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 700; - background: linear-gradient(135deg, var(--primary-color) 0%, var(--color-red-orange) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.modal-close { - background: var(--bg-secondary); - border: 1px solid rgba(228, 93, 4, 0.2); - font-size: 1.25rem; - cursor: pointer; - color: var(--primary-color); - padding: 0; - width: 2.5rem; - height: 2.5rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: all 0.2s ease; - font-weight: 300; -} - -.modal-close:hover { - background: var(--primary-color); - color: white; - transform: scale(1.1); -} - -.modal-body { - padding: 0 2rem; - max-height: calc(90vh - 200px); - overflow-y: auto; -} - -.trip-form { - padding: 1.5rem 0; -} - -.trip-form .form-group { - margin-bottom: 1.5rem; -} - -.trip-form textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--primary-color); - border-radius: var(--border-radius-md); - font-size: 1rem; - font-family: inherit; - resize: vertical; - min-height: 80px; - box-sizing: border-box; - background: #fff4e6; - color: #000; -} - -.trip-form textarea:focus { - outline: none; - border-color: var(--color-red-orange); - box-shadow: 0 0 0 2px rgba(220, 47, 2, 0.25); -} - -.date-format-hint { - font-size: 0.875rem; - color: #666; - font-weight: normal; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 1rem; - padding: 1.5rem; - border-top: 1px solid rgba(228, 93, 4, 0.1); - background: var(--bg-secondary); - border-radius: 0 0 16px 16px; -} - -.btn-primary { - padding: 0.75rem 1.5rem; - background: var(--primary-color); - color: white; - border: none; - border-radius: var(--border-radius-md); - cursor: pointer; - font-weight: 500; - transition: background-color var(--transition-normal); -} - -.btn-primary:hover:not(:disabled) { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--color-red-orange) 100%); -} - -.btn-primary:disabled { - background: var(--text-muted); - cursor: not-allowed; -} - -.btn-secondary { - padding: 0.75rem 1.5rem; - background: transparent; - color: var(--secondary-color); - border: 2px solid var(--secondary-color); - border-radius: var(--border-radius-md); - cursor: pointer; - font-weight: 500; - transition: all var(--transition-normal); -} - -.btn-secondary:hover:not(:disabled) { - background: linear-gradient(135deg, var(--secondary-color) 0%, var(--color-burgundy) 100%); - border-color: var(--color-burgundy); - color: white; - transform: translateY(-1px); -} - -.btn-secondary:disabled { - background: var(--text-muted); - cursor: not-allowed; -} - -.btn-danger { - padding: 0.75rem 1.5rem; - background: var(--danger-color); - color: white; - border: none; - border-radius: var(--border-radius-md); - cursor: pointer; - font-weight: 500; - transition: background-color var(--transition-normal); -} - -.btn-danger:hover:not(:disabled) { - background: var(--danger-hover); -} - -.btn-danger:disabled { - background: var(--text-muted); - cursor: not-allowed; -} - -.confirm-dialog { - max-width: 400px; -} - -.confirm-dialog-body { - padding: 1.5rem; -} - -.confirm-dialog-body p { - margin: 0; - color: var(--text-primary); - line-height: 1.5; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .dashboard { - padding: 1rem; - } - - .dashboard-header { - flex-direction: column; - gap: 1rem; - align-items: stretch; - } - - .dashboard-header-actions { - justify-content: space-between; - } - - .trips-grid { - grid-template-columns: 1fr; - } - - .form-row { - grid-template-columns: 1fr; - } - - .modal-content { - margin: 1rem; - max-width: none; - } - - .modal-actions { - flex-direction: column; - } -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9ce837a..3e17bf1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,27 +1,16 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider } from './contexts/AuthContext' -import { ToastProvider } from './components/common/ToastContainer' import AuthGuard from './components/auth/AuthGuard' import Dashboard from './components/Dashboard' -import TripDetail from './components/TripDetail' import './App.css' function App() { return ( - - -
- - - } /> - } /> - } /> - - -
-
-
+
+ + + +
) } diff --git a/frontend/src/components/BaseModal.jsx b/frontend/src/components/BaseModal.jsx deleted file mode 100644 index 589efe4..0000000 --- a/frontend/src/components/BaseModal.jsx +++ /dev/null @@ -1,48 +0,0 @@ -const BaseModal = ({ - isOpen, - onClose, - title, - children, - actions, - maxWidth = "500px", - className = "" -}) => { - if (!isOpen) return null; - - const handleOverlayClick = (e) => { - if (e.target === e.currentTarget) { - onClose(); - } - }; - - return ( -
-
e.stopPropagation()} - > - {title && ( -
-

{title}

- -
- )} - -
- {children} -
- - {actions && ( -
- {actions} -
- )} -
-
- ); -}; - -export default BaseModal; \ No newline at end of file diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx deleted file mode 100644 index 7488504..0000000 --- a/frontend/src/components/ConfirmDialog.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import BaseModal from './BaseModal'; - -const ConfirmDialog = ({ - isOpen, - onClose, - onConfirm, - title = "Confirm Action", - message = "Are you sure you want to proceed?", - confirmText = "Confirm", - cancelText = "Cancel", - isLoading = false, - variant = "danger" // "danger" or "primary" -}) => { - const handleConfirm = async () => { - try { - await onConfirm(); - onClose(); - } catch (error) { - console.error('Confirm action failed:', error); - } - }; - - const actions = ( - <> - - - - ); - - return ( - -
-

{message}

-
-
- ); -}; - -export default ConfirmDialog; \ No newline at end of file diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index a939c7d..415914a 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -1,164 +1,47 @@ -import { useState, useEffect, useRef } from 'react'; import { useAuth } from '../contexts/AuthContext'; -import api from '../utils/api'; -import TripList from './TripList'; -import TripModal from './TripModal'; -import ConfirmDialog from './ConfirmDialog'; const Dashboard = () => { const { user, logout } = useAuth(); - const [trips, setTrips] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [showTripModal, setShowTripModal] = useState(false); - const [selectedTrip, setSelectedTrip] = useState(null); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [tripToDelete, setTripToDelete] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const [showUserDropdown, setShowUserDropdown] = useState(false); - const userDropdownRef = useRef(null); - - useEffect(() => { - fetchTrips(); - }, []); - - useEffect(() => { - const handleClickOutside = (event) => { - if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) { - setShowUserDropdown(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const fetchTrips = async () => { - try { - setIsLoading(true); - const response = await api.get('/trips'); - setTrips(response.data.data); - } catch (error) { - console.error('Error fetching trips:', error); - } finally { - setIsLoading(false); - } - }; const handleLogout = async () => { await logout(); }; - const handleCreateTrip = () => { - setSelectedTrip(null); - setShowTripModal(true); - }; - - const handleEditTrip = (trip) => { - setSelectedTrip(trip); - setShowTripModal(true); - }; - - const handleDeleteTrip = (trip) => { - setTripToDelete(trip); - setShowDeleteConfirm(true); - }; - - const handleTripSubmit = async (tripData) => { - setIsSubmitting(true); - try { - if (selectedTrip) { - const response = await api.put(`/trips/${selectedTrip.id}`, tripData); - setTrips(trips.map(trip => - trip.id === selectedTrip.id ? response.data.data : trip - )); - } else { - const response = await api.post('/trips', tripData); - setTrips([response.data.data, ...trips]); - } - setShowTripModal(false); - setSelectedTrip(null); - } catch (error) { - console.error('Error saving trip:', error); - throw error; - } finally { - setIsSubmitting(false); - } - }; - - const confirmDeleteTrip = async () => { - try { - await api.delete(`/trips/${tripToDelete.id}`); - setTrips(trips.filter(trip => trip.id !== tripToDelete.id)); - setShowDeleteConfirm(false); - setTripToDelete(null); - } catch (error) { - console.error('Error deleting trip:', error); - throw error; - } - }; - return (
-
-

TripPlanner

-
+

Trip Planner Dashboard

- Welcome back! -
- - {showUserDropdown && ( -
- -
- )} -
+ Welcome, {user?.name}! +
- +
+

Welcome to Your Trip Planner

+

Start planning your next adventure!

+
+ +
+
+

Plan Trips

+

Create and organize your travel itineraries

+
+ +
+

Save Destinations

+

Keep track of places you want to visit

+
+ +
+

Share Plans

+

Collaborate with friends and family

+
+
- - { - setShowTripModal(false); - setSelectedTrip(null); - }} - onSubmit={handleTripSubmit} - trip={selectedTrip} - isLoading={isSubmitting} - /> - - { - setShowDeleteConfirm(false); - setTripToDelete(null); - }} - onConfirm={confirmDeleteTrip} - title="Delete Trip" - message={`Are you sure you want to delete "${tripToDelete?.name}"? This action cannot be undone.`} - confirmText="Delete" - variant="danger" - />
); }; diff --git a/frontend/src/components/TripCard.jsx b/frontend/src/components/TripCard.jsx deleted file mode 100644 index 164ba0c..0000000 --- a/frontend/src/components/TripCard.jsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useState, useRef, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { formatDateShort, getDuration } from '../utils/dateFormatter'; - -const TripCard = ({ trip, onEdit, onDelete }) => { - const navigate = useNavigate(); - const [showDropdown, setShowDropdown] = useState(false); - const dropdownRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { - setShowDropdown(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - // Removed - using shared utility - - const handleEdit = () => { - setShowDropdown(false); - onEdit(trip); - }; - - const handleDelete = () => { - setShowDropdown(false); - onDelete(trip); - }; - - // Removed - using shared utility - - const handleCardClick = (e) => { - // Don't navigate if clicking on menu buttons - if (e.target.closest('.trip-card-menu')) { - return; - } - navigate(`/trip/${trip.id}`); - }; - - return ( -
-
-

{trip.name}

-
- - {showDropdown && ( -
- - -
- )} -
-
- - {trip.description && ( -

{trip.description}

- )} - -
- {trip.start_date && trip.end_date ? ( -
- - {formatDateShort(trip.start_date)} - {formatDateShort(trip.end_date)} - - - {getDuration(trip.start_date, trip.end_date)} - -
- ) : trip.start_date ? ( - - Starts: {formatDateShort(trip.start_date)} - - ) : trip.end_date ? ( - - Ends: {formatDateShort(trip.end_date)} - - ) : ( - - Dates not set - - )} -
- -
- - Created {formatDateShort(trip.created_at)} - -
-
- ); -}; - -export default TripCard; \ No newline at end of file diff --git a/frontend/src/components/TripDetail.css b/frontend/src/components/TripDetail.css deleted file mode 100644 index 27ef97c..0000000 --- a/frontend/src/components/TripDetail.css +++ /dev/null @@ -1,198 +0,0 @@ -.trip-detail { - min-height: 100vh; - display: flex; - flex-direction: column; - background: var(--color-bg-secondary); -} - -/* Header */ -.trip-detail-header { - background: var(--color-bg-primary); - border-bottom: 1px solid var(--color-border); - padding: var(--spacing-md) var(--spacing-xl) var(--spacing-lg); - box-shadow: var(--shadow-sm); -} - -.header-nav { - margin-bottom: 1rem; -} - -.btn-back { - color: var(--color-text-secondary); - text-decoration: none; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - transition: color var(--transition-normal); -} - -.btn-back:hover { - color: var(--color-text-primary); -} - -.header-content h1 { - margin: 0 0 0.5rem 0; - font-size: 2rem; - color: var(--color-text-primary); -} - -.trip-description { - color: var(--color-text-secondary); - margin: 0 0 1rem 0; - font-size: 1.1rem; -} - -.trip-dates { - display: flex; - align-items: center; - gap: var(--spacing-sm); - font-size: 0.95rem; -} - -.date-label { - color: var(--color-text-muted); - font-weight: 500; -} - -.date-value { - color: var(--color-text-primary); -} - -.date-separator { - color: var(--color-text-light); - margin: 0 0.5rem; -} - -/* Content Layout */ -.trip-detail-content { - flex: 1; - display: grid; - grid-template-columns: 1fr var(--sidebar-width); - gap: 0; - height: calc(100vh - var(--header-height)); - min-height: 0; -} - -.trip-detail-main { - padding: var(--spacing-xl); - overflow-y: auto; - background: var(--color-bg-secondary); - order: 1; -} - -.trip-detail-sidebar { - width: var(--sidebar-width); - background: var(--color-bg-primary); - border-left: 1px solid var(--color-border); - overflow-y: auto; - order: 2; -} - -/* Calendar Placeholder */ -.calendar-placeholder { - background: var(--color-bg-primary); - border-radius: var(--border-radius-lg); - padding: var(--spacing-xl); - text-align: center; - box-shadow: var(--shadow-sm); -} - -.calendar-placeholder h2 { - color: var(--color-text-primary); - margin: 0 0 1rem 0; -} - -.calendar-placeholder p { - color: var(--color-text-muted); -} - -/* Loading State */ -.trip-detail-loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - gap: var(--spacing-md); -} - -.spinner { - width: 40px; - height: 40px; - border: 3px solid #f3f3f3; - border-top: 3px solid #333; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Error State */ -.trip-detail-error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - text-align: center; - gap: var(--spacing-md); -} - -.trip-detail-error h2 { - color: var(--color-danger); - margin: 0; -} - -.trip-detail-error p { - color: var(--color-text-secondary); - margin: 0; -} - -.trip-detail-error .btn-back { - margin-top: 1rem; - padding: var(--spacing-sm) var(--spacing-md); - background: #333; - color: white; - text-decoration: none; - border-radius: var(--border-radius-sm); - transition: background var(--transition-normal); -} - -.trip-detail-error .btn-back:hover { - background: #555; -} - -/* Responsive */ -@media (max-width: 768px) { - .trip-detail-content { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr; - height: auto; - } - - .trip-detail-main { - order: 2; - padding: var(--spacing-md); - } - - .trip-detail-sidebar { - order: 1; - width: 100%; - border-left: none; - border-bottom: 1px solid var(--color-border); - min-height: 300px; - max-height: 50vh; - } - - .trip-detail-header { - padding: var(--spacing-md); - } - - .header-content h1 { - font-size: 1.5rem; - } -} \ No newline at end of file diff --git a/frontend/src/components/TripDetail.jsx b/frontend/src/components/TripDetail.jsx deleted file mode 100644 index 85b376b..0000000 --- a/frontend/src/components/TripDetail.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useState, useEffect, useMemo } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; -import { formatDate } from '../utils/dateFormatter'; -import { useTrip } from '../hooks/useTrip'; -import PlannablesList from './plannables/PlannablesList'; -import TripTimeline from './timeline/TripTimeline'; -import axios from 'axios'; -import './TripDetail.css'; - -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; - -const TripDetail = () => { - const { id } = useParams(); - const navigate = useNavigate(); - const [trip, setTrip] = useState(null); - const [plannableItems, setPlannableItems] = useState([]); - const { fetchTrip, loading, error } = useTrip(); - - useEffect(() => { - const loadTrip = async () => { - try { - const tripData = await fetchTrip(id); - setTrip(tripData); - } catch (err) { - console.error('Error loading trip:', err); - } - }; - - loadTrip(); - }, [id, fetchTrip]); - - useEffect(() => { - const loadPlannableItems = async () => { - try { - const token = localStorage.getItem('token'); - const response = await axios.get(`${API_URL}/api/trips/${id}/plannables`, { - headers: { Authorization: `Bearer ${token}` } - }); - setPlannableItems(response.data.data || []); - } catch (err) { - console.error('Error loading plannable items:', err); - } - }; - - if (id) { - loadPlannableItems(); - } - }, [id]); - - // Memoize trip dates display to prevent unnecessary re-renders - const tripDatesDisplay = useMemo(() => { - if (!trip) return null; - - return ( -
- Start: - {formatDate(trip.start_date)} - - End: - {formatDate(trip.end_date)} -
- ); - }, [trip]); - - if (loading) { - return ( -
-
-

Loading trip details...

-
- ); - } - - if (error) { - return ( -
-

Error

-

{error}

- Back to Dashboard -
- ); - } - - if (!trip) { - return null; - } - - return ( -
-
-
- ← Back to Dashboard -
-
-

{trip.name}

- {trip.description && ( -

{trip.description}

- )} - {tripDatesDisplay} -
-
- -
-
- setPlannableItems(items)} /> -
-
- { - // Optional: refresh plannable items if needed - }} - /> -
-
-
- ); -}; - -export default TripDetail; \ No newline at end of file diff --git a/frontend/src/components/TripList.jsx b/frontend/src/components/TripList.jsx deleted file mode 100644 index b75df17..0000000 --- a/frontend/src/components/TripList.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import TripCard from './TripCard'; -import { GlobeEuropeAfricaIcon } from '@heroicons/react/24/outline'; - -const TripList = ({ trips, isLoading, onEdit, onDelete, onCreateTrip }) => { - if (isLoading) { - return ( -
-
- Loading trips... -
-
- ); - } - - if (!trips || trips.length === 0) { - return ( -
-

Your Trips

-
-
-
- - Create New Trip -
-
-
-
- ); - } - - return ( -
-

Your Trips

-
- {trips.map((trip) => ( - - ))} -
-
- - Create New Trip -
-
-
-
- ); -}; - -export default TripList; \ No newline at end of file diff --git a/frontend/src/components/TripModal.jsx b/frontend/src/components/TripModal.jsx deleted file mode 100644 index 541bf36..0000000 --- a/frontend/src/components/TripModal.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useState, useEffect } from 'react'; -import BaseModal from './BaseModal'; - -const TripModal = ({ isOpen, onClose, onSubmit, trip = null, isLoading = false }) => { - const [formData, setFormData] = useState({ - name: '', - description: '', - start_date: '', - end_date: '' - }); - const [errors, setErrors] = useState({}); - - useEffect(() => { - if (trip) { - setFormData({ - name: trip.name || '', - description: trip.description || '', - start_date: trip.start_date ? trip.start_date.split('T')[0] : '', - end_date: trip.end_date ? trip.end_date.split('T')[0] : '' - }); - } else { - setFormData({ - name: '', - description: '', - start_date: '', - end_date: '' - }); - } - setErrors({}); - }, [trip, isOpen]); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value - })); - - if (errors[name]) { - setErrors(prev => ({ - ...prev, - [name]: '' - })); - } - }; - - const validateForm = () => { - const newErrors = {}; - - if (!formData.name.trim()) { - newErrors.name = 'Trip name is required'; - } - - if (formData.start_date && formData.end_date && formData.start_date > formData.end_date) { - newErrors.end_date = 'End date must be after start date'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - try { - await onSubmit(formData); - onClose(); - } catch (error) { - if (error.response?.data?.errors) { - setErrors(error.response.data.errors); - } - } - }; - - const handleClose = () => { - setFormData({ - name: '', - description: '', - start_date: '', - end_date: '' - }); - setErrors({}); - onClose(); - }; - - const actions = ( - <> - - - - ); - - return ( - -
-
- - - {errors.name && {errors.name}} -
- -
- -