withQueryString() — сохраняет фильтры в ссылках страниц // Вьюха: resources/views/admin/cars/index.blade.php public function index(Request $request) { $query = Car::query()->orderByDesc('id'); if ($request->filled('q')) { $q = '%'.$request->q.'%'; $query->where(function ($qb) use ($q) { $qb->where('make', 'like', $q) ->orWhere('model', 'like', $q) ->orWhere('title', 'like', $q) ->orWhere('vin', 'like', $q); }); } foreach (['status', 'condition', 'make', 'engine_type', 'transmission', 'drive', 'platform'] as $field) { if ($request->filled($field)) { $query->where($field, $request->input($field)); } } $cars = $query->paginate(20)->withQueryString(); $makes = Car::select('make')->distinct()->orderBy('make')->pluck('make'); // Площадки строго из справочника — чтобы отображались все, даже без привязанных авто $platformSection = DictSection::where('code', 'platforms')->with('values')->first(); $platforms = $platformSection ? $platformSection->values->pluck('value') : collect(); return view('admin.cars.index', compact('cars', 'makes', 'platforms')); } // Форма создания нового автомобиля // keyBy('code') — индексирует коллекцию по полю code для удобного доступа в шаблоне: $dictSections->get('makes') // Вьюха: resources/views/admin/cars/form.blade.php (общая для create и edit) public function create() { $dictSections = DictSection::with('values')->orderBy('sort_order')->get()->keyBy('code'); return view('admin.cars.form', compact('dictSections')); } // Сохранение нового автомобиля: validated() → Car::create() → handlePhotos() → редирект public function store(Request $request) { $data = $this->validated($request); $car = Car::create($data); $this->handlePhotos($request, $car); Cache::flush(); // каталог с фильтрами — сбрасываем весь кеш при изменении авто return redirect()->route('admin.cars.index')->with('success', 'Автомобиль добавлен.'); } // Форма редактирования: $car передаётся через route model binding по {car} в URL // Та же вьюха form.blade.php — внутри проверяется isset($car) public function edit(Car $car) { $dictSections = DictSection::with('values')->orderBy('sort_order')->get()->keyBy('code'); return view('admin.cars.form', compact('car', 'dictSections')); } // Обновление данных автомобиля: логика аналогична store() public function update(Request $request, Car $car) { $data = $this->validated($request); $car->update($data); $this->handlePhotos($request, $car); Cache::flush(); // каталог с фильтрами — сбрасываем весь кеш при изменении авто return redirect()->route('admin.cars.index')->with('success', 'Автомобиль обновлён.'); } // Удаление: сначала удаляет файлы с диска, затем запись из БД // photo_main и photos_gallery (JSON-массив) удаляются из Storage::disk('public') public function destroy(Car $car) { if ($car->photo_main) { Storage::disk('public')->delete($car->photo_main); } if ($car->photos_gallery) { foreach ($car->photos_gallery as $path) { Storage::disk('public')->delete($path); } } $car->delete(); Cache::flush(); return redirect()->route('admin.cars.index')->with('success', 'Автомобиль удалён.'); } // Правила валидации для формы автомобиля — вынесены в приватный метод // чтобы не дублировать между store() и update(); фото-поля здесь не включены private function validated(Request $request): array { return $request->validate([ 'status' => 'required|in:active,sold,draft', 'condition' => 'required|in:new,used', 'make' => 'required|string|max:64', 'model' => 'required|string|max:64', 'generation' => 'nullable|string|max:64', 'year' => 'required|integer|min:1900|max:2100', 'vin' => 'nullable|string|max:17', 'plate' => 'nullable|string|max:20', 'body_type' => 'nullable|string|max:32', 'doors' => 'nullable|integer|min:1|max:10', 'color_exterior' => 'nullable|string|max:32', 'color_interior' => 'nullable|string|max:32', 'engine_type' => 'nullable|in:petrol,diesel,hybrid,electric,gas,other', 'engine_volume' => 'nullable|numeric|min:0|max:99', 'engine_power_hp' => 'nullable|integer|min:0|max:5000', 'transmission' => 'nullable|in:manual,automatic,robot,variator,electric', 'drive' => 'nullable|in:FWD,RWD,AWD,4WD', 'mileage_km' => 'nullable|integer|min:0', 'steering' => 'required|in:left,right', 'owners_count' => 'nullable|integer|min:0', 'customs_cleared' => 'nullable|boolean', 'pts' => 'nullable|in:original,duplicate,electronic', 'accident_free' => 'nullable|boolean', 'price_usd' => 'nullable|integer|min:0', 'price_rub' => 'nullable|integer|min:0', 'price_vladivostok' => 'nullable|integer|min:0', 'price_moscow' => 'nullable|integer|min:0', 'price_negotiable' => 'nullable|boolean', 'country_origin' => 'nullable|string|max:64', 'city' => 'nullable|string|max:64', 'platform' => 'required|string|max:64', 'options' => 'nullable|array', 'title' => 'nullable|string|max:128', 'description' => 'nullable|string', ]); } // Загрузка и обновление фото: вызывается из store() и update() после сохранения записи // 1. photo_main — старый файл удаляется, новый → storage/app/public/cars/{id}/main/ // 2. photos_gallery — новые фото добавляются к JSON-массиву → cars/{id}/gallery/ // 3. delete_gallery — пути из формы → удаление файла + фильтрация массива + array_values() // Доступ через web: /storage/cars/... (требует: php artisan storage:link) private function handlePhotos(Request $request, Car $car): void { if ($request->hasFile('photo_main') && $request->file('photo_main')->isValid()) { if ($car->photo_main) { Storage::disk('public')->delete($car->photo_main); } $path = $request->file('photo_main')->store("cars/{$car->id}/main", 'public'); $car->update(['photo_main' => $path]); } if ($request->hasFile('photos_gallery')) { $gallery = $car->photos_gallery ?? []; foreach ($request->file('photos_gallery') as $file) { if ($file->isValid()) { $gallery[] = $file->store("cars/{$car->id}/gallery", 'public'); } } $car->update(['photos_gallery' => $gallery]); } if ($request->filled('delete_gallery')) { $toDelete = (array) $request->input('delete_gallery'); $gallery = $car->photos_gallery ?? []; foreach ($toDelete as $path) { Storage::disk('public')->delete($path); $gallery = array_filter($gallery, fn ($p) => $p !== $path); } $car->update(['photos_gallery' => array_values($gallery)]); } } }