feat(orders): implement default sorting for order queries

Added default sorting by date in descending order for order queries.
Updated related views and tests to reflect this change.
This commit is contained in:
Jérémy Munsch 2026-10-29 21:16:06 +01:00
parent 0e05779f89
commit cd7d5f5fe9
No known key found for this signature in database
GPG key ID: 2AAECDA6B7330F91
8 changed files with 164 additions and 25 deletions

View file

@ -68,7 +68,13 @@ final readonly class GetBoPaginatedOrdersQuery implements Query
$query->where('status', $input->status);
}
$this->dataTable->sortQuery(Order::class, $query);
$this->dataTable->sortQuery(
Order::class,
$query,
defaultSortCol: 'date',
defaultSortWay: \SortDirection::Descending,
columnMap: ['date' => 'id'],
);
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);
/** @var LengthAwarePaginator<int, OrderDto> $ordersDtoPaginator */

View file

@ -37,9 +37,15 @@ final readonly class GetBoTodayOrdersQuery implements Query
$query = Order::query()->with(['shop']);
$query->where('pickup_day', Date::now()->toDateString());
$query->whereCreatedAtBetween(Date::now(), Date::now());
$this->dataTable->sortQuery(Order::class, $query);
$this->dataTable->sortQuery(
Order::class,
$query,
defaultSortCol: 'date',
defaultSortWay: \SortDirection::Descending,
columnMap: ['date' => 'id'],
);
$paginator = $this->dataTable->paginateQuery($query, $input->pagination);

View file

@ -98,11 +98,15 @@ class DataTableService
* @param class-string<TModel> $modelClass
* @param TBuilder $query
* @param array<int, string> $ignore Columns to ignore for sorting.
* @param array<string, string> $columnMap Map virtual column names to database columns.
*/
public function sortQuery(
string $modelClass,
Builder $query,
array $ignore = []
array $ignore = [],
?string $defaultSortCol = null,
SortDirection $defaultSortWay = SortDirection::Ascending,
array $columnMap = [],
): Builder {
$table = (new $modelClass())->getTable();
$rName = $this->request->route()?->getName() ?? 'noroute';
@ -115,15 +119,22 @@ class DataTableService
$sort_col = $sort['sort_col'];
$sort_way = $sort['sort_way'];
if ((! $sort_col || $sort_col === 'undefined') && $defaultSortCol !== null) {
$sort_col = $defaultSortCol;
$sort_way = $defaultSortWay;
}
$this->persistSortSession($rName, $sort_col, $sort_way);
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $sort_col)) {
$dbCol = $columnMap[$sort_col] ?? $sort_col;
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $dbCol)) {
$direction = match ($sort_way) {
SortDirection::Descending => 'desc',
default => 'asc',
};
$query->orderBy("{$table}.{$sort_col}", $direction);
$query->orderBy("{$table}.{$dbCol}", $direction);
} elseif ($this->hasTableColumn($table, self::ORDER_COLUMN)) {
$query->orderBy(self::ORDER_COLUMN, 'asc');

View file

@ -2,9 +2,16 @@
// * Route vars
$routeName = request()->route()?->getName() ?? 'noroute';
// * Default sort vars
$defaultSortCol = $defaultSortCol ?? 'order';
$defaultSortWay = $defaultSortWay ?? 'asc';
$defaultSortWayStr = $defaultSortWay instanceof \SortDirection
? ($defaultSortWay === \SortDirection::Descending ? 'desc' : 'asc')
: (string) $defaultSortWay;
// * Request vars
$currentSortCol = session()->get("{$routeName}.sort_col");
$currentSortWay = session()->get("{$routeName}.sort_way");
$currentSortCol = session()->get("{$routeName}.sort_col", $defaultSortCol);
$currentSortWay = session()->get("{$routeName}.sort_way", $defaultSortWayStr);
$searchQuery = request()->input('search');
@ -18,7 +25,7 @@
View::share('noOrder', $noOrder);
// * Reset button
$hasCustomSort = ($currentSortCol && $currentSortCol !== 'order') || ($currentSortCol === 'order' && $currentSortWay === 'desc');
$hasCustomSort = ($currentSortCol && $currentSortCol !== $defaultSortCol) || ($currentSortCol === $defaultSortCol && $currentSortWay !== $defaultSortWayStr);
$showReset = !empty($searchQuery) || $hasCustomSort;
@endphp

View file

@ -16,6 +16,8 @@
'cols' => $cols,
'mobileHide' => [],
'ignore' => ['customer', 'pickup_date'],
'defaultSortCol' => 'date',
'defaultSortWay' => 'desc',
])
</thead>
<tbody>

View file

@ -1,10 +1,6 @@
@extends('bo.layouts.layout')
@if (request()->routeIs('bo.merch.starred'))
@section('title', __('crud.helpers.starred'))
@else
@section('title', Str::ucfirst(trans_choice('models.classes.orders', \INF)))
@endif
@section('title', __('Today\'s Orders'))
@section('breadcrumb', request()->route()->getName())
@ -14,7 +10,7 @@
<div class="row">
<div class="col">
<h1 class="h3">
{{ Str::ucfirst(trans_choice('models.classes.orders', \INF)) }}&nbsp;{{ Str::ucfirst(__('crud.actions.list')) }}
{{ __('Today\'s Orders') }}
</h1>
</div>
</div>

View file

@ -113,25 +113,72 @@ describe('Bo\OrdersController', function (): void {
// THEN
expect($response->status())->toBe(200);
$response->assertViewHas('orders', function ($orders) use ($ref): bool {
return $orders instanceof LengthAwarePaginator
&& $orders->total() === 1
&& (string) $orders->first()->reference === $ref;
$response->assertViewHas('orders', function (mixed $orders) use ($ref): bool {
/** @var \Illuminate\Pagination\LengthAwarePaginator<int, OrderDto> $orders */
return $orders instanceof \Illuminate\Pagination\LengthAwarePaginator
&& $orders->total() === 1
&& (string) $orders->first()?->reference === $ref;
});
});
it('can access today orders', function (): void {
it('sorts index orders by default by date from newest to oldest', function (): void {
/** @var \Tests\TestCase $this */
// GIVEN
$shop = Shop::factory()->create();
Order::forceCreate([
'id' => Uuid::uuid7()->toString(),
$shop = Shop::factory()->create();
$olderOrder = Order::forceCreate([
'id' => Uuid::uuid7(Date::now()->subHours(2))->toString(),
'shop_id' => $shop->id,
'reference' => new OrderReference(Uuid::uuid7()),
'status' => OrderStatus::VALIDATED->value,
'total' => new Money(1000, new Currency('EUR')),
'employee' => new OrderEmployee(new Matricule('1000198'), 'Old', 'Order', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')),
'delivery_address' => new DeliveryAddress('Shop Name', 'Address', '75000', 'Paris'),
'pickup_day' => Date::now()->addDays(7)->toDateString(),
]);
$newerOrder = Order::forceCreate([
'id' => Uuid::uuid7(Date::now())->toString(),
'shop_id' => $shop->id,
'reference' => new OrderReference(Uuid::uuid7()),
'status' => OrderStatus::VALIDATED->value,
'total' => new Money(2000, new Currency('EUR')),
'employee' => new OrderEmployee(new Matricule('1000199'), 'New', 'Order', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')),
'delivery_address' => new DeliveryAddress('Shop Name', 'Address', '75000', 'Paris'),
'pickup_day' => Date::now()->addDays(7)->toDateString(),
]);
// WHEN
$response = $this->get('/bo/orders');
// THEN
expect($response->status())->toBe(200);
/** @var \Illuminate\Pagination\LengthAwarePaginator<int, OrderDto> $orders */
$orders = $response->viewData('orders');
expect($orders->first()?->id)->toBe($newerOrder->id)
->and($orders->last()?->id)->toBe($olderOrder->id);
});
it('can access today orders and filters out past orders', function (): void {
/** @var \Tests\TestCase $this */
// GIVEN
$shop = Shop::factory()->create();
$todayOrder = Order::forceCreate([
'id' => Uuid::uuid7(Date::now())->toString(),
'shop_id' => $shop->id,
'reference' => new OrderReference(Uuid::uuid7()),
'status' => OrderStatus::VALIDATED->value,
'total' => new Money(3000, new Currency('EUR')),
'employee' => new OrderEmployee(new Matricule('1000103'), 'Charlie', 'Brown', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')),
'delivery_address' => new DeliveryAddress('Shop Name', 'Address', '75000', 'Paris'),
'pickup_day' => Date::now()->addDays(7)->toDateString(),
]);
$pastOrder = Order::forceCreate([
'id' => Uuid::uuid7(Date::now()->subDays(3))->toString(),
'shop_id' => $shop->id,
'reference' => new OrderReference(Uuid::uuid7()),
'status' => OrderStatus::VALIDATED->value,
'total' => new Money(4000, new Currency('EUR')),
'employee' => new OrderEmployee(new Matricule('1000104'), 'Past', 'Order', new ShopperEstablishmentCode('FAC01'), new ShopperSiteCode('SITE'), new ShopperServiceCode('101')),
'delivery_address' => new DeliveryAddress('Shop Name', 'Address', '75000', 'Paris'),
'pickup_day' => Date::now()->toDateString(),
]);
@ -141,7 +188,10 @@ describe('Bo\OrdersController', function (): void {
// THEN
expect($response->status())->toBe(200);
$response->assertViewIs('bo.orders.index-today');
$response->assertViewHas('orders', fn ($orders) => $orders instanceof LengthAwarePaginator && $orders->total() >= 1);
/** @var \Illuminate\Pagination\LengthAwarePaginator<int, OrderDto> $orders */
$orders = $response->viewData('orders');
expect($orders->total())->toBe(1)
->and($orders->first()?->id)->toBe($todayOrder->id);
});
it('can access order show detail', function (): void {
@ -215,7 +265,7 @@ describe('Bo\OrdersController', function (): void {
$refreshedOrder = Order::query()->find($order->id);
expect($refreshedOrder)->not->toBeNull()
->and($refreshedOrder->status)->toBe(OrderStatus::CANCELED);
->and($refreshedOrder?->status)->toBe(OrderStatus::CANCELED);
});
it('rejects unauthenticated users', function (): void {

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Unit\Services;
use App\Models\Order;
use App\Models\Product;
use App\Services\DataTableService;
use Illuminate\Contracts\Database\Query\Builder;
@ -129,4 +130,64 @@ describe('DataTableService', function () {
// THEN
// Mockery verifies on teardown that orderBy('order', 'asc') was called once.
});
it('sorts by custom default column and way when no sort column is specified', function () {
// GIVEN
$request = Request::create('/bo/orders', 'GET');
$session = mock(Session::class);
$session->shouldReceive('get')->andReturn(null);
Schema::shouldReceive('getColumnListing')
->once()
->with('orders')
->andReturn(['id', 'reference', 'status', 'total']);
$query = mock(Builder::class);
$query->shouldReceive('orderBy')->once()->with('orders.id', 'desc');
$service = new DataTableService($request, $session);
// WHEN
$service->sortQuery(
Order::class,
$query,
defaultSortCol: 'date',
defaultSortWay: SortDirection::Descending,
columnMap: ['date' => 'id'],
);
// THEN
// Mockery verifies on teardown that orderBy('orders.id', 'desc') was called once.
});
it('uses columnMap when sorting by an explicit virtual column', function () {
// GIVEN
$request = Request::create('/bo/orders', 'GET', [
'sort_col' => 'date',
'sort_way' => 'asc',
]);
$session = mock(Session::class);
$session->shouldReceive('get')->andReturn(null);
$session->shouldReceive('put')->twice();
Schema::shouldReceive('getColumnListing')
->once()
->with('orders')
->andReturn(['id', 'reference', 'status', 'total']);
$query = mock(Builder::class);
$query->shouldReceive('orderBy')->once()->with('orders.id', 'asc');
$service = new DataTableService($request, $session);
// WHEN
$service->sortQuery(
Order::class,
$query,
columnMap: ['date' => 'id'],
);
// THEN
// Mockery verifies on teardown that orderBy('orders.id', 'asc') was called once.
});
});