Skip to content

Asignaciones

Una asignación (VacancyAssignment) es la relación explícita entre un candidato y una vacante. Sin ella, el candidato no es considerado para esa vacante.

Modelo

VacancyAssignment
├─ vacancy_id            FK Vacancy
├─ candidate_profile_id  FK CandidateProfile
├─ assigned_by           FK User (quién la creó)
├─ stage                 AssignmentStage enum
├─ priority              Priority enum
├─ score                 int 1-10 (subjetivo del recruiter)
├─ recruiter_notes       text
├─ company_notes         text
├─ rejection_reason      string
├─ presented_at          timestamp (cuándo pasó a 'presented')
├─ interviewed_at        timestamp
├─ hired_at              timestamp
├─ rejected_at           timestamp
├─ withdrawn_at          timestamp
├─ created_at / updated_at / deleted_at

Constraint de unicidad

sql
UNIQUE (vacancy_id, candidate_profile_id)

Un candidato solo puede ser asignado UNA VEZ a una vacante. Si se intenta duplicar, se reactiva la existente (si estaba en withdrawn) o se devuelve 422.

Filtro en el dropdown de asignación

Para evitar que el recruiter intente asignar a una vacante donde el candidato ya está, el endpoint GET /api/v1/vacancies acepta el query param excluding_assigned_candidate_id que excluye toda vacante con un assignment existente para ese candidato. El AssignToVacancyDialog lo pasa automáticamente, así que esa vacante no aparece en la lista.

Cómo se crea una asignación

Desde el directorio

El recruiter abre el detalle de un candidato → clic en "Asignar a vacante" → selector con vacantes activas → confirmar.

POST /api/v1/vacancies/{vacancyId}/assignments
{
  "candidate_profile_id": 123,
  "priority": "high",
  "notes": "Perfil alineado con todos los requisitos"
}

Backend:

  1. Valida ownership de la vacante (VacancyPolicy::update).
  2. Verifica que la vacante esté en estado que admita asignaciones (activa, en_busqueda, con_candidatos_asignados, entrevistas_en_curso).
  3. Verifica que el candidato tenga membresía activa.
  4. Crea VacancyAssignment con stage = sourced.
  5. Si era la primera asignación, mueve Vacancy.state a con_candidatos_asignados (acepta venir desde activa o en_busqueda — el estado en_busqueda es opcional y se puede saltar).
  6. Responde 201 con la asignación completa.

En bulk

No hay endpoint de bulk assign en el MVP. Se hace una por una (intencional: cada asignación merece criterio).

Reglas de autorización

  • Solo recruiter o admin pueden crear/editar asignaciones.
  • Un company_user puede ver las asignaciones de sus vacantes (read-only) y añadir company_notes.
  • Un candidato NO ve las asignaciones que otros tienen en su misma vacante.

Policy: VacancyAssignmentPolicy.

Campos editables después de creada

CampoQuiénEndpoint
stagerecruiter, adminPATCH /vacancies/{vid}/assignments/{aid}/stage
priority, scorerecruiter, adminPATCH /vacancies/{vid}/assignments/{aid}
recruiter_notesrecruiter, adminnotes endpoint
company_notescompany_user, recruiter, adminnotes endpoint
rejection_reasonrecruiter, admin (al mover a rejected)stage endpoint

Relaciones

VacancyAssignment  (1) ─── (N) Interview           entrevistas agendadas
                   (1) ─── (N) VacancyAssignmentNote  historial de notas
                   (1) ──────► Vacancy             la vacante
                   (1) ──────► CandidateProfile    el candidato
                   (1) ──────► User (assigned_by)  quien la creó

Vista detallada de una asignación

URL: /recruiter/vacantes/{vid}/pipeline/{aid} (o como modal dentro del kanban).

Muestra todo:

  • Datos del candidato (resumen + acciones)
  • Etapa actual + historial de cambios
  • Prioridad + score editables
  • Notas del recruiter (timeline)
  • Notas de la empresa (timeline)
  • Entrevistas asociadas (con estado y acciones)
  • Documentos que el candidato adjuntó
  • Botones de acción según la etapa actual

Historial de cambios

Cada vez que stage cambia, se guardan los timestamps:

  • presented_at, interviewed_at, hired_at, rejected_at, withdrawn_at.

Con esto se calculan reportes:

  • Tiempo promedio de sourced a hired.
  • Tiempo promedio en cada etapa.
  • Cuellos de botella.

Fase 2: activar spatie/laravel-activitylog en este modelo para ver quién cambió qué y cuándo.

Soft delete

Si un recruiter descarta una asignación sin rechazarla formalmente (ej. error de selección), usa DELETE /vacancies/{vid}/assignments/{aid}:

  • Hace soft delete (deleted_at).
  • No dispara notificación.
  • Un admin puede restaurarla desde el panel.

Estadísticas de asignaciones

Visible en el dashboard del recruiter:

  • Mis asignaciones activas: conteo por etapa.
  • Mi tasa de conversión: % de sourced que llegaron a hired.
  • Tiempo promedio de cierre: días desde asignación a hire.
  • Últimas 7 días: asignaciones creadas + movidas.

Fuente: ReportsService::recruiterEffectiveness.

Eventos que disparan notificaciones

AcciónNotificación
Create (sourced)— (silencioso)
Move a presentedAssignmentPresentedNotification → company_user
Move a interviewingdisparado por agendar entrevista
Move a finalistCandidateFinalistNotification → company_user
Move a hiredCandidateHiredNotification → candidato + company_user
Move a rejectedAssignmentRejectedNotification (opcional) → candidato
Company note añadidonotify al recruiter asignado

Errores comunes

ErrorHTTPCausa
"Candidato ya asignado"422Unique constraint hit
"Vacante cerrada"422Estado terminal
"Membresía del candidato expirada"422No se asigna sin membresía activa
"Transición no permitida"422FSM rechaza el cambio
"No autorizado"403No tiene role recruiter/admin

Siguiente

La siguiente capa de gestión: Entrevistas desde el lado recruiter →

Manual de usuario HUMAE · Uso interno