Files
compras/app/Http/Controllers/PeopleHiringController.php
T
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

195 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\AuditLog;
use App\Models\PeopleProcess;
use App\Models\Tenant;
use App\Models\User;
use App\Services\AiPreparationPlan;
use App\Services\CandidateTriage;
use App\Services\IntelligencePolicy;
use App\Services\PeopleAccess;
use App\Services\PeopleClarifications;
use App\Services\PeopleEvidenceBrief;
use App\Services\PeopleForms;
use App\Services\PeopleHiring;
use App\Services\PeopleOperationalInsights;
use App\Services\PeopleProcessProjection;
use App\Services\PeopleRegistry;
use App\Services\PeopleWorkdesk;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class PeopleHiringController extends Controller
{
public function index(Request $request, PeopleHiring $workflow): Response
{
return DB::transaction(function () use ($request, $workflow) {
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
}
$kinds = $workflow->kinds($this->actor($request));
abort_unless($kinds !== [], 403);
$availableKinds = $kinds;
$filter = $request->validate(['kind' => 'nullable|in:service,staff', 'search' => 'nullable|string|max:100', 'status' => ['nullable', Rule::in(PeopleWorkdesk::STATES)],
'from' => 'nullable|date_format:Y-m-d', 'to' => ['nullable', 'date_format:Y-m-d', Rule::when($request->filled('from'), 'after_or_equal:from')]]);
if (isset($filter['kind'])) {
abort_unless(in_array($filter['kind'], $kinds, true), 403);
$kinds = [$filter['kind']];
}
$ready = Schema::hasTable('people_processes') && Schema::hasTable('people_process_access');
$query = app(PeopleAccess::class)->visible(PeopleProcess::query()->whereIn('kind', $kinds), $this->actor($request));
if (($filter['search'] ?? '') !== '') {
$search = $filter['search'];
$query->where(function ($q) use ($search) {
$q->whereLike('title', '%'.$search.'%');
if (preg_match('/^(?:PES-)?([1-9][0-9]*)$/D', $search, $matches)) {
$reference = filter_var($matches[1], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($reference !== false) {
$q->orWhere('id', $reference);
}
}
});
}
if (! empty($filter['status'])) {
$query->where('status', $filter['status']);
}
if (! empty($filter['from'])) {
$query->whereDate('created_at', '>=', $filter['from']);
}
if (! empty($filter['to'])) {
$query->whereDate('created_at', '<=', $filter['to']);
}
$workdesk = $ready ? app(PeopleWorkdesk::class)->build(clone $query, $this->actor($request)) : null;
Inertia::encryptHistory();
$response = Inertia::render('people/index', ['ready' => $ready, 'kinds' => $kinds, 'availableKinds' => $availableKinds,
'filters' => $filter, 'workdesk' => $workdesk, 'insights' => $workdesk === null ? null : app(PeopleOperationalInsights::class)->build($workdesk),
'company' => Tenant::query()->findOrFail($this->actor($request)->tenant_id)->only(['name']),
'canPrepare' => array_values(array_filter($availableKinds, fn ($kind) => $workflow->allowed($this->actor($request), $kind, 'prepare'))),
'formFields' => ['staff' => PeopleForms::STAFF, 'service' => PeopleForms::SERVICE],
'administration' => $ready && $this->actor($request)->hasFunctions(['admin']) ? PeopleProcess::query()->whereIn('kind', $kinds)->orderByDesc('id')->get(['id', 'kind', 'status']) : [],
'processes' => $ready ? (clone $query)->select(['id', 'title', 'kind', 'status', 'closes_on', 'created_at'])->latest('id')->paginate(15)->withQueryString() : null])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
});
}
public function show(Request $request, int $process, PeopleHiring $workflow): Response
{
return DB::transaction(function () use ($request, $process, $workflow) {
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
}
$record = PeopleProcess::query()->findOrFail($process);
$access = app(PeopleAccess::class);
abort_unless($access->can($this->actor($request), $record, 'view'), 403);
$registrySearch = $request->validate(['cadastro' => 'nullable|string|max:100'])['cadastro'] ?? '';
$projection = app(PeopleProcessProjection::class)->read($this->actor($request), $record);
$snapshot = $projection['process'];
$rows = $projection['candidates'];
$clarifications = app(PeopleClarifications::class)->page($this->actor($request), $record);
$company = Tenant::query()->findOrFail($this->actor($request)->tenant_id);
$policy = app(IntelligencePolicy::class)->company($company);
$brief = app(PeopleEvidenceBrief::class)->build($snapshot, $rows, $clarifications, $access->can($this->actor($request), $record, 'financial'),
$access->can($this->actor($request), $record, 'contract') || $access->can($this->actor($request), $record, 'contract_view'));
Inertia::encryptHistory();
$response = Inertia::render('people/show', ['process' => $snapshot, 'candidates' => $rows, 'formFields' => $record->kind === 'staff' ? PeopleForms::STAFF : PeopleForms::SERVICE,
'clarifications' => $clarifications, 'brief' => $policy['people_brief'] ? $brief : null,
'registry' => app(PeopleRegistry::class)->lookup($this->actor($request), $record, $registrySearch),
'aiPreparation' => app(AiPreparationPlan::class)->preview($company, 'people', $brief),
'canPrepare' => $workflow->allowed($this->actor($request), $record->kind, 'prepare') && $access->can($this->actor($request), $record, 'prepare'),
'canViewElectronicSources' => app(CandidateTriage::class)->canRead($this->actor($request), $record),
'canReview' => $workflow->allowed($this->actor($request), $record->kind, 'review') && $access->can($this->actor($request), $record, 'review') && $this->actor($request)->id !== $record->created_by,
'canApprove' => $workflow->allowed($this->actor($request), $record->kind, 'review') && $access->can($this->actor($request), $record, 'approve') && ! in_array($this->actor($request)->id, $workflow->decisionContributors($record), true),
'canContract' => $workflow->allowed($this->actor($request), $record->kind, 'prepare') && $access->can($this->actor($request), $record, 'contract'),
'canManage' => $access->manages($this->actor($request), $record),
'history' => AuditLog::query()->where('action', 'like', 'people.%')->where('after->id', $record->id)->orderBy('id')->get(['id', 'action', 'user_id', 'created_at'])])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
});
}
public function dossier(Request $request, int $process, PeopleProcessProjection $projection): Response
{
return DB::transaction(function () use ($request, $process, $projection) {
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
}
$record = PeopleProcess::query()->findOrFail($process);
$data = $projection->read($this->actor($request), $record);
return response()->json(['schema' => 'sgc.people.process-dossier.v1', 'exported_at' => now()->toIso8601String(),
'effect' => 'preparation_only',
'process' => Arr::only($data['process'], ['id', 'kind', 'title', 'purpose', 'closes_on', 'status', 'version', 'selected_application_id', 'rules', 'selection', 'contract']),
'candidates' => $data['candidates']])
->header('Content-Disposition', 'attachment; filename="PES-'.$record->id.'-dossier-v'.$record->version.'.json"')
->header('Cache-Control', 'private, no-store, max-age=0')->header('X-Content-Type-Options', 'nosniff');
});
}
public function access(Request $request, int $process, PeopleAccess $access): Response
{
$record = PeopleProcess::query()->findOrFail($process);
abort_unless($access->manages($this->actor($request), $record), 403);
Inertia::encryptHistory();
$response = Inertia::render('people/access', ['process' => $record->only(['id', 'kind', 'status']),
'version' => DB::table('people_access_states')->where('people_process_id', $record->id)->value('version'), 'scopes' => PeopleAccess::SCOPES,
'users' => User::query()->where('tenant_id', $record->tenant_id)->orderBy('name')->get(['id', 'name', 'tenant_id', 'purchasing_role'])
->filter(fn ($user) => app(PeopleHiring::class)->allowed($user, $record->kind) || DB::table('people_process_access')->where('people_process_id', $record->id)->where('user_id', $user->id)->exists())
->map(fn ($user) => $user->only(['id', 'name']))->values(),
'assignments' => DB::table('people_process_access')->where('tenant_id', $record->tenant_id)->where('people_process_id', $record->id)->get()->map(fn ($row) => ['user_id' => $row->user_id, 'scopes' => json_decode($row->scopes, true, 16, JSON_THROW_ON_ERROR), 'expires_at' => $row->expires_at]),
'canView' => $access->can($this->actor($request), $record, 'view')])->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
return $response;
}
public function assign(Request $request, int $process, PeopleAccess $access): Response
{
$access->assign($this->actor($request), $process, $request->all());
return redirect('/pessoas/'.$process.'/acessos');
}
public function command(Request $request, int $process, string $command, PeopleHiring $workflow): Response
{
abort_unless(Schema::hasTable('people_processes'), 503, 'Instalação de Pessoas e contratação pendente.');
$record = $workflow->execute($this->actor($request), $process, $command, $request->all());
return redirect('/pessoas/'.$record->id);
}
public function store(Request $request, PeopleHiring $workflow): Response
{
abort_unless(Schema::hasTable('people_processes'), 503);
$record = $workflow->execute($this->actor($request), null, 'create', $request->all());
return redirect('/pessoas/'.$record->id);
}
public function clarification(Request $request, int $process, string $command, PeopleClarifications $workflow): Response
{
abort_unless(Schema::hasTable('people_clarifications'), 503, 'Instalação dos esclarecimentos pendente.');
$data = $request->validate(['clarification_id' => 'nullable|integer|min:1']);
$workflow->execute($this->actor($request), $process, $data['clarification_id'] ?? null, $command, $request->all());
return redirect('/pessoas/'.$process.'#people-clarifications');
}
private function actor(Request $request): User
{
$actor = $request->user('web');
abort_unless($actor instanceof User, 403);
return $actor;
}
}