Skip to content

Perfil profesional

El perfil es la tarjeta de presentación del candidato dentro de HUMAE. Es lo que ven los recruiters en el directorio y lo que alimenta el CV en PDF.

Secciones del perfil

El perfil está compuesto por 9 sub-entidades, cada una editable por separado desde /me/profile:

SecciónEndpoint baseCantidad típica
Datos básicos (incluye categoría empleado/practicante)GET/PATCH /me/profile1 (el propio perfil)
Áreas de interés laboralPATCH /me/profile (campo functional_areas[])1–10
Foto de perfilPOST /me/profile/avatar1
Experiencias laborales/me/profile/experiences0–N
Educación/me/profile/educations0–N
Cursos y certificaciones/me/profile/courses, /me/profile/certifications0–N
Habilidades (skills)/me/profile/skills3+ recomendado
Idiomas/me/profile/languages1+
Referencias/me/profile/references0–N
Documentos (CV, cartas, etc.)/me/profile/documents0–N

1. Datos básicos (CandidateProfile)

Todos los campos son editables en /me/profile. Vistos en el directorio por recruiters.

Campos principales:

CampoTipoObligatorioVisible directorio
first_name, last_namestring
headlinestring (200)⬜ (recomendado)
summarytext (5000)✅ (al abrir detalle)
birth_datedateSolo admin
contact_phone, whatsappstringSí (para recruiter)
linkedin_url, portfolio_url, github_urlURL
country_id, state_id, city_idFK catálogo
career_level_idFK✅ (filtro)
functional_area_idFK✅ (filtro · derivado del área principal)
position_idFK
candidate_kindenum employee | intern⬜ (recomendado)✅ (filtro · badge en card)
other_area_textstring (200)✅ (al abrir detalle)
years_of_experienceint (0–70)✅ (filtro)
expected_salary_min, expected_salary_maxdecimal✅ (filtro salary_max)
availabilitystring✅ (filtro)
open_to_relocation, open_to_remoteboolean✅ (filtros)

Validación del formulario (src/schemas/profile.ts):

ts
updateProfileSchema = z.object({
  first_name: z.string().min(1).max(120),
  last_name:  z.string().min(1).max(120),
  headline:   z.string().max(200).optional(),
  // ...
  linkedin_url: z.string().url().max(300).optional().or(z.literal("")),
});

Estados del perfil (CandidateState):

  • sin_perfil — recién creado
  • en_registro — llenando datos
  • activo — listo para ser visto
  • en_proceso — ya hay asignaciones activas
  • presentado_empresa — está siendo considerado por una empresa
  • entrevistado — en fase de entrevistas
  • contratado — finalizó exitosamente
  • rechazado, retirado, inactivo — terminales

El estado se mueve automáticamente por el sistema al avanzar el pipeline. El candidato no puede cambiarlo manualmente.

1.bis Categoría de postulación (empleado o practicante)

Desde el perfil el candidato debe indicar bajo qué categoría desea ser considerado. Es uno de los datos más importantes para el matching: define qué vacantes son relevantes para esta persona.

Campo: candidate_kind — enum employee (Empleado) o intern (Practicante).

Por qué importa:

  • El reclutador filtra el directorio por categoría desde el sidebar (Todos / Empleado / Practicante).
  • Cada vacante tiene un campo equivalente target_candidate_kind (employee, intern, any). El matching estructurado compara ambos: si la vacante busca un practicante y el candidato se registró como empleado, el eje "Categoría" del score queda en cero.
  • En cards de directorio aparece un badge distintivo: ámbar para Practicante, color marca para Empleado.

Cómo se llena en la UI (/me/profile):

¿Cómo te quieres postular?

  ( • ) Empleado            (   ) Practicante

Elige si buscas colocarte como empleado o como practicante.
Los reclutadores filtran candidatos por esta categoría.

Validación: el backend acepta null (campo opcional al crear el perfil) pero idealmente el candidato lo selecciona antes de pagar la membresía. Valores fuera del enum ("banana") devuelven 422.

1.ter Áreas de interés laboral

El candidato puede indicar una o varias áreas en las que le gustaría trabajar (PDF cosasfaltanteshumae, punto 1). Esto reemplaza al viejo campo único functional_area_id, que ahora se mantiene en sincronía con la área principal elegida por el candidato.

Almacén: tabla pivote candidate_functional_areas con columnas:

  • functional_area_id — FK a functional_areas.
  • is_primary — true en exactamente una de las áreas (la "principal").
  • sort_order — orden de captura.

Catálogo de áreas (las 15 del PDF + 6 auxiliares):

Producción · Calidad · Mantenimiento · Logística · Recursos Humanos · Administración · Seguridad Industrial · Almacén · Ventas · Ingeniería · Compras · Sistemas · Atención al cliente · Operación · Finanzas · Producto · Diseño · Datos / Analítica · Marketing · Legal / Compliance · Otra.

El admin puede agregar / desactivar áreas desde /admin/catalogos.

Endpoint:

http
PATCH /api/v1/me/profile
Content-Type: application/json

{
  "functional_areas": [
    { "id": 4, "is_primary": true },
    { "id": 7, "is_primary": false },
    { "id": 12, "is_primary": false }
  ],
  "other_area_text": "Bioingeniería"
}

Reglas:

  • Máximo 10 áreas por perfil.
  • Solo una puede ser is_primary = true. Si el cliente envía dos primarias, el ProfileService respeta la primera y descarta el resto.
  • Al guardar, el campo legacy candidate_profiles.functional_area_id queda sincronizado con la primaria.
  • other_area_text (máx. 200 caracteres) es para áreas que no estén en el catálogo. Es texto libre, no participa del matching estructurado.
  • Quitar todas las áreas deja functional_area_id = null.

Cómo se llena en la UI:

El componente FunctionalAreasMultiSelect muestra:

  • Chips superiores con las áreas seleccionadas (estrella ⭐ marca la principal).
  • Buscador + lista filtrable del catálogo con check al elegir.
  • Botón "Quitar" (✕) en cada chip; click en la estrella vacía promueve esa área a principal.
  • Input de texto opcional "Otra (texto libre)" debajo de la lista.
Áreas en las que te gustaría trabajar

  [⭐ Producción ✕]  [Calidad ☆ ✕]  [Mantenimiento ☆ ✕]

  🔍 Buscar área…
  ┌──────────────────────────────────┐
  │ ✓ Producción                     │
  │ ✓ Calidad                        │
  │ ✓ Mantenimiento                  │
  │   Logística                      │
  │   Recursos Humanos               │
  │   …                              │
  └──────────────────────────────────┘

  Otra (texto libre): [_______________]

Caso de uso típico (registro de un practicante):

  1. Pablo se registra como candidato y entra a /me/profile.
  2. Marca el RadioGroup Practicante en la sección "¿Cómo te quieres postular?".
  3. En "Áreas en las que te gustaría trabajar" elige Ingeniería (estrella → principal) + Producción (sin estrella).
  4. Guarda. El backend persiste candidate_kind = 'intern', sincroniza el pivote con dos filas (una con is_primary = true) y deja functional_area_id = id_de_ingenieria.
  5. Al día siguiente un reclutador filtra el directorio por candidate_kind=intern + functional_area_ids[]=ingenieria,produccion y Pablo aparece. Si entra a una vacante "Practicante de Ingeniería Industrial · Producción", Pablo aparece arriba en el panel de Sugerencias con score alto.

2. Foto de perfil

Endpoint: POST /me/profile/avatar (multipart con campo avatar).

  • Validación: archivo imagen jpg/jpeg/png/webp, máximo 4 MB.
  • Se guarda en el disco public del propio servidor bajo storage/app/public/avatars/{user_id}/..., reencodeado a WebP 400×400.
  • La URL pública (/storage/avatars/{user_id}/{hash}.webp) se guarda en User.avatar_url; la ruta relativa queda en User.avatar_path para permitir el borrado del archivo físico cuando se reemplaza.
  • La versión anterior se elimina automáticamente del disco al subir una nueva.
  • Rate limit: 10 uploads por minuto.

Ver Storage local para detalles.

3. Experiencias laborales

Endpoint: POST /me/profile/experiences + PATCH + DELETE.

Campos (CandidateExperience):

CampoValidación
company_nameRequerido, 1–200
position_titleRequerido, 1–200
location200 max
start_dateRequerido (date)
end_dateOpcional (date >= start_date)
is_currentBoolean; si true, end_date se ignora
description5000 max

Orden: las experiencias se muestran en orden cronológico inverso (más recientes primero).

4. Educación

Endpoint: /me/profile/educations.

Campos similares a experiencias, más:

  • degree_level_id → FK a catálogo degree_levels (Bachillerato, Licenciatura, Maestría, Doctorado, Certificación).
  • institution_name, field_of_study, start_year, end_year, is_current.

5. Cursos y certificaciones

Dos entidades separadas (CandidateCourse, CandidateCertification) con endpoints propios:

  • Cursos: nombre, institución, duración, fecha de término, URL de certificado.
  • Certificaciones: nombre, entidad emisora, fecha de emisión, fecha de vencimiento, código de verificación, URL.

6. Habilidades (skills)

Endpoints:

  • POST /me/profile/skills → agrega una skill con nivel
  • DELETE /me/profile/skills/{id}

Estructura:

  • skill_id: FK al catálogo skills (≈ 150 skills seed: React, Python, SQL, Excel, ...).
  • level: basico, intermedio, avanzado, experto.
  • Unique constraint: (candidate_profile_id, skill_id) — no se duplican.

El directorio permite filtrado AND por múltiples skills: "candidatos con Python AND SQL".

Skills no listadas

Si el candidato no encuentra su skill, el admin puede agregarla al catálogo desde /admin/catalogos/skills.

7. Idiomas

Endpoints similares a skills.

  • language_id: FK a languages (Español, Inglés, Francés, Alemán, Portugués, Italiano, Chino, Japonés, ...).
  • level: A1C2 (escala MCER) o nativo.

Filtro del directorio soporta AND: "español nativo AND inglés B2+".

8. Referencias

Endpoint: /me/profile/references.

  • name, relationship (jefe, colega, cliente), company, contact_email, contact_phone.
  • Máximo recomendado: 3 referencias.
  • Solo visibles a recruiters (no aparecen en el CV público automáticamente a menos que el candidato lo permita).

9. Documentos adjuntos

Endpoint: POST /me/profile/documents (multipart con file + type).

  • Tipos (DocumentType enum): cv_personal, carta_recomendacion, constancia_estudios, certificado, identificacion, rfc, curp, otros.
  • Validación: PDF, JPG, PNG, WEBP, DOC, DOCX; máximo 10 MB por archivo.
  • Se guardan en el disco local privado (storage/app/private/documents/{candidate_profile_id}/...) del propio servidor.
  • La descarga pasa por un endpoint autenticado: GET /api/v1/me/profile/documents/{id}/download (rate limit 60/min) con verificación de ownership.
  • Rate limit de upload: 20 por minuto.
  • Los documentos son solo visibles al candidato y a recruiters/admin. Las empresas NO los ven directamente.

Acceso a los datos

Quién ve qué

AccesoCandidato propioRecruiterCompany userAdmin
Perfil básico✅ edit✅ read✅ read (solo si presentado)✅ edit
Foto de perfil
Teléfono/email⬜ (oculto)
Salario esperado
Documentos
Referencias

Implementado con CandidateProfilePolicy + Resources que filtran campos sensibles según $request->user()->role.

Soft delete

Cuando un candidato elimina su perfil, se hace soft delete (deleted_at): el registro queda en DB para auditoría pero deja de aparecer. Un admin puede restaurarlo desde el panel.

Completitud del perfil

El dashboard muestra un progreso porcentual calculado en frontend:

Foto              20%  ← 1 pt si hay avatar
Resumen           10%  ← 1 pt si headline + summary ≥ 100 chars
Ubicación         10%  ← 1 pt si country+state+city
Experiencia       20%  ← 2 pts si hay ≥ 1 experiencia
Educación         10%  ← 1 pt si hay ≥ 1 educación
Skills            15%  ← 1.5 pts si hay ≥ 3 skills
Idiomas           10%  ← 1 pt si hay ≥ 1 idioma
Preferencias      5%   ← 0.5 pts si rellenó salario/modalidad
────────────────────
Total            100%

Validaciones globales

  • Todos los endpoints de perfil están bajo auth:sanctum + middleware role:candidate.
  • Cada acción valida ownership: un candidato solo puede editar su propio CandidateProfile.
  • Las FormRequests (UpdateCandidateProfileRequest, CreateExperienceRequest, ...) contienen las reglas de Zod-equivalente.

Siguiente

Ya con el perfil completo, el candidato debe responder las pruebas psicométricas. Pruebas psicométricas →

Manual de usuario HUMAE · Uso interno