'reclamations', 'title' => 'Рекламации', 'id' => 'reclamations', 'header' => [ 'id' => 'ID', 'user_name' => 'Менеджер', 'status_name' => 'Статус', 'district_name' => 'Округ', 'area_name' => 'Район', 'object_address' => 'Адрес объекта', 'maf_installation_year' => 'Год установки МАФ', 'create_date' => 'Дата создания', 'finish_date' => 'Дата завершения', 'start_work_date' => 'Дата начала работ', 'work_days' => 'Срок работ, дней', 'brigadier_name' => 'Бригадир', 'reason' => 'Причина', 'guarantee' => 'Гарантии', 'whats_done' => 'Что сделано', 'comment' => 'Комментарий', ], 'searchFields' => [ 'reason', 'guarantee', 'whats_done', 'comment', ], 'ranges' => [], ]; public function __construct() { $this->data['users'] = User::query()->whereIn('role', [Role::MANAGER, Role::ADMIN])->get()->pluck('name', 'id'); $this->data['statuses'] = ReclamationStatus::query()->get()->pluck('name', 'id'); } public function index(Request $request) { session(['gp_reclamations' => $request->all()]); $model = new ReclamationView(); // fill filters $this->createFilters($model, 'user_name', 'status_name'); $this->createDateFilters($model, 'create_date', 'finish_date'); $q = $model::query(); $this->acceptFilters($q, $request); $this->acceptSearch($q, $request); $this->setSortAndOrderBy($model, $request); if (hasRole(Role::BRIGADIER)) { $q->where('brigadier_id', auth()->id()); } $this->applyStableSorting($q); $this->data['reclamations'] = $q->paginate($this->data['per_page'])->withQueryString(); return view('reclamations.index', $this->data); } public function export(Request $request) { $gp = session('gp_reclamations') ?? []; $filterRequest = new Request($gp); $model = new ReclamationView(); $this->createFilters($model, 'user_name', 'status_name'); $this->createDateFilters($model, 'create_date', 'finish_date'); $q = $model::query(); $this->acceptFilters($q, $filterRequest); $this->acceptSearch($q, $filterRequest); $this->setSortAndOrderBy($model, $filterRequest); $this->applyStableSorting($q); $reclamationIds = $q->pluck('id')->toArray(); ExportReclamationsJob::dispatch($reclamationIds, $request->user()->id); return redirect()->route('reclamations.index', session('gp_reclamations')) ->with(['success' => 'Задача экспорта рекламаций создана!']); } public function create(CreateReclamationRequest $request, Order $order, NotificationService $notificationService) { $reclamation = Reclamation::query()->create([ 'order_id' => $order->id, 'user_id' => $request->user()->id, 'status_id' => Reclamation::STATUS_NEW, 'create_date' => Carbon::now(), 'finish_date' => Carbon::now()->addDays(30), ]); $skus = $request->validated('skus'); $reclamation->skus()->attach($skus); $notificationService->notifyReclamationCreated($reclamation->fresh(['order', 'status'])); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->previous()]); } public function show(Request $request, Reclamation $reclamation) { $this->ensureCanViewReclamation($reclamation); $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id'); $this->data['reclamation'] = $reclamation; $this->data['previous_url'] = $this->resolvePreviousUrl( $request, 'previous_url_reclamations', route('reclamations.index', session('gp_reclamations')) ); return view('reclamations.edit', $this->data); } public function update(StoreReclamationRequest $request, Reclamation $reclamation, NotificationService $notificationService) { $data = $request->validated(); $oldStatusId = $reclamation->status_id; $reclamation->update($data); if ((int) $oldStatusId !== (int) $reclamation->status_id) { $notificationService->notifyReclamationStatusChanged($reclamation->fresh(['order', 'status'])); } $previousUrl = $this->previousUrlForRedirect($request, 'previous_url_reclamations'); if (!empty($previousUrl)) { return redirect()->route('reclamations.show', [ 'reclamation' => $reclamation, 'previous_url' => $previousUrl, ]); } return redirect()->route('reclamations.show', $reclamation->id); } public function updateStatus(Request $request, Reclamation $reclamation, NotificationService $notificationService) { if (!hasRole('admin,manager')) { abort(403); } $validated = $request->validate([ 'status_id' => 'required|exists:reclamation_statuses,id', ]); $reclamation->update(['status_id' => $validated['status_id']]); $notificationService->notifyReclamationStatusChanged($reclamation->fresh(['order', 'status'])); return response()->noContent(); } public function delete(Reclamation $reclamation) { $reclamation->delete(); return redirect()->route('reclamations.index'); } public function uploadPhotoBefore(Request $request, Reclamation $reclamation, FileService $fileService) { $this->ensureHasRole([Role::ADMIN, Role::MANAGER]); $this->ensureCanViewReclamation($reclamation); $data = $request->validate([ 'photo.*' => 'mimes:jpeg,jpg,png|max:8192', ]); try { $f = []; foreach ($data['photo'] as $photo) { $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/photo_before', $photo); } $reclamation->photos_before()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['error' => 'Ошибка загрузки фотографий проблемы. Проверьте имя файла и повторите попытку.']); } return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Фотографии проблемы успешно загружены!']); } public function uploadPhotoAfter(Request $request, Reclamation $reclamation, FileService $fileService) { $this->ensureCanViewReclamation($reclamation); $data = $request->validate([ 'photo.*' => 'mimes:jpeg,jpg,png|max:8192', ]); try { $f = []; foreach ($data['photo'] as $photo) { $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/photo_after', $photo); } $reclamation->photos_after()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['error' => 'Ошибка загрузки фотографий после устранения. Проверьте имя файла и повторите попытку.']); } return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Фотографии после устранения успешно загружены!']); } public function deletePhotoBefore(Request $request, Reclamation $reclamation, File $file, FileService $fileService) { $this->ensureHasRole([Role::ADMIN, Role::MANAGER]); $this->ensureCanViewReclamation($reclamation); $reclamation->photos_before()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function deletePhotoAfter(Request $request, Reclamation $reclamation, File $file, FileService $fileService) { $this->ensureHasRole([Role::ADMIN, Role::MANAGER]); $this->ensureCanViewReclamation($reclamation); $reclamation->photos_after()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function uploadDocument(Request $request, Reclamation $reclamation, FileService $fileService) { $this->ensureHasRole([Role::ADMIN, Role::MANAGER]); $this->ensureCanViewReclamation($reclamation); $data = $request->validate([ 'document.*' => 'file', ]); try { $f = []; $i = 0; foreach ($data['document'] as $document) { if ($i++ >= 5) break; $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/document', $document); } $reclamation->documents()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['error' => 'Ошибка загрузки документов рекламации. Проверьте имя файла и повторите попытку.']); } return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Документы рекламации успешно загружены!']); } public function deleteDocument(Request $request, Reclamation $reclamation, File $file) { $this->ensureHasRole([Role::ADMIN]); $this->ensureCanViewReclamation($reclamation); $reclamation->documents()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function uploadAct(Request $request, Reclamation $reclamation, FileService $fileService) { $this->ensureHasRole([Role::ADMIN, Role::MANAGER, Role::BRIGADIER, Role::WAREHOUSE_HEAD]); $this->ensureCanViewReclamation($reclamation); $data = $request->validate([ 'acts.*' => 'file', ]); try { $f = []; $i = 0; foreach ($data['acts'] as $document) { if ($i++ >= 5) break; $f[] = $fileService->saveUploadedFile('reclamations/' . $reclamation->id . '/act', $document); } $reclamation->acts()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['error' => 'Ошибка загрузки актов. Проверьте имя файла и повторите попытку.']); } return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Акты успешно загружены!']); } public function deleteAct(Request $request, Reclamation $reclamation, File $file) { $this->ensureHasRole([Role::ADMIN]); $this->ensureCanViewReclamation($reclamation); $reclamation->acts()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function updateDetails(StoreReclamationDetailsRequest $request, Reclamation $reclamation) { $names = $request->validated('name'); $quantity = $request->validated('quantity'); $withDocuments = $request->validated('with_documents'); $reservationService = app(SparePartReservationService::class); foreach ($names as $key => $name) { if (!$name) continue; if ((int)$quantity[$key] >= 1) { // Проверяем, является ли это запчастью $sparePart = \App\Models\SparePart::where('article', $name)->first(); if ($sparePart) { // Резервирование вместо прямого списания $withDocs = isset($withDocuments[$key]) && $withDocuments[$key]; $qty = (int)$quantity[$key]; // Получаем текущее количество в pivot $currentPivot = $reclamation->spareParts()->find($sparePart->id); $currentQty = $currentPivot?->pivot->quantity ?? 0; $diff = $qty - $currentQty; if ($diff > 0) { // Нужно зарезервировать дополнительное количество $result = $reservationService->reserve( $sparePart->id, $diff, $withDocs, $reclamation->id ); // Обновляем pivot с учётом результата $reclamation->spareParts()->syncWithoutDetaching([ $sparePart->id => [ 'quantity' => $qty, 'with_documents' => $withDocs, 'status' => $result->isFullyReserved() ? 'reserved' : 'pending', 'reserved_qty' => $currentQty + $result->reserved, ] ]); } elseif ($diff < 0) { // Уменьшение — отменяем часть резерва $reservationService->adjustReservation( $reclamation->id, $sparePart->id, $withDocs, $qty ); $reclamation->spareParts()->syncWithoutDetaching([ $sparePart->id => [ 'quantity' => $qty, 'with_documents' => $withDocs, 'reserved_qty' => $qty, ] ]); } else { // Количество не изменилось, возможно изменился with_documents $reclamation->spareParts()->syncWithoutDetaching([ $sparePart->id => [ 'quantity' => $qty, 'with_documents' => $withDocs, ] ]); } } else { // Обычная деталь ReclamationDetail::query()->updateOrCreate( ['reclamation_id' => $reclamation->id, 'name' => $name], ['quantity' => $quantity[$key]] ); } } else { // Удаление // Проверяем, является ли это запчастью — отменяем резервы $sparePartToRemove = \App\Models\SparePart::where('article', $name)->first(); if ($sparePartToRemove) { // Отменяем все резервы для этой запчасти в рекламации $reservationService->cancelForReclamation( $reclamation->id, $sparePartToRemove->id ); // Удаляем связь $reclamation->spareParts()->detach($sparePartToRemove->id); } else { // Обычная деталь ReclamationDetail::query() ->where('reclamation_id', $reclamation->id) ->where('name', $name) ->delete(); } } } return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function updateSpareParts(StoreReclamationSparePartsRequest $request, Reclamation $reclamation) { $rows = $request->validated('rows') ?? []; $reservationService = app(SparePartReservationService::class); // Получаем текущие привязки для сравнения $currentSpareParts = $reclamation->spareParts->keyBy('id'); // Определяем какие запчасти были удалены $newSparePartIds = collect($rows)->pluck('spare_part_id')->filter()->toArray(); $removedIds = $currentSpareParts->keys()->diff($newSparePartIds); // Отменяем резервы для удалённых запчастей foreach ($removedIds as $removedId) { $current = $currentSpareParts->get($removedId); if ($current) { $reservationService->cancelForReclamation( $reclamation->id, $removedId, $current->pivot->with_documents ); } } // Собираем новые привязки $newSpareParts = []; foreach ($rows as $row) { $sparePartId = $row['spare_part_id'] ?? null; if (empty($sparePartId)) continue; $quantity = (int)($row['quantity'] ?? 0); if ($quantity < 1) continue; $withDocs = !empty($row['with_documents']) && $row['with_documents'] != '0'; // Проверяем, изменилось ли количество $currentQty = $currentSpareParts->get($sparePartId)?->pivot->quantity ?? 0; $currentReserved = $currentSpareParts->get($sparePartId)?->pivot->reserved_qty ?? 0; $diff = $quantity - $currentQty; $status = 'pending'; $reservedQty = $currentReserved; if ($diff > 0) { // Нужно зарезервировать дополнительное количество $result = $reservationService->reserve( $sparePartId, $diff, $withDocs, $reclamation->id ); $reservedQty = $currentReserved + $result->reserved; $status = $reservedQty >= $quantity ? 'reserved' : 'pending'; } elseif ($diff < 0) { // Уменьшение — отменяем часть резерва $reservationService->adjustReservation( $reclamation->id, $sparePartId, $withDocs, $quantity ); $reservedQty = $quantity; $status = 'reserved'; } else { // Количество не изменилось $status = $currentReserved >= $quantity ? 'reserved' : 'pending'; } $newSpareParts[$sparePartId] = [ 'quantity' => $quantity, 'with_documents' => $withDocs, 'status' => $status, 'reserved_qty' => $reservedQty, ]; } // Синхронизируем (заменяем все старые привязки новыми) $reclamation->spareParts()->sync($newSpareParts); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]); } public function generateReclamationPack(Request $request, Reclamation $reclamation) { GenerateReclamationPack::dispatch($reclamation, auth()->user()->id); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Задача генерации документов создана!']); } public function generateReclamationPaymentPack(Request $request, Reclamation $reclamation) { GenerateReclamationPaymentPack::dispatch($reclamation, auth()->user()->id); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Задача генерации пакета документов на оплату создана!']); } public function generatePhotosBeforePack(Request $request, Reclamation $reclamation) { GenerateFilesPack::dispatch($reclamation, $reclamation->photos_before, auth()->user()->id, 'Фото проблемы'); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Задача архивации создана!']); } public function generatePhotosAfterPack(Request $request, Reclamation $reclamation) { GenerateFilesPack::dispatch($reclamation, $reclamation->photos_after, auth()->user()->id, 'Фото после'); return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')]) ->with(['success' => 'Задача архивации создана!']); } private function ensureCanViewReclamation(Reclamation $reclamation): void { if (hasRole(Role::BRIGADIER) && (int)$reclamation->brigadier_id !== (int)auth()->id()) { abort(403); } } private function ensureHasRole(array $roles): void { if (!count(array_intersect($roles, Role::effectiveRoles((string)auth()->user()?->role)))) { abort(403); } } }