Просмотр исходного кода

filters in contractors catalog

Alexander Musikhin 16 часов назад
Родитель
Сommit
099f85475f

+ 42 - 2
app/Http/Controllers/ContractorController.php

@@ -25,6 +25,16 @@ class ContractorController extends Controller
         'hidden_txt' => 'Скрыт',
     ];
     protected array $searchFields = ['name', 'legal_name', 'contract_number', 'director_name'];
+    protected array $priceHeader = [
+        'image' => 'Картинка МАФ',
+        'article' => 'Артикул МАФ',
+        'nomenclature_number' => 'Номер номенклатуры',
+        'name_in_spec' => 'Наименование по спецификации',
+        'installation_price_txt' => 'Цена монтажа',
+        'status_name' => 'Статус',
+        'actions' => '',
+    ];
+    protected array $priceSearchFields = ['article', 'nomenclature_number', 'name_in_spec'];
 
     public function index(Request $request): View
     {
@@ -92,6 +102,11 @@ class ContractorController extends Controller
             'product_id' => ['required', 'integer', 'exists:products,id'],
             'name_in_spec' => ['nullable', 'string', 'max:255'],
             'price' => ['nullable', 'numeric', 'min:0'],
+            'nav' => ['nullable', 'string'],
+            'filters' => ['nullable', 'array'],
+            's' => ['nullable', 'string'],
+            'sortBy' => ['nullable', 'string'],
+            'order' => ['nullable', 'string'],
         ]);
 
         $product = Product::withoutGlobalScope(YearScope::class)->withTrashed()->findOrFail($validated['product_id']);
@@ -103,7 +118,15 @@ class ContractorController extends Controller
             (float) ($validated['price'] ?? 0),
         );
 
-        return redirect()->route('contractors.show', $contractor)->with('success', 'Цена монтажа сохранена');
+        return redirect()
+            ->route('contractors.show', array_filter([
+                'contractor' => $contractor,
+                'nav' => $validated['nav'] ?? null,
+                's' => $validated['s'] ?? null,
+                'sortBy' => $validated['sortBy'] ?? null,
+                'order' => $validated['order'] ?? null,
+            ]) + $request->only('filters'))
+            ->with('success', 'Цена монтажа сохранена');
     }
 
     public function importPrices(Request $request, Contractor $contractor, ContractorPriceService $priceService): RedirectResponse
@@ -137,14 +160,31 @@ class ContractorController extends Controller
         $nav = $this->resolveNavToken($request);
         $this->rememberNavigation($request, $nav);
         $year = year();
+        $filters = $request->input('filters', []);
+        if (!is_array($filters)) {
+            $filters = [];
+        }
+        $allowedSortFields = array_keys($this->priceHeader);
+        $priceSortBy = in_array($request->get('sortBy'), $allowedSortFields, true) ? $request->get('sortBy') : 'article';
+        $priceOrderBy = $request->get('order') === 'desc' ? 'desc' : 'asc';
+        $allPriceRows = $contractor ? $priceService->rowsForContractor($contractor, $year) : collect();
+        $priceRows = $contractor
+            ? $priceService->rowsForContractor($contractor, $year, $filters, $request->string('s')->toString(), $priceSortBy, $priceOrderBy)
+            : collect();
 
         return view('contractors.edit', [
             'active' => 'contractors',
             'contractor' => $contractor,
             'organizationForms' => Contractor::ORGANIZATION_FORMS,
             'taxRates' => Contractor::TAX_RATES,
-            'priceRows' => $contractor ? $priceService->rowsForContractor($contractor, $year) : collect(),
+            'priceRows' => $priceRows,
             'catalogYear' => $year,
+            'priceHeader' => $this->priceHeader,
+            'priceSearchFields' => $this->priceSearchFields,
+            'priceSortBy' => $priceSortBy,
+            'priceOrderBy' => $priceOrderBy,
+            'priceFilters' => $priceService->filterOptionsForRows($allPriceRows, $this->priceHeader),
+            'priceRanges' => [],
             'nav' => $nav,
             'back_url' => $this->navigationBackUrl($request, $nav, route('contractors.index', session('gp_contractors'))),
         ]);

+ 159 - 12
app/Services/ContractorPriceService.php

@@ -6,8 +6,10 @@ use App\Models\Contractor;
 use App\Models\ContractorInstallationPrice;
 use App\Models\Product;
 use App\Models\Scopes\YearScope;
+use Illuminate\Support\Arr;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Collection;
+use stdClass;
 use PhpOffice\PhpSpreadsheet\IOFactory;
 use PhpOffice\PhpSpreadsheet\Spreadsheet;
 use PhpOffice\PhpSpreadsheet\Style\Border;
@@ -16,7 +18,54 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 
 class ContractorPriceService
 {
-    public function rowsForContractor(Contractor $contractor, int $year): Collection
+    public function rowsForContractor(
+        Contractor $contractor,
+        int $year,
+        array $filters = [],
+        string $search = '',
+        string $sortBy = 'article',
+        string $orderBy = 'asc'
+    ): Collection
+    {
+        return $this->sortRows(
+            $this->applySearch($this->applyFilters($this->baseRowsForContractor($contractor, $year), $filters), $search),
+            $sortBy,
+            $orderBy,
+        )->values();
+    }
+
+    public function filterOptionsForRows(Collection $rows, array $header): array
+    {
+        return [
+            'article' => [
+                'title' => $header['article'],
+                'values' => $this->uniqueFilterValues($rows, 'article'),
+            ],
+            'nomenclature_number' => [
+                'title' => $header['nomenclature_number'],
+                'values' => $this->uniqueFilterValues($rows, 'nomenclature_number'),
+            ],
+            'name_in_spec' => [
+                'title' => $header['name_in_spec'],
+                'values' => $this->uniqueFilterValues($rows, 'name_in_spec'),
+            ],
+            'installation_price_txt' => [
+                'title' => $header['installation_price_txt'],
+                'values' => $this->uniqueFilterValues($rows, 'installation_price_filter'),
+            ],
+            'status_name' => [
+                'title' => $header['status_name'],
+                'values' => [
+                    'Доступен' => 'Доступен',
+                    'МАФ недоступен' => 'МАФ недоступен',
+                    'С ценой' => 'С ценой',
+                    'Без цены' => 'Без цены',
+                ],
+            ],
+        ];
+    }
+
+    private function baseRowsForContractor(Contractor $contractor, int $year): Collection
     {
         $currentProducts = Product::query()
             ->where('year', $year)
@@ -47,6 +96,17 @@ class ContractorPriceService
         return $rows;
     }
 
+    private function uniqueFilterValues(Collection $rows, string $field): array
+    {
+        return $rows
+            ->pluck($field)
+            ->map(fn($value) => $value === null || $value === '' ? '-пусто-' : (string) $value)
+            ->unique()
+            ->sort(SORT_NATURAL)
+            ->mapWithKeys(fn($value) => [$value => $value])
+            ->all();
+    }
+
     public function updatePrice(Contractor $contractor, Product $product, int $year, ?string $nameInSpec, float $price): ContractorInstallationPrice
     {
         return ContractorInstallationPrice::query()->updateOrCreate(
@@ -137,11 +197,11 @@ class ContractorPriceService
 
         $rowNumber = 2;
         foreach ($this->rowsForContractor($contractor, $year) as $row) {
-            $this->insertProductImage($sheet, $row['product'], $rowNumber);
-            $sheet->setCellValue('B' . $rowNumber, $row['product']->article);
-            $sheet->setCellValue('C' . $rowNumber, $row['product']->nomenclature_number);
-            $sheet->setCellValue('D' . $rowNumber, $row['price']?->name_in_spec ?? '');
-            $sheet->setCellValue('E' . $rowNumber, $row['price']?->price ?? 0);
+            $this->insertProductImage($sheet, $row->product, $rowNumber);
+            $sheet->setCellValue('B' . $rowNumber, $row->product->article);
+            $sheet->setCellValue('C' . $rowNumber, $row->product->nomenclature_number);
+            $sheet->setCellValue('D' . $rowNumber, $row->price?->name_in_spec ?? '');
+            $sheet->setCellValue('E' . $rowNumber, $row->price?->price ?? 0);
             $sheet->getRowDimension($rowNumber)->setRowHeight(55);
             $rowNumber++;
         }
@@ -161,13 +221,100 @@ class ContractorPriceService
         return $path;
     }
 
-    private function buildRow(Product $product, ?ContractorInstallationPrice $price, bool $available): array
+    private function buildRow(Product $product, ?ContractorInstallationPrice $price, bool $available): stdClass
     {
-        return [
-            'product' => $product,
-            'price' => $price,
-            'available' => $available && is_null($product->deleted_at),
-        ];
+        $row = new stdClass();
+        $row->id = $product->id;
+        $row->product = $product;
+        $row->price = $price;
+        $row->product_id = $product->id;
+        $row->image = $product->image;
+        $row->article = $product->article;
+        $row->nomenclature_number = $product->nomenclature_number;
+        $row->name_in_spec = $price?->name_in_spec ?? '';
+        $row->installation_price = (float) ($price?->price ?? 0);
+        $row->installation_price_txt = $price?->price_txt ?? '0.00₽';
+        $row->installation_price_filter = str_replace(' ', ' ', $row->installation_price_txt);
+        $row->available = $available && is_null($product->deleted_at);
+        $row->status_name = $row->available ? 'Доступен' : 'МАФ недоступен';
+
+        return $row;
+    }
+
+    private function applyFilters(Collection $rows, array $filters): Collection
+    {
+        $article = trim((string) ($filters['article'] ?? ''));
+        $nomenclatureNumber = trim((string) ($filters['nomenclature_number'] ?? ''));
+        $nameInSpec = trim((string) ($filters['name_in_spec'] ?? ''));
+        $installationPrice = trim((string) ($filters['installation_price_txt'] ?? ''));
+        $status = (string) ($filters['status_name'] ?? '');
+
+        return $rows->filter(function (stdClass $row) use ($article, $nomenclatureNumber, $nameInSpec, $installationPrice, $status): bool {
+            if (!$this->matchesListFilter((string) $row->article, $article)) {
+                return false;
+            }
+
+            if (!$this->matchesListFilter((string) $row->nomenclature_number, $nomenclatureNumber)) {
+                return false;
+            }
+
+            if (!$this->matchesListFilter((string) $row->name_in_spec, $nameInSpec)) {
+                return false;
+            }
+
+            if (!$this->matchesListFilter((string) $row->installation_price_filter, $installationPrice)) {
+                return false;
+            }
+
+            return match ($status) {
+                'С ценой' => $row->installation_price > 0,
+                'Без цены' => $row->installation_price <= 0,
+                'Доступен' => (bool) $row->available,
+                'МАФ недоступен' => !$row->available,
+                default => true,
+            };
+        });
+    }
+
+    private function matchesListFilter(string $value, string $filter): bool
+    {
+        if ($filter === '') {
+            return true;
+        }
+
+        $normalizedValue = $value === '' ? '-пусто-' : $value;
+        $values = explode('||', $filter);
+
+        return in_array($normalizedValue, $values, true);
+    }
+
+    private function applySearch(Collection $rows, string $search): Collection
+    {
+        $search = trim(mb_strtolower($search));
+        if ($search === '') {
+            return $rows;
+        }
+
+        return $rows->filter(fn(stdClass $row) => str_contains(mb_strtolower((string) $row->article), $search)
+            || str_contains(mb_strtolower((string) $row->nomenclature_number), $search)
+            || str_contains(mb_strtolower((string) $row->name_in_spec), $search));
+    }
+
+    private function sortRows(Collection $rows, string $sortBy, string $orderBy): Collection
+    {
+        $sortField = match ($sortBy) {
+            'installation_price_txt' => 'installation_price',
+            'status_name' => 'status_name',
+            'nomenclature_number' => 'nomenclature_number',
+            'name_in_spec' => 'name_in_spec',
+            default => 'article',
+        };
+
+        return $rows->sortBy(
+            fn(stdClass $row) => Arr::get((array) $row, $sortField),
+            SORT_REGULAR,
+            $orderBy === 'desc',
+        );
     }
 
     private function parsePrice(mixed $value): float

+ 42 - 59
resources/views/contractors/edit.blade.php

@@ -1,6 +1,11 @@
 @extends('layouts.app')
 
 @section('content')
+    @php
+        $hasCatalogFilters = collect(request()->filters ?? [])
+            ->filter(fn ($value) => $value !== null && $value !== '')
+            ->isNotEmpty() || filled(request()->s) || filled(request()->sortBy) || filled(request()->order);
+    @endphp
     <div class="px-3">
         <h4 class="mb-4">{{ $contractor ? 'Редактирование подрядчика' : 'Добавление подрядчика' }}</h4>
 
@@ -32,21 +37,24 @@
 
         <ul class="nav nav-tabs mb-3" role="tablist">
             <li class="nav-item" role="presentation">
-                <button class="nav-link active" id="contractor-main-tab" data-bs-toggle="tab" data-bs-target="#contractor-main-pane" type="button" role="tab">
+                <button class="nav-link @unless($hasCatalogFilters) active @endunless" id="contractor-main-tab" data-bs-toggle="tab" data-bs-target="#contractor-main-pane" type="button" role="tab">
                     Карточка
                 </button>
             </li>
             @if($contractor)
                 <li class="nav-item" role="presentation">
-                    <button class="nav-link" id="contractor-prices-tab" data-bs-toggle="tab" data-bs-target="#contractor-prices-pane" type="button" role="tab">
+                    <button class="nav-link @if($hasCatalogFilters) active @endif" id="contractor-prices-tab" data-bs-toggle="tab" data-bs-target="#contractor-prices-pane" type="button" role="tab">
                         Цены монтажа
+                        @if($hasCatalogFilters)
+                            <span class="badge text-bg-primary ms-1">фильтр</span>
+                        @endif
                     </button>
                 </li>
             @endif
         </ul>
 
         <div class="tab-content">
-            <div class="tab-pane fade show active" id="contractor-main-pane" role="tabpanel" aria-labelledby="contractor-main-tab">
+            <div class="tab-pane fade @unless($hasCatalogFilters) show active @endunless" id="contractor-main-pane" role="tabpanel" aria-labelledby="contractor-main-tab">
                 <div class="col-xxl-7 offset-xxl-1">
                     <form action="{{ $contractor ? route('contractors.update', $contractor) : route('contractors.store') }}" method="post">
                         @csrf
@@ -74,7 +82,7 @@
             </div>
 
             @if($contractor)
-                <div class="tab-pane fade" id="contractor-prices-pane" role="tabpanel" aria-labelledby="contractor-prices-tab">
+                <div class="tab-pane fade @if($hasCatalogFilters) show active @endif" id="contractor-prices-pane" role="tabpanel" aria-labelledby="contractor-prices-tab">
                     <div class="row mb-3 align-items-end">
                         <div class="col-md-3">
                             <div class="text-muted small">
@@ -92,61 +100,19 @@
                         </div>
                     </div>
 
-                    <div class="table-responsive">
-                        <table class="table table-sm table-bordered align-middle">
-                            <thead class="table-primary">
-                            <tr>
-                                <th>Картинка МАФ</th>
-                                <th>Артикул МАФ</th>
-                                <th>Номер номенклатуры</th>
-                                <th>Наименование по спецификации</th>
-                                <th>Цена монтажа</th>
-                                <th>Статус</th>
-                                <th></th>
-                            </tr>
-                            </thead>
-                            <tbody>
-                            @foreach($priceRows as $row)
-                                @php
-                                    $product = $row['product'];
-                                    $price = $row['price'];
-                                @endphp
-                                <tr>
-                                    <td style="width: 120px">
-                                        @if($product->image)
-                                            <img src="{{ $product->image }}" alt="" class="img-thumbnail maf-img">
-                                        @endif
-                                    </td>
-                                    <td>{{ $product->article }}</td>
-                                    <td>{{ $product->nomenclature_number }}</td>
-                                    <td>{{ $price?->name_in_spec }}</td>
-                                    <td>{{ $price?->price_txt ?? '0.00₽' }}</td>
-                                    <td>
-                                        @if($row['available'])
-                                            <span class="badge text-bg-success">Доступен</span>
-                                        @else
-                                            <span class="badge text-bg-warning">МАФ недоступен</span>
-                                        @endif
-                                    </td>
-                                    <td class="text-end">
-                                        <button
-                                            type="button"
-                                            class="btn btn-sm btn-outline-primary edit-price"
-                                            data-bs-toggle="modal"
-                                            data-bs-target="#editPriceModal"
-                                            data-product-id="{{ $product->id }}"
-                                            data-article="{{ $product->article }}"
-                                            data-name="{{ e($price?->name_in_spec ?? '') }}"
-                                            data-price="{{ $price?->price ?? 0 }}"
-                                        >
-                                            Изменить
-                                        </button>
-                                    </td>
-                                </tr>
-                            @endforeach
-                            </tbody>
-                        </table>
-                    </div>
+                    @include('partials.table', [
+                        'id' => 'contractor_prices',
+                        'header' => $priceHeader,
+                        'strings' => $priceRows,
+                        'searchFields' => $priceSearchFields,
+                        'sortBy' => $priceSortBy,
+                        'orderBy' => $priceOrderBy,
+                        'filters' => $priceFilters,
+                        'ranges' => $priceRanges,
+                        'dates' => [],
+                        'enableColumnFilters' => true,
+                        'nav' => $nav ?? null,
+                    ])
 
                     <div class="modal fade" id="editPriceModal" tabindex="-1" aria-labelledby="editPriceModalLabel" aria-hidden="true">
                         <div class="modal-dialog">
@@ -159,6 +125,23 @@
                                     <form action="{{ route('contractors.prices.update', $contractor) }}" method="post">
                                         @csrf
                                         <input type="hidden" name="product_id" id="price_product_id">
+                                        @if($nav ?? null)
+                                            <input type="hidden" name="nav" value="{{ $nav }}">
+                                        @endif
+                                        @if(request()->s)
+                                            <input type="hidden" name="s" value="{{ request()->s }}">
+                                        @endif
+                                        @if(request()->sortBy)
+                                            <input type="hidden" name="sortBy" value="{{ request()->sortBy }}">
+                                        @endif
+                                        @if(request()->order)
+                                            <input type="hidden" name="order" value="{{ request()->order }}">
+                                        @endif
+                                        @foreach(request()->filters ?? [] as $filterName => $filterValue)
+                                            @if($filterValue !== null && $filterValue !== '')
+                                                <input type="hidden" name="filters[{{ $filterName }}]" value="{{ $filterValue }}">
+                                            @endif
+                                        @endforeach
                                         <div class="mb-2 small text-muted" id="price_article"></div>
                                         @include('partials.input', ['name' => 'name_in_spec', 'title' => 'Наименование по спецификации'])
                                         @include('partials.input', ['name' => 'price', 'title' => 'Цена монтажа', 'type' => 'number', 'min' => '0'])

+ 22 - 18
resources/views/partials/newFilterElement.blade.php

@@ -114,7 +114,7 @@
                 const existingFilter = urlParams.searchParams.get(`filters[{{$id}}]`);
                 const selectedValues = existingFilter ? existingFilter.split("||") : null;
 
-                let filterData = [];
+                let filterData = @json(isset($data['values']) ? array_values(array_keys($data['values'])) : []);
                 let sortAsc = true;
 
                 function renderFilterList(data) {
@@ -146,25 +146,29 @@
                     });
                 }
 
-                try {
-                    const response = await fetch(`{!! route('getFilters', ['column' => $id, 'table' => $table]) !!}`);
-                    const data = await response.json();
+                if (filterData.length) {
+                    renderFilterList(sortData(sortAsc));
+                } else {
+                    try {
+                        const response = await fetch(`{!! route('getFilters', ['column' => $id, 'table' => $table]) !!}`);
+                        const data = await response.json();
+
+                        if (Array.isArray(data) && data.length) {
+                            if(data[0] === null) data[0] = '-пусто-';
+                            filterData = data;
+                            const sortedData = sortData(sortAsc);
+                            renderFilterList(sortedData);
+                        } else {
+                            $("#search_{{$id}}").parent().hide();
+                            $bulkToggle.hide();
+                            $("#modal-footer_{{$id}}").hide();
+                            $container.html('<div class="text-muted">Нет данных</div>');
+                        }
 
-                    if (Array.isArray(data) && data.length) {
-                        if(data[0] === null) data[0] = '-пусто-';
-                        filterData = data;
-                        const sortedData = sortData(sortAsc);
-                        renderFilterList(sortedData);
-                    } else {
-                        $("#search_{{$id}}").parent().hide();
-                        $bulkToggle.hide();
-                        $("#modal-footer_{{$id}}").hide();
-                        $container.html('<div class="text-muted">Нет данных</div>');
+                    } catch (error) {
+                        console.error("Ошибка при загрузке фильтров:", error);
+                        $container.html('<div class="text-danger">Ошибка загрузки</div>');
                     }
-
-                } catch (error) {
-                    console.error("Ошибка при загрузке фильтров:", error);
-                    $container.html('<div class="text-danger">Ошибка загрузки</div>');
                 }
 
                 $sortBtn.on("click", function (e) {

+ 21 - 0
resources/views/partials/table.blade.php

@@ -64,6 +64,8 @@
                                         data-bs-toggle="dropdown"
                                         class="dropdown-toggle bi
                                 @if(isset(request()->filters[$headerName]) ||
+                                    isset(request()->filters[$headerName . '_from']) ||
+                                    isset(request()->filters[$headerName . '_to']) ||
                                     isset(request()->filters[str_replace('_txt', '', $headerName) . '_from']) ||
                                     isset(request()->filters[str_replace('_txt', '', $headerName) . '_to'])
                                     )
@@ -221,6 +223,25 @@
                                 @method('DELETE')
                                 <button type="submit" class="btn btn-sm btn-danger">Удалить</button>
                             </form>
+                        @elseif($id === 'contractor_prices' && $headerName === 'status_name')
+                            @if($string->available)
+                                <span class="badge text-bg-success">Доступен</span>
+                            @else
+                                <span class="badge text-bg-warning">МАФ недоступен</span>
+                            @endif
+                        @elseif($id === 'contractor_prices' && $headerName === 'actions')
+                            <button
+                                type="button"
+                                class="btn btn-sm btn-outline-primary edit-price"
+                                data-bs-toggle="modal"
+                                data-bs-target="#editPriceModal"
+                                data-product-id="{{ $string->product_id }}"
+                                data-article="{{ $string->article }}"
+                                data-name="{{ e($string->name_in_spec) }}"
+                                data-price="{{ $string->installation_price }}"
+                            >
+                                Изменить
+                            </button>
                         @elseif($id === 'notifications' && $headerName === 'type')
                             <span class="badge text-bg-{{ \App\Models\UserNotification::TYPE_COLORS[$string->type] ?? 'secondary' }}">{{ $string->type_name }}</span>
                         @elseif($id === 'notifications' && $headerName === 'event')