101 lines
6.2 KiB
PHP
101 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\PeopleProcess;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class PeopleAccess
|
|
{
|
|
public const SCOPES = ['view' => 'Consultar processo', 'prepare' => 'Preparar e gerir candidaturas', 'review' => 'Conferir abertura e avaliar',
|
|
'approve' => 'Decidir a selecção', 'contract' => 'Preparar minuta contratual', 'manage' => 'Atribuir equipa deste processo',
|
|
'financial' => 'Consultar condições financeiras', 'interviews' => 'Consultar registos de entrevista e evidências', 'contract_view' => 'Consultar minuta contratual'];
|
|
|
|
/** @return list<string> */
|
|
public function scopes(User $actor, PeopleProcess $record): array
|
|
{
|
|
if ($actor->tenant_id !== $record->tenant_id || ! Schema::hasTable('people_process_access') || ! app(PeopleHiring::class)->allowed($actor, $record->kind)) {
|
|
return [];
|
|
}
|
|
$row = DB::table('people_process_access')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $record->id)->where('user_id', $actor->id)
|
|
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->first();
|
|
|
|
return $row === null ? [] : json_decode($row->scopes, true, 16, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
public function can(User $actor, PeopleProcess $record, string $scope): bool
|
|
{
|
|
return in_array($scope, $this->scopes($actor, $record), true);
|
|
}
|
|
|
|
/** @param Builder<PeopleProcess> $query
|
|
* @return Builder<PeopleProcess>
|
|
*/
|
|
public function visible(Builder $query, User $actor): Builder
|
|
{
|
|
if (! Schema::hasTable('people_process_access')) {
|
|
return $query->whereRaw('1 = 0');
|
|
}
|
|
|
|
return $query->whereExists(fn ($q) => $q->selectRaw('1')->from('people_process_access as pa')->whereColumn('pa.people_process_id', 'people_processes.id')
|
|
->where('pa.tenant_id', $actor->tenant_id)->where('pa.user_id', $actor->id)->whereJsonContains('pa.scopes', 'view')->where(fn ($q) => $q->whereNull('pa.expires_at')->orWhere('pa.expires_at', '>', now())));
|
|
}
|
|
|
|
public function manages(User $actor, PeopleProcess $record): bool
|
|
{
|
|
return $actor->tenant_id === $record->tenant_id && ($actor->hasFunctions(['admin']) || $this->can($actor, $record, 'manage'));
|
|
}
|
|
|
|
/** @param list<string> $scopes */
|
|
public function insert(PeopleProcess $record, int $user, array $scopes, string $reason): void
|
|
{
|
|
DB::table('people_process_access')->insert(['tenant_id' => $record->tenant_id, 'people_process_id' => $record->id, 'user_id' => $user,
|
|
'scopes' => json_encode($scopes, JSON_THROW_ON_ERROR), 'reason' => $reason, 'created_at' => now(), 'updated_at' => now()]);
|
|
}
|
|
|
|
/** @param array<string,mixed> $input */
|
|
public function assign(User $actor, int $id, array $input): void
|
|
{
|
|
DB::transaction(function () use ($actor, $id, $input) {
|
|
app(ConflictGuard::class)->lockCompany($actor);
|
|
$actor = app(CurrentActor::class)->lock($actor);
|
|
abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403);
|
|
$record = PeopleProcess::query()->lockForUpdate()->findOrFail($id);
|
|
abort_unless($this->manages($actor, $record), 403);
|
|
if (($input['scopes'] ?? null) === []) {
|
|
$input['expires_at'] = null;
|
|
}
|
|
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'user_id' => 'required|integer|min:1', 'scopes' => 'present|array',
|
|
'scopes.*' => ['string', 'distinct', Rule::in(array_keys(self::SCOPES))], 'expires_at' => 'nullable|date|after:now', 'reason' => 'required|string|min:10|max:4000'])->validate();
|
|
$target = User::query()->where('tenant_id', $actor->tenant_id)->where('id', $data['user_id'])->firstOrFail();
|
|
abort_unless($data['scopes'] === [] || app(PeopleHiring::class)->allowed($target, $record->kind), 422, 'Atribua primeiro um perfil compatível com este percurso na parametrização da empresa.');
|
|
foreach ($data['scopes'] as $scope) {
|
|
$function = match ($scope) {
|
|
'review', 'approve' => 'review', 'prepare', 'contract' => 'prepare', default => null
|
|
};
|
|
if ($function !== null && ! app(PeopleHiring::class)->allowed($target, $record->kind, $function)) {
|
|
throw ValidationException::withMessages(['scopes' => 'O perfil desta pessoa não permite a intervenção seleccionada. Ajuste o perfil da empresa ou escolha outra intervenção.']);
|
|
}
|
|
}
|
|
$state = DB::table('people_access_states')->where('people_process_id', $record->id)->lockForUpdate()->first();
|
|
if ((int) $data['version'] !== (int) $state->version) {
|
|
throw ValidationException::withMessages(['version' => 'A equipa mudou. Actualize antes de guardar.']);
|
|
}
|
|
$before = DB::table('people_process_access')->where('people_process_id', $record->id)->where('user_id', $target->id)->first();
|
|
$scopes = $data['scopes'] === [] ? [] : array_values(array_unique(['view', ...$data['scopes']]));
|
|
DB::table('people_process_access')->updateOrInsert(['people_process_id' => $record->id, 'user_id' => $target->id],
|
|
['tenant_id' => $record->tenant_id, 'scopes' => json_encode($scopes, JSON_THROW_ON_ERROR), 'expires_at' => isset($data['expires_at']) ? now()->parse($data['expires_at'])->utc()->format('Y-m-d H:i:s') : null,
|
|
'reason' => $data['reason'], 'created_at' => $before->created_at ?? now(), 'updated_at' => now()]);
|
|
DB::table('people_access_states')->where('id', $state->id)->update(['version' => $state->version + 1]);
|
|
app(RequisitionWorkflow::class)->audit($actor, 'people.access', null, $before === null ? null : (array) $before,
|
|
['id' => $record->id, 'user_id' => $target->id, 'scopes' => $scopes, 'expires_at' => $data['expires_at'] ?? null, 'reason' => $data['reason']]);
|
|
});
|
|
}
|
|
}
|