Initial commit
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
ssww23
2026-03-10 00:55:37 +03:00
parent fc0f28d830
commit 93a655235a
155 changed files with 24768 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
<?php
namespace App\Http\Controllers\Shop;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class CatalogController extends Controller
{
private const PER_PAGE = 20;
public function search(Request $request)
{
$searchQuery = trim((string) $request->input('q', ''));
$query = Product::query()
->where('is_active', true)
->with('category');
if ($searchQuery === '') {
$query->whereRaw('1 = 0');
} else {
$this->applyNameSearch($query, $searchQuery);
}
$sort = $request->string('sort')->toString();
match ($sort) {
'price_asc' => $query->orderBy('price'),
'price_desc' => $query->orderByDesc('price'),
'name_asc' => $query->orderBy('name'),
default => $query->orderByDesc('id'),
};
$products = $query->paginate(self::PER_PAGE)->withQueryString();
return view('shop.search', [
'products' => $products,
'searchQuery' => $searchQuery,
'sort' => $sort ?: 'newest',
]);
}
public function index(Request $request)
{
$query = Product::query()->where('is_active', true)->with('category');
if ($request->filled('q')) {
$this->applyNameSearch($query, (string) $request->input('q'));
}
if ($request->filled('category')) {
$query->whereHas('category', function ($builder) use ($request) {
$builder->where('slug', $request->string('category'));
});
}
$products = $query->orderByDesc('id')->paginate(self::PER_PAGE)->withQueryString();
$categories = Category::query()
->where('is_active', true)
->orderBy('name')
->get();
return view('shop.catalog', [
'products' => $products,
'categories' => $categories,
]);
}
public function category(Request $request, Category $category)
{
$baseQuery = Product::query()
->where('category_id', $category->id)
->where('is_active', true);
$query = (clone $baseQuery)->with('category');
$appliedFilters = collect((array) $request->input('filters', []))
->map(function ($value) {
if (!is_scalar($value)) {
return '';
}
return trim((string) $value);
})
->filter(fn (string $value) => $value !== '')
->all();
if ($request->filled('q')) {
$searchTerm = (string) $request->input('q');
$this->applyNameSearch($baseQuery, $searchTerm);
$this->applyNameSearch($query, $searchTerm);
}
$filters = $this->filtersForCategory($category->slug);
$priceBounds = $this->priceBounds($baseQuery);
$priceFrom = $this->parseFilterNumber($request->input('price_from'));
if ($priceFrom !== null) {
$query->where('price', '>=', $priceFrom);
}
$priceTo = $this->parseFilterNumber($request->input('price_to'));
if ($priceTo !== null) {
$query->where('price', '<=', $priceTo);
}
$rangeFilters = [];
foreach ($filters as $filter) {
if (!$this->isRangeFilter($filter)) {
continue;
}
$key = (string) ($filter['key'] ?? '');
if ($key === '') {
continue;
}
$bounds = $this->numericSpecBounds($baseQuery, $key);
$fromParam = $key . '_from';
$toParam = $key . '_to';
$rangeFrom = $this->parseFilterNumber($request->input($fromParam));
if ($rangeFrom !== null) {
$query->whereRaw(
"NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric >= ?",
[$key, $rangeFrom]
);
}
$rangeTo = $this->parseFilterNumber($request->input($toParam));
if ($rangeTo !== null) {
$query->whereRaw(
"NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric <= ?",
[$key, $rangeTo]
);
}
$rangeFilters[$key] = [
'from' => $request->filled($fromParam)
? trim((string) $request->input($fromParam))
: $this->formatFilterNumber($bounds['min']),
'to' => $request->filled($toParam)
? trim((string) $request->input($toParam))
: $this->formatFilterNumber($bounds['max']),
'min' => $this->formatFilterNumber($bounds['min']),
'max' => $this->formatFilterNumber($bounds['max']),
];
}
foreach ($filters as $filter) {
if ($this->isRangeFilter($filter)) {
continue;
}
$key = (string) ($filter['key'] ?? '');
if ($key === '') {
continue;
}
$value = $appliedFilters[$key] ?? null;
if ($value !== null) {
$query->whereRaw("specs->>? = ?", [$key, $value]);
}
}
$sort = $request->string('sort')->toString();
match ($sort) {
'price_asc' => $query->orderBy('price'),
'price_desc' => $query->orderByDesc('price'),
'name_asc' => $query->orderBy('name'),
default => $query->orderByDesc('id'),
};
$products = $query->paginate(self::PER_PAGE)->withQueryString();
$filterOptions = $this->filterOptions($category, $filters);
$priceFilter = [
'from' => $request->filled('price_from')
? trim((string) $request->input('price_from'))
: $this->formatFilterNumber($priceBounds['min']),
'to' => $request->filled('price_to')
? trim((string) $request->input('price_to'))
: $this->formatFilterNumber($priceBounds['max']),
'min' => $this->formatFilterNumber($priceBounds['min']),
'max' => $this->formatFilterNumber($priceBounds['max']),
];
return view('shop.category', [
'category' => $category,
'products' => $products,
'filters' => $filters,
'filterOptions' => $filterOptions,
'appliedFilters' => $appliedFilters,
'rangeFilters' => $rangeFilters,
'priceFilter' => $priceFilter,
'sort' => $sort ?: 'newest',
]);
}
private function filtersForCategory(string $slug): array
{
return config('product_specs.categories.' . $slug, []);
}
private function filterOptions(Category $category, array $filters): array
{
if (empty($filters)) {
return [];
}
$specs = $category->products()
->where('is_active', true)
->pluck('specs');
$options = [];
foreach ($filters as $filter) {
if ($this->isRangeFilter($filter)) {
continue;
}
$key = (string) ($filter['key'] ?? '');
if ($key === '') {
continue;
}
$valuesFromProducts = $specs
->pluck($key)
->filter()
->unique()
->sort()
->values()
->all();
$presetValues = collect((array) ($filter['options'] ?? []))
->filter(fn ($value) => $value !== null && $value !== '')
->map(fn ($value) => (string) $value)
->values()
->all();
$options[$key] = collect($presetValues)
->concat($valuesFromProducts)
->map(fn ($value) => (string) $value)
->filter()
->unique()
->values()
->all();
}
return $options;
}
private function applyNameSearch(Builder $query, string $term): void
{
$normalizedTerm = mb_strtolower(trim($term));
if ($normalizedTerm === '') {
return;
}
$query->whereRaw('LOWER(name) LIKE ?', ['%' . $normalizedTerm . '%']);
}
private function isRangeFilter(array $filter): bool
{
return (string) ($filter['filter'] ?? 'select') === 'range';
}
private function parseFilterNumber(mixed $value): ?float
{
$string = trim((string) $value);
if ($string === '') {
return null;
}
$normalized = str_replace([' ', ','], ['', '.'], $string);
if (!is_numeric($normalized)) {
return null;
}
return (float) $normalized;
}
private function formatFilterNumber(?float $value): string
{
if ($value === null) {
return '';
}
if (abs($value - round($value)) < 0.000001) {
return (string) (int) round($value);
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
private function priceBounds(Builder $query): array
{
$bounds = (clone $query)
->selectRaw('MIN(price) as min_value, MAX(price) as max_value')
->first();
return [
'min' => $this->parseFilterNumber($bounds?->min_value),
'max' => $this->parseFilterNumber($bounds?->max_value),
];
}
private function numericSpecBounds(Builder $query, string $key): array
{
if ($key === '') {
return ['min' => null, 'max' => null];
}
$bounds = (clone $query)
->selectRaw(
"MIN(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as min_value, MAX(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as max_value",
[$key, $key]
)
->first();
return [
'min' => $this->parseFilterNumber($bounds?->min_value),
'max' => $this->parseFilterNumber($bounds?->max_value),
];
}
}