'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_codes_list' => 'Шифр расценки и коды ресурсов', 'min_stock' => 'Минимальный остаток', ], 'searchFields' => [ 'article', 'used_in_maf', 'note', 'tsn_number', ], 'routeName' => 'spare_parts.show', ]; public function index(Request $request) { session(['gp_spare_parts' => $request->query()]); $nav = $this->startNavigationContext($request); $model = new SparePartsView(); // Для админа добавляем колонку цены закупки 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'); $this->data['filters']['customer_price_txt'] = [ 'title' => $this->data['header']['customer_price_txt'], 'values' => [], ]; $this->data['filters']['expertise_price_txt'] = [ 'title' => $this->data['header']['expertise_price_txt'], 'values' => [], ]; $this->data['filters']['pricing_codes_list'] = [ 'title' => $this->data['header']['pricing_codes_list'], 'values' => [], ]; if (hasRole('admin')) { $this->data['filters']['purchase_price_txt'] = [ 'title' => $this->data['header']['purchase_price_txt'], 'values' => [], ]; } // Запрос $q = $model::query()->with('pricingCodes'); $this->acceptFilters($q, $request); $this->acceptSearch($q, $request); $this->setSortAndOrderBy($model, $request); if ($request->get('sortBy') === 'pricing_codes_list') { $this->data['sortBy'] = 'pricing_codes_list'; $q->orderBy( DB::table('spare_part_pricing_code as sppc') ->join('pricing_codes as pc', 'pc.id', '=', 'sppc.pricing_code_id') ->selectRaw('MIN(pc.code)') ->whereColumn('sppc.spare_part_id', 'spare_parts_view.id'), $this->data['orderBy'] ?? 'asc' )->orderBy('id', $this->data['orderBy'] ?? 'asc'); } else { $this->applyStableSorting($q); } $this->data['spare_parts'] = $q->paginate($this->data['per_page'])->withQueryString(); $this->data['strings'] = $this->data['spare_parts']; $this->data['tab'] = 'catalog'; $this->data['nav'] = $nav; return view('spare_parts.index', $this->data); } public function show(Request $request, SparePart $sparePart) { $nav = $this->resolveNavToken($request); $this->rememberNavigation($request, $nav); $this->data['nav'] = $nav; $this->data['back_url'] = $this->navigationBackUrl( $request, $nav, route('spare_parts.index', session('gp_spare_parts')) ); $this->data['spare_part'] = $sparePart; return view('spare_parts.edit', $this->data); } public function help() { $markdownPath = base_path('docs/spare-parts.md'); $markdown = File::exists($markdownPath) ? File::get($markdownPath) : '# Справка не найдена'; $this->data['helpContent'] = Str::markdown($markdown, [ 'heading_permalink' => [ 'html_class' => 'heading-permalink', 'id_prefix' => '', 'fragment_prefix' => '', 'insert' => 'none', 'apply_id_to_heading' => true, 'min_heading_level' => 1, 'max_heading_level' => 6, 'symbol' => '', ], ], [ new HeadingPermalinkExtension(), ]); $this->data['tab'] = 'help'; return view('spare_parts.index', $this->data); } public function create(Request $request) { $nav = $this->resolveNavToken($request); $this->rememberNavigation($request, $nav); $this->data['nav'] = $nav; $this->data['back_url'] = $this->navigationBackUrl( $request, $nav, route('spare_parts.index', session('gp_spare_parts')) ); $this->data['spare_part'] = null; return view('spare_parts.edit', $this->data); } public function store(StoreSparePartRequest $request): RedirectResponse { $sparePart = SparePart::create($request->validated()); $this->syncPricingCodes($sparePart, $request); $nav = $this->resolveNavToken($request); $backUrl = $this->navigationParentUrl( $nav, route('spare_parts.index', session('gp_spare_parts')) ); return redirect()->to($backUrl)->with(['success' => 'Запчасть успешно создана!']); } public function update(StoreSparePartRequest $request, SparePart $sparePart): RedirectResponse { $sparePart->update($request->validated()); $this->syncPricingCodes($sparePart, $request); $nav = $this->resolveNavToken($request); $backUrl = $this->navigationParentUrl( $nav, route('spare_parts.index', session('gp_spare_parts')) ); return redirect()->to($backUrl)->with(['success' => 'Запчасть успешно обновлена!']); } protected function syncPricingCodes(SparePart $sparePart, StoreSparePartRequest $request): void { $codes = $request->input('pricing_codes', []); $descriptions = $request->input('pricing_codes_descriptions', []); $pricingCodeIds = []; foreach ($codes as $index => $code) { $code = trim($code); if (empty($code)) { continue; } $description = trim($descriptions[$index] ?? ''); // Находим или создаём PricingCode $pricingCode = \App\Models\PricingCode::where('type', \App\Models\PricingCode::TYPE_PRICING_CODE) ->where('code', $code) ->first(); if ($pricingCode) { // Обновляем расшифровку, если предоставлена if ($description && $description !== $pricingCode->description) { $pricingCode->update(['description' => $description]); } } else { // Создаём новый код $pricingCode = \App\Models\PricingCode::create([ 'type' => \App\Models\PricingCode::TYPE_PRICING_CODE, 'code' => $code, 'description' => $description ?: null, ]); } $pricingCodeIds[] = $pricingCode->id; } // Синхронизируем связи $sparePart->pricingCodes()->sync($pricingCodeIds); } 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); Cache::forget('spare_part_image:' . $sparePart->article); return $this->redirectToSparePartShow($request, $sparePart) ->with(['success' => 'Изображение успешно загружено!']); } return $this->redirectToSparePartShow($request, $sparePart) ->with(['error' => 'Ошибка загрузки изображения!']); } /** * API метод для поиска запчастей (autocomplete) */ public function search(Request $request) { $query = $request->get('query', ''); $spareParts = SparePart::query() ->where(function ($q) use ($query) { $q->where('article', 'LIKE', '%' . $query . '%') ->orWhere('note', 'LIKE', '%' . $query . '%'); }) ->orderBy('article') ->limit(20) ->get(['id', 'article', 'note']); return response()->json($spareParts); } private function redirectToSparePartShow(Request $request, SparePart $sparePart): RedirectResponse { $nav = $this->resolveNavToken($request); return redirect()->route('spare_parts.show', $this->withNav(['sparePart' => $sparePart], $nav)); } }