Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion app/Http/Controllers/TicketController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
}

Expand Down
8 changes: 4 additions & 4 deletions resources/views/tickets/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Reference</th>
<th>Type</th>
<th>Seat</th>
<th>Event</th>
<th>@include('partials._sortheader', ['title' => 'Reference', 'field' => 'reference'])</th>
<th>@include('partials._sortheader', ['title' => 'Type', 'field' => 'type'])</th>
<th>@include('partials._sortheader', ['title' => 'Seat', 'field' => 'seat'])</th>
<th>@include('partials._sortheader', ['title' => 'Event', 'field' => 'event', 'direction' => 'desc'])</th>
<th class="w-1"></th>
</tr>
</thead>
Expand Down
46 changes: 46 additions & 0 deletions tests/Feature/TicketSortingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Tests\Feature;

use App\Models\Event;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TicketSortingTest extends TestCase
{
use RefreshDatabase;

public function testTicketsPageRendersSortableReferenceHeaderLink()
{
$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']);

$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']);
}
}
155 changes: 155 additions & 0 deletions tests/Unit/app/Http/Controllers/TicketControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading