| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <?php
- namespace App\Http\Controllers;
- use App\Http\Requests\StoreSparePartRequest;
- use App\Jobs\Export\ExportSparePartsJob;
- use App\Jobs\Import\ImportSparePartsJob;
- use App\Models\Import;
- use App\Models\SparePart;
- use Illuminate\Http\RedirectResponse;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\File;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Support\Str;
- class SparePartController extends Controller
- {
- protected array $data = [
- 'active' => '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()]);
- $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 help()
- {
- $markdownPath = base_path('docs/spare-parts.md');
- $markdown = File::exists($markdownPath) ? File::get($markdownPath) : '# Справка не найдена';
- $this->data['helpContent'] = Str::markdown($markdown);
- $this->data['tab'] = 'help';
- return view('spare_parts.index', $this->data);
- }
- public function create()
- {
- $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);
- $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());
- $this->syncPricingCodes($sparePart, $request);
- $previous_url = $request->get('previous_url') ?? route('spare_parts.index', session('gp_spare_parts'));
- return redirect()->to($previous_url)->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);
- return redirect()->route('spare_parts.show', $sparePart)
- ->with(['success' => 'Изображение успешно загружено!']);
- }
- return redirect()->route('spare_parts.show', $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('used_in_maf', 'LIKE', '%' . $query . '%');
- })
- ->orderBy('article')
- ->limit(20)
- ->get(['id', 'article', 'used_in_maf']);
- return response()->json($spareParts);
- }
- /**
- * Создание 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,
- ];
- }
- }
- }
- }
|