Merge branch 'develop' into trunk
This commit is contained in:
commit
e8a6ab07de
10 changed files with 225 additions and 41 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
52
resources/sass/fo/vendor/_sliders.scss
vendored
52
resources/sass/fo/vendor/_sliders.scss
vendored
|
|
@ -1,6 +1,14 @@
|
|||
#products-carousel {
|
||||
position: relative;
|
||||
|
||||
&.has-controls,
|
||||
&:has(.carousel-control-prev) {
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-left: 3.5rem;
|
||||
padding-right: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
|
|
@ -18,6 +26,50 @@
|
|||
transition: none !important;
|
||||
}
|
||||
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
color: var(--bs-dark);
|
||||
cursor: pointer;
|
||||
background-color: var(--bs-primary);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
transform: translateY(-50%);
|
||||
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--bs-dark);
|
||||
background-color: color-mix(in srgb, var(--bs-primary) 85%, black);
|
||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-50%) scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-control-prev {
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
.carousel-control-next {
|
||||
right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
'cols' => $cols,
|
||||
'mobileHide' => [],
|
||||
'ignore' => ['customer', 'pickup_date'],
|
||||
'defaultSortCol' => 'date',
|
||||
'defaultSortWay' => 'desc',
|
||||
])
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
|||
|
|
@ -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)) }} {{ Str::ucfirst(__('crud.actions.list')) }}
|
||||
{{ __('Today\'s Orders') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="row">
|
||||
<div class="col-12 position-relative overflow-hidden">
|
||||
|
||||
<div class="carousel slide carousel-dark" id="products-carousel" data-bs-ride="false" data-bs-touch="true" role="region"
|
||||
<div class="carousel slide carousel-dark @if (count($starProductList) > 3) has-controls @endif" id="products-carousel" data-bs-ride="false" data-bs-touch="true" role="region"
|
||||
aria-roledescription="carousel" aria-label="{{ __('Offers Slider') }}">
|
||||
<div class="carousel-inner p-2">
|
||||
|
||||
|
|
@ -91,8 +91,7 @@
|
|||
</small>
|
||||
</li>
|
||||
|
||||
<li class="position-relative z-3 py-2 text-center">
|
||||
<li class="d-flex justify-content-between flex-row py-2">
|
||||
<li class="position-relative z-3 d-flex justify-content-between flex-row py-2">
|
||||
@if ($productWithPricing->product->inco?->nutriscore)
|
||||
<img class="object-fit-contain img-fluid mw-5" data-bs-toggle="tooltip"
|
||||
src="{{ Vite::asset("resources/assets/images/nutriscore/nutriscore-{$productWithPricing->product->inco->nutriscore->value()}.png") }}"
|
||||
|
|
@ -101,7 +100,7 @@
|
|||
@endif
|
||||
|
||||
@can('addToBasket', $productWithPricing->product)
|
||||
<div class="basket-buttons" data-json='@json(['id' => $productWithPricing->product->id])'></div>
|
||||
<div class="basket-buttons ms-auto" data-json='@json(['id' => $productWithPricing->product->id])'></div>
|
||||
@endcan
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -115,21 +114,15 @@
|
|||
</div>
|
||||
|
||||
@if (count($starProductList) > 3)
|
||||
<button class="carousel-control-prev d-none d-lg-flex justify-content-start opacity-100" data-bs-target="#products-carousel"
|
||||
data-bs-slide="prev" type="button" style="filter: none; z-index: 10;">
|
||||
<div class="btn btn-primary rounded-circle d-flex justify-content-center align-items-center ms-2 shadow-sm"
|
||||
style="width: 45px; height: 45px; pointer-events: none;">
|
||||
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
||||
</div>
|
||||
<button class="carousel-control-prev d-none d-lg-flex" data-bs-target="#products-carousel"
|
||||
data-bs-slide="prev" type="button" aria-label="{{ __('pagination.previous') }}">
|
||||
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ __('pagination.previous') }}</span>
|
||||
</button>
|
||||
|
||||
<button class="carousel-control-next d-none d-lg-flex justify-content-end opacity-100" data-bs-target="#products-carousel"
|
||||
data-bs-slide="next" type="button" style="filter: none; z-index: 10;">
|
||||
<div class="btn btn-primary rounded-circle d-flex justify-content-center align-items-center me-2 shadow-sm"
|
||||
style="width: 45px; height: 45px; pointer-events: none;">
|
||||
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<button class="carousel-control-next d-none d-lg-flex" data-bs-target="#products-carousel"
|
||||
data-bs-slide="next" type="button" aria-label="{{ __('pagination.next') }}">
|
||||
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ __('pagination.next') }}</span>
|
||||
</button>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue