feat(editorial): review demographic batches before explicit publication

This commit is contained in:
2026-11-14 08:35:11 +02:00
parent 2459d0ca37
commit 3c4c2599f7
17 changed files with 825 additions and 62 deletions
@@ -7,8 +7,10 @@ use App\Services\ContentManagement;
use App\Services\DocumentManagementAccess;
use App\Services\PortalEditorialWorkflow;
use App\Services\VerifiedDocumentBinary;
use App\Support\CheckedFile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
@@ -41,7 +43,7 @@ final class PortalEditorialWorkflowController extends Controller
$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)];
'portfolioUrl' => $destinations['portfolio']['index'] ?? null, 'portfolioPending' => $workflow->portfolioPending($actor), 'demographyUrl' => $destinations['demography']['index'] ?? null, 'demographyPending' => $workflow->demographyPending($actor)];
return $this->screen($request, 'management/editorial-workflow', $data);
}
@@ -105,6 +107,53 @@ final class PortalEditorialWorkflowController extends Controller
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);