feat(projects): RegionsBulkDialog — subject-level regions (89 RF subjects) #1426
Bulk regions dialog reworked from federal-district bitmask to subject/region selection, consistent with ProjectDetailsDrawer/NewProjectDialog. Full-stack: add_regions/remove_regions on projects.regions INT[], BulkProjectActionRequest split validation, ProjectService model-instance update. federal-districts.ts removed (zero consumers). +menuRepositionFix util for v-autocomplete menu. phpstan-baseline: bump actingAs ignore count 14->15 (new validation test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,21 +115,40 @@ class ProjectService
|
||||
}
|
||||
|
||||
/**
|
||||
* LEGACY (Plan 6): обновляет только bitmask `region_mask` федеральных округов.
|
||||
* После Plan 6 источник истины региональной фильтрации — `regions` INT[];
|
||||
* outbound SyncSupplierProjectsJob читает `regions[]`, НЕ `region_mask`. Значит
|
||||
* этот bulk-action на реальную фильтрацию у поставщика не влияет. Субъект-уровневый
|
||||
* bulk-edit `regions[]` запланирован в Plan 6.5 (spec §13 — out of scope C9).
|
||||
* Plan 6.5: субъект-уровневый bulk-edit `regions` INT[].
|
||||
*
|
||||
* Для каждого проекта: regions := unique(regions ∪ add_regions) \ remove_regions,
|
||||
* отсортировано по возрастанию. `regions[]` — источник истины региональной
|
||||
* фильтрации с Plan 6 (outbound SyncSupplierProjectsJob читает именно его).
|
||||
* Legacy `region_mask` здесь не трогается — как и в одиночном PATCH
|
||||
* /api/projects/{id}; его удаление — Plan 6.5 cleanup.
|
||||
*
|
||||
* NB: проект с regions=[] («вся РФ») при add_regions сужается до выбранных
|
||||
* субъектов — это осознанное действие оператора bulk-диалога.
|
||||
*
|
||||
* Обновление идёт через model-инстанс (не query-builder mass update): каст
|
||||
* PostgresIntArray::set() сериализует PHP-массив в PG-литерал `{1,2,3}`, а
|
||||
* mass update каст не применяет. count ≤ BULK_MAX (500) — допустимо.
|
||||
*/
|
||||
private function bulkUpdateRegions($query, array $payload): array
|
||||
{
|
||||
$add = (int) ($payload['add'] ?? 0);
|
||||
$remove = (int) ($payload['remove'] ?? 0);
|
||||
$add = array_map('intval', $payload['add_regions'] ?? []);
|
||||
$remove = array_map('intval', $payload['remove_regions'] ?? []);
|
||||
|
||||
// region_mask = (region_mask | add) & ~remove, clamped to 8 bits (0–255)
|
||||
$updated = $query->update([
|
||||
'region_mask' => \DB::raw("(region_mask | {$add}) & ~{$remove} & 255"),
|
||||
]);
|
||||
if ($add === [] && $remove === []) {
|
||||
return ['updated' => 0, 'skipped' => [], 'warnings' => []];
|
||||
}
|
||||
|
||||
$projects = (clone $query)->get(['id', 'regions']);
|
||||
$updated = 0;
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$next = array_values(array_unique([...($project->regions ?? []), ...$add]));
|
||||
$next = array_values(array_diff($next, $remove));
|
||||
sort($next);
|
||||
$project->update(['regions' => $next]);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
return ['updated' => $updated, 'skipped' => [], 'warnings' => []];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user