user('web'); abort_unless($actor instanceof User, 403); return DB::transaction(function () use ($request, $planning, $actor) { if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); } return $this->page($request, $planning, $actor); }); } private function page(Request $request, SupplyPlanning $planning, User $actor): HttpResponse { Inertia::encryptHistory(); $view = $request->validate(['view' => 'nullable|in:agreements,calendar,imports,alternatives,reservations,deliveries,suggestions,history'])['view'] ?? 'agreements'; if ($view === 'history') { return $this->privateResponse($request, $this->history($request, $actor, $planning)); } if ($view === 'suggestions') { return $this->privateResponse($request, $this->suggestions($request, $actor, $planning)); } if (in_array($view, ['alternatives', 'reservations', 'deliveries'], true)) { return $this->privateResponse($request, $this->coordination($request, $actor, $view, $planning)); } $import = $view === 'imports'; $capabilities = $import ? ['import_read', 'import_prepare', 'import_customs'] : ['supply_read', 'supply_prepare', 'supply_review']; abort_unless(collect($capabilities)->contains(fn ($p) => $planning->allows($actor, $p)), 403); $ready = Schema::hasTable('supply_records'); $kind = $import ? 'import' : ($view === 'calendar' ? 'policy' : 'agreement'); $focus = $this->focusedRecord($request, $actor, $kind); [$records, $page] = $this->recordPage($request, $planning, $ready, $kind, $focus); $projections = []; if ($view === 'calendar' && $ready) { foreach ($records->where('state', 'approved') as $record) { $projections[$record->record_key] = $planning->calendar($record, today()->toDateString()); } } $prepare = $planning->allows($actor, $import ? 'import_prepare' : 'supply_prepare'); $orderFilters = $import ? $request->validate([ 'order_search' => 'nullable|string|max:100', 'selected_order_id' => ['nullable', 'integer', 'min:1', Rule::exists('purchase_orders', 'id')->where('tenant_id', $actor->tenant_id)], ]) : []; $historyFilters = $kind === 'policy' ? $request->validate([ 'history_stock_item_id' => ['nullable', 'required_with:history_period_from,history_period_until,selected_history_key', 'integer', Rule::exists('stock_items', 'id')->where('tenant_id', $actor->tenant_id)], 'history_warehouse_id' => ['nullable', 'required_with:history_stock_item_id', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)], 'history_purpose' => 'nullable|required_with:history_stock_item_id|in:internal,resale', 'history_period_from' => 'nullable|date_format:Y-m-d', 'history_period_until' => ['nullable', 'date_format:Y-m-d', $request->filled('history_period_from') ? 'after_or_equal:history_period_from' : 'nullable'], 'selected_history_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'history')], 'history_form_key' => 'nullable|string|max:80', ]) : []; $agreementFilters = $kind === 'policy' ? $request->validate([ 'agreement_search' => 'nullable|string|max:100', 'agreement_stock_item_id' => ['nullable', 'required_with:agreement_search,selected_agreement_key', 'integer', Rule::exists('stock_items', 'id')->where('tenant_id', $actor->tenant_id)], 'agreement_warehouse_id' => ['nullable', 'required_with:agreement_stock_item_id', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)], 'selected_agreement_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'agreement')], 'agreement_form_key' => 'nullable|string|max:80', ]) : []; return $this->privateResponse($request, Inertia::render('supply/index', ['view' => $view, 'kind' => $kind, 'ready' => $ready, 'agreementSearch' => $agreementFilters['agreement_search'] ?? '', 'agreementFormKey' => $agreementFilters['agreement_form_key'] ?? null, 'recordPagination' => $this->pagination($page), 'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id, 'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $planning->canReview($actor, $record), 'canReturn' => $planning->canReview($actor, $record, 'return'), 'canSuspend' => $planning->canReview($actor, $record, 'suspend')]), 'projections' => $projections, 'asOf' => today()->toDateString(), 'historyFormKey' => $historyFilters['history_form_key'] ?? null, 'histories' => $ready && $prepare && $kind === 'policy' ? $this->approvedOptions($planning, 'history', $records->pluck('data.history_key')->push($historyFilters['selected_history_key'] ?? null), $historyFilters)->map(fn ($r) => [ 'key' => $r->record_key, 'version' => $r->version, 'stock_item_id' => $r->stock_item_id, 'warehouse_id' => $r->warehouse_id, 'purpose' => $r->data['purpose'], 'period_from' => $r->data['period_from'], 'period_until' => $r->data['period_until'], 'quality' => $r->data['quality'], 'pattern' => $r->data['pattern'], 'days' => $r->snapshot['analysis']['days'], 'daily_mean_milli' => $r->snapshot['analysis']['daily_mean_milli'], ]) : [], 'agreements' => $ready && $prepare && $kind === 'policy' ? $this->approvedOptions($planning, 'agreement', $records->pluck('data.agreement_key')->push($agreementFilters['selected_agreement_key'] ?? null), $agreementFilters)->map(fn ($r) => $r->only(['record_key', 'version', 'stock_item_id', 'warehouse_id', 'data', 'snapshot'])) : [], 'history' => $ready ? SupplyRecord::query()->where('kind', $kind)->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], 'canPrepare' => $prepare, 'canReview' => $planning->allows($actor, 'supply_review'), 'canCustoms' => $planning->allows($actor, 'import_customs'), 'items' => $prepare && ! $import ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $prepare && ! $import ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'suppliers' => $prepare && $kind === 'agreement' ? Supplier::query()->where('status', 'active')->orderBy('name')->get(['id', 'name']) : [], 'orderSearch' => $orderFilters['order_search'] ?? '', 'orders' => $prepare && $import ? $this->orderOptions($records->pluck('purchase_order_id')->push($orderFilters['selected_order_id'] ?? null), $orderFilters['order_search'] ?? '') : [], ])); } private function privateResponse(Request $request, Response $page): HttpResponse { $response = $page->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } private function focusedRecord(Request $request, User $actor, string $kind): ?SupplyRecord { $filter = $request->validate(['record_id' => ['nullable', 'integer', 'min:1', Rule::exists('supply_records', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', $kind)]]); return isset($filter['record_id']) ? SupplyRecord::query()->where('tenant_id', $actor->tenant_id)->where('kind', $kind)->findOrFail((int) $filter['record_id']) : null; } /** @return array{Collection,LengthAwarePaginator|null} */ private function recordPage(Request $request, SupplyPlanning $planning, bool $ready, string $kind, ?SupplyRecord $focus): array { $request->validate(['records_page' => 'nullable|integer|min:1|max:1000000']); if (! $ready) { return [collect(), null]; } $query = $planning->latest($kind)->latest('id'); if ($focus !== null) { return [$query->where('record_key', $focus->record_key)->get(), null]; } $page = $query->paginate(25, ['*'], 'records_page')->withQueryString(); return [$page->getCollection(), $page]; } /** @param LengthAwarePaginator|null $page * @return array|null */ private function pagination(?LengthAwarePaginator $page): ?array { return $page === null ? null : ['current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'total' => $page->total(), 'prev_page_url' => $page->previousPageUrl(), 'next_page_url' => $page->nextPageUrl()]; } /** Preserve already selected sources even outside the recent choices. * @param Collection $selected * @param array $filters * @return Collection */ private function approvedOptions(SupplyPlanning $planning, string $kind, Collection $selected, array $filters = []): Collection { $query = $planning->latest($kind)->where('state', 'approved'); if ($kind === 'agreement' && isset($filters['agreement_stock_item_id'])) { $query->where('stock_item_id', $filters['agreement_stock_item_id'])->where('warehouse_id', $filters['agreement_warehouse_id']); } if ($kind === 'history' && isset($filters['history_stock_item_id'])) { $query->where('stock_item_id', $filters['history_stock_item_id'])->where('warehouse_id', $filters['history_warehouse_id'])->where('data->purpose', $filters['history_purpose']); } $recent = (clone $query) ->when(trim($filters['agreement_search'] ?? '') !== '', fn ($q) => $q->where(fn ($match) => $match ->whereLike('data->reference', '%'.trim($filters['agreement_search']).'%') ->orWhereLike('snapshot->supplier->name', '%'.trim($filters['agreement_search']).'%'))) ->when(trim($filters['policy_search'] ?? '') !== '', fn ($q) => $q->where(fn ($match) => $match ->whereLike('snapshot->item->code', '%'.trim($filters['policy_search']).'%') ->orWhereLike('snapshot->item->name', '%'.trim($filters['policy_search']).'%') ->orWhereLike('snapshot->warehouse->name', '%'.trim($filters['policy_search']).'%'))) ->when($filters['history_period_from'] ?? null, fn ($q, $date) => $q->where('data->period_from', '>=', $date)) ->when($filters['history_period_until'] ?? null, fn ($q, $date) => $q->where('data->period_until', '<=', $date)) ->latest('id')->limit(100)->pluck('id'); return $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('record_key', $selected->filter()->unique()))->latest('id')->get(); } /** @param Collection $selected * @return Collection */ private function orderOptions(Collection $selected, string $search = ''): Collection { $recent = PurchaseOrder::query()->when(trim($search) !== '', fn ($q) => $q->whereRaw('LOWER(number) LIKE ?', ['%'.mb_strtolower(trim($search)).'%']))->latest('id')->limit(100)->pluck('id'); return PurchaseOrder::query()->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('id', $selected->filter()->unique()))->latest('id')->get(['id', 'number']); } private function history(Request $request, User $actor, SupplyPlanning $planning): Response { $prepare = $planning->allows($actor, 'supply_history_prepare'); $review = $planning->allows($actor, 'supply_history_review'); abort_unless($prepare || $review || $planning->allows($actor, 'supply_read'), 403); $ready = Schema::hasTable('supply_records'); $focus = $this->focusedRecord($request, $actor, 'history'); [$records, $page] = $this->recordPage($request, $planning, $ready, 'history', $focus); $service = app(SupplyHistory::class); return Inertia::render('supply/history', ['ready' => $ready, 'actorId' => $actor->id, 'recordPagination' => $this->pagination($page), 'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id, 'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $service->canReview($actor, $record), 'canReturn' => $service->canReview($actor, $record, 'return'), 'canSuspend' => $service->canReview($actor, $record, 'suspend')]), 'canPrepare' => $prepare, 'canReview' => $review, 'defaultPeriod' => ['from' => today()->subDays(30)->toDateString(), 'until' => today()->subDay()->toDateString()], 'items' => $prepare ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $prepare ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'history' => $ready ? SupplyRecord::query()->where('kind', 'history')->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } private function suggestions(Request $request, User $actor, SupplyPlanning $planning): Response { $prepare = $planning->allows($actor, 'supply_suggest_prepare'); $review = $planning->allows($actor, 'supply_suggest_review'); $convertPermission = $planning->allows($actor, 'supply_suggest_convert'); $convert = $convertPermission && app(OrganizationAccess::class)->canRequest($actor); abort_unless($planning->allows($actor, 'supply_read') || $prepare || $review || $convertPermission, 403); $ready = Schema::hasTable('supply_records'); $focus = $this->focusedRecord($request, $actor, 'suggestion'); [$records, $page] = $this->recordPage($request, $planning, $ready, 'suggestion', $focus); $filters = $request->validate([ 'policy_search' => 'nullable|string|max:100', 'selected_policy_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'policy')], 'policy_form_key' => 'nullable|string|max:80', ]); $policies = []; if ($ready && $prepare) { foreach ($this->approvedOptions($planning, 'policy', $records->pluck('data.policy_key')->push($filters['selected_policy_key'] ?? null), $filters) as $policy) { $option = ['key' => $policy->record_key, 'version' => $policy->version, 'label' => $policy->snapshot['item']['code'].' · '.$policy->snapshot['item']['name'].' · '.$policy->snapshot['warehouse']['name'], 'issue' => null]; try { $basis = app(SupplySuggestions::class)->basis($policy->record_key, today()->toDateString()); $option += ['unit_price' => $basis['unit_price'], 'tax_rate' => $basis['tax_rate'], 'quantity_milli' => $basis['projection']['quantity_milli'] ?? 0, 'delivery_on' => $basis['projection']['risk_date'] ?? null, 'unit' => $basis['item']['unit']]; } catch (ValidationException $e) { $option['issue'] = $e->getMessage(); } $policies[] = $option; } } $requests = Requisition::query()->visibleTo($actor)->whereIn('id', $records->pluck('data.requisition_id')->filter())->get(['id', 'number'])->keyBy('id'); $service = app(SupplySuggestions::class); return Inertia::render('supply/suggestions', ['ready' => $ready, 'actorId' => $actor->id, 'policySearch' => $filters['policy_search'] ?? '', 'policyFormKey' => $filters['policy_form_key'] ?? null, 'recordPagination' => $this->pagination($page), 'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id, 'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $service->canAct($actor, $record, 'approve'), 'canReturn' => $service->canAct($actor, $record, 'return'), 'canConvert' => $service->canAct($actor, $record, 'convert'), 'canCancel' => $service->canAct($actor, $record, 'cancel'), 'canClose' => $service->canAct($actor, $record, 'close')]), 'policies' => $policies, 'canPrepare' => $prepare, 'canReview' => $review, 'canConvert' => $convert, 'costCenters' => $convert ? CostCenter::query()->where('department_id', $actor->department_id)->orderBy('code')->get(['id', 'code', 'name']) : [], 'requestLinks' => $requests, 'excess' => $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplySuggestions::class)->hasExcess($r)]), 'history' => $ready ? SupplyRecord::query()->where('kind', 'suggestion')->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } private function coordination(Request $request, User $actor, string $view, SupplyPlanning $planning): Response { [$kind, $prepare, $review] = match ($view) { 'alternatives' => ['alternative', 'supply_technical_prepare', 'supply_technical_review'], 'reservations' => ['reservation', 'supply_reserve', 'supply_reserve'], 'deliveries' => ['delivery', 'supply_delivery', 'supply_delivery'], default => abort(404), }; $read = app(SupplyCoordinationRead::class); $read->kind($actor, $view); $ready = Schema::hasTable('supply_records'); $focus = $this->focusedRecord($request, $actor, $kind); $request->validate(['records_page' => 'nullable|integer|min:1|max:1000000']); $filters = $read->filters($request, $kind); $query = $ready ? ($focus === null ? $read->query($kind, $filters) : $planning->latest($kind))->latest('id') : null; $page = $focus === null ? $query?->paginate(25, ['*'], 'records_page')->withQueryString() : null; $records = $focus !== null ? $query?->where('record_key', $focus->record_key)->get() ?? collect() : $page?->getCollection() ?? collect(); $canPrepare = $planning->allows($actor, $prepare); $lineFilters = in_array($kind, ['reservation', 'delivery'], true) ? $request->validate([ 'line_search' => 'nullable|string|max:100', 'line_form_key' => 'nullable|string|max:80', 'selected_order_line_id' => ['nullable', 'integer', 'min:1', Rule::exists('purchase_order_items', 'id')->where('tenant_id', $actor->tenant_id)], 'selected_request_line_id' => ['nullable', 'integer', 'min:1', Rule::exists('requisition_items', 'id')->where('tenant_id', $actor->tenant_id)], ]) : []; $lineSearch = trim($lineFilters['line_search'] ?? ''); $reversedIds = $ready && $kind === 'reservation' ? array_keys(app(InventoryReversals::class)->corrections('operation')) : []; $orders = collect(); if ($canPrepare && $kind === 'delivery') { $recentOrders = PurchaseOrder::query()->where('status', 'issued')->when($lineSearch !== '', fn ($q) => $q->whereLike('number', '%'.$lineSearch.'%'))->latest('id')->limit(100)->pluck('id'); $selectedOrders = $records->pluck('purchase_order_id')->filter(); if (isset($lineFilters['selected_order_line_id'])) { $selectedOrders->push(PurchaseOrderItem::query()->whereKey($lineFilters['selected_order_line_id'])->value('purchase_order_id')); } $orders = PurchaseOrder::query()->where('status', 'issued')->where(fn ($q) => $q->whereIn('id', $recentOrders)->orWhereIn('id', $selectedOrders))->latest('id')->get()->keyBy('id'); } $orderLines = PurchaseOrderItem::query()->whereIn('purchase_order_id', $orders->keys())->get()->map(fn ($line) => [ 'id' => $line->id, 'label' => $orders[$line->purchase_order_id]->number.' · '.$line->description.' · '.$line->quantity.' '.$line->unit, 'stock_item_id' => $orders[$line->purchase_order_id]->snapshot['catalogue_sources'][$line->requisition_item_id]['stock_item_id'] ?? null, 'quantity_milli' => ReceiptWorkflow::milli($line->quantity), ]); $requests = collect(); if ($canPrepare && $kind === 'reservation') { $recentRequests = Requisition::query()->where('status', 'approved')->when($lineSearch !== '', fn ($q) => $q->whereLike('number', '%'.$lineSearch.'%'))->latest('id')->limit(100)->pluck('id'); $selectedRequests = RequisitionItem::query()->whereIn('id', $records->pluck('data.requisition_item_id')->push($lineFilters['selected_request_line_id'] ?? null)->filter())->pluck('requisition_id'); $requests = Requisition::query()->where('status', 'approved')->where(fn ($q) => $q->whereIn('id', $recentRequests)->orWhereIn('id', $selectedRequests))->latest('id')->get()->keyBy('id'); } $requestLines = RequisitionItem::query()->whereIn('requisition_id', $requests->keys())->get()->filter(fn ($line) => isset($line->catalogue_snapshot['stock_item_id']))->map(fn ($line) => [ 'id' => $line->id, 'label' => $requests[$line->requisition_id]->number.' · '.$line->description, 'stock_item_id' => $line->catalogue_snapshot['stock_item_id'], 'quantity_milli' => ReceiptWorkflow::milli($line->quantity), 'reference' => $requests[$line->requisition_id]->number.' / '.$line->id, 'needed_on' => $requests[$line->requisition_id]->needed_by->toDateString(), ])->values(); $coordination = app(SupplyCoordination::class); $issueFilters = $kind === 'reservation' ? $request->validate([ 'issue_search' => 'nullable|string|max:100', 'issue_record_id' => ['nullable', 'required_with:issue_search,selected_issue_id', 'integer', 'min:1', Rule::exists('supply_records', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', 'reservation')], 'selected_issue_id' => ['nullable', 'integer', 'min:1', Rule::exists('stock_operations', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', 'issue')], ]) : []; $operations = collect(); if ($ready && $canPrepare && $kind === 'reservation') { $issueRecord = isset($issueFilters['issue_record_id']) ? SupplyRecord::query()->findOrFail((int) $issueFilters['issue_record_id']) : null; if ($issueRecord !== null) { $issueRecord = $planning->latest('reservation')->where('record_key', $issueRecord->record_key)->firstOrFail(); abort_unless($coordination->canManage($actor, $issueRecord, 'fulfil'), 403); } $query = StockOperation::query()->where('kind', 'issue')->whereNotIn('id', $reversedIds) ->when($issueRecord !== null, fn ($q) => $q->where('stock_item_id', $issueRecord->stock_item_id)->where('warehouse_id', $issueRecord->warehouse_id)); $search = trim($issueFilters['issue_search'] ?? ''); $recent = (clone $query)->when($search !== '', fn ($q) => $q->whereLike('reference', '%'.$search.'%'))->latest('id')->limit(100)->pluck('id'); $operations = $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhere('id', $issueFilters['selected_issue_id'] ?? 0)) ->latest('id')->get(['id', 'reference', 'stock_item_id', 'warehouse_id', 'quantity_milli']); } return Inertia::render('supply/coordination', ['kind' => $kind, 'ready' => $ready, 'recordFilters' => ['record_search' => trim($filters['record_search'] ?? ''), 'record_state' => $filters['record_state'] ?? ''], 'recordPagination' => $page === null ? null : ['current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'total' => $page->total(), 'prev_page_url' => $page->previousPageUrl(), 'next_page_url' => $page->nextPageUrl()], 'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id, 'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $coordination->canReviewAlternative($actor, $record), 'canReturn' => $coordination->canReviewAlternative($actor, $record, 'return'), 'canSuspend' => $coordination->canReviewAlternative($actor, $record, 'suspend'), 'canEdit' => $coordination->canManage($actor, $record, 'edit'), 'canFulfil' => $coordination->canManage($actor, $record, 'fulfil'), 'canCancel' => $coordination->canManage($actor, $record, 'cancel')]), 'eligible' => $kind === 'alternative' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->eligibleAlternative($r)]) : [], 'pending' => $kind === 'delivery' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->remainingDelivery($r)]) : ($kind === 'reservation' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->remainingReservation($r)]) : []), 'reversedOperations' => $reversedIds, 'canPrepare' => $canPrepare, 'canReview' => $planning->allows($actor, $review), 'items' => $canPrepare ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $canPrepare ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'orderLines' => $orderLines, 'requestLines' => $requestLines, 'lineSearch' => $lineSearch, 'lineFormKey' => $lineFilters['line_form_key'] ?? null, 'operations' => $operations, 'issueSearch' => $issueFilters['issue_search'] ?? '', 'issueRecordId' => isset($issueFilters['issue_record_id']) ? (int) $issueFilters['issue_record_id'] : null, 'history' => $ready ? SupplyRecord::query()->where('kind', $kind)->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } public function save(Request $request, string $kind, SupplyPlanning $planning): RedirectResponse { $actor = $request->user('web'); abort_unless($actor instanceof User, 403); $planning->save($actor, $kind, $request->all()); return back(); } }