Apariencia
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_atConstraint 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:
- Valida ownership de la vacante (
VacancyPolicy::update). - Verifica que la vacante esté en estado que admita asignaciones (
activa,en_busqueda,con_candidatos_asignados,entrevistas_en_curso). - Verifica que el candidato tenga membresía activa.
- Crea
VacancyAssignmentconstage = sourced. - Si era la primera asignación, mueve
Vacancy.stateacon_candidatos_asignados(acepta venir desdeactivaoen_busqueda— el estadoen_busquedaes opcional y se puede saltar). - 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
recruiteroadminpueden crear/editar asignaciones. - Un
company_userpuede ver las asignaciones de sus vacantes (read-only) y añadircompany_notes. - Un candidato NO ve las asignaciones que otros tienen en su misma vacante.
Policy: VacancyAssignmentPolicy.
Campos editables después de creada
| Campo | Quién | Endpoint |
|---|---|---|
stage | recruiter, admin | PATCH /vacancies/{vid}/assignments/{aid}/stage |
priority, score | recruiter, admin | PATCH /vacancies/{vid}/assignments/{aid} |
recruiter_notes | recruiter, admin | notes endpoint |
company_notes | company_user, recruiter, admin | notes endpoint |
rejection_reason | recruiter, 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
sourcedahired. - 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
sourcedque llegaron ahired. - 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ón | Notificación |
|---|---|
| Create (sourced) | — (silencioso) |
| Move a presented | AssignmentPresentedNotification → company_user |
| Move a interviewing | disparado por agendar entrevista |
| Move a finalist | CandidateFinalistNotification → company_user |
| Move a hired | CandidateHiredNotification → candidato + company_user |
| Move a rejected | AssignmentRejectedNotification (opcional) → candidato |
| Company note añadido | notify al recruiter asignado |
Errores comunes
| Error | HTTP | Causa |
|---|---|---|
| "Candidato ya asignado" | 422 | Unique constraint hit |
| "Vacante cerrada" | 422 | Estado terminal |
| "Membresía del candidato expirada" | 422 | No se asigna sin membresía activa |
| "Transición no permitida" | 422 | FSM rechaza el cambio |
| "No autorizado" | 403 | No tiene role recruiter/admin |
Siguiente
La siguiente capa de gestión: Entrevistas desde el lado recruiter →

