commit15
This commit is contained in:
@@ -101,24 +101,112 @@ final class ModuleAssociationReview
|
||||
default => null,
|
||||
};
|
||||
if (($d['search'] ?? '') !== '') $query->where('name', 'ilike', '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $d['search']).'%');
|
||||
$page = $query->orderBy('id')->paginate($d['per_page'] ?? 25, ['id', 'name', 'version'], 'page', $d['page'] ?? 1);
|
||||
$rows = [];
|
||||
foreach ($page->items() as $row) {
|
||||
$links = $this->links($area, $row->id);
|
||||
$deps = []; $restricted = false;
|
||||
try {
|
||||
$graph = []; $this->graph($area, $row->id, $graph);
|
||||
unset($graph["$area:$row->id"]);
|
||||
$deps = array_values(array_map(fn ($g) => array_diff_key($g, ['checksum' => true]), $graph));
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $e) { $restricted = true;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { if ($e->getStatusCode() !== 403) throw $e; $restricted = true; }
|
||||
$rows[] = [...(array) $row, ...$links, 'dependencies' => $deps, 'dependencies_restricted' => $restricted];
|
||||
}
|
||||
$page = $query->orderBy('id')->paginate($d['per_page'] ?? 25, $this->pageColumns($area, true), 'page', $d['page'] ?? 1);
|
||||
$rows = $this->describePage($area, $page->items(), $scope);
|
||||
return ['records' => [$area => $rows], 'modules' => app(ModuleNavigation::class)->available($this->context),
|
||||
'pagination' => ['page' => $page->currentPage(), 'per_page' => $page->perPage(), 'total' => $page->total(), 'last_page' => $page->lastPage()],
|
||||
'notice' => 'Cada decisão exige conferência individual. Dependências não são associadas automaticamente.'];
|
||||
}
|
||||
|
||||
/** Request-local data only: the selected page and its explicit dependency closure.
|
||||
* Never reused by preview/confirm, which still read and lock current state.
|
||||
*/
|
||||
private function describePage(string $area, array $roots, array $scope): array
|
||||
{
|
||||
$records = []; $edges = []; $scopes = [$area => $scope]; $pending = [];
|
||||
foreach ($roots as $row) { $records[$area][$row->id] = $row; $pending["$area:$row->id"] = [$area, (int) $row->id]; }
|
||||
while ($pending) {
|
||||
$needed = [];
|
||||
foreach ($pending as $key => [$kind, $id]) {
|
||||
$row = $records[$kind][$id]; $refs = [];
|
||||
if (isset($row->client_id)) $refs[] = ['clients', (int) $row->client_id];
|
||||
if (isset($row->source_id)) $refs[] = ['service_drafts', (int) $row->source_id];
|
||||
if (str_ends_with($kind, '_drafts')) {
|
||||
foreach (json_decode($row->association_service_ids, true, 512, JSON_THROW_ON_ERROR) as $serviceId) $refs[] = ['services', (int) $serviceId];
|
||||
}
|
||||
$edges[$key] = $refs;
|
||||
foreach ($refs as [$depArea, $depId]) if (!array_key_exists($depId, $records[$depArea] ?? [])) $needed[$depArea][$depId] = $depId;
|
||||
}
|
||||
$pending = [];
|
||||
foreach ($needed as $depArea => $ids) {
|
||||
if (!array_key_exists($depArea, $scopes)) {
|
||||
try { $scopes[$depArea] = $this->scope($depArea); }
|
||||
catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { if ($e->getStatusCode() !== 403) throw $e; $scopes[$depArea] = null; }
|
||||
}
|
||||
foreach ($ids as $id) $records[$depArea][$id] = null;
|
||||
if ($scopes[$depArea] === null) continue;
|
||||
foreach (array_chunk(array_values($ids), 500) as $chunk) {
|
||||
foreach (DB::table('a_'.$depArea)->where($scopes[$depArea])->whereIn('id', $chunk)->get($this->pageColumns($depArea)) as $row) {
|
||||
$records[$depArea][$row->id] = $row;
|
||||
$pending["$depArea:$row->id"] = [$depArea, (int) $row->id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$states = $this->pageStates($records);
|
||||
$out = [];
|
||||
foreach ($roots as $row) {
|
||||
$deps = []; $restricted = false;
|
||||
try { $seen = []; $this->walkPage($area, $row->id, $records, $edges, $states, $seen); unset($seen["$area:$row->id"]); $deps = array_values($seen); }
|
||||
catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { if ($e->getStatusCode() !== 403) throw $e; $restricted = true; }
|
||||
$out[] = ['id' => $row->id, 'name' => $row->name, 'version' => $row->version, ...$states[$area][$row->id], 'dependencies' => $deps, 'dependencies_restricted' => $restricted];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Fetch metadata and ordered references, not document bodies. This projection
|
||||
* is only for listing; the decision snapshot still hashes the complete rows.
|
||||
*/
|
||||
private function pageColumns(string $area, bool $root = false): array
|
||||
{
|
||||
$columns = $root ? ['id', 'name', 'version'] : ['id', 'version'];
|
||||
if (str_ends_with($area, '_drafts')) {
|
||||
$columns[] = 'client_id';
|
||||
if ($area === 'billing_drafts') $columns[] = 'source_id';
|
||||
$columns[] = DB::raw("(SELECT coalesce(jsonb_agg(ref.line->>'service_id' ORDER BY ref.ord)
|
||||
FILTER (WHERE ref.line->>'service_id' IS NOT NULL),'[]'::jsonb)
|
||||
FROM jsonb_array_elements(data->'lines') WITH ORDINALITY AS ref(line,ord)) AS association_service_ids");
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
private function walkPage(string $area, int $id, array $records, array $edges, array $states, array &$seen): void
|
||||
{
|
||||
$key = "$area:$id";
|
||||
if (isset($seen[$key])) return;
|
||||
$row = $records[$area][$id] ?? null;
|
||||
abort_unless($row, 403, 'Dependência fora do âmbito autorizado.');
|
||||
$seen[$key] = ['area' => $area, 'id' => $id, 'version' => (int) $row->version, ...$states[$area][$id]];
|
||||
foreach ($edges[$key] ?? [] as [$depArea, $depId]) $this->walkPage($depArea, $depId, $records, $edges, $states, $seen);
|
||||
}
|
||||
|
||||
/** SQL reduces history to one state per selected record; no event universe in PHP.
|
||||
* Review-only events advance revision but never create an active association.
|
||||
*/
|
||||
private function pageStates(array $records): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($records as $area => $items) {
|
||||
$ids = [];
|
||||
foreach ($items as $id => $row) if ($row !== null) { $ids[] = (string) $id; $out[$area][$id] = ['modules' => [], 'revision' => 0]; }
|
||||
foreach (array_chunk($ids, 500) as $chunk) {
|
||||
$q = DB::table('identity_audit')->where($this->context->identifiers())
|
||||
->whereIn('action', ['module.record.associated', 'module.record.disassociated', 'module.record.reviewed'])
|
||||
->whereRaw("details::jsonb->>'area' = ?", [$area])->whereIn(DB::raw("details::jsonb->>'record_id'"), $chunk)
|
||||
->selectRaw("id,action,details::jsonb->>'record_id' AS record_id,details::jsonb->>'module_id' AS module_id");
|
||||
$sql = "WITH scoped AS MATERIALIZED (".$q->toSql()."),
|
||||
latest AS (SELECT DISTINCT ON (record_id,module_id) record_id,module_id,action FROM scoped
|
||||
WHERE action <> 'module.record.reviewed' ORDER BY record_id,module_id,id DESC),
|
||||
revisions AS (SELECT record_id,max(id) AS revision FROM scoped GROUP BY record_id)
|
||||
SELECT r.record_id,r.revision,coalesce(jsonb_agg(l.module_id ORDER BY l.module_id)
|
||||
FILTER (WHERE l.action='module.record.associated'),'[]'::jsonb) AS modules
|
||||
FROM revisions r LEFT JOIN latest l ON l.record_id=r.record_id GROUP BY r.record_id,r.revision";
|
||||
foreach (DB::select($sql, $q->getBindings()) as $row) $out[$area][(int) $row->record_id] = ['modules' => json_decode($row->modules, true, 512, JSON_THROW_ON_ERROR), 'revision' => (int) $row->revision];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function preview(Request $r): array
|
||||
{
|
||||
$d = $this->input($r);
|
||||
|
||||
Reference in New Issue
Block a user