Apariencia
Matriz de roles y permisos
Tabla exhaustiva de qué puede hacer cada rol. Implementado con Spatie Permission + Laravel Policies.
Los 4 roles
Enum App\Enums\UserRole:
candidaterecruitercompany_useradmin
Matriz de acceso por recurso
Leyenda: ✅ sí · 🔒 solo si es suyo · ⬜ no · 🏢 solo en su empresa
Auth
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Login | ✅ | ✅ | ✅ | ✅ |
| Logout | ✅ | ✅ | ✅ | ✅ |
| Register (auto) | ✅ | ⬜ | ⬜ | ⬜ |
| Invitación | ⬜ | ⬜ | 🏢 (miembros) | ✅ |
| Cambio de rol | ⬜ | ⬜ | ⬜ | ✅ |
| Suspender usuarios | ⬜ | ⬜ | ⬜ | ✅ |
Perfil del candidato
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Leer perfil | 🔒 | ✅ (directorio) | ✅ (si presentado) | ✅ |
| Editar perfil | 🔒 | ⬜ | ⬜ | ✅ |
| Subir avatar | 🔒 | ⬜ | ⬜ | ✅ |
| Subir documentos | 🔒 | ⬜ | ⬜ | ✅ |
| Descargar CV PDF | 🔒 | ✅ | ✅ (solo del asignado) | ✅ |
| Ver datos de contacto | 🔒 | ✅ | ⬜ (ocultos) | ✅ |
| Cambiar estado | ⬜ | ⬜ (automático) | ⬜ | ✅ |
Membresía
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Iniciar checkout | 🔒 | ⬜ | ⬜ | ⬜ |
| Ver mi membresía | 🔒 | ⬜ | ⬜ | ✅ |
| Ver histórico pagos | 🔒 | ⬜ | ⬜ | ✅ |
| Cancelar membresía | ⬜ | ⬜ | ⬜ | ✅ |
| Refund | ⬜ | ⬜ | ⬜ | ✅ (manual en Stripe) |
Psicométricas
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Ver pruebas disponibles | 🔒 | ⬜ | ⬜ | ✅ |
| Iniciar intento | 🔒 | ⬜ | ⬜ | ⬜ |
| Enviar respuestas | 🔒 | ⬜ | ⬜ | ⬜ |
| Ver resultados propios | 🔒 | ⬜ | ⬜ | ✅ |
| Ver scores del candidato | 🔒 | ✅ | ✅ (si presentado) | ✅ |
| Ver respuestas individuales | 🔒 | ⬜ | ⬜ | ✅ |
| Crear/editar pruebas | ⬜ | ⬜ | ⬜ | ✅ |
Directorio
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Buscar candidatos | ⬜ | ✅ | ⬜ | ✅ |
| Ver detalle de candidato | ⬜ | ✅ | ⬜ | ✅ |
| Agregar a favoritos | ⬜ | ✅ | ⬜ | ✅ |
| Asignar a vacante | ⬜ | ✅ | ⬜ | ✅ |
Empresas
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Ver datos de empresa | ⬜ | ✅ | 🏢 | ✅ |
| Editar datos | ⬜ | ✅ | 🏢 (owner) | ✅ |
| Invitar miembros | ⬜ | ⬜ | 🚧 pospuesto | 🚧 pospuesto |
| Remover miembros | ⬜ | ⬜ | 🚧 pospuesto | 🚧 pospuesto |
| Crear company | ⬜ | ⬜ | ⬜ | ✅ |
Vacantes
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Ver listado global | ⬜ | ✅ | 🏢 | ✅ |
| Crear vacante | ⬜ | ✅ | 🏢 | ✅ |
| Editar vacante | ⬜ | ✅ | 🏢 | ✅ |
| Publicar | ⬜ | ✅ | 🏢 | ✅ |
| Cancelar | ⬜ | ✅ | 🏢 | ✅ |
| Eliminar | ⬜ | ⬜ | ⬜ | ✅ |
| Asignar recruiter | ⬜ | ⬜ | ⬜ | ✅ |
Pipeline / Asignaciones
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Ver asignaciones propias | 🔒 | ✅ | 🏢 | ✅ |
| Crear asignación | ⬜ | ✅ | ⬜ | ✅ |
| Cambiar stage | ⬜ | ✅ | ⬜ | ✅ |
| Ver notas recruiter | ⬜ | ✅ | ⬜ | ✅ |
| Ver notas empresa | ⬜ | ✅ | 🏢 | ✅ |
| Escribir nota empresa | ⬜ | ⬜ | 🏢 | ✅ |
| Escribir nota recruiter | ⬜ | ✅ | ⬜ | ✅ |
Entrevistas
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Listar propias | 🔒 | ✅ (asignado) | 🏢 | ✅ |
| Agendar | ⬜ | ✅ | ⬜ | ✅ |
| Confirmar | 🔒 | ✅ | 🏢 | ✅ |
| Reprogramar | 🔒 | ✅ | 🏢 | ✅ |
| Cancelar | 🔒 | ✅ | 🏢 | ✅ |
| Marcar realizada + feedback | ⬜ | ✅ | ⬜ | ✅ |
| Marcar no_asisto | ⬜ | ✅ | ⬜ | ✅ |
Notificaciones
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Ver propias | 🔒 | 🔒 | 🔒 | 🔒 |
| Marcar como leída | 🔒 | 🔒 | 🔒 | 🔒 |
| Eliminar | 🔒 | 🔒 | 🔒 | 🔒 |
Admin / catálogos / reportes
| Acción | Candidato | Recruiter | Company | Admin |
|---|---|---|---|---|
| Gestionar catálogos | ⬜ | ⬜ | ⬜ | ✅ |
| Ver reportes | ⬜ | ✅ (algunos) | ⬜ | ✅ |
| Configuración global | ⬜ | ⬜ | ⬜ | ✅ |
| Auditoría | ⬜ | ⬜ | ⬜ | ✅ |
Los 45 permisos (Spatie)
Agrupados en 11 categorías. Sembrados en RolesAndPermissionsSeeder.
| Categoría | Permisos |
|---|---|
| Users | users.view, users.create, users.update, users.delete, users.suspend, users.assign_role |
| Candidates | candidate.view_directory, candidate.view_detail, candidate.download_cv, candidate.favorite |
| Vacancies | vacancy.view, vacancy.create, vacancy.update, vacancy.publish, vacancy.cancel, vacancy.delete |
| Assignments | assignment.create, assignment.update, assignment.delete, assignment.move_stage |
| Interviews | interview.view, interview.schedule, interview.confirm, interview.reschedule, interview.cancel, interview.complete |
| Notes | note.create, note.update_own, note.delete_own, note.view_private |
| Psychometrics | test.view, test.take, test.manage, result.view_own, result.view_any |
| Memberships | membership.view_own, membership.cancel, payment.view, payment.refund |
| Companies | company.view, company.update, company.manage_members, company.delete |
| Catalogs | catalog.view, catalog.manage |
| Reports | report.view, report.export |
Asignación a roles
admin → TODOS
recruiter → candidate.*, vacancy.*, assignment.*, interview.*,
note.*, test.view_any, result.view_any, report.view
company_user → vacancy.view (own company), assignment.view, note.create,
interview.view/confirm/reschedule/cancel, candidate.view_detail (asignados)
candidate → membership.view_own, test.take, result.view_own,
interview.view/confirm/reschedule/cancel (propias), note.*Implementación
En rutas
php
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
Route::apiResource('catalogos', CatalogController::class);
});En controllers (Policies)
php
public function update(UpdateVacancyRequest $request, Vacancy $vacancy)
{
$this->authorize('update', $vacancy);
// ...
}En VacancyPolicy
php
public function update(User $user, Vacancy $vacancy): bool
{
if ($user->hasRole('admin')) return true;
if ($user->hasRole('recruiter')) return true;
if ($user->hasRole('company_user')) {
return $user->companies()->where('companies.id', $vacancy->company_id)->exists();
}
return false;
}Before bypass
Solo en Policies que lo documentan explícitamente:
php
public function before(User $user): ?bool
{
return $user->hasRole('admin') ? true : null;
}Usado con cuidado; se prefiere checks explícitos.
Frontend
useAuth()exponehasRole(role)yhasPermission(perm).- Componentes
<ProtectedRoute>redirigen a/loginsi no hay rol. - Los botones de acción se renderizan condicionalmente:
tsx
{hasRole("recruiter") && <Button onClick={assignToVacancy}>Asignar</Button>}Eso es UX — la autorización real vive en el backend.
Siguiente
Vocabulario común: Glosario →

