'spare_parts', 'title' => 'Каталог запчастей', 'id' => 'spare_parts', 'header' => [ 'image' => 'Картинка', 'id' => 'ID', 'article' => 'Артикул', 'used_in_maf' => 'Где используется', 'quantity_without_docs' => 'Кол-во без док', 'quantity_with_docs' => 'Кол-во с док', 'total_quantity' => 'Кол-во общее', 'note' => 'Примечание', 'customer_price_txt' => 'Цена для заказчика', 'expertise_price_txt' => 'Цена экспертизы', 'tsn_number' => '№ по ТСН', 'pricing_code' => 'Шифр расценки', 'min_stock' => 'Минимальный остаток', ], 'searchFields' => [ 'article', 'used_in_maf', 'note', 'tsn_number', 'pricing_code', ], 'routeName' => 'spare_parts.show', ]; public function index(Request $request) { session(['gp_spare_parts' => $request->query()]); $model = new SparePart(); // Для админа добавляем колонку цены закупки if (hasRole('admin')) { $this->data['header'] = array_merge( array_slice($this->data['header'], 0, 8, true), ['purchase_price_txt' => 'Цена закупки'], array_slice($this->data['header'], 8, null, true) ); } // Фильтры $this->createFilters($model, 'used_in_maf'); // Для range фильтров нужно использовать реальные поля БД (без _txt) // но заголовки брать из header с _txt $this->createRangeFiltersForPrices($model, 'customer_price', 'expertise_price', 'min_stock'); if (hasRole('admin')) { $this->createRangeFiltersForPrices($model, 'purchase_price'); } // Запрос $q = $model::query(); $this->acceptFilters($q, $request); $this->acceptSearch($q, $request); $this->setSortAndOrderBy($model, $request); $q->orderBy($this->data['sortBy'], $this->data['orderBy']); $this->data['spare_parts'] = $q->paginate(session('per_page', config('pagination.per_page')))->withQueryString(); $this->data['strings'] = $this->data['spare_parts']; $this->data['tab'] = 'catalog'; return view('spare_parts.index', $this->data); } public function show(Request $request, SparePart $sparePart) { $this->data['previous_url'] = $request->get('previous_url'); $this->data['spare_part'] = $sparePart; return view('spare_parts.edit', $this->data); } public function create() { $this->data['spare_part'] = null; return view('spare_parts.edit', $this->data); } public function store(StoreSparePartRequest $request): RedirectResponse { SparePart::create($request->validated()); $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts')); return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно создана!']); } public function update(StoreSparePartRequest $request, SparePart $sparePart): RedirectResponse { $sparePart->update($request->validated()); $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts')); return redirect()->to($previous_url)->with(['success' => 'Запчасть успешно обновлена!']); } public function destroy(SparePart $sparePart): RedirectResponse { // Проверка на наличие заказов if ($sparePart->orders()->count() > 0) { return redirect()->route('spare_parts.index', session('gp_spare_parts')) ->with(['error' => 'Невозможно удалить запчасть, т.к. для неё есть заказы!']); } $sparePart->delete(); return redirect()->route('spare_parts.index', session('gp_spare_parts')) ->with(['success' => 'Запчасть успешно удалена!']); } public function export(Request $request): RedirectResponse { // Запускаем Job для экспорта ExportSparePartsJob::dispatch(auth()->id()); Log::info('ExportSparePartsJob created!'); return redirect()->route('spare_parts.index', session('gp_spare_parts')) ->with(['success' => 'Задача экспорта успешно создана!']); } public function import(Request $request): RedirectResponse { $request->validate([ 'file' => 'required|mimes:xlsx,xls|max:10240', ]); try { // Сохраняем временный файл $file = $request->file('file'); $tempPath = $file->storeAs('temp/imports', 'spare_parts_import_' . time() . '.xlsx'); $fullPath = Storage::path($tempPath); // Создаём запись импорта $import = Import::create([ 'user_id' => auth()->id(), 'type' => 'spare_parts', 'status' => Import::STATUS_PENDING, 'file_path' => $tempPath, 'original_filename' => $file->getClientOriginalName(), ]); // Запускаем Job для импорта ImportSparePartsJob::dispatch($fullPath, auth()->id(), $import->id); Log::info('ImportSparePartsJob created!', ['import_id' => $import->id]); return redirect()->route('spare_parts.index', session('gp_spare_parts')) ->with(['success' => 'Задача импорта успешно создана! Следите за статусом в разделе импорта.']); } catch (\Exception $e) { Log::error('Ошибка создания задачи импорта: ' . $e->getMessage()); return redirect()->route('spare_parts.index', session('gp_spare_parts')) ->with(['error' => 'Ошибка импорта: ' . $e->getMessage()]); } } public function uploadImage(Request $request, SparePart $sparePart): RedirectResponse { $request->validate([ 'image' => 'required|image|mimes:jpeg,jpg,png|max:2048', ]); if ($request->hasFile('image')) { $image = $request->file('image'); $filename = $sparePart->article . '.jpg'; // Создаём директорию если её нет $directory = public_path('images/spare_parts'); if (!file_exists($directory)) { mkdir($directory, 0755, true); } // Сохраняем изображение $image->move($directory, $filename); return redirect()->route('spare_parts.show', $sparePart) ->with(['success' => 'Изображение успешно загружено!']); } return redirect()->route('spare_parts.show', $sparePart) ->with(['error' => 'Ошибка загрузки изображения!']); } /** * Создание range фильтров для полей с ценами * Использует правильные заголовки из header (_txt версии) */ protected function createRangeFiltersForPrices(SparePart $model, string ...$columnNames): void { foreach ($columnNames as $columnName) { // Определяем ключ заголовка $headerKey = str_ends_with($columnName, '_price') ? $columnName . '_txt' : $columnName; // Проверяем, есть ли заголовок if (!isset($this->data['header'][$headerKey])) { continue; } if (str_ends_with($columnName, '_price')) { $min = $model::query()->min($columnName); $max = $model::query()->max($columnName); $this->data['ranges'][$columnName] = [ 'title' => $this->data['header'][$headerKey], 'min' => $min ? $min / 100 : 0, 'max' => $max ? $max / 100 : 0, ]; } else { $this->data['ranges'][$columnName] = [ 'title' => $this->data['header'][$headerKey], 'min' => $model::query()->min($columnName) ?? 0, 'max' => $model::query()->max($columnName) ?? 0, ]; } } } }