| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- <?php
- declare(strict_types=1);
- namespace App\Http\Controllers;
- use App\Enums\ProductionOrderExecutionType;
- use App\Enums\ProductionOrderNotificationEvent;
- use App\Enums\ProductionOrderStatus;
- use App\Http\Requests\SaveProductionOrderRequest;
- use App\Jobs\Export\ExportProductionOrdersJob;
- use App\Models\CommonCatalogItem;
- use App\Models\ProductionOrder;
- use App\Models\Role;
- use App\Models\User;
- use App\Services\Access\FieldAccessService;
- use App\Services\NotificationService;
- use App\Services\ProductionOrderService;
- use Illuminate\Contracts\View\View;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Http\RedirectResponse;
- use Illuminate\Http\Request;
- use Illuminate\Support\Arr;
- class ProductionOrderController extends Controller
- {
- private const INDEX_HEADER = [
- 'id' => '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 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]);
- }
- }
|