'resource'->>'legislation' = 'true' OR e.payload->'resource'->>'document_type' = 'legislation' THEN 'legislation' WHEN e.payload->'resource'->>'editorial_content' IS NOT NULL THEN 'article' ELSE 'document' END"; /** @param array $data */ private function screen(Request $request, string $component, array $data): Response { $response = $request->expectsJson() ? response()->json($data) : Inertia::render($component, $data)->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store'); return $response; } public function index(Request $request, PortalEditorialWorkflow $workflow): Response { abort_unless($workflow->enabled(), 503); $actor = $request->user(); $access = app(DocumentManagementAccess::class); $query = $workflow->queue($actor); $filters = array_filter($request->validate(['state' => 'nullable|in:draft,submitted,in_review,approved,published,returned,rejected,archived', 'kind' => 'nullable|in:article,legislation,document']), fn ($value) => $value !== null && $value !== ''); if (isset($filters['state'])) { $query->where('e.state', $filters['state']); } if (isset($filters['kind'])) { $query->whereRaw(self::KIND.' = ?', [$filters['kind']]); } $items = $query->orderByDesc('e.updated_at')->paginate(25, ['r.id', DB::raw("e.payload->'resource'->>'title' AS title"), 'r.document_type', 'r.published', 'e.id as edition_id', 'e.revision_no', 'e.state', 'e.legal_required', 'u.name as responsible'])->withQueryString(); $destinations = app(ContentManagement::class)->access($actor); $data = ['items' => $items, 'filters' => $filters, 'notices' => $workflow->notices($actor), 'resourceAccess' => $access->full($actor) || $access->editorial($actor), 'opportunitiesUrl' => $destinations['opportunities']['index'] ?? null, 'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null, 'portfolioUrl' => $destinations['portfolio']['index'] ?? null, 'portfolioPending' => $workflow->portfolioPending($actor), 'demographyUrl' => $destinations['demography']['index'] ?? null, 'demographyPending' => $workflow->demographyPending($actor), 'territorialUrl' => app(TerritorialRegistry::class)->editorialAvailable() && $actor->can('manage-portal') ? '/management/territorial/editorial' : null]; return $this->screen($request, 'management/editorial-workflow', $data); } public function show(Request $request, int $id, PortalEditorialWorkflow $workflow): Response { $workflow->authorize($request->user(), $id); $resource = DB::table('portal_resources')->where('id', $id)->first(); abort_unless($resource !== null, 404); $selected = $request->validate(['edition' => 'nullable|uuid'])['edition'] ?? null; $latest = $workflow->latest($id); $e = $selected ? DB::table('portal_editorial_editions')->where('resource_id', $id)->where('id', $selected)->first() : $latest; abort_if($selected && ! $e, 404); $payload = $e ? $workflow->payload($e) : null; $independent = ! $e || ! $workflow->requiresIndependent($e) || ! in_array($request->user()->id, $payload['contributors'], true); $data = ['resource' => ['id' => $id, 'title' => $resource->title, 'published' => (bool) $resource->published, 'public_edition_no' => $resource->published ? DB::table('portal_editorial_editions')->where('resource_id', $id)->where('state', 'published')->max('revision_no') : null], 'edition' => $e ? [...(array) $e, 'payload' => $payload, 'fresh' => $e->state === 'published' || $workflow->fresh($e)] : null, 'current' => $e && $e->id === $latest?->id, 'independentRequired' => $e ? $workflow->requiresIndependent($e) : false, 'canApprove' => $independent && app(DocumentManagementAccess::class)->full($request->user()), 'manager' => app(DocumentManagementAccess::class)->full($request->user()), 'reviewers' => User::whereIn('email', config('portal.managers', []))->whereNotNull('email_verified_at')->get(['id', 'name']), 'legalReviewer' => in_array($request->user()->id, config('editorial_workflow.legal_reviewers', []), true), 'history' => DB::table('audit_events')->where('source_system', 'document-access-v1')->where('action', 'like', 'editorial_%')->where('after_data->resource_id', $id)->orderByDesc('id')->get(['id', 'action', 'actor_user_id', 'after_data', 'occurred_at']), 'editions' => DB::table('portal_editorial_editions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['id', 'revision_no', 'state', 'content_hash', 'author_id', 'created_at'])]; return $this->screen($request, 'management/editorial-edition', $data); } public function transition(Request $request, int $id, string $edition, PortalEditorialWorkflow $workflow): Response { $result = $workflow->transition($request->user(), $id, $edition, $request->all()); return $request->expectsJson() ? response()->json(['edition' => $result])->header('Cache-Control', 'private, no-store') : back()->with('success', 'Operação editorial registada.'); } public function portfolio(Request $request, string $id, PortalEditorialWorkflow $workflow): Response { $workflow->authorizePortfolio($request->user(), $id); $record = DB::table('investment_dossiers')->where('id', $id)->first(); abort_unless($record !== null, 404); $selected = $request->validate(['edition' => 'nullable|uuid'])['edition'] ?? null; $latest = $workflow->latestPortfolio($id); $edition = $selected ? DB::table('portal_editorial_editions')->where('dossier_id', $id)->where('id', $selected)->first() : $latest; abort_if($selected && ! $edition, 404); $payload = $edition ? $workflow->payload($edition) : null; $independent = ! $edition || ! in_array($request->user()->id, $payload['contributors'], true); return $this->screen($request, 'management/editorial-edition', ['subject' => 'portfolio', 'resource' => ['id' => $id, 'title' => $record->title, 'published' => $record->publication_status === 'published', 'public_edition_no' => $record->publication_status === 'published' ? DB::table('portal_editorial_editions')->where('dossier_id', $id)->where('state', 'published')->max('revision_no') : null], 'edition' => $edition ? [...get_object_vars($edition), 'payload' => $payload, 'fresh' => $edition->state === 'published' || $workflow->fresh($edition)] : null, 'current' => $edition && $edition->id === $latest?->id, 'independentRequired' => true, 'canApprove' => $independent, 'manager' => true, 'legalReviewer' => false, 'reviewers' => User::whereIn('email', config('portal.managers', []))->whereNotNull('email_verified_at')->get(['id', 'name']), 'history' => DB::table('audit_events')->where('source_system', 'document-access-v1')->where('action', 'like', 'editorial_%')->where('after_data->dossier_id', $id)->orderByDesc('id')->get(['id', 'action', 'actor_user_id', 'after_data', 'occurred_at']), 'editions' => DB::table('portal_editorial_editions')->where('dossier_id', $id)->orderByDesc('revision_no')->get(['id', 'revision_no', 'state', 'content_hash', 'author_id', 'created_at'])]); } public function portfolioTransition(Request $request, string $id, string $edition, PortalEditorialWorkflow $workflow): Response { $result = $workflow->transition($request->user(), $id, $edition, $request->all(), true); return $request->expectsJson() ? response()->json(['edition' => $result])->header('Cache-Control', 'private, no-store') : back()->with('success', 'Operação editorial da carteira registada.'); } public function demography(Request $request, string $id, PortalEditorialWorkflow $workflow): Response { $workflow->authorizeDemography($request->user(), $id); $batch = DB::table('demographic_imports')->where('id', $id)->first(); abort_unless($batch !== null, 404); $selected = $request->validate(['edition' => 'nullable|uuid'])['edition'] ?? null; $latest = $workflow->latestDemography($id); $edition = $selected ? DB::table('portal_editorial_editions')->where('demographic_import_id', $id)->where('id', $selected)->first() : $latest; abort_if($selected && ! $edition, 404); $payload = $edition ? $workflow->payload($edition) : null; $independent = ! $edition || ! $workflow->requiresIndependent($edition) || ! in_array($request->user()->id, $payload['contributors'], true); return $this->screen($request, 'management/editorial-edition', ['subject' => 'demography', 'resource' => ['id' => $id, 'title' => $batch->file_name, 'published' => $batch->status === 'approved', 'public_edition_no' => $batch->status === 'approved' ? DB::table('portal_editorial_editions')->where('demographic_import_id', $id)->where('state', 'published')->max('revision_no') : null], 'edition' => $edition ? [...get_object_vars($edition), 'payload' => $payload, 'fresh' => $edition->state === 'published' || $workflow->fresh($edition)] : null, 'current' => $edition && $edition->id === $latest?->id, 'independentRequired' => $edition ? $workflow->requiresIndependent($edition) : false, 'canApprove' => $independent, 'manager' => true, 'legalReviewer' => false, 'reviewers' => User::whereIn('email', config('portal.managers', []))->whereNotNull('email_verified_at')->get(['id', 'name']), 'history' => DB::table('audit_events')->where('source_system', 'document-access-v1')->where('action', 'like', 'editorial_%')->where('after_data->demographic_import_id', $id)->orderByDesc('id')->get(['id', 'action', 'actor_user_id', 'after_data', 'occurred_at']), 'editions' => DB::table('portal_editorial_editions')->where('demographic_import_id', $id)->orderByDesc('revision_no')->get(['id', 'revision_no', 'state', 'content_hash', 'author_id', 'created_at'])]); } public function demographyDraft(Request $request, string $id, PortalEditorialWorkflow $workflow): Response { $data = $request->validate(['expected_revision' => 'required|integer|min:0']); $edition = $workflow->saveDemography($request->user(), $id, (int) $data['expected_revision']); return $request->expectsJson() ? response()->json(['edition' => $edition], 201)->header('Cache-Control', 'private, no-store') : redirect('/management/editorial/demography/'.$id); } public function demographyTransition(Request $request, string $id, string $edition, PortalEditorialWorkflow $workflow): Response { $result = $workflow->transition($request->user(), $id, $edition, $request->all(), false, true); return $request->expectsJson() ? response()->json(['edition' => $result])->header('Cache-Control', 'private, no-store') : back()->with('success', 'Operação editorial do lote registada.'); } public function demographyOriginal(Request $request, string $id, PortalEditorialWorkflow $workflow): Response { $workflow->authorizeDemography($request->user(), $id); $batch = DB::table('demographic_imports')->where('id', $id)->first(); abort_unless($batch !== null, 404); $path = Storage::disk('local')->path('demography/'.$id.'/original.xlsx'); abort_unless(CheckedFile::matches($path, trim($batch->sha256)), 409, 'Original indisponível ou alterado.'); return response()->download($path, basename($batch->file_name), ['Cache-Control' => 'private, no-store', 'X-Robots-Tag' => 'noindex, nofollow']); } public function file(Request $request, int $id, string $edition, string $document, PortalEditorialWorkflow $workflow): Response { $workflow->authorize($request->user(), $id); $e = DB::table('portal_editorial_editions')->where('id', $edition)->where('resource_id', $id)->first(); abort_unless($e !== null, 404); $file = null; foreach ($workflow->payload($e)['files'] as $entry) { if ($entry['id'] === $document) { $file = $entry; break; } } abort_unless($file !== null, 404); abort_unless($workflow->canReadFile($request->user(), $document), 403); $record = DB::table('documents')->where('id', $document)->first(); abort_unless($record && hash_equals(trim($file['sha256']), trim($record->sha256)) && (int) $file['version_no'] === (int) $record->version_no, 409, 'O anexo mudou; esta edição conserva a referência anterior.'); return $this->binary($record); } private function binary(\stdClass $record): Response { $response = app(VerifiedDocumentBinary::class)->respond($record); $response->headers->set('Cache-Control', 'private, no-store'); $response->headers->set('X-Robots-Tag', 'noindex, nofollow'); return $response; } public function markRead(Request $request, int $notice, PortalEditorialWorkflow $workflow): Response { abort_unless($workflow->enabled(), 503); $n = DB::table('portal_editorial_notifications')->where('id', $notice)->where('user_id', $request->user()->id)->first(); abort_unless($n !== null, 404); abort_unless($workflow->visibleNotice($request->user(), $n), 404); DB::table('portal_editorial_notifications')->where('id', $notice)->where('user_id', $request->user()->id)->whereNull('read_at')->update(['read_at' => now()]); return $request->expectsJson() ? response()->json(['read' => true]) : back(); } }