'orders', 'title' => 'Заказы', 'id' => 'orders', 'header' => [ 'id' => 'ID', 'name' => 'Название', 'user_name' => 'Менеджер', 'district_name' => 'Округ', 'area_name' => 'Район', 'object_address' => 'Адрес объекта', 'object_type_name' => 'Тип объекта', 'comment' => 'Комментарий', 'installation_date' => 'Дата выхода на монтаж', 'ready_date' => 'Дата готовности площадки', 'brigadier_name' => 'Бригадир', 'order_status_name' => 'Статус', 'tg_group_name' => 'Имя группы в ТГ', 'tg_group_link' => 'Ссылка на группу в ТГ', 'products_with_count' => 'МАФы', 'ready_to_mount' => 'Все МАФы на складе', ], 'searchFields' => [ 'name', 'user_name', 'district_name', 'area_name', 'object_type_name', 'comment', 'object_address', 'installation_date', 'tg_group_name', 'tg_group_link', ], ]; public function __construct() { $this->data['districts'] = District::query()->get()->pluck('name', 'id'); $this->data['areas'] = Area::query()->get()->pluck('name', 'id'); $this->data['objectTypes'] = ObjectType::query()->get()->pluck('name', 'id'); $this->data['orderStatuses'] =OrderStatus::query()->get()->pluck('name', 'id'); $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id'); $this->data['users'] = User::query()->whereIn('role', [Role::MANAGER, Role::ADMIN])->get()->pluck('name', 'id'); } /** * Display a listing of the resource. */ public function index(Request $request) { session(['gp_orders' => $request->all()]); $model = new OrderView; // fill filters $this->createFilters($model, 'user_name', 'district_name', 'area_name', 'object_type_name', 'brigadier_name', 'order_status_name', 'ready_to_mount'); $this->createDateFilters($model, 'installation_date', 'ready_date'); $this->data['ranges'] = []; $q = $model::query(); $this->acceptFilters($q, $request); $this->acceptSearch($q, $request); $this->setSortAndOrderBy($model, $request); if(hasRole('brigadier')) { $q->where('brigadier_id', auth()->id()); } if(hasRole(Role::WAREHOUSE_HEAD)) { $q->whereNotNull('brigadier_id'); $q->whereNotNull('installation_date'); } $this->applyStableSorting($q); $this->data['orders'] = $q->paginate($this->data['per_page'])->withQueryString(); foreach ($this->data['orders'] as $order) { Order::where('id', $order->id)->first()?->recalculateReadyToMount(); } $this->data['statuses'] = OrderStatus::query()->get()->pluck('name', 'id'); return view('orders.index', $this->data); } /** * Show the form for creating a new resource. */ public function create(Request $request) { $this->data['previous_url'] = $this->resolvePreviousUrl( $request, 'previous_url_orders', route('order.index', session('gp_orders')) ); return view('orders.edit', $this->data); } /** * Store a newly created resource in storage. */ public function store(StoreOrderRequest $request, NotificationService $notificationService) { $data = $request->validated(); $products = $request->validated('products'); $quantities = $request->validated('quantity'); unset($data['products']); if(isset($data['id'])) { $order = Order::query()->where('id', $data['id'])->first(); $status = $order->order_status_id; $order->update($data); $order->refresh(); if($order->order_status_id != $status) { $notificationService->notifyOrderStatusChanged($order); } } else { $data['order_status_id'] = Order::STATUS_NEW; $order = Order::query()->create($data); $tg_group_name = '/' . $order->district->shortname . ' ' . $order->object_address . ' (' . $order->area->name . ')' . ' - ' . $order->user->name; $order->update(['tg_group_name' => $tg_group_name]); $notificationService->notifyOrderCreated($order); } // меняем список товаров заказа только если статус новый if($products && $quantities && ($order->order_status_id == 1)) { $desiredCounts = []; foreach ($products as $key => $productId) { $quantity = (int)($quantities[$key] ?? 0); if ($quantity < 1) { continue; } if (!isset($desiredCounts[$productId])) { $desiredCounts[$productId] = 0; } $desiredCounts[$productId] += $quantity; } $attachedSkus = ProductSKU::query() ->where('order_id', $order->id) ->whereNotNull('maf_order_id') ->with('product:id,article') ->get(); $attachedByProduct = []; foreach ($attachedSkus as $sku) { if (!isset($attachedByProduct[$sku->product_id])) { $attachedByProduct[$sku->product_id] = 0; } $attachedByProduct[$sku->product_id]++; } $currentCounts = ProductSKU::query() ->where('order_id', $order->id) ->selectRaw('product_id, COUNT(*) as cnt') ->groupBy('product_id') ->pluck('cnt', 'product_id') ->map(fn ($count) => (int)$count) ->toArray(); $errors = []; foreach ($attachedByProduct as $productId => $attachedCount) { $desiredCount = $desiredCounts[$productId] ?? 0; if ($desiredCount < $attachedCount) { $article = $attachedSkus->firstWhere('product_id', $productId)?->product?->article ?? ('ID ' . $productId); $errors[] = "Нельзя удалить привязанные МАФ {$article}: привязано {$attachedCount} шт."; } } // Если есть хотя бы один привязанный МАФ, запрещаем добавлять новые позиции/количество. if (!empty($attachedByProduct)) { foreach ($desiredCounts as $productId => $desiredCount) { $currentCount = $currentCounts[$productId] ?? 0; if ($desiredCount > $currentCount) { $errors[] = 'Нельзя добавлять новые позиции МАФ, пока на площадке есть привязанные МАФ.'; break; } } } if (!empty($errors)) { return redirect()->back()->withInput()->with(['danger' => $errors]); } // Удаляем только непривязанные SKU и пересоздаём их по новому списку. ProductSKU::query() ->where('order_id', $order->id) ->whereNull('maf_order_id') ->delete(); foreach ($desiredCounts as $productId => $desiredCount) { $attachedCount = $attachedByProduct[$productId] ?? 0; $toCreate = $desiredCount - $attachedCount; for ($i = 0; $i < $toCreate; $i++) { ProductSKU::query()->create([ 'order_id' => $order->id, 'product_id' => $productId, 'status' => 'требуется' ]); } } } $order->refresh(); $order->autoChangeStatus(); return redirect()->route('order.show', ['order' => $order, 'previous_url' => $request->get('previous_url')]); } /** * Display the specified resource. */ public function show(Request $request, int $order) { $this->data['order'] = Order::query()->withoutGlobalScope(\App\Models\Scopes\YearScope::class)->find($order); if ($request->boolean('sync_year') && $this->data['order']) { $previousYear = year(); $targetYear = (int)$this->data['order']->year; if ($previousYear !== $targetYear) { session(['year' => $targetYear]); session()->flash('warning', "Год переключен: {$previousYear} → {$targetYear} (по году площадки)."); } } $this->data['previous_url'] = $this->resolvePreviousUrl( $request, 'previous_url_orders', route('order.index', session('gp_orders')) ); return view('orders.show', $this->data); } /** * Show the form for editing the specified resource. */ public function edit(Request $request, Order $order) { $this->data['order'] = $order; $this->data['previous_url'] = $this->resolvePreviousUrl( $request, 'previous_url_orders', route('order.index', session('gp_orders')) ); return view('orders.edit', $this->data); } /** * Привязка товаров к заказу * @param Order $order * @return RedirectResponse * @throws Throwable */ public function getMafToOrder(Order $order) { $attached = 0; $missingByProduct = []; DB::transaction(function () use ($order, &$attached, &$missingByProduct) { foreach ($order->products_sku as $productSku) { // Уже привязан, пропускаем if ($productSku->maf_order_id) { continue; } $mafOrder = MafOrder::query() ->where('product_id', $productSku->product_id) ->where('in_stock', '>', 0) ->orderBy('created_at') ->lockForUpdate() ->first(); if (!$mafOrder) { $article = $productSku->product?->article ?? ('ID ' . $productSku->product_id); if (!isset($missingByProduct[$productSku->product_id])) { $missingByProduct[$productSku->product_id] = ['article' => $article, 'count' => 0]; } $missingByProduct[$productSku->product_id]['count']++; continue; } $productSku->update(['maf_order_id' => $mafOrder->id, 'status' => 'отгружен']); $mafOrder->decrement('in_stock'); $attached++; } }); $order->autoChangeStatus(); $success = []; $danger = []; if ($attached > 0) { $success[] = "Привязано МАФ: {$attached}."; } foreach ($missingByProduct as $missing) { $danger[] = "Не удалось привязать {$missing['count']} шт. МАФ {$missing['article']}: нет остатка на складе."; } if ($attached === 0 && empty($danger)) { $danger[] = 'Нет МАФ для привязки: все МАФы на площадке уже привязаны.'; } $flash = []; if (!empty($success)) { $flash['success'] = $success; } if (!empty($danger)) { $flash['danger'] = $danger; } return redirect()->route('order.show', $order)->with($flash); } /** * @param Order $order * @return RedirectResponse */ public function destroy(Order $order) { Order::query()->where('id', $order->id)->delete(); ProductSKU::query()->where('order_id', $order->id)->delete(); return redirect()->route('order.index', session('gp_orders')); } public function revertMaf(Order $order) { $detached = 0; $notFoundMafOrders = 0; DB::transaction(function () use ($order, &$detached, &$notFoundMafOrders) { foreach ($order->products_sku as $maf) { if (!$maf->maf_order_id) { continue; } $affectedRows = MafOrder::query() ->where('id', $maf->maf_order_id) ->lockForUpdate() ->increment('in_stock'); if (!$affectedRows) { $notFoundMafOrders++; continue; } $maf->update(['maf_order_id' => null, 'status' => 'требуется']); $detached++; } }); $order->autoChangeStatus(); $success = []; $danger = []; if ($detached > 0) { $success[] = "Отвязано МАФ: {$detached}."; } if ($notFoundMafOrders > 0) { $danger[] = "Не удалось отвязать {$notFoundMafOrders} шт. МАФ: связанный заказ МАФ не найден."; } if ($detached === 0 && empty($danger)) { $danger[] = 'Нет МАФ для отвязки: на площадке нет привязанных МАФ.'; } $flash = []; if (!empty($success)) { $flash['success'] = $success; } if (!empty($danger)) { $flash['danger'] = $danger; } return redirect()->route('order.show', $order)->with($flash); } public function moveMaf(Request $request) { $data = $request->validate([ 'new_order_id' => 'required', 'ids' => 'required|json', ]); $ids = json_decode($data['ids'], true); $updated = []; foreach ($ids as $mafId) { $maf = ProductSKU::query() ->where('id', $mafId) ->first(); if($maf) { $comment = $maf->comment . "\n" . date('Y-m-d H:i') . ' Перенесено с площадки: ' . $maf->order->common_name; $maf->update(['order_id' => $data['new_order_id'], 'comment' => $comment]); $updated[] = $maf; } } return response()->json(['success' => true]); } public function search(Request $request): array { $ret = []; $s = $request->get('s'); $searchFields = $this->data['searchFields']; $result = Order::query(); if($s) { $result->where(function ($query) use ($searchFields, $s) { foreach ($searchFields as $searchField) { $query->orWhere($searchField, 'LIKE', '%' . $s . '%'); } }); } $result->orderBy('object_address'); foreach ($result->get() as $p) { $ret[$p->id] = $p->common_name; } return $ret; } public function uploadPhoto(Request $request, Order $order, FileService $fileService) { $data = $request->validate([ 'photo.*' => 'mimes:jpeg,jpg,png', ]); try { $f = []; foreach ($data['photo'] as $photo) { $f[] = $fileService->saveUploadedFile('orders/' . $order->id . '/photo', $photo); } $order->photos()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('order.show', $order) ->with(['error' => 'Ошибка загрузки фотографий. Проверьте имя файла и повторите попытку.']); } return redirect()->route('order.show', $order)->with(['success' => 'Фотографии успешно загружены!']); } public function deletePhoto(Order $order, File $file, FileService $fileService) { $order->photos()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('order.show', $order); } public function deleteAllPhotos(Order $order) { $files = $order->photos; $order->photos()->detach(); foreach ($files as $file) { Storage::disk('public')->delete($file->path); $file->delete(); } return redirect()->route('order.show', $order); } public function uploadDocument(Request $request, Order $order, FileService $fileService) { $data = $request->validate([ 'document.*' => 'file', ]); try { $f = []; $i = 0; foreach ($data['document'] as $document) { if ($i++ >= 5) break; $f[] = $fileService->saveUploadedFile('orders/' . $order->id . '/document', $document); } $order->documents()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('order.show', $order) ->with(['error' => 'Ошибка загрузки документов. Проверьте имя файла и повторите попытку.']); } return redirect()->route('order.show', $order)->with(['success' => 'Документы успешно загружены!']); } public function deleteDocument(Order $order, File $file) { $order->documents()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('order.show', $order); } public function deleteAllDocuments(Order $order) { $files = $order->documents; $order->documents()->detach(); foreach ($files as $file) { Storage::disk('public')->delete($file->path); $file->delete(); } return redirect()->route('order.show', $order); } public function uploadStatement(Request $request, Order $order, FileService $fileService) { $data = $request->validate([ 'statement.*' => 'file', ]); try { $f = []; $i = 0; foreach ($data['statement'] as $statement) { if ($i++ >= 5) break; $f[] = $fileService->saveUploadedFile('orders/' . $order->id . '/statement', $statement); } $order->statements()->syncWithoutDetaching($f); } catch (Throwable $e) { report($e); return redirect()->route('order.show', $order) ->with(['error' => 'Ошибка загрузки ведомостей. Проверьте имя файла и повторите попытку.']); } return redirect()->route('order.show', $order)->with(['success' => 'Ведомости успешно загружены!']); } public function deleteStatement(Order $order, File $file) { $order->statements()->detach($file); Storage::disk('public')->delete($file->path); $file->delete(); return redirect()->route('order.show', $order); } public function deleteAllStatements(Order $order) { $files = $order->statements; $order->statements()->detach(); foreach ($files as $file) { Storage::disk('public')->delete($file->path); $file->delete(); } return redirect()->route('order.show', $order); } public function generateInstallationPack(Order $order) { $errors = []; if(!in_array($order->order_status_id, [Order::STATUS_READY_TO_MOUNT, Order::STATUS_IN_MOUNT])) $errors[] = 'Статус должен быть "Готов к монтажу" или "В монтаже"!'; $errors = array_merge($errors, $order->isAllMafConnected()); if($errors) { return redirect()->route('order.show', $order)->with(['danger' => $errors]); } GenerateInstallationPack::dispatch($order, auth()->user()->id); return redirect()->route('order.show', $order)->with(['success' => 'Задача генерации документов создана!']); } public function generateHandoverPack(Order $order) { if($errors = $order->canCreateHandover()) { return redirect()->route('order.show', $order)->with(['danger' => $errors]); } GenerateHandoverPack::dispatch($order, auth()->user()->id); return redirect()->route('order.show', $order)->with(['success' => 'Задача генерации документов создана!']); } public function generatePhotosPack(Order $order) { GenerateFilesPack::dispatch($order, $order->photos, auth()->user()->id, 'Фото'); return redirect()->back()->with(['success' => 'Задача архивации создана!']); } public function createTtn(CreteTtnRequest $request) { $ttn = Ttn::query()->create([ 'year' => date('Y'), 'ttn_number' => Ttn::getTtnNumber() + 1, 'ttn_number_suffix' => 'И', 'order_number' => $request->order_number, 'order_date' => $request->order_date, 'order_sum' => $request->order_sum, 'skus' => json_encode($request->skus), ]); GenerateTtnPack::dispatch($ttn, auth()->user()->id); return redirect()->back()->with(['success' => 'Задача формирования ТН создана!']); } public function export(Request $request) { $schedules = Order::query() ->get(); ExportOrdersJob::dispatch($schedules, $request->user()->id); return redirect()->route('order.index') ->with(['success' => 'Задача выгрузки создана!']); } public function exportOne(Order $order, Request $request) { ExportOneOrderJob::dispatch($order, $request->user()->id); return redirect()->route('order.show', $order->id) ->with(['success' => 'Задача выгрузки создана!']); } public function downloadTechDocs(Order $order): StreamedResponse { $techDocsPath = base_path('tech-docs'); $articles = $order->products_sku ->pluck('product.article') ->unique() ->filter() ->values(); if ($articles->isEmpty()) { abort(404, 'Нет МАФов на площадке'); } $archiveName = 'Тех.документация - ' . fileName($order->object_address) . '.zip'; return response()->streamDownload(function () use ($techDocsPath, $articles) { $zip = new ZipArchive(); $tempFile = tempnam(storage_path('app/temp/'), 'tech_docs_'); $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE); $filesAdded = 0; foreach ($articles as $article) { $articlePath = $techDocsPath . '/' . $article; if (!is_dir($articlePath)) { continue; } $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($articlePath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($files as $file) { if ($file->isFile()) { $relativePath = $article . '/' . $file->getFilename(); $zip->addFile($file->getRealPath(), $relativePath); $filesAdded++; } } } if(!$filesAdded) { $zip->addFromString('readme.txt', 'Нет документации для этих МАФ!'); } $zip->close(); readfile($tempFile); unlink($tempFile); }, $archiveName, [ 'Content-Type' => 'application/zip', ]); } }