625 lines
24 KiB
PHP
625 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Ticket\Services;
|
|
|
|
use App\Domains\Auth\Models\User;
|
|
use App\Domains\Purchase\Models\PurchaseItem;
|
|
use App\Domains\Purchase\Services\PurchaseRefundSummaryService;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Ticket\Models\Ticket;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AdminAppTicketService
|
|
{
|
|
private const RELATIONS = [
|
|
...TicketValidityResolver::RELATIONS,
|
|
...TicketPresentationResolver::RELATIONS,
|
|
'tenant',
|
|
'user',
|
|
'scannerUser',
|
|
'sourceCatalogItem.category',
|
|
'sourcePurchaseItem.purchase',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly AdminAppTicketColumnService $columnService,
|
|
private readonly AdminAppTicketRowService $rowService,
|
|
private readonly PurchaseRefundSummaryService $refundSummaryService,
|
|
) {}
|
|
|
|
/**
|
|
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, page?: int, per_page?: int, sort_by?: string|null, sort_direction?: string|null} $filters
|
|
*/
|
|
public function search(Tenant $tenant, array $filters = []): AdminAppTicketResult
|
|
{
|
|
$query = $this->baseQuery($tenant, $filters);
|
|
$countQuery = clone $query;
|
|
|
|
$databaseSorted = $this->applyDatabaseSort($query, $tenant, $filters);
|
|
|
|
if (($filters['sort_by'] ?? null) && ! $databaseSorted) {
|
|
$matchingTickets = (clone $query)
|
|
->with(self::RELATIONS)
|
|
->get();
|
|
$matchingTickets = $this->sortTickets($matchingTickets, $tenant, $filters);
|
|
$tickets = $this->paginate($matchingTickets, $filters);
|
|
$scannedTickets = $matchingTickets
|
|
->filter(fn (Ticket $ticket): bool => $ticket->status === Ticket::STATUS_USED)
|
|
->count();
|
|
$activeTickets = $matchingTickets
|
|
->filter(fn (Ticket $ticket): bool => $ticket->is_active())
|
|
->count();
|
|
$totalTickets = $activeTickets + $scannedTickets;
|
|
} else {
|
|
$tickets = (clone $query)
|
|
->with(self::RELATIONS)
|
|
->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id'))
|
|
->paginateFromRequest()
|
|
->withQueryString();
|
|
|
|
$counts = $this->calculateTicketCounts($countQuery);
|
|
$scannedTickets = $counts['scanned'];
|
|
$totalTickets = $counts['total'];
|
|
}
|
|
|
|
return new AdminAppTicketResult(
|
|
tickets: $tickets,
|
|
scannedTickets: $scannedTickets,
|
|
totalTickets: $totalTickets,
|
|
refundedTotal: $this->refundSummaryService->totalForTenant($tenant),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, sort_by?: string|null, sort_direction?: string|null} $filters
|
|
* @return Collection<int, Ticket>
|
|
*/
|
|
public function ticketsForExport(Tenant $tenant, array $filters = []): Collection
|
|
{
|
|
$query = $this->baseQuery($tenant, $filters);
|
|
$databaseSorted = $this->applyDatabaseSort($query, $tenant, $filters);
|
|
$tickets = $query
|
|
->with(self::RELATIONS)
|
|
->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id'))
|
|
->get();
|
|
|
|
return $databaseSorted ? $tickets : $this->sortTickets($tickets, $tenant, $filters);
|
|
}
|
|
|
|
public function cancel(Tenant $tenant, int $ticketId): Ticket
|
|
{
|
|
return DB::transaction(function () use ($tenant, $ticketId): Ticket {
|
|
$ticket = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->lockForUpdate()
|
|
->findOrFail($ticketId);
|
|
|
|
if (! $ticket->can_cancel()) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'El ticket debe estar activo para poder cancelarlo.',
|
|
]);
|
|
}
|
|
|
|
$ticket->markAsCancelled();
|
|
$ticket->save();
|
|
|
|
return $ticket->refresh()->load(self::RELATIONS);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* total: string|null,
|
|
* partial: string|null,
|
|
* }
|
|
*/
|
|
public function calculateRefund(Tenant $tenant, int $ticketId): array
|
|
{
|
|
$ticket = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->findOrFail($ticketId);
|
|
|
|
if (! $ticket->can_refund()) {
|
|
throw ValidationException::withMessages([
|
|
'refund' => 'El reembolso no está disponible para este ticket.',
|
|
]);
|
|
}
|
|
|
|
$purchaseItem = PurchaseItem::query()
|
|
->find($ticket->source_purchase_item_id);
|
|
|
|
if ($purchaseItem === null) {
|
|
throw ValidationException::withMessages([
|
|
'ticket' => 'El ticket no tiene un ítem de compra asociado para reembolsar.',
|
|
]);
|
|
}
|
|
|
|
$unitPrice = (float) $purchaseItem->precio_unitario;
|
|
$itemTotal = (float) $purchaseItem->total;
|
|
$itemRefundedAmount = (float) ($purchaseItem->refunded_amount ?? 0);
|
|
$remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2));
|
|
|
|
$total = null;
|
|
if ($tenant->allow_refund() && $tenant->allow_ticket_total_refund && $unitPrice <= $remainingItemAmount) {
|
|
$total = number_format($unitPrice, 2, '.', '');
|
|
}
|
|
|
|
$partial = null;
|
|
if ($tenant->allow_refund() && $tenant->allow_partial_refund()) {
|
|
$partialAmount = $this->refundAmount($purchaseItem, $tenant, 'partial');
|
|
if ($partialAmount <= $remainingItemAmount) {
|
|
$partial = number_format($partialAmount, 2, '.', '');
|
|
}
|
|
}
|
|
|
|
return [
|
|
'total' => $total,
|
|
'partial' => $partial,
|
|
];
|
|
}
|
|
|
|
public function refund(Tenant $tenant, int $ticketId, string $refundType): Ticket
|
|
{
|
|
$this->ensureRefundIsAllowed($tenant, $refundType);
|
|
|
|
return DB::transaction(function () use ($tenant, $ticketId, $refundType): Ticket {
|
|
$ticket = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->lockForUpdate()
|
|
->findOrFail($ticketId);
|
|
|
|
if (! $ticket->can_refund()) {
|
|
if ($ticket->status !== Ticket::STATUS_ACTIVE) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'El ticket debe estar activo para poder reembolsarlo.',
|
|
]);
|
|
}
|
|
|
|
throw ValidationException::withMessages([
|
|
'refund' => 'El reembolso no está disponible para este ticket.',
|
|
]);
|
|
}
|
|
|
|
$purchaseItem = PurchaseItem::query()
|
|
->lockForUpdate()
|
|
->find($ticket->source_purchase_item_id);
|
|
|
|
if ($purchaseItem === null) {
|
|
throw ValidationException::withMessages([
|
|
'ticket' => 'El ticket no tiene un ítem de compra asociado para reembolsar.',
|
|
]);
|
|
}
|
|
|
|
$refundAmount = $this->refundAmount($purchaseItem, $tenant, $refundType);
|
|
$refundedAmount = round((float) $purchaseItem->refunded_amount + $refundAmount, 2);
|
|
|
|
if ($refundedAmount > (float) $purchaseItem->total) {
|
|
throw ValidationException::withMessages([
|
|
'refund_type' => 'El importe reembolsado no puede superar el total del ítem de compra.',
|
|
]);
|
|
}
|
|
|
|
$ticket->markAsRefunded();
|
|
$ticket->save();
|
|
|
|
$purchaseItem->update([
|
|
'refunded_amount' => number_format($refundedAmount, 2, '.', ''),
|
|
]);
|
|
|
|
return $ticket->refresh()->load(self::RELATIONS);
|
|
});
|
|
}
|
|
|
|
private function ensureRefundIsAllowed(Tenant $tenant, string $refundType): void
|
|
{
|
|
$isAllowed = match ($refundType) {
|
|
'partial' => $tenant->allow_refund() && $tenant->allow_partial_refund(),
|
|
'total' => $tenant->allow_refund() && (bool) $tenant->allow_ticket_total_refund,
|
|
};
|
|
|
|
if (! $isAllowed) {
|
|
throw ValidationException::withMessages([
|
|
'refund_type' => 'El tipo de reembolso solicitado no está habilitado para este tenant.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function refundAmount(PurchaseItem $purchaseItem, Tenant $tenant, string $refundType): float
|
|
{
|
|
$ticketAmount = (float) $purchaseItem->precio_unitario;
|
|
|
|
return match ($refundType) {
|
|
'partial' => round($ticketAmount * (float) $tenant->ticket_partial_refund_percentage / 100, 2),
|
|
'total' => $ticketAmount,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, page?: int, per_page?: int} $filters
|
|
* @return Builder<Ticket>
|
|
*/
|
|
private function baseQuery(Tenant $tenant, array $filters): Builder
|
|
{
|
|
$search = trim((string) ($filters['q'] ?? ''));
|
|
|
|
$query = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->when($search !== '', function (Builder $query) use ($search): void {
|
|
$this->applySearchFilter($query, $search);
|
|
})
|
|
->when($filters['category'] ?? null, function (Builder $query, string $category): void {
|
|
$query->whereHas('sourceCatalogItem.category', fn (Builder $categoryQuery): Builder => $categoryQuery
|
|
->whereRaw('LOWER(nombre) = ?', [mb_strtolower(trim($category))]));
|
|
})
|
|
->when($filters['product'] ?? null, function (Builder $query, string $product) use ($filters): void {
|
|
$this->applyProductFilter($query, (string) ($filters['category'] ?? ''), $product);
|
|
})
|
|
->when($filters['type'] ?? null, function (Builder $query, string $type) use ($filters): void {
|
|
$this->applyTypeFilter($query, (string) ($filters['category'] ?? ''), $type);
|
|
})
|
|
->when($filters['date'] ?? null, function (Builder $query, string $date) use ($tenant): void {
|
|
if ($tenant->codigo === 'fiesta_futbol_infantil') {
|
|
$this->applyEventDateFilter($query, $date);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->whereHas('sourcePurchaseItem.purchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery
|
|
->whereDate('created_at', $date));
|
|
})
|
|
->when($filters['size'] ?? null, function (Builder $query, string $size) use ($filters): void {
|
|
$this->applySizeFilter($query, (string) ($filters['category'] ?? ''), $size);
|
|
});
|
|
|
|
$this->applyStatusFilter($query, $filters['status'] ?? null);
|
|
|
|
return $query;
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applySearchFilter(Builder $query, string $search): void
|
|
{
|
|
$containsPattern = '%'.mb_strtolower($search).'%';
|
|
$amount = $this->searchAmount($search);
|
|
|
|
$query->where(function (Builder $searchQuery) use ($search, $containsPattern, $amount): void {
|
|
$searchQuery
|
|
->where(function (Builder $clientQuery) use ($containsPattern): void {
|
|
$clientQuery
|
|
->whereHas('sourcePurchaseItem.purchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery
|
|
->whereRaw('LOWER(nombre_apellido) LIKE ?', [$containsPattern]))
|
|
->orWhere(function (Builder $fallbackClientQuery) use ($containsPattern): void {
|
|
$fallbackClientQuery
|
|
->where(function (Builder $missingPurchaseClientQuery): void {
|
|
$missingPurchaseClientQuery
|
|
->whereDoesntHave('sourcePurchaseItem.purchase')
|
|
->orWhereHas('sourcePurchaseItem.purchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery
|
|
->whereNull('nombre_apellido'));
|
|
})
|
|
->whereHas('user', fn (Builder $userQuery): Builder => $userQuery
|
|
->whereRaw('LOWER(nombre_apellido) LIKE ?', [$containsPattern]));
|
|
});
|
|
})
|
|
->orWhereHas('scannerUser', fn (Builder $scannerQuery): Builder => $scannerQuery
|
|
->whereRaw('LOWER(nombre_apellido) LIKE ?', [$containsPattern]));
|
|
|
|
if (ctype_digit($search)) {
|
|
$searchQuery
|
|
->orWhere('tickets.id', (int) $search)
|
|
->orWhereHas('sourcePurchaseItem', fn (Builder $purchaseItemQuery): Builder => $purchaseItemQuery
|
|
->where('compra_id', (int) $search));
|
|
}
|
|
|
|
if ($amount !== null) {
|
|
$searchQuery->orWhereHas('sourcePurchaseItem', fn (Builder $purchaseItemQuery): Builder => $purchaseItemQuery
|
|
->where('precio_unitario', $amount));
|
|
}
|
|
});
|
|
}
|
|
|
|
private function searchAmount(string $search): ?string
|
|
{
|
|
$value = preg_replace('/[\s$]/u', '', trim($search));
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/^\d{1,3}(?:\.\d{3})+(?:,\d{1,2})?$/', $value) === 1) {
|
|
$value = str_replace(['.', ','], ['', '.'], $value);
|
|
} elseif (preg_match('/^\d{1,3}(?:,\d{3})+(?:\.\d{1,2})?$/', $value) === 1) {
|
|
$value = str_replace(',', '', $value);
|
|
} elseif (preg_match('/^\d+(?:[.,]\d{1,2})?$/', $value) === 1) {
|
|
$value = str_replace(',', '.', $value);
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
return number_format((float) $value, 2, '.', '');
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyProductFilter(Builder $query, string $category, string $product): void
|
|
{
|
|
$category = $this->normalizedCategory($category);
|
|
|
|
if (in_array($category, ['alojamientos', 'camping'], true)) {
|
|
$this->whereVariantDefinition($query, 'tipo_alojamiento', $product);
|
|
|
|
return;
|
|
}
|
|
|
|
if (in_array($category, ['comidas', 'comida'], true)) {
|
|
$this->whereVariantDefinition($query, 'horario', $product);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->whereHas('sourceCatalogItem', fn (Builder $itemQuery): Builder => $itemQuery
|
|
->where('slug', $product));
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyTypeFilter(Builder $query, string $category, string $type): void
|
|
{
|
|
$attribute = match ($this->normalizedCategory($category)) {
|
|
'comidas', 'comida' => 'servicio',
|
|
'merchandising' => 'color',
|
|
default => null,
|
|
};
|
|
|
|
if ($attribute !== null) {
|
|
$this->whereVariantDefinition($query, $attribute, $type);
|
|
}
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyEventDateFilter(Builder $query, string $date): void
|
|
{
|
|
$query
|
|
->whereHas('sourceCatalogItem.category', fn (Builder $categoryQuery): Builder => $categoryQuery
|
|
->whereRaw('LOWER(nombre) IN (?, ?)', ['comidas', 'comida']))
|
|
->whereHas('sourceVariant', function (Builder $variantQuery) use ($date): void {
|
|
$variantQuery->where(function (Builder $dateQuery) use ($date): void {
|
|
$dateQuery
|
|
->whereHas('eventDate', fn (Builder $eventDateQuery): Builder => $eventDateQuery
|
|
->whereDate('date', $date))
|
|
->orWhereHas('eventDates', fn (Builder $eventDateQuery): Builder => $eventDateQuery
|
|
->whereDate('date', $date));
|
|
});
|
|
});
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applySizeFilter(Builder $query, string $category, string $size): void
|
|
{
|
|
if ($this->normalizedCategory($category) === 'merchandising') {
|
|
$this->whereVariantDefinition($query, 'talle', $size);
|
|
}
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function whereVariantDefinition(Builder $query, string $attribute, string $value): void
|
|
{
|
|
$query->whereHas('sourceVariant.definitions', fn (Builder $definitionQuery): Builder => $definitionQuery
|
|
->where('value', $value)
|
|
->whereHas('itemAttribute.attribute', fn (Builder $attributeQuery): Builder => $attributeQuery
|
|
->where('codigo', $attribute)));
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyStatusFilter(Builder $query, ?string $status): void
|
|
{
|
|
if ($status === null || $status === '') {
|
|
return;
|
|
}
|
|
|
|
if ($status === Ticket::STATUS_USED) {
|
|
$query
|
|
->whereNotNull('used_at')
|
|
->whereNull('disabled_at')
|
|
->whereNull('cancelled_at')
|
|
->whereNull('refunded_at');
|
|
|
|
return;
|
|
}
|
|
|
|
$timestampColumn = match ($status) {
|
|
Ticket::STATUS_DISABLED => 'disabled_at',
|
|
Ticket::STATUS_CANCELLED => 'cancelled_at',
|
|
Ticket::STATUS_REFUNDED => 'refunded_at',
|
|
default => null,
|
|
};
|
|
|
|
if ($timestampColumn !== null) {
|
|
$query->whereNotNull($timestampColumn);
|
|
|
|
if ($status === Ticket::STATUS_DISABLED) {
|
|
$query->whereNull('cancelled_at')->whereNull('refunded_at');
|
|
}
|
|
|
|
if ($status === Ticket::STATUS_CANCELLED) {
|
|
$query->whereNull('refunded_at');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$matchingIds = (clone $query)
|
|
->whereNull('used_at')
|
|
->whereNull('disabled_at')
|
|
->whereNull('cancelled_at')
|
|
->whereNull('refunded_at')
|
|
->with(TicketValidityResolver::RELATIONS)
|
|
->get()
|
|
->filter(fn (Ticket $ticket): bool => $ticket->status === $status)
|
|
->pluck('id');
|
|
|
|
$query->whereIn('tickets.id', $matchingIds);
|
|
}
|
|
|
|
/**
|
|
* @param Builder<Ticket> $countQuery
|
|
* @return array{scanned: int, total: int}
|
|
*/
|
|
private function calculateTicketCounts(Builder $countQuery): array
|
|
{
|
|
$scannedTickets = (clone $countQuery)
|
|
->whereNotNull('used_at')
|
|
->whereNull('disabled_at')
|
|
->whereNull('cancelled_at')
|
|
->whereNull('refunded_at')
|
|
->count();
|
|
|
|
$activeTickets = (clone $countQuery)
|
|
->whereNull('used_at')
|
|
->whereNull('disabled_at')
|
|
->whereNull('cancelled_at')
|
|
->whereNull('refunded_at')
|
|
->with(TicketValidityResolver::RELATIONS)
|
|
->get()
|
|
->filter(fn (Ticket $ticket): bool => $ticket->is_active())
|
|
->count();
|
|
|
|
return [
|
|
'scanned' => $scannedTickets,
|
|
'total' => $activeTickets + $scannedTickets,
|
|
];
|
|
}
|
|
|
|
private function normalizedCategory(string $category): string
|
|
{
|
|
return mb_strtolower(trim($category));
|
|
}
|
|
|
|
/**
|
|
* @param Builder<Ticket> $query
|
|
* @param array{sort_by?: string|null, sort_direction?: string|null} $filters
|
|
*/
|
|
private function applyDatabaseSort(Builder $query, Tenant $tenant, array $filters): bool
|
|
{
|
|
$sortBy = (string) ($filters['sort_by'] ?? '');
|
|
if ($sortBy === '') {
|
|
return false;
|
|
}
|
|
|
|
$direction = ($filters['sort_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
|
|
|
|
$sortExpression = match ($sortBy) {
|
|
'order_number' => $this->purchaseItemColumnQuery('compra_id'),
|
|
'id' => 'tickets.id',
|
|
'amount' => $this->purchaseItemColumnQuery('precio_unitario'),
|
|
'scanned_by' => User::query()
|
|
->withTrashed()
|
|
->select('nombre_apellido')
|
|
->whereColumn('users.id', 'tickets.scanner_user_id'),
|
|
'product' => $tenant->codigo === 'fiesta_futbol_infantil'
|
|
? null
|
|
: $this->purchaseItemColumnQuery('item_nombre'),
|
|
default => null,
|
|
};
|
|
|
|
if ($sortExpression === null) {
|
|
return false;
|
|
}
|
|
|
|
$query->orderBy($sortExpression, $direction)->orderByDesc('tickets.id');
|
|
|
|
return true;
|
|
}
|
|
|
|
/** @return Builder<PurchaseItem> */
|
|
private function purchaseItemColumnQuery(string $column): Builder
|
|
{
|
|
return PurchaseItem::query()
|
|
->select($column)
|
|
->whereColumn('compra_items.id', 'tickets.source_purchase_item_id')
|
|
->limit(1);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Ticket> $tickets
|
|
* @param array{sort_by?: string|null, sort_direction?: string|null} $filters
|
|
* @return Collection<int, Ticket>
|
|
*/
|
|
private function sortTickets(Collection $tickets, Tenant $tenant, array $filters): Collection
|
|
{
|
|
$sortBy = (string) ($filters['sort_by'] ?? '');
|
|
if ($sortBy === '') {
|
|
return $tickets;
|
|
}
|
|
|
|
$column = collect($this->columnService->columns($tenant))
|
|
->firstWhere('sort_param', $sortBy);
|
|
if ($column === null) {
|
|
return $tickets;
|
|
}
|
|
|
|
$direction = ($filters['sort_direction'] ?? 'asc') === 'desc' ? -1 : 1;
|
|
$values = $tickets->mapWithKeys(fn (Ticket $ticket): array => [
|
|
$ticket->getKey() => $this->rowService->values($ticket)[$column['key']] ?? null,
|
|
]);
|
|
|
|
return $tickets->sort(function (Ticket $left, Ticket $right) use ($column, $direction, $values): int {
|
|
$leftValue = $values->get($left->getKey());
|
|
$rightValue = $values->get($right->getKey());
|
|
|
|
if ($leftValue === null || $leftValue === '') {
|
|
return $rightValue === null || $rightValue === '' ? $right->id <=> $left->id : 1;
|
|
}
|
|
if ($rightValue === null || $rightValue === '') {
|
|
return -1;
|
|
}
|
|
|
|
$comparison = $this->compareValues($leftValue, $rightValue, $column['type']);
|
|
|
|
return $comparison === 0
|
|
? $right->id <=> $left->id
|
|
: $comparison * $direction;
|
|
})->values();
|
|
}
|
|
|
|
private function compareValues(mixed $left, mixed $right, string $type): int
|
|
{
|
|
if (in_array($type, ['currency', 'order_number'], true)) {
|
|
return (float) $left <=> (float) $right;
|
|
}
|
|
|
|
if ($type === 'date') {
|
|
$leftTimestamp = $left instanceof \DateTimeInterface ? $left->getTimestamp() : strtotime((string) $left);
|
|
$rightTimestamp = $right instanceof \DateTimeInterface ? $right->getTimestamp() : strtotime((string) $right);
|
|
|
|
return $leftTimestamp <=> $rightTimestamp;
|
|
}
|
|
|
|
if ($type === 'status') {
|
|
$left = $this->rowService->displayValue($left, $type, 'UTC');
|
|
$right = $this->rowService->displayValue($right, $type, 'UTC');
|
|
}
|
|
|
|
return strnatcasecmp((string) $left, (string) $right);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Ticket> $tickets
|
|
* @param array{page?: int, per_page?: int} $filters
|
|
* @return LengthAwarePaginator<Ticket>
|
|
*/
|
|
private function paginate(Collection $tickets, array $filters): LengthAwarePaginator
|
|
{
|
|
$page = (int) ($filters['page'] ?? 1);
|
|
$perPage = (int) ($filters['per_page'] ?? 15);
|
|
|
|
return (new LengthAwarePaginator(
|
|
$tickets->forPage($page, $perPage)->values(),
|
|
$tickets->count(),
|
|
$perPage,
|
|
$page,
|
|
['path' => request()->url()],
|
|
))->withQueryString();
|
|
}
|
|
}
|