Permisos
Resumen
Sección titulada «Resumen»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 tieneActiva_Acuerdo_Vacacioneshabilitado, 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
Endpoints
Sección titulada «Endpoints»GET /me/time-off/requests/all/permissions
Sección titulada «GET /me/time-off/requests/all/permissions»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.
POST /me/time-off/requests/permissions
Sección titulada «POST /me/time-off/requests/permissions»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 | Sí | — | Fecha/hora de inicio. |
endDate |
date | Sí | — | Fecha/hora de fin (no puede ser menor a startDate). |
permissionReasons |
string | Sí | — | 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.
Lógica de cliente
Sección titulada «Lógica de cliente»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:
- 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). - Coherencia de unidades: si
hasTime = S,unitsdel motivo debe coincidir conlimitBy; si no coinciden, se trata como configuración inválida y bloquea el envío. - Duración distinta de cero: la duración calculada (días u horas según
units) debe ser mayor a0. - Límite por solicitud: si
hasTime = Sytimeno esnull, la duración de esta solicitud no puede superartime. - Límite anual: si
isAnnual != N, el saldo restante (annualTimemenos aprobadas y pendientes del mismo motivo en el año) debe alcanzar para esta solicitud, y el acumulado total no puede superarannualTime. - Adjunto obligatorio: si
hasAttachment = B,attachedFiledebe 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.
Qué mostrar/ocultar según estado y motivo
Sección titulada «Qué mostrar/ocultar según estado y motivo»- Selector de motivo: sólo motivos con
visibilityenPoA(ver caution del endpoint de catálogo). - Campos de hora (
startTime/endTime): sólo sipermissionReasons.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únrecurrenceFrequency(dayspara Semanal,dayOfMonthpara Mensual,intervalpara 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 endpointDELETE). - Acción “Adjuntar comprobante” en el historial: ocultar si
hasAttachment === 'N'del motivo de esa solicitud, u ocultar si el estado esC(Cancelada) oN(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.
Estados vacíos y casos borde
Sección titulada «Estados vacíos y casos borde»- Sin solicitudes:
requests/all/permissionsdevuelve[]— mostrar estado vacío (“Aún no tienes solicitudes de permiso”). - Sin motivos activos para la empresa:
permissions-reasons/activedevuelve[]— deshabilitar el formulario y mostrar un mensaje en vez de un selector vacío. - Motivo con
hasAgreement = Sy convenio no habilitado en el tenant: el saldo de convenio llega en cero (assignedDays,usedDays,availableDaystodos0) — 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.