*/ private const CATEGORY_PRESENTATIONS = [ 'entradas' => [ 'label' => null, 'product' => self::PRODUCT, 'type' => null, 'order' => 1, ], 'alojamientos' => [ 'label' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'order' => 2, ], 'camping' => [ 'label' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'order' => 2, ], 'comidas' => [ 'label' => 'Comida', 'product' => 'event_date', 'type' => 'horario', 'order' => 3, ], 'comida' => [ 'label' => null, 'product' => 'event_date', 'type' => 'horario', 'order' => 3, ], 'merchandising' => [ 'label' => null, 'product' => self::PRODUCT, 'type' => 'color', 'order' => 4, ], ]; /** * @var array */ private const FILTER_CATEGORY_PRESENTATIONS = [ 'entradas' => ['label' => null, 'product' => self::PRODUCT, 'type' => null, 'size' => null, 'order' => 1], 'alojamientos' => ['label' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null, 'order' => 2], 'camping' => ['label' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null, 'order' => 2], 'comidas' => ['label' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'size' => null, 'order' => 3], 'comida' => ['label' => null, 'product' => 'horario', 'type' => 'servicio', 'size' => null, 'order' => 3], 'merchandising' => ['label' => null, 'product' => self::PRODUCT, 'type' => 'color', 'size' => 'talle', 'order' => 4], ]; /** * @return array{ * statuses: list, * categories: list * }> * }> * } */ public function get(Tenant $tenant): array { $items = CatalogItem::query() ->where('tenant_code', $tenant->codigo) ->where('has_tickets', true) ->whereHas('category') ->with($this->relations()) ->orderBy('group_order') ->orderBy('nombre') ->get(); return $this->build($items, self::CATEGORY_PRESENTATIONS); } /** * Return active catalog options plus soft-deleted sources still referenced by * tickets, so historical tickets never become impossible to filter. * * @return array{ * statuses: list, * categories: list * }> * }> * } */ public function getForFilters(Tenant $tenant): array { $historicalVariantIds = Ticket::query() ->where('tenant_code', $tenant->codigo) ->whereNotNull('source_variant_id') ->distinct() ->pluck('source_variant_id') ->map(fn ($id): int => (int) $id) ->all(); $historicalCatalogItemIds = Ticket::query() ->where('tenant_code', $tenant->codigo) ->whereNotNull('source_catalog_item_id') ->distinct() ->pluck('source_catalog_item_id') ->map(fn ($id): int => (int) $id) ->merge( Variant::withTrashed() ->whereKey($historicalVariantIds) ->pluck('catalog_item_id') ->map(fn ($id): int => (int) $id), ) ->unique() ->values() ->all(); $items = CatalogItem::withTrashed() ->where('tenant_code', $tenant->codigo) ->whereHas('category') ->where(function ($query) use ($historicalCatalogItemIds): void { $query ->where(function ($activeQuery): void { $activeQuery ->whereNull('catalog_items.deleted_at') ->where('has_tickets', true); }) ->orWhereIn('catalog_items.id', $historicalCatalogItemIds); }) ->with([ 'category', 'itemAttributes.attribute.options', 'variants' => fn ($query) => $query ->withTrashed() ->where(function ($variantQuery) use ($historicalVariantIds): void { $variantQuery ->whereNull('variantes.deleted_at') ->orWhereIn('variantes.id', $historicalVariantIds); }), 'variants.definitions.itemAttribute.attribute.options', 'variants.eventDates', 'variants.eventDate', ]) ->orderBy('group_order') ->orderBy('nombre') ->get(); return $this->build($items, self::FILTER_CATEGORY_PRESENTATIONS, includeSizes: true); } /** * @param Collection $items * @return array{ * statuses: list, * categories: list * }> * }> * } */ private function build(Collection $items, array $presentations, bool $includeSizes = false): array { $categories = []; foreach ($items as $item) { $sourceCategory = trim((string) $item->category?->nombre); $categoryValue = mb_strtolower($sourceCategory); $presentation = $presentations[$categoryValue] ?? [ 'label' => null, 'product' => self::PRODUCT, 'type' => null, 'size' => null, 'order' => PHP_INT_MAX, ]; $categories[$categoryValue] ??= [ 'value' => $categoryValue, 'label' => $presentation['label'] ?? $sourceCategory, 'order' => $presentation['order'], 'products' => [], ]; foreach ($this->products( $item, $presentation['product'], $presentation['type'], $presentation['size'] ?? null, ) as $product) { $productValue = $product['value']; $existingProduct = $categories[$categoryValue]['products'][$productValue] ?? [ 'value' => $productValue, 'label' => $product['label'], 'types' => [], 'sizes' => [], ]; foreach ($product['types'] as $type) { $existingProduct['types'][$type['value']] = $type; } foreach ($product['sizes'] as $size) { $existingProduct['sizes'][$size['value']] = $size; } $categories[$categoryValue]['products'][$productValue] = $existingProduct; } } uasort($categories, fn (array $left, array $right): int => $left['order'] <=> $right['order'] ?: $left['label'] <=> $right['label']); return [ 'statuses' => [ ['value' => Ticket::STATUS_ACTIVE, 'label' => 'Activo'], ['value' => Ticket::STATUS_USED, 'label' => 'Usado'], ['value' => Ticket::STATUS_EXPIRED, 'label' => 'Vencido'], ], 'categories' => array_values(array_map( fn (array $category): array => [ 'value' => $category['value'], 'label' => $category['label'], 'products' => array_values(array_map( fn (array $product): array => [ 'value' => $product['value'], 'label' => $product['label'], 'types' => array_values($product['types']), ...($includeSizes ? ['sizes' => array_values($product['sizes'])] : []), ], $category['products'], )), ], $categories, )), ]; } /** @return list */ private function relations(): array { return [ 'category', 'itemAttributes.attribute.options', 'variants.definitions.itemAttribute.attribute.options', 'variants.eventDates', 'variants.eventDate', ]; } /** * @return list, * sizes: list * }> */ private function products(CatalogItem $item, string $productCode, ?string $typeCode, ?string $sizeCode): array { if ($productCode === self::PRODUCT) { return [[ 'value' => $item->slug, 'label' => $item->nombre, 'types' => $this->types($item, $typeCode, $sizeCode), 'sizes' => $this->types($item, $sizeCode), ]]; } $products = []; foreach ($item->variants as $variant) { foreach ($this->variantOptions($variant, $productCode) as $productOption) { $productValue = $productOption['value']; $products[$productValue] ??= [ 'value' => $productValue, 'label' => $this->optionLabel($productOption['label'], $productCode), 'types' => [], 'sizes' => [], ]; foreach ($this->variantOptions($variant, $typeCode) as $typeOption) { $this->mergeTypeOption( $products[$productValue]['types'], $typeOption, $variant, $sizeCode, ); } foreach ($this->variantOptions($variant, $sizeCode) as $sizeOption) { $products[$productValue]['sizes'][$sizeOption['value']] = $sizeOption; } } } return array_values(array_map( fn (array $product): array => [ 'value' => $product['value'], 'label' => $product['label'], 'types' => array_values($product['types']), 'sizes' => array_values($product['sizes']), ], $products, )); } /** @return list> */ private function types(CatalogItem $item, ?string $typeCode, ?string $sizeCode = null): array { $types = []; foreach ($item->variants as $variant) { foreach ($this->variantOptions($variant, $typeCode) as $typeOption) { $this->mergeTypeOption($types, $typeOption, $variant, $sizeCode); } } return array_values(array_map(function (array $type) use ($sizeCode): array { if ($sizeCode !== null) { $type['sizes'] = array_values($type['sizes']); } return $type; }, $types)); } /** * @param array> $types * @param array{value: string, label: string} $typeOption */ private function mergeTypeOption( array &$types, array $typeOption, Variant $variant, ?string $sizeCode, ): void { $typeValue = $typeOption['value']; $types[$typeValue] ??= [ ...$typeOption, ...($sizeCode !== null ? ['sizes' => []] : []), ]; foreach ($this->variantOptions($variant, $sizeCode) as $sizeOption) { $types[$typeValue]['sizes'][$sizeOption['value']] = $sizeOption; } } /** @return list */ private function variantOptions(Variant $variant, ?string $attributeCode): array { if ($attributeCode === null) { return []; } $selection = $variant->selectionOptions($variant->catalogItem->itemAttributes) ->get($attributeCode); if ($selection === null) { return []; } return array_is_list($selection) ? $selection : [$selection]; } private function optionLabel(string $label, string $attributeCode): string { if ($attributeCode !== 'event_date') { return $label; } [$day, $month] = array_pad(explode('/', $label), 2, null); return $day !== null && $month !== null ? "{$day}/{$month}" : $label; } }