'ID', 'order_number' => 'Номер заказа', 'customer_name' => 'Заказчик', 'object_address' => 'Адрес объекта', 'invoice_number' => '№ счёта', 'payment_date' => 'Дата оплаты', 'supply_working_days' => 'Срок поставки', 'contract_shipment_date' => 'Дата отгрузки по договору', 'application_shipment_date' => 'Дата отгрузки по заявке', 'status_name' => 'Статус', 'delivery_dates' => 'Дата доставки', 'installation_dates' => 'Дата монтажа', 'manager_id' => 'Менеджер', 'note' => 'Примечание', ]; private const SEARCH_FIELDS = [ 'order_number', 'customer_name', 'object_address', 'invoice_number', 'contract_number', 'note', ]; private const FIELD_MAP = [ 'status_name' => 'status', 'execution_type_name' => 'execution_type', 'items_count' => 'id', 'delivery_dates' => 'delivery_date', 'installation_dates' => 'installation_date', ]; protected array $data = [ 'active' => 'schedule_orders', 'title' => 'График заказов', 'id' => 'schedule_orders', 'header' => self::INDEX_HEADER, 'searchFields' => self::SEARCH_FIELDS, ]; public function index(Request $request): View { $this->data['header'] = self::INDEX_HEADER; $this->data['searchFields'] = self::SEARCH_FIELDS; unset($this->data['dates'], $this->data['ranges'], $this->data['filters']); session(['gp_schedule_orders' => $request->query()]); $nav = $this->startNavigationContext($request); $model = new ProductionOrder; $this->data['header'] = array_filter( $this->data['header'], fn (string $field): bool => $request->user()->canViewField( 'schedule-orders', self::FIELD_MAP[$field] ?? $field, ), ARRAY_FILTER_USE_KEY, ); $this->data['searchFields'] = array_values(array_filter( $this->data['searchFields'], fn (string $field): bool => $request->user()->canViewField('schedule-orders', $field), )); $this->createDateFilters( $model, ...array_values(array_intersect([ 'payment_date', 'contract_shipment_date', 'application_shipment_date', ], array_keys($this->data['header']))), ); if (isset($this->data['header']['supply_working_days'])) { $this->createRangeFilters($model, 'supply_working_days'); } foreach (['status_name', 'execution_type_name'] as $field) { if (isset($this->data['header'][$field])) { $this->data['filters'][$field] = [ 'title' => $this->data['header'][$field], 'values' => [], ]; } } $query = ProductionOrder::query() ->with([ 'manager', 'deliveries:id,production_order_id,delivery_date', 'installations:id,production_order_id,installation_date', ]) ->withCount('items') ->withMin('deliveries', 'delivery_date') ->withMin('installations', 'installation_date'); $this->acceptOrderFilters($query, $request); $this->acceptSearch($query, $request); $requestedSort = $request->string('sortBy')->toString(); $sortMap = [ 'status_name' => 'status', 'execution_type_name' => 'execution_type', 'items_count' => 'items_count', 'delivery_dates' => 'deliveries_min_delivery_date', 'installation_dates' => 'installations_min_installation_date', ]; if (isset($sortMap[$requestedSort])) { $request->merge(['sortBy' => $sortMap[$requestedSort]]); } $this->setSortAndOrderBy($model, $request); if ($requestedSort !== '' && isset($sortMap[$requestedSort])) { $this->data['sortBy'] = $requestedSort; $query->orderBy($sortMap[$requestedSort], $this->data['orderBy']); $query->orderBy('id', $this->data['orderBy']); } else { $this->applyStableSorting($query); } $this->data['orders'] = $query->paginate($this->data['per_page'])->withQueryString(); $this->data['nav'] = $nav; return view('production_orders.index', $this->data); } public function create(Request $request, FieldAccessService $fieldAccess): View { return $this->orderView($request, $fieldAccess); } public function export(Request $request): RedirectResponse { $validated = $request->validate([ 'filters' => ['nullable', 'array'], 'filters.*' => ['nullable', 'string', 'max:1000'], 's' => ['nullable', 'string', 'max:500'], ]); $request->merge([ 'filters' => Arr::only($validated['filters'] ?? [], [ 'id', 'order_number', 'customer_name', 'object_address', 'invoice_number', 'payment_date', 'payment_date_from', 'payment_date_to', 'supply_working_days', 'supply_working_days_from', 'supply_working_days_to', 'contract_shipment_date', 'contract_shipment_date_from', 'contract_shipment_date_to', 'application_shipment_date', 'application_shipment_date_from', 'application_shipment_date_to', 'status_name', 'execution_type_name', 'manager_id', 'delivery_dates', 'installation_dates', 'note', ]), 's' => $validated['s'] ?? null, ]); $this->data['dates'] = array_fill_keys([ 'payment_date', 'contract_shipment_date', 'application_shipment_date', ], true); $this->data['ranges'] = ['supply_working_days' => true]; $query = ProductionOrder::query(); $this->acceptOrderFilters($query, $request); $this->acceptSearch($query, $request); $orderIds = $query->orderByDesc('created_at')->orderByDesc('id')->pluck('id')->all(); ExportProductionOrdersJob::dispatch($orderIds, (int) $request->user()->id); return redirect() ->route('schedule.orders', session('gp_schedule_orders')) ->with('success', 'Задача экспорта графика заказов создана.'); } public function import(Request $request): RedirectResponse { $validated = $request->validate([ 'import_file' => ['required', 'file', 'mimes:xlsx', 'max:20480'], ]); /** @var UploadedFile $uploadedFile */ $uploadedFile = $validated['import_file']; $path = 'production_orders/'.Str::uuid().'.xlsx'; Storage::disk('upload')->put($path, $uploadedFile->getContent()); $import = Import::query()->create([ 'type' => 'production_orders', 'year' => null, 'filename' => $path, 'status' => 'new', 'user_id' => $request->user()->getKey(), 'file_path' => $path, 'original_filename' => $uploadedFile->getClientOriginalName(), ]); ImportJob::dispatch($import, (int) $request->user()->getKey()); return redirect() ->route('schedule.orders', session('gp_schedule_orders')) ->with('success', 'Задача импорта графика заказов создана.'); } public function show( Request $request, ProductionOrder $productionOrder, FieldAccessService $fieldAccess, ): View { return $this->orderView( $request, $fieldAccess, $productionOrder->load([ 'manager', 'items.catalogItem.imageFile', 'items.passportFile', 'deliveries.driver', 'installations.brigadier', 'activities.user', 'documents', 'photos', 'chatMessages.user', 'chatMessages.targetUser', 'chatMessages.notifiedUsers', 'chatMessages.files', ]), ); } public function store( SaveProductionOrderRequest $request, FieldAccessService $fieldAccess, ProductionOrderService $service, NotificationService $notificationService, ): RedirectResponse { $validated = $request->validated(); $items = $validated['items']; $fields = array_keys(config('access.schedule-orders.fields', [])); $payload = $fieldAccess->filterValidatedPayload( $request->user(), 'schedule-orders', Arr::only($validated, $fields), ); $payload['items'] = $items; $order = $service->create($payload, $request->user()); $notificationService->notifyProductionOrderEvent( $order, ProductionOrderNotificationEvent::Created, $request->user(), ); return redirect()->route('schedule.orders.show', $this->withNav( ['productionOrder' => $order], $this->resolveNavToken($request), ))->with('success', 'Заказ создан.'); } public function update( SaveProductionOrderRequest $request, ProductionOrder $productionOrder, FieldAccessService $fieldAccess, ProductionOrderService $service, NotificationService $notificationService, ): RedirectResponse { $validated = $request->validated(); $fields = array_keys(config('access.schedule-orders.fields', [])); $payload = $fieldAccess->filterValidatedPayload( $request->user(), 'schedule-orders', Arr::only($validated, $fields), ); $canManageItems = $request->user()->hasPermission('schedule-orders.items.manage'); $editableItemFields = array_values(array_filter( ['factory_number', 'manufacture_date'], fn (string $field): bool => $request->user()->canUpdateField('schedule-orders', $field), )); if (($canManageItems || $editableItemFields !== []) && isset($validated['items'])) { $payload['items'] = $validated['items']; } $previousStatus = $productionOrder->status; $updatedOrder = $service->update( $productionOrder, $payload, $request->user(), $canManageItems, $editableItemFields, ); if ($updatedOrder->status !== $previousStatus) { $notificationService->notifyProductionOrderEvent( $updatedOrder, ProductionOrderNotificationEvent::StatusChanged, $request->user(), ); } return redirect()->route('schedule.orders.show', $this->withNav( ['productionOrder' => $productionOrder], $this->resolveNavToken($request), ))->with('success', 'Заказ обновлён.'); } public function destroy(Request $request, ProductionOrder $productionOrder): RedirectResponse { $productionOrder->delete(); return redirect() ->route('schedule.orders', session('gp_schedule_orders')) ->with('success', 'Заказ удалён.'); } public function searchCatalogItems(Request $request): JsonResponse { $validated = $request->validate([ 'q' => ['nullable', 'string', 'max:200'], ]); $search = trim((string) ($validated['q'] ?? '')); if (mb_strlen($search) < 2) { return response()->json([]); } $items = CommonCatalogItem::query() ->with('imageFile') ->where(function ($query) use ($search): void { $query ->where('article', 'like', "%{$search}%") ->orWhere('calculator_name', 'like', "%{$search}%") ->orWhere('print_name', 'like', "%{$search}%"); }) ->orderBy('article') ->limit(50) ->get(['id', 'image_file_id', 'article', 'calculator_name', 'print_name']) ->map(static fn (CommonCatalogItem $item): array => [ 'id' => $item->id, 'label' => $item->article.' — '.($item->calculator_name ?: $item->print_name ?: 'Без наименования'), 'image' => $item->imageFile?->thumbnail_link, 'image_full' => $item->imageFile?->link, ]) ->values(); return response()->json($items); } private function orderView( Request $request, FieldAccessService $fieldAccess, ?ProductionOrder $order = null, ): View { $nav = $this->resolveNavToken($request); $this->rememberNavigation($request, $nav); $fields = array_keys(config('access.schedule-orders.fields', [])); $catalogItemIds = collect($order?->items ?? []) ->pluck('common_catalog_item_id') ->merge(collect($request->old('items', []))->pluck('common_catalog_item_id')) ->filter() ->unique() ->values(); $chatUsers = User::query()->orderBy('name')->get(['id', 'name', 'role']); $responsibleUserIds = User::query() ->withPermission('schedule-orders.create') ->pluck('id') ->map(static fn ($id): int => (int) $id) ->merge([$order?->manager_id]) ->merge($order?->deliveries->pluck('driver_id') ?? []) ->merge($order?->installations->pluck('brigadier_id') ?? []) ->filter() ->unique() ->values() ->all(); return view('production_orders.edit', [ 'active' => 'schedule_orders', 'title' => $order ? 'Заказ '.$order->order_number : 'Новый заказ', 'order' => $order, 'nav' => $nav, 'back_url' => $this->navigationBackUrl( $request, $nav, route('schedule.orders', session('gp_schedule_orders')), ), 'readableFields' => array_fill_keys( $fieldAccess->filterReadableFields($request->user(), 'schedule-orders', $fields), true, ), 'writableFields' => collect($fields)->mapWithKeys(fn (string $field): array => [ $field => $request->user()->canUpdateField('schedule-orders', $field), ])->all(), 'canManageItems' => $request->user()->hasPermission('schedule-orders.items.manage'), 'canManageDeliveries' => $request->user()->hasPermission('schedule-orders.deliveries.manage'), 'canManageInstallations' => $request->user()->hasPermission('schedule-orders.installations.manage'), 'canManageDocuments' => $request->user()->hasPermission('schedule-orders.documents.manage'), 'canManagePhotos' => $request->user()->hasPermission('schedule-orders.photos.manage'), 'canManagePassports' => $request->user()->hasPermission('schedule-orders.passports.manage'), 'canCreateChatMessages' => $request->user()->hasPermission('schedule-orders.chat.create'), 'canDeleteChatMessages' => $request->user()->hasPermission('schedule-orders.chat.delete'), 'canUpdateFactoryNumber' => $request->user()->canUpdateField('schedule-orders', 'factory_number'), 'canUpdateManufactureDate' => $request->user()->canUpdateField('schedule-orders', 'manufacture_date'), 'managers' => User::query() ->where('role', Role::MANAGER) ->orderBy('name') ->pluck('name', 'id'), 'drivers' => User::query() ->whereIn('role', [Role::DRIVER, Role::BRIGADIER]) ->orderBy('name') ->pluck('name', 'id'), 'brigadiers' => User::query() ->where('role', Role::BRIGADIER) ->orderBy('name') ->pluck('name', 'id'), 'catalogItems' => CommonCatalogItem::query() ->with('imageFile') ->whereKey($catalogItemIds) ->get(['id', 'image_file_id', 'article', 'calculator_name', 'print_name']) ->keyBy('id'), 'statuses' => ProductionOrderStatus::options(), 'executionTypes' => ProductionOrderExecutionType::options(), 'chatUsers' => $chatUsers->map(static fn (User $user): array => [ 'id' => $user->id, 'name' => $user->name, 'role' => $user->role, ])->keyBy('id'), 'chatResponsibleUserIds' => $responsibleUserIds, ]); } private function acceptOrderFilters(Builder $query, Request $request): void { $filters = (array) $request->input('filters', []); $managerFilter = trim((string) ($filters['manager_id'] ?? '')); if ($managerFilter !== '') { $values = explode('||', $managerFilter); $managerIds = array_values(array_filter( $values, static fn (string $value): bool => ctype_digit($value), )); $managerNames = array_values(array_filter( $values, static fn (string $value): bool => $value !== '-пусто-' && ! ctype_digit($value), )); $includeEmpty = in_array('-пусто-', $values, true); $query->where(function (Builder $managerQuery) use ($managerIds, $managerNames, $includeEmpty): void { if ($managerIds !== []) { $managerQuery->whereIn('manager_id', $managerIds); } if ($managerNames !== []) { $method = $managerIds !== [] ? 'orWhereHas' : 'whereHas'; $managerQuery->{$method}( 'manager', static fn (Builder $userQuery): Builder => $userQuery->whereIn('name', $managerNames), ); } if ($includeEmpty) { ($managerIds !== [] || $managerNames !== []) ? $managerQuery->orWhereNull('manager_id') : $managerQuery->whereNull('manager_id'); } }); } $relationFilters = [ 'delivery_dates' => ['deliveries', 'delivery_date'], 'installation_dates' => ['installations', 'installation_date'], ]; foreach ($relationFilters as $filterName => [$relation, $column]) { $value = trim((string) ($filters[$filterName] ?? '')); if ($value === '') { continue; } $values = explode('||', $value); $dates = array_values(array_filter( $values, static fn (string $date): bool => $date !== '-пусто-', )); $includeEmpty = in_array('-пусто-', $values, true); $query->where(function ($relationQuery) use ($relation, $column, $dates, $includeEmpty): void { if ($dates !== []) { $relationQuery->whereHas( $relation, static fn ($dateQuery) => $dateQuery->whereIn($column, $dates), ); } if ($includeEmpty) { $dates !== [] ? $relationQuery->orWhereDoesntHave($relation) : $relationQuery->whereDoesntHave($relation); } }); } $request->merge(['filters' => Arr::except($filters, [ 'manager_id', ...array_keys($relationFilters), ])]); $this->acceptFilters($query, $request); $request->merge(['filters' => $filters]); } }