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->whereNotNull('used_at')->count(); } else { $tickets = (clone $query) ->with(self::RELATIONS) ->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id')) ->paginateFromRequest() ->withQueryString(); $scannedTickets = $countQuery->whereNotNull('used_at')->count(); } return new AdminAppTicketResult( tickets: $tickets, scannedTickets: $scannedTickets, totalTickets: $tickets->total(), ); } /** * @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, sort_by?: string|null, sort_direction?: string|null} $filters * @return Collection */ 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); } /** * @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, page?: int, per_page?: int} $filters * @return Builder */ 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 { $query->when( ctype_digit($search), fn (Builder $searchQuery): Builder => $searchQuery ->where('tickets.id', (int) $search), fn (Builder $searchQuery): Builder => $searchQuery ->where('ticket', 'like', "%{$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, fn (Builder $query, string $date): Builder => $query ->whereHas('sourcePurchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery ->whereDate('created_at', $date))); $this->applyStatusFilter($query, $filters['status'] ?? null); return $query; } /** @param Builder $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)) { $query->whereHas('sourceVariant', function (Builder $variantQuery) use ($product): void { $variantQuery->where(function (Builder $dateQuery) use ($product): void { $dateQuery ->where('event_date_id', $product) ->orWhereHas('eventDates', fn (Builder $eventDateQuery): Builder => $eventDateQuery ->whereKey($product)); }); }); return; } $query->whereHas('sourceCatalogItem', fn (Builder $itemQuery): Builder => $itemQuery ->where('slug', $product)); } /** @param Builder $query */ private function applyTypeFilter(Builder $query, string $category, string $type): void { $attribute = match ($this->normalizedCategory($category)) { 'comidas', 'comida' => 'horario', 'merchandising' => 'color', default => null, }; if ($attribute !== null) { $this->whereVariantDefinition($query, $attribute, $type); } } /** @param Builder $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 $query */ private function applyStatusFilter(Builder $query, ?string $status): void { if ($status === null || $status === '') { return; } if ($status === Ticket::STATUS_USED) { $query->whereNotNull('used_at'); return; } $matchingIds = (clone $query) ->whereNull('used_at') ->with(TicketValidityResolver::RELATIONS) ->get() ->filter(fn (Ticket $ticket): bool => $ticket->status === $status) ->pluck('id'); $query->whereIn('tickets.id', $matchingIds); } private function normalizedCategory(string $category): string { return mb_strtolower(trim($category)); } /** * @param Builder $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' => 'tickets.source_purchase_id', 'ticket' => 'tickets.ticket', 'date' => Purchase::query() ->select('created_at') ->whereColumn('compras.id', 'tickets.source_purchase_id'), 'amount' => $this->purchaseItemSortQuery('precio_unitario'), 'scanned_by' => User::query() ->select('nombre_apellido') ->whereColumn('users.id', 'tickets.scanner_user_id'), 'product' => $tenant->codigo === 'fiesta_futbol_infantil' ? null : $this->purchaseItemSortQuery('item_nombre'), default => null, }; if ($sortExpression === null) { return false; } $query->orderBy($sortExpression, $direction)->orderByDesc('tickets.id'); return true; } /** @return Builder */ private function purchaseItemSortQuery(string $column): Builder { return PurchaseItem::query() ->select($column) ->whereColumn('compra_items.compra_id', 'tickets.source_purchase_id') ->where(function (Builder $query): void { $query ->where(function (Builder $variantQuery): void { $variantQuery ->whereNotNull('tickets.source_variant_id') ->whereColumn('compra_items.source_variant_id', 'tickets.source_variant_id'); }) ->orWhere(function (Builder $itemQuery): void { $itemQuery ->whereNull('tickets.source_variant_id') ->whereNull('compra_items.source_variant_id') ->whereColumn('compra_items.source_catalog_item_id', 'tickets.source_catalog_item_id'); }); }) ->limit(1); } /** * @param Collection $tickets * @param array{sort_by?: string|null, sort_direction?: string|null} $filters * @return Collection */ 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 $tickets * @param array{page?: int, per_page?: int} $filters * @return LengthAwarePaginator */ 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(); } }