Files
compras/app/Http/Controllers/CandidatePortalController.php
T
2026-10-30 20:52:10 +02:00

293 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\CandidateAccount;
use App\Models\PeopleProcess;
use App\Models\Tenant;
use App\Models\User;
use App\Services\CandidateAccess;
use App\Services\CandidateDocuments;
use App\Services\CandidateProfiles;
use App\Services\CandidateSubmissions;
use App\Services\CandidateTriage;
use App\Services\ConflictGuard;
use App\Services\CurrentActor;
use App\Services\OrganizationConfiguration;
use App\Services\RequisitionWorkflow;
use App\Services\SubmissionForms;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class CandidatePortalController extends Controller
{
public function settings(Request $request, CandidateProfiles $profiles, OrganizationConfiguration $configuration): Response
{
$actor = $configuration->administrator($this->staff($request));
$portal = $profiles->portal($actor->tenant_id);
return $this->page($request, 'company/candidate-portal', ['portal' => $portal === null ? ['version' => 0, 'registration_open' => false, 'notice' => ''] : (array) $portal,
'canManage' => app(SubmissionForms::class)->canManage($actor), 'portalUrl' => '/candidaturas/'.$actor->tenant_id, 'emailDeliveryAvailable' => app(CandidateAccess::class)->deliveryAvailable()]);
}
public function configure(Request $request, CandidateProfiles $profiles): Response
{
$profiles->configure($this->staff($request), $request->all());
return back();
}
public function entry(Request $request, int $company, CandidateProfiles $profiles): Response
{
$portal = $profiles->portal($company);
abort_unless($portal !== null, 404);
$tenant = Tenant::query()->findOrFail($company);
return $this->page($request, 'candidate/entry', ['company' => ['id' => $company, 'name' => $tenant->name],
'portal' => ['version' => $portal->version, 'registration_open' => (bool) $portal->registration_open, 'notice' => $portal->notice]]);
}
public function register(Request $request, int $company, CandidateProfiles $profiles): Response
{
$account = $profiles->register($company, $request->all());
Auth::guard('candidate')->login($account, false);
$request->session()->put('candidate.session_version', $account->session_version);
$request->session()->regenerate();
Inertia::clearHistory();
return redirect('/candidaturas/'.$company.'/perfil');
}
public function login(Request $request, int $company, CandidateProfiles $profiles): Response
{
abort_unless($profiles->portal($company) !== null, 404);
$data = $request->validate(['email' => 'required|string|max:254', 'password' => 'required|string|max:128']);
$account = CandidateAccount::query()->where('tenant_id', $company)->where('email', mb_strtolower(trim($data['email'])))->first();
$dummy = Hash::make('candidate-unavailable-account');
$valid = Hash::check($data['password'], $account->password ?? $dummy);
if ($account === null || ! $valid) {
throw ValidationException::withMessages(['email' => 'Não foi possível entrar com os dados indicados.']);
}
$profiles->event($account, 'account.login', []);
Auth::guard('candidate')->login($account, false);
$request->session()->put('candidate.session_version', $account->session_version);
$request->session()->regenerate();
Inertia::clearHistory();
return redirect('/candidaturas/'.$company.'/perfil');
}
public function show(Request $request, int $company, CandidateProfiles $profiles): Response
{
$account = $this->account($request, $company);
if ($account === null) {
return redirect('/candidaturas/'.$company);
}
$profiles->event($account, 'profile.consulted', []);
return $this->page($request, 'candidate/profile', ['companyId' => $company, 'email' => $account->email, 'profile' => $profiles->profile($account),
'emailVerified' => $account->email_verified_at !== null, 'accessStatus' => $request->session()->get('candidate_access_status'), 'emailDeliveryAvailable' => app(CandidateAccess::class)->deliveryAvailable(),
'opportunities' => Schema::hasTable('candidate_opportunity_publications') ? DB::table('candidate_opportunity_publications as p')->join('people_processes as r', 'r.id', '=', 'p.people_process_id')
->where('p.tenant_id', $company)->where('r.tenant_id', $company)->where('p.active', true)->where('r.status', 'open')->where('r.closes_on', '>=', today()->toDateString())
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('candidate_opportunity_publications as newer')->whereColumn('newer.people_process_id', 'p.people_process_id')->whereColumn('newer.version', '>', 'p.version'))
->orderBy('r.closes_on')->limit(20)->get(['r.id', 'r.title', 'r.closes_on']) : [],
'submissions' => Schema::hasTable('candidate_submissions') ? DB::table('candidate_submissions')->where('tenant_id', $company)->where('candidate_account_id', $account->id)->latest('id')->limit(20)->get(['id', 'reference', 'created_at']) : []]);
}
public function save(Request $request, int $company, CandidateProfiles $profiles): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$profiles->save($account, $request->all());
return back();
}
public function logout(Request $request, int $company, CandidateProfiles $profiles): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$profiles->event($account, 'account.logout', []);
Auth::guard('candidate')->logout();
$request->session()->forget('candidate.session_version');
$request->session()->regenerate();
$request->session()->regenerateToken();
Inertia::clearHistory();
return redirect('/candidaturas/'.$company);
}
private function account(Request $request, int $company): ?CandidateAccount
{
$account = Auth::guard('candidate')->user();
if ($account instanceof CandidateAccount) {
$account = CandidateAccount::query()->where('id', $account->id)->where('tenant_id', $company)->first();
}
return $account instanceof CandidateAccount && $account->tenant_id === $company && $request->session()->get('candidate.session_version') === $account->session_version ? $account : null;
}
public function accessPage(Request $request, int $company, string $purpose, CandidateProfiles $profiles): Response
{
abort_unless($profiles->portal($company) !== null, 404);
return $this->page($request, 'candidate/access', ['companyId' => $company, 'purpose' => $purpose,
'accessStatus' => $request->session()->get('candidate_access_status'), 'emailDeliveryAvailable' => app(CandidateAccess::class)->deliveryAvailable()]);
}
public function requestAccess(Request $request, int $company, CandidateAccess $access, CandidateProfiles $profiles): Response
{
abort_unless($profiles->portal($company) !== null, 404);
$data = $request->validate(['email' => 'required|string|email|max:254']);
$access->request($company, $data['email'], 'reset');
return back()->with('candidate_access_status', 'requested');
}
public function requestVerification(Request $request, int $company, CandidateAccess $access): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$access->request($company, $account->email, 'verify');
return back()->with('candidate_access_status', 'requested');
}
public function confirmAccess(Request $request, int $company, string $purpose, CandidateAccess $access): Response
{
$access->consume($company, $purpose === 'confirmar-email' ? 'verify' : 'reset', $request->all());
Inertia::clearHistory();
return redirect('/candidaturas/'.$company.'/'.$purpose)->with('candidate_access_status', 'completed');
}
private function staff(Request $request): User
{
$actor = $request->user('web');
abort_unless($actor instanceof User, 403);
return $actor;
}
public function opportunity(Request $request, int $company, int $process, CandidateSubmissions $submissions): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
return $this->page($request, 'candidate/opportunity', ['companyId' => $company, 'opportunity' => $submissions->opportunity($company, $process),
'profile' => app(CandidateProfiles::class)->profile($account), 'email' => $account->email, 'emailVerified' => $account->email_verified_at !== null]);
}
public function submit(Request $request, int $company, int $process, CandidateSubmissions $submissions): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$id = $submissions->submit($account, $process, $request->all());
return redirect('/candidaturas/'.$company.'/submissoes/'.$id);
}
public function receipt(Request $request, int $company, int $submission): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$row = DB::table('candidate_submissions')->where('tenant_id', $company)->where('candidate_account_id', $account->id)->where('id', $submission)->first();
abort_unless($row !== null, 404);
app(CandidateProfiles::class)->event($account, 'submission.consulted', ['submission_id' => $row->id]);
return $this->page($request, 'candidate/receipt', ['companyId' => $company, 'decision' => app(CandidateTriage::class)->publicDecision($company, $submission), 'receipt' => ['id' => $row->id, 'reference' => $row->reference, 'received_at' => $row->created_at, 'snapshot' => json_decode($row->snapshot, true, 32, JSON_THROW_ON_ERROR)]]);
}
public function candidateDocument(Request $request, int $company, int $submission, string $document): Response
{
$account = $this->account($request, $company);
abort_unless($account !== null, 401);
$row = DB::table('candidate_submissions')->where('tenant_id', $company)->where('candidate_account_id', $account->id)->where('id', $submission)->first();
abort_unless($row !== null, 404);
$file = $this->submissionDocument($company, $row->people_process_id, $submission, $document);
app(CandidateProfiles::class)->event($account, 'submission.document_downloaded', ['submission_id' => $submission, 'document_key' => $document]);
return app(CandidateDocuments::class)->download((array) $file);
}
public function staffDocument(Request $request, int $process, int $submission, string $document): Response
{
return DB::transaction(function () use ($request, $process, $submission, $document) {
$actor = $this->staff($request);
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor);
$record = PeopleProcess::query()->where('tenant_id', $actor->tenant_id)->where('id', $process)->firstOrFail();
abort_unless(app(CandidateTriage::class)->canRead($actor, $record), 403);
$file = $this->submissionDocument($actor->tenant_id, $process, $submission, $document);
app(RequisitionWorkflow::class)->audit($actor, 'candidate_submission.document_downloaded', null, null, ['process_id' => $process, 'submission_id' => $submission, 'document_key' => $document]);
return app(CandidateDocuments::class)->download((array) $file);
});
}
private function submissionDocument(int $company, int $process, int $submission, string $key): \stdClass
{
abort_unless(Schema::hasTable('candidate_submission_documents'), 404);
$row = DB::table('candidate_submission_documents')->where('tenant_id', $company)->where('people_process_id', $process)->where('candidate_submission_id', $submission)->where('document_key', $key)->first();
abort_unless($row !== null, 404);
return $row;
}
public function inbox(Request $request, int $process, CandidateSubmissions $submissions): Response
{
return DB::transaction(function () use ($request, $process, $submissions) {
$actor = $this->staff($request);
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor);
$record = PeopleProcess::query()->where('tenant_id', $actor->tenant_id)->where('id', $process)->firstOrFail();
$triage = app(CandidateTriage::class);
abort_unless($triage->canRead($actor, $record), 403);
$filter = $request->validate(['submission' => 'nullable|integer|min:1']);
$rows = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->orderBy('id')->paginate(20);
if (isset($filter['submission'])) {
$rows = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->where('id', $filter['submission'])->paginate(20)->withQueryString();
}
$decisions = Schema::hasTable('candidate_triage') ? DB::table('candidate_triage')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->whereIn('candidate_submission_id', $rows->pluck('id'))->get(['candidate_submission_id', 'outcome', 'reason', 'public_message', 'created_at', 'people_application_id'])->keyBy('candidate_submission_id') : collect();
$rows->through(fn ($row) => ['id' => $row->id, 'reference' => $row->reference, 'received_at' => $row->created_at, 'snapshot' => json_decode($row->snapshot, true, 32, JSON_THROW_ON_ERROR), 'decision' => $decisions->get($row->id)]);
app(RequisitionWorkflow::class)->audit($actor, 'candidate_submissions.consulted', null, null, ['process_id' => $process]);
return $this->page($request, 'people/submissions', ['process' => ['id' => $record->id, 'title' => $record->title, 'version' => $record->version], 'publication' => $submissions->publication($record), 'submissions' => $rows,
'canPrepare' => $triage->canPrepare($actor, $record), 'canTriage' => Schema::hasTable('candidate_triage') && $triage->canPrepare($actor, $record) && in_array($record->status, ['open', 'assessment'], true),
'publicUrl' => '/candidaturas/'.$actor->tenant_id.'/oportunidades/'.$record->id]);
});
}
public function publish(Request $request, int $process, CandidateSubmissions $submissions): Response
{
$submissions->publish($this->staff($request), $process, $request->all());
return back();
}
public function triage(Request $request, int $process, int $submission, CandidateTriage $triage): Response
{
$triage->decide($this->staff($request), $process, $submission, $request->all());
return back();
}
/** @param array<string,mixed> $props */
private function page(Request $request, string $component, array $props): Response
{
Inertia::encryptHistory();
$response = Inertia::render($component, $props)->toResponse($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
$response->headers->set('X-Robots-Tag', 'noindex, nofollow');
$response->headers->set('Referrer-Policy', 'no-referrer');
return $response;
}
}