Ir al contenido

Permisos

El módulo cubre el flujo de autoservicio de permisos del empleado autenticado: consultar el historial y estado de sus solicitudes, ver el catálogo de motivos de permiso activos para su empresa, crear una solicitud (simple o recurrente), cancelarla mientras está pendiente, adjuntar o reemplazar el comprobante de respaldo y descargar el comprobante en PDF.

Acciones cubiertas:

  • Ver historial y estado de mis solicitudes de permiso.
  • Ver el catálogo de motivos de permiso disponibles (reglas de horas/días, límites, si requiere adjunto, si permite recurrencia).
  • Ver el saldo aplicable a un motivo (anual del propio motivo, o el saldo de convenio de vacaciones cuando el motivo está ligado a él — ver Vacaciones).
  • Crear una solicitud de permiso simple o recurrente.
  • Cancelar una solicitud de permiso pendiente.
  • Adjuntar o reemplazar el comprobante de una solicitud existente.
  • Descargar el comprobante PDF de una solicitud.

Prerrequisitos:

  • El usuario debe tener información organizacional activa (cargo, departamento). Sin ella, el backend rechaza todos los endpoints de este módulo con un error genérico (ver Convenciones).
  • A diferencia de Vacaciones, este módulo no exige que el tipo de contrato del usuario incluya beneficios de vacaciones — cualquier usuario con información organizacional activa puede solicitar permisos.
  • Si un motivo tiene hasAgreement: 'S', su saldo anual se calcula a partir del mismo convenio de vacaciones del tenant (GET /me/time-off/vacation-agreement-balance, documentado en Vacaciones). Si el tenant no tiene Activa_Acuerdo_Vacaciones habilitado, ese saldo llega en cero.
  • El parámetro de tenant Controla_Fecha_Solicitud_Permisos (S/N) condiciona si se pueden solicitar permisos con fecha/hora pasada. El portal de referencia lo aplica sólo en el cliente (fecha mínima del selector = hoy, hora mínima = ahora si la fecha es hoy); el backend no repite esta validación en el POST de creación.
sequenceDiagram
    participant App as App móvil
    participant API as th-core-api

    Note over App,API: Carga inicial de la pantalla de permisos
    App->>API: GET /me/time-off/requests/all/permissions
    API-->>App: historial de solicitudes propias

    Note over App,API: Usuario abre el formulario Solicitar permiso
    App->>API: GET /me/time-off/requests/permissions-reasons/active
    API-->>App: catálogo de motivos activos
    App->>API: GET /me/time-off/vacation-agreement-balance
    API-->>App: saldo de convenio, usado por motivos con hasAgreement S
    Note over App: La duración y las validaciones del formulario se calculan en el cliente<br/>con el catálogo y el historial ya cargados - no existe un equivalente a calculate-days para permisos

    App->>API: POST /me/time-off/requests/permissions
    API-->>App: solicitud creada, o recurrencia + instancias si isRecurrent
    App->>API: GET /me/time-off/requests/all/permissions
    API-->>App: refresco del historial

    Note over App,API: Cancelar una solicitud pendiente
    App->>API: DELETE /me/time-off/requests/permissions/:id
    API-->>App: confirmacion
    App->>API: GET /me/time-off/requests/all/permissions
    API-->>App: refresco del historial

    Note over App,API: Adjuntar o reemplazar comprobante desde el historial
    App->>API: PATCH /me/time-off/requests/permissions/:id
    API-->>App: solicitud actualizada
    App->>API: GET /me/time-off/requests/all/permissions
    API-->>App: refresco del historial

    Note over App,API: Opcional, desde cualquier solicitud del historial
    App->>API: GET /me/permission/generate?permissionId=:id
    API-->>App: url del PDF generado

Historial completo de solicitudes de permiso del usuario autenticado (todos los estados). Se usa tanto para la tabla de historial como para el resumen de contadores por estado.

No recibe query params.

Response — array de:

Campo Tipo Descripción
id number Id de la solicitud.
requestDate date Fecha en que se creó la solicitud.
startDate date Fecha/hora de inicio del permiso.
endDate date Fecha/hora de fin del permiso.
status enum PermissionRequestStatusEnum Ver tabla de estados en Lógica de cliente.
observationRequester string Observación ingresada al crear la solicitud.
attachedFile string Nombre del archivo adjunto ({uuid}-{nombre}), o vacío si no tiene.
permissionReasons.id number Id del motivo.
permissionReasons.name string Nombre del motivo.
permissionReasons.units enum PermissionReasonUnitsEnum Unidad del motivo (H Horas, D Días).
[
{
"id": 5210,
"requestDate": "2026-06-18T14:02:00.000Z",
"startDate": "2026-06-20T09:00:00.000Z",
"endDate": "2026-06-20T13:00:00.000Z",
"status": "A",
"observationRequester": "Cita médica",
"attachedFile": "8c1ec6db-f964-40c3-ae21-37063cc41def-certificado_medico.pdf",
"permissionReasons": {
"id": 3,
"name": "Permiso Médico",
"units": "H"
}
},
{
"id": 5238,
"requestDate": "2026-07-15T08:40:00.000Z",
"startDate": "2026-07-20T00:00:00.000Z",
"endDate": "2026-07-20T23:59:59.000Z",
"status": "P",
"observationRequester": "Trámite personal",
"attachedFile": "",
"permissionReasons": {
"id": 7,
"name": "Asuntos Personales",
"units": "D"
}
}
]

Sin errores específicos adicionales a los comunes.

GET /me/time-off/requests/permissions-reasons/active

Sección titulada «GET /me/time-off/requests/permissions-reasons/active»

Catálogo de motivos de permiso activos, filtrado por la empresa del usuario autenticado (incluye motivos configurados para “todas las empresas”). Alimenta el selector de motivo del formulario y define sus reglas.

No recibe query params.

Response — array de:

Campo Tipo Enum Descripción
companyId string Empresa(s) a la que aplica el motivo (uso interno, no relevante para la UI).
id number Id del motivo, usarlo como valor del campo permissionReasons al crear la solicitud.
name string Nombre a mostrar en el selector.
hasTime string S/N Si el motivo tiene límite de tiempo por solicitud individual.
limitBy string H/D Unidad del límite por solicitud (horas o días).
time number Valor del límite por solicitud, sólo relevante si hasTime = S.
isAnnual string S/N Si el motivo tiene límite anual acumulado.
annualType string H/D Unidad del límite anual.
annualTime number Valor del límite anual, sólo relevante si isAnnual = S.
isCompensatory string S/N Si el permiso es compensatorio (uso interno de nómina).
units enum PermissionReasonUnitsEnum H/D Unidad en la que se solicita el motivo. Determina si el formulario pide fecha o fecha+hora.
overlapDate string S/N Si N, el backend rechaza una nueva solicitud si el usuario ya tiene una pendiente/aprobada que se superpone en fechas.
adjustsVacation string S/N Si el permiso ajusta el saldo de vacaciones (uso interno).
isPocket string S/N Campo informativo del catálogo, no aplicado por ninguna validación del cliente ni del backend en este flujo.
hasAgreement string S/N Si S, el saldo anual del motivo se toma del convenio de vacaciones del tenant en vez de annualTime.
hasBlock string S/N Campo informativo del catálogo, no aplicado por ninguna validación del cliente ni del backend en este flujo.
hasAttachment enum PermissionReasonHasAttachmentEnum B/O/N Si el adjunto es obligatorio (B), opcional (O) o no aplica (N).
status string A/I/X Estado del motivo en el catálogo (siempre A en esta respuesta, ya filtrada por activos).
allowsRecurrence string S/N Si el motivo admite marcar la solicitud como recurrente.
visibility string P/G/A Portal, Admin o Ambos.
[
{
"companyId": "-1",
"id": 3,
"name": "Permiso Médico",
"hasTime": "S",
"limitBy": "H",
"time": 4,
"isAnnual": "S",
"annualType": "H",
"annualTime": 40,
"isCompensatory": "N",
"units": "H",
"overlapDate": "N",
"adjustsVacation": "N",
"isPocket": "N",
"hasAgreement": "N",
"hasBlock": "N",
"hasAttachment": "B",
"status": "A",
"allowsRecurrence": "N",
"visibility": "P"
},
{
"companyId": "17",
"id": 7,
"name": "Asuntos Personales",
"hasTime": "N",
"limitBy": "D",
"time": null,
"isAnnual": "S",
"annualType": "D",
"annualTime": 5,
"isCompensatory": "N",
"units": "D",
"overlapDate": "S",
"adjustsVacation": "N",
"isPocket": "N",
"hasAgreement": "N",
"hasBlock": "N",
"hasAttachment": "N",
"status": "A",
"allowsRecurrence": "S",
"visibility": "A"
}
]

Sin errores específicos adicionales a los comunes.

Crea una solicitud de permiso para el usuario autenticado. Si isRecurrent es true, delega en el motor de recurrencia: crea una recurrencia y genera todas las instancias (PermissionRequest) del patrón en estado pendiente, en vez de una única solicitud.

Body (MeCreatePermissionRequestDto):

Campo Tipo Obligatorio Enum Descripción
startDate date Fecha/hora de inicio.
endDate date Fecha/hora de fin (no puede ser menor a startDate).
permissionReasons string Id del motivo (del catálogo), como string — pese al nombre en plural, es un único id, no un array.
observationRequester string No Motivo/observación libre.
attachedFile string No Nombre del archivo ya subido ({uuid}-{nombre}), obtenido de un flujo de subida previo (presigned URL) fuera de este endpoint. Requerido en el cliente si hasAttachment = B del motivo seleccionado.
isRecurrent boolean No Si true, se activa el flujo de recurrencia. Sólo debe ofrecerse si el motivo tiene allowsRecurrence = S.
recurrenceType string Sólo si isRecurrent D Diaria, W Semanal, M Mensual, L Días laborales, C Personalizada Tipo de patrón de recurrencia (RecurrenceIntervalEnum).
recurrenceConfig object Sólo si isRecurrent Configuración del patrón, ver tabla siguiente.
username string No Sólo para uso administrativo (fuera del alcance self-service de este documento). Omitir en el flujo normal de la app.

recurrenceConfig (RecurrenceConfigDto):

Campo Tipo Cuándo aplica Descripción
days number[] recurrenceType = W (Semanal) Días de la semana, 0 = domingo … 6 = sábado.
dayOfMonth number recurrenceType = M (Mensual) Día del mes (1–31).
interval number recurrenceType = C (Personalizada) Cada cuántos días se repite.
excludeHolidays boolean Cualquiera Si se deben excluir feriados al generar ocurrencias.
startTime / endTime string HH:mm Motivo por horas (units = H) en D, L, W o M Hora de inicio/fin de cada ocurrencia. Si el motivo es por días, el backend ignora la hora y ajusta cada ocurrencia a día completo.

Response no recurrente (MeCreatePermissionResponseDto):

{
"permissionRequestResult": {
"id": 5239,
"personalActionId": -1,
"username": "jperez",
"requestDate": "2026-07-16T18:20:00.000Z",
"startDate": "2026-07-22T09:00:00.000Z",
"endDate": "2026-07-22T13:00:00.000Z",
"status": "P",
"isCompensatory": "N",
"observationRequester": "Cita médica",
"attachedFile": "8c1ec6db-f964-40c3-ae21-37063cc41def-certificado_medico.pdf",
"adjustsVacation": "N",
"recurrenceId": null,
"user": {
"username": "jperez",
"fullName": "Juan Pérez",
"mail": "jperez@twiinshrm.com",
"avatar": "https://cdn.twiinshrm.com/avatars/jperez.png"
},
"permissionReasons": {
"id": 3,
"name": "Permiso Médico",
"hasTime": "S",
"limitBy": "H",
"time": 4,
"isAnnual": "S",
"annualType": "H",
"annualTime": 40,
"isCompensatory": "N",
"units": "H",
"overlapDate": "N",
"adjustsVacation": "N",
"isPocket": "N",
"hasAgreement": "N",
"hasBlock": "N",
"hasAttachment": "B",
"status": "A",
"visibility": "P",
"allowsRecurrence": "N"
}
},
"approverWorkflowRequest": {
"id": 6011,
"username": "jperez",
"alternateUser": "",
"type": "PERMISSIONS",
"approverOrder": 1,
"status": "S",
"assignmentDate": "2026-07-16T18:20:00.000Z",
"approvalDate": null,
"observation": null,
"result": "",
"userApprover": {
"username": "lgomez",
"fullName": "Laura Gómez"
}
}
}

Response recurrente (isRecurrent: true):

{
"recurrence": {
"id": 812,
"username": "jperez",
"permissionReasonId": 7,
"startDate": "2026-08-03T00:00:00.000Z",
"endDate": "2026-12-31T00:00:00.000Z",
"intervalType": "W",
"recurrenceConfig": { "days": [1, 3, 5] },
"status": "P",
"observation": "Estudios - recurrencia semanal (Lun, Mié, Vie)",
"approverObservation": null,
"requestDate": "2026-07-16T18:20:00.000Z",
"approvalDate": null
},
"approverWorkflowRequest": [
{
"id": 6012,
"username": "jperez",
"alternateUser": "",
"type": "PERMISSIONS_RECURRENT",
"approverOrder": 1,
"status": "S",
"assignmentDate": "2026-07-16T18:20:00.000Z",
"approvalDate": null,
"observation": null,
"result": ""
}
],
"isRecurrent": true
}

Errores específicos:

Código Cuándo ocurre
400 endDate anterior a startDate.
404 permissionReasons no corresponde a un motivo activo ("Motivo de permiso no encontrado").
400 El motivo tiene overlapDate = N y el usuario ya tiene una solicitud pendiente/aprobada que se superpone en fechas ("Ya existe una solicitud de permiso para este usuario en este rango de fechas").
400 El motivo tiene hasAgreement = S y el usuario no tiene saldo de convenio disponible, o la solicitud excede el saldo disponible ("Saldo insuficiente. Saldo disponible: X días").
400 isRecurrent = true con un motivo cuyo allowsRecurrence es N ("Este motivo no permite permisos recurrentes").
400 El patrón de recurrencia no genera ninguna ocurrencia dentro del rango ("El patrón no genera ninguna ocurrencia").
400 El patrón de recurrencia genera más de 365 instancias ("Máximo 365 instancias permitidas").

PATCH /me/time-off/requests/permissions/{permissionId}

Sección titulada «PATCH /me/time-off/requests/permissions/{permissionId}»

Adjunta o reemplaza el archivo de respaldo de una solicitud existente. Se usa tanto para completar el adjunto de una solicitud creada sin archivo (motivo con hasAttachment = O) como para reemplazarlo.

Path param: permissionId (number, id de la solicitud).

Body (UpdatePermissionAttachmentDto):

Campo Tipo Obligatorio Descripción
attachedFile string No Nombre del archivo ya subido ({uuid}-{nombre}). Enviar vacío/omitir para quitar el adjunto.

Response: la entidad de la solicitud actualizada (no pasa por un DTO de whitelist como los demás endpoints, pero la consulta no trae relaciones — no incluye datos de usuario ni de motivo).

{
"id": 5238,
"personalActionId": -1,
"username": "jperez",
"requestDate": "2026-07-15T08:40:00.000Z",
"startDate": "2026-07-20T00:00:00.000Z",
"endDate": "2026-07-20T23:59:59.000Z",
"status": "P",
"isCompensatory": "N",
"observationRequester": "Trámite personal",
"attachedFile": "9f2ab1c0-1234-4c9d-8b21-abcde1234567-comprobante.pdf",
"adjustsVacation": "N",
"recurrenceId": null
}

Errores específicos:

Código Cuándo ocurre
404 La solicitud no existe o no pertenece al usuario autenticado.
400 La solicitud no está en estado P (Pendiente) ni A (Aprobada) ("Solo se pueden modificar solicitudes en estado pendiente o aprobadas").
400 attachedFile no cumple el formato {uuid}-{nombre} (error de validación del DTO).

DELETE /me/time-off/requests/permissions/{id}

Sección titulada «DELETE /me/time-off/requests/permissions/{id}»

Cancela una solicitud de permiso propia. Marca la solicitud como CANCELLED y cancela cualquier paso de aprobación pendiente asociado.

Path param: id (number, id de la solicitud).

Response: sin cuerpo relevante (confirmación de la cancelación).

Errores específicos:

Código Cuándo ocurre
404 La solicitud no existe o no pertenece al usuario autenticado.

GET /me/permission/generate (comprobante PDF)

Sección titulada «GET /me/permission/generate (comprobante PDF)»

Genera y devuelve la URL del comprobante PDF de una solicitud de permiso (servicio legacy). Se ofrece desde el historial, para cualquier solicitud, sin importar su estado.

Query param: permissionId (number, requerido).

Response: { "url": "https://.../SolicitudPermiso_5238.pdf" } (url puede venir null).

Errores específicos: 400 si falla la generación en el servicio legacy.

Estados de una solicitud (PermissionRequestStatusEnum)

Sección titulada «Estados de una solicitud (PermissionRequestStatusEnum)»
Valor Significado backend Texto sugerido Color sugerido
P Pendiente Pendiente warning / amarillo
A Aprobada Aprobada success / verde
V Validada Validada info / azul (el portal de referencia no tiene un mapeo explícito para este estado y cae en “desconocido”; la app debe definir un texto propio)
N No aprobada / Negada Rechazada error / rojo
C Cancelada Cancelada error / rojo
X Eliminada — (excluir de contadores y listados)

Al calcular contadores de estado (por ejemplo un resumen “Pendientes: 2, Aprobadas: 5…”), excluir siempre las solicitudes en X (eliminada) — no representan una solicitud visible para el usuario.

Catálogo de motivos: qué controla cada bandera

Sección titulada «Catálogo de motivos: qué controla cada bandera»
Bandera del motivo Controla en el formulario
units (H/D) Si el campo de fecha pide sólo día o día+hora, y en qué unidad se expresa la duración calculada.
hasTime + time Si existe, valida que la duración de esta solicitud no supere time unidades (en la unidad de limitBy).
isAnnual + annualTime Si isAnnual = S, valida que el acumulado del año (aprobadas + pendientes + esta solicitud) no supere annualTime.
hasAgreement Si S, el balance anual a mostrar/validar no es annualTime sino el saldo de convenio de vacaciones (assignedDays/usedDays de GET /me/time-off/vacation-agreement-balance).
hasAttachment N oculta el campo de adjunto; O lo muestra opcional; B lo muestra y lo exige antes de habilitar “Guardar”.
allowsRecurrence Muestra u oculta el switch “Es recurrente” y los campos de patrón.
overlapDate Informativo: si es N, anticipar que el backend puede rechazar con 400 si las fechas se solapan con otra solicitud pendiente/aprobada del mismo usuario.
isPocket, hasBlock Informativos, no accionados por el cliente de referencia ni por el backend en este flujo.
visibility Filtrar el catálogo a P (Portal) o A (Ambos) antes de mostrarlo — ver caution en el endpoint del catálogo.

Reglas de validación antes de habilitar “Guardar”

Sección titulada «Reglas de validación antes de habilitar “Guardar”»

El formulario de referencia calcula la duración en el cliente (no llama a un endpoint de cálculo) y deshabilita el botón de envío si alguna de estas reglas falla:

  1. Fecha/hora pasada: si el tenant tiene Controla_Fecha_Solicitud_Permisos = S, la fecha de inicio no puede ser anterior a hoy (o a la hora actual, si el motivo es por horas).
  2. Coherencia de unidades: si hasTime = S, units del motivo debe coincidir con limitBy; si no coinciden, se trata como configuración inválida y bloquea el envío.
  3. Duración distinta de cero: la duración calculada (días u horas según units) debe ser mayor a 0.
  4. Límite por solicitud: si hasTime = S y time no es null, la duración de esta solicitud no puede superar time.
  5. Límite anual: si isAnnual != N, el saldo restante (annualTime menos aprobadas y pendientes del mismo motivo en el año) debe alcanzar para esta solicitud, y el acumulado total no puede superar annualTime.
  6. Adjunto obligatorio: si hasAttachment = B, attachedFile debe tener un valor no vacío.

El adjunto se sube antes de enviar el formulario (flujo de subida con URL prefirmada, fuera de este documento) y el nombre resultante se envía como attachedFile en el POST; no existe un paso de “subir después de crear” salvo usando el PATCH de este módulo.

  • Selector de motivo: sólo motivos con visibility en P o A (ver caution del endpoint de catálogo).
  • Campos de hora (startTime/endTime): sólo si permissionReasons.units === 'H' del motivo seleccionado.
  • Sección de recurrencia: sólo si allowsRecurrence === 'S' del motivo seleccionado. Dentro de ella, los campos de patrón varían según recurrenceFrequency (days para Semanal, dayOfMonth para Mensual, interval para Personalizada).
  • Campo de adjunto: oculto si hasAttachment === 'N'; visible y opcional si 'O'; visible y obligatorio si 'B'.
  • Acción “Cancelar” en el historial: mostrar únicamente cuando status === 'P' (ver caution en el endpoint DELETE).
  • Acción “Adjuntar comprobante” en el historial: ocultar si hasAttachment === 'N' del motivo de esa solicitud, u ocultar si el estado es C (Cancelada) o N (Rechazada). En cualquier otro estado, mostrar “Adjuntar” si no hay archivo aún y “Ver adjunto” si ya existe.
  • Acción “Generar comprobante”: siempre disponible en el historial, sin depender del estado.
  • Sin solicitudes: requests/all/permissions devuelve [] — mostrar estado vacío (“Aún no tienes solicitudes de permiso”).
  • Sin motivos activos para la empresa: permissions-reasons/active devuelve [] — deshabilitar el formulario y mostrar un mensaje en vez de un selector vacío.
  • Motivo con hasAgreement = S y convenio no habilitado en el tenant: el saldo de convenio llega en cero (assignedDays, usedDays, availableDays todos 0) — mostrar el balance en cero, no un error; la validación de “Límite anual” bloqueará el envío igualmente si corresponde.
  • Estado V (Validada) en el historial: sin mapeo de color/texto explícito en el cliente de referencia — definir un texto propio en vez de dejar la etiqueta “desconocido” visible al usuario final.