diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php
index 9f4ae075..4d7f403f 100644
--- a/app/Http/Controllers/TicketController.php
+++ b/app/Http/Controllers/TicketController.php
@@ -21,13 +21,52 @@ public function index(Request $request)
});
}
+ $sortable = ['reference', 'type', 'seat', 'event'];
+ $order = in_array($request->input('order'), $sortable, true)
+ ? $request->input('order')
+ : 'event';
+
+ $direction = strtolower((string) $request->input('order_direction'));
+ if (!in_array($direction, ['asc', 'desc'], true)) {
+ $direction = $order === 'event' ? 'desc' : 'asc';
+ }
+
+ switch ($order) {
+ case 'reference':
+ $query->orderBy('tickets.reference', $direction);
+ break;
+ case 'type':
+ $query->join('ticket_types', 'tickets.ticket_type_id', '=', 'ticket_types.id')
+ ->orderBy('ticket_types.name', $direction);
+ break;
+ case 'seat':
+ $query->leftJoin('seats', 'seats.ticket_id', '=', 'tickets.id')
+ ->orderBy('seats.row', $direction)
+ ->orderBy('seats.number', $direction);
+ break;
+ case 'event':
+ default:
+ $query->join('events', 'tickets.event_id', '=', 'events.id')
+ ->orderBy('events.starts_at', $direction);
+ break;
+ }
+
+ $params = [
+ 'order' => $order,
+ 'order_direction' => $direction,
+ ];
+
$tickets = $query->with(['event' => function ($query) {
$query->orderBy('starts_at', 'DESC');
}, 'type', 'seat'])
- ->paginate();
+ ->select('tickets.*')
+ ->orderBy('tickets.id')
+ ->paginate()
+ ->appends($params);
return view('tickets.index', [
'tickets' => $tickets,
+ 'params' => $params,
]);
}
diff --git a/resources/views/tickets/index.blade.php b/resources/views/tickets/index.blade.php
index 313866b0..15867162 100644
--- a/resources/views/tickets/index.blade.php
+++ b/resources/views/tickets/index.blade.php
@@ -27,10 +27,10 @@
- | Reference |
- Type |
- Seat |
- Event |
+ @include('partials._sortheader', ['title' => 'Reference', 'field' => 'reference']) |
+ @include('partials._sortheader', ['title' => 'Type', 'field' => 'type']) |
+ @include('partials._sortheader', ['title' => 'Seat', 'field' => 'seat']) |
+ @include('partials._sortheader', ['title' => 'Event', 'field' => 'event', 'direction' => 'desc']) |
|
diff --git a/tests/Feature/TicketSortingTest.php b/tests/Feature/TicketSortingTest.php
new file mode 100644
index 00000000..5dd75d61
--- /dev/null
+++ b/tests/Feature/TicketSortingTest.php
@@ -0,0 +1,46 @@
+create(['first_login' => false]);
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']);
+
+ $response = $this->actingAs($user)->get(route('tickets.index'));
+
+ // assertOk should hold even BEFORE the view change (plain-text headers still render 200).
+ // If this fails before you touch the view, it is an environment/layout problem to resolve first,
+ // not part of this feature.
+ $response->assertOk();
+
+ // This is the real red -> green assertion for this task: the Reference header must link with order=reference.
+ // On the default request the pagination links use order=event, so this string only appears once the
+ // Reference sort header exists.
+ $response->assertSee('order=reference', false);
+ }
+
+ public function testTicketsPageOrdersRowsByReferenceAscending()
+ {
+ $user = User::factory()->create(['first_login' => false]);
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']);
+ Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']);
+
+ $response = $this->actingAs($user)->get(route('tickets.index', ['order' => 'reference', 'order_direction' => 'asc']));
+
+ $response->assertOk();
+ $response->assertSeeInOrder(['AAA-001', 'ZZZ-999']);
+ }
+}
diff --git a/tests/Unit/app/Http/Controllers/TicketControllerTest.php b/tests/Unit/app/Http/Controllers/TicketControllerTest.php
index 0cafb240..441cca67 100644
--- a/tests/Unit/app/Http/Controllers/TicketControllerTest.php
+++ b/tests/Unit/app/Http/Controllers/TicketControllerTest.php
@@ -5,8 +5,10 @@
use App\Http\Controllers\TicketController;
use App\Http\Requests\TicketTransferRequest;
use App\Models\Event;
+use App\Models\Seat;
use App\Models\Setting;
use App\Models\Ticket;
+use App\Models\TicketType;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
@@ -136,4 +138,157 @@ public function testTransferWhenTransfersDisabledReturnsError()
$this->assertStringContainsString('/tickets', $response->getTargetUrl());
$this->assertNotEmpty($response->getSession()->get('errorMessage'));
}
+
+ public function testIndexDefaultsToNewestEventFirst()
+ {
+ $user = User::factory()->create();
+ $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]);
+ $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]);
+ $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]);
+ $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]);
+
+ $tickets = $this->indexResponse($user)->getData()['tickets'];
+
+ $this->assertSame([$newTicket->id, $oldTicket->id], $tickets->pluck('id')->all());
+ }
+
+ public function testIndexPassesResolvedSortParamsToView()
+ {
+ $user = User::factory()->create();
+
+ $data = $this->indexResponse($user)->getData();
+
+ $this->assertSame(['order' => 'event', 'order_direction' => 'desc'], $data['params']);
+ }
+
+ public function testIndexSortsByReference()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ $a = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']);
+ $z = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']);
+
+ $asc = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'asc'])->getData()['tickets'];
+ $this->assertSame([$a->id, $z->id], $asc->pluck('id')->all());
+
+ $desc = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'desc'])->getData()['tickets'];
+ $this->assertSame([$z->id, $a->id], $desc->pluck('id')->all());
+ }
+
+ public function testIndexSortsByType()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ $typeA = TicketType::factory()->create(['name' => 'AAA Type']);
+ $typeZ = TicketType::factory()->create(['name' => 'ZZZ Type']);
+ $ticketA = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'ticket_type_id' => $typeA->id]);
+ $ticketZ = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'ticket_type_id' => $typeZ->id]);
+
+ $asc = $this->indexResponse($user, ['order' => 'type', 'order_direction' => 'asc'])->getData()['tickets'];
+ $this->assertSame([$ticketA->id, $ticketZ->id], $asc->pluck('id')->all());
+
+ $desc = $this->indexResponse($user, ['order' => 'type', 'order_direction' => 'desc'])->getData()['tickets'];
+ $this->assertSame([$ticketZ->id, $ticketA->id], $desc->pluck('id')->all());
+ }
+
+ public function testIndexSortsBySeat()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ $front = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]);
+ $back = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]);
+ Seat::factory()->create(['ticket_id' => $front->id, 'row' => 1, 'number' => 1]);
+ Seat::factory()->create(['ticket_id' => $back->id, 'row' => 9, 'number' => 9]);
+
+ $asc = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'asc'])->getData()['tickets'];
+ $this->assertSame([$front->id, $back->id], $asc->pluck('id')->all());
+
+ $desc = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'desc'])->getData()['tickets'];
+ $this->assertSame([$back->id, $front->id], $desc->pluck('id')->all());
+ }
+
+ public function testIndexSeatSortKeepsUnseatedTickets()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+
+ $frontRow = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]);
+ $backRow = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]);
+ $unseated = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]);
+
+ Seat::factory()->create(['ticket_id' => $frontRow->id, 'row' => 1, 'number' => 1, 'label' => '1-1']);
+ Seat::factory()->create(['ticket_id' => $backRow->id, 'row' => 2, 'number' => 5, 'label' => '2-5']);
+
+ $tickets = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'asc'])->getData()['tickets'];
+ $ids = $tickets->pluck('id')->all();
+
+ $this->assertCount(3, $tickets);
+ $this->assertContains($unseated->id, $ids);
+
+ $frontPosition = array_search($frontRow->id, $ids);
+ $backPosition = array_search($backRow->id, $ids);
+ $this->assertLessThan($backPosition, $frontPosition);
+ }
+
+ public function testIndexSortsByEventStartsAt()
+ {
+ $user = User::factory()->create();
+ $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]);
+ $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]);
+ $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]);
+ $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]);
+
+ $asc = $this->indexResponse($user, ['order' => 'event', 'order_direction' => 'asc'])->getData()['tickets'];
+ $this->assertSame([$oldTicket->id, $newTicket->id], $asc->pluck('id')->all());
+ }
+
+ public function testIndexInvalidOrderFallsBackToNewestEventFirst()
+ {
+ $user = User::factory()->create();
+ $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]);
+ $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]);
+ $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]);
+ $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]);
+
+ $tickets = $this->indexResponse($user, ['order' => 'bogus', 'order_direction' => 'desc'])->getData()['tickets'];
+
+ $this->assertSame([$newTicket->id, $oldTicket->id], $tickets->pluck('id')->all());
+ }
+
+ public function testIndexInvalidDirectionFallsBackToAscending()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ $a = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']);
+ $z = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']);
+
+ $tickets = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'sideways'])->getData()['tickets'];
+
+ $this->assertSame([$a->id, $z->id], $tickets->pluck('id')->all());
+ }
+
+ public function testIndexPaginationPreservesSort()
+ {
+ $user = User::factory()->create();
+ $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]);
+ Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']);
+
+ $tickets = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'asc'])->getData()['tickets'];
+ $url = $tickets->url(1);
+
+ $this->assertStringContainsString('order=reference', $url);
+ $this->assertStringContainsString('order_direction=asc', $url);
+ }
+
+ private function indexResponse(User $user, array $query = []): View
+ {
+ $this->actingAs($user);
+ $controller = new TicketController();
+ $request = Request::create('/', 'GET', $query);
+ $request->setUserResolver(function () use ($user) {
+ return $user;
+ });
+
+ return $controller->index($request);
+ }
}