# Plan: Módulo RRHH — Adición Legajos Digitales (M23–M26)

**Fecha**: 2026-05-19
**Fuente funcional**: `Novasis_ERP_RRHH_Legajos_v1.pdf` (versión 1.0 — Mayo 2026)
**Documento base**: [`plan-rrhh.md`](./plan-rrhh.md) (M01–M15) + [`plan-rrhh-presentismo.md`](./plan-rrhh-presentismo.md) (M16–M22)
**Estándares**: [`PROJECT_STANDARDS.md`](./PROJECT_STANDARDS.md) y [`backend/CLAUDE.md`](../CLAUDE.md)
**Estado**: Implementado técnicamente — pendiente cierre operativo (smoke UI manual)

---

## Estado actual (2026-05-19)

- Implementación técnica completada en backend/frontend para M23–M26.
- Integraciones M14 y M18 implementadas y cubiertas en pruebas unitarias.
- Build backend/frontend en verde (incluyendo ajuste PWA).
- Evidencia técnica consolidada en `backend/docs/qa-legajos-evidencia-2026-05-19.md`.
- Pendiente para declarar cierre definitivo del módulo:
  - smoke funcional manual UI con usuarios/permisos reales (checklist Fase 5–8).

---

## Contexto

El módulo RRHH v1 (M01–M15) y la adición Presentismo v2 (M16–M22) están operativos. Hoy no existe un repositorio documental estructurado por funcionario: la única evidencia digital es el campo `foto_url` en [`rrhh_empleados`](../prisma/schema.prisma) (línea 6463) y archivos sueltos cargados ad-hoc para planillas externas y marcaciones, sin trazabilidad.

La adición **Legajos Digitales (M23–M26)** introduce:

- Cargar documentos de cualquier tipo (PDF/DOCX/XLSX/imágenes/comprimidos) categorizados, con fecha de vencimiento, alertas y versionado.
- Catálogo configurable de tipos de documento (con tipos del sistema bloqueados).
- Job diario de control de vencimientos + alertas escalonadas por email.
- Consultas y reportes por funcionario, tipo, fecha de carga y vencimiento, con exportación a Excel y PDF.
- Integración con M14 (Desvinculación) para verificar completitud antes del finiquito y con M18 (Permisos de Presentismo) para vincular certificados médicos a permisos.

El módulo no rompe nada existente: extiende `rrhh_empleados` solo por relación FK desde nuevas tablas y agrega un campo opcional `legajo_documento_id` a `rrhh_permisos_presentismo`. Todas las tablas nuevas llevan prefijo `rrhh_legajo_*`.

---

## Ajustes al PDF respecto del stack real

El PDF asume MinIO + Next.js. El stack real es **NestJS + Prisma + DigitalOcean Spaces (compatible S3) + React 18/Vite/JSX (no TS) + MUI v7**. El plan se adapta a este stack manteniendo todas las reglas funcionales del PDF.

| Tema | PDF | Decisión adoptada |
|---|---|---|
| Object storage | MinIO/S3 con SDK `minio` | **DigitalOcean Spaces** (S3 compatible) vía `@aws-sdk/client-s3` ya en uso. Mismo bucket actual con prefijo `legajos/{empresa_id}/...` y ACL `private`. Acceso por presigned URLs. |
| Estructura proyecto | `apps/api/modules/legajos/...` (Next.js mono-repo) | Patrón NestJS estándar: `src/rrhh/controllers/legajos*.controller.ts` + `src/rrhh/services/legajos*.service.ts`. |
| Rutas frontend | `apps/web/app/(rrhh)/legajos/...` (Next App Router) | **Pestaña padre nueva "Legajos"** dentro de `frontend/src/components/templates/RRHHTemplate.jsx` con sub-tabs (mismo patrón que Presentismo). |
| ORM | Sin especificar | Prisma migrations en `prisma/migrations/YYYYMMDD_*/migration.sql`, idempotentes (regla [`backend/CLAUDE.md`](../CLAUDE.md)). |
| Multipart | `multer` directo | `FileInterceptor` de `@nestjs/platform-express` (mismo patrón que [`planillas.controller.ts:74`](../src/rrhh/controllers/planillas.controller.ts#L74) y [`marcaciones.controller.ts:68`](../src/rrhh/controllers/marcaciones.controller.ts#L68)). |
| Email | nodemailer | `@nestjs-modules/mailer` ya configurado en [`src/mail/mail.service.ts`](../src/mail/mail.service.ts). Se agrega template `legajo-vencimiento.hbs`. |
| Scheduler | Cron genérico | BullMQ en [`src/queues/queues.module.ts`](../src/queues/queues.module.ts) (patrón ya usado por `sifen-sync` cada 5 min). |
| Exportación PDF | Puppeteer o @react-pdf | **Decisión de usuario: incluir en v1**. Se usa `@react-pdf/renderer` (frontend-side, sin instalar Puppeteer en el backend). |
| Foto de perfil | Coexistir o migrar | **Decisión de usuario: migrar en seed**. Script `rrhh-legajos-foto-migration.seed.ts` crea registro `FOTO_PERFIL` por cada empleado con `foto_url` no nula. |

---

## Decisiones de diseño

| # | Decisión | Resolución |
|---|---|---|
| 1 | Alcance v1 Legajos | M23–M26 completos con cat. de tipos, carga/versionado, alertas y consultas/exportación. |
| 2 | Naming de tablas | Prefijo `rrhh_legajo_*` (consistente con `rrhh_*` del módulo). Tres tablas nuevas + extensión M18. |
| 3 | Enums Prisma | Todos los estados/tipos como `enum` Prisma (regla PROJECT_STANDARDS punto 2). Sin `VARCHAR + // comentario`. |
| 4 | Storage | DigitalOcean Spaces (S3) con prefijo `legajos/{empresa_id}/{empleado_id}/{tipo_codigo}/{año}/{uuid}.{ext}` y ACL `private`. Nuevo `LegajosStorageService` que abstrae @aws-sdk/client-s3. |
| 5 | Presigned URLs | Default 15 min (env `LEGAJO_PRESIGNED_URL_EXPIRY_SEC=900`). Endpoints `/descargar` y `/preview` regeneran URL on-demand. Nunca se exponen URLs públicas permanentes. |
| 6 | Validación MIME | Por magic bytes con `file-type` (nueva dep). Extensión sola NO es suficiente (regla PDF 18). Rechaza con HTTP 415. |
| 7 | Tamaño máximo | Param `LEGAJO_TAMANIO_MAX_ARCHIVO_MB=20` editable en M15. Middleware `FileInterceptor({ limits: { fileSize } })` rechaza con HTTP 413. |
| 8 | Hash SHA-256 | Calculado server-side en streaming durante upload. Se almacena pero NO bloquea (solo alerta de duplicado en respuesta — regla PDF 25). |
| 9 | Versionado | Al cargar de un tipo que ya tiene ACTIVO sin `permite_multiples`: el anterior pasa a `REEMPLAZADO` y el nuevo apunta a él vía `documento_previo_id`. Trazabilidad permanente. |
| 10 | Anulación | Solo desde `ACTIVO`. El archivo permanece en storage. La purga física es manual (regla PDF 27). |
| 11 | Estado vencimiento | Columna `estado_vencimiento` materializada + recalculada en cada lectura crítica + job diario BullMQ 09:00. Idempotente. |
| 12 | Alertas escalonadas | Param `LEGAJO_DIAS_ANTICIPACION_EMAIL=30,15,7`. Tabla `rrhh_legajo_alertas_vencimiento` evita duplicados (unique compuesto). |
| 13 | Multi-empresa | Todas las tablas con `empresa_id` obligatorio + índice. Mismo RLS de RRHH. |
| 14 | Tipos del sistema | Sembrados en `seed.service.ts` con `es_del_sistema=true`. No editables/eliminables. SUPER_ADMIN puede desactivar (`activo=false`). |
| 15 | Foto de perfil | Migrar fotos de `rrhh_empleados.foto_url` → `rrhh_legajo_documentos` tipo `FOTO_PERFIL` en seed. Se mantiene `foto_url` como espejo para compatibilidad. |
| 16 | Integración M14 | Param `LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA=false` por defecto (solo advertencia). Si `true`, [`desvinculaciones.service.ts`](../src/rrhh/services/desvinculaciones.service.ts) verifica antes de aprobar/pagar y devuelve HTTP 422. |
| 17 | Integración M18 | Campo opcional `legajo_documento_id` en `rrhh_permisos_presentismo`. UI de permiso muestra dropdown buscable de documentos del legajo del empleado (filtrado por tipo `CERTIFICADO_MED` por default). |
| 18 | Frontend | Pestaña padre **Legajos** en `RRHHTemplate.jsx` con 4 sub-tabs: Legajo por funcionario, Dashboard vencimientos, Consulta avanzada, Catálogo de tipos. Botón "Ver legajo" en RowActions de `EmpleadosTab.jsx`. |
| 19 | Selectores buscables | Todo dropdown (funcionario, tipo, sucursal, departamento) usa `Autocomplete` MUI o `SearchableSelect` (regla `feedback_selectores_buscables`). |
| 20 | Diálogos | Confirmaciones (anular, reemplazar) → `ConfirmDialog` + `useConfirmDialog`. Captura de motivo → `PromptDialog`. Nunca `window.confirm/alert/prompt` (regla `feedback_no_alert_browser`). |
| 21 | Tour y onboarding | Extender `rrhhTour.js` con pasos para Legajos. Slide 5 en `RRHHOnboardingDialog`. |
| 22 | AI Dashboard | Registrar área `RRHH / Legajos` en [`src/ai-dashboard/chat/schema-context.ts`](../src/ai-dashboard/chat/schema-context.ts) con tablas y ejemplos SQL (regla PROJECT_STANDARDS punto 15). |
| 23 | Exportación | Excel con `exceljs` (ya en stack). PDF con `@react-pdf/renderer` (frontend-side, evita Puppeteer en backend). |
| 24 | Estrategia entrega | 5 fases incrementales (~16 semanas) alineadas al PDF. |

---

## Alcance funcional (M23–M26)

| Módulo | Descripción | Prioridad |
|---|---|---|
| M23 | Catálogo de Tipos de Documento (sistema + personalizados) | Alta |
| M24 | Gestión de Legajo por Funcionario (carga, descarga, versionado, anulación) | Crítica |
| M25 | Control de Vencimientos y Alertas (job diario + email escalonado) | Alta |
| M26 | Consultas y Reportes con exportación Excel/PDF + Dashboard | Alta |

---

## Parámetros nuevos en `rrhh_parametros_sistema`

Sembrados vía `seed.service.ts` con vigencia `2026-06-01`:

| Clave | Default | Categoría | Editable |
|---|---|---|---|
| `LEGAJO_DIAS_ALERTA_VENCIMIENTO` | `30` | LEGAJOS | ✅ |
| `LEGAJO_TAMANIO_MAX_ARCHIVO_MB` | `20` | LEGAJOS | ✅ |
| `LEGAJO_MIME_HABILITADOS` | (CSV) | LEGAJOS | ✅ |
| `LEGAJO_ALERTAS_EMAIL_HABILITADO` | `true` | LEGAJOS | ✅ |
| `LEGAJO_DIAS_ANTICIPACION_EMAIL` | `30,15,7` | LEGAJOS | ✅ |
| `LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA` | `false` | LEGAJOS | ✅ |
| `LEGAJO_PRESIGNED_URL_EXPIRY_SEC` | `900` | LEGAJOS | ✅ |

Reglas no parametrizables (validadas en código):

- `es_del_sistema=true` ⇒ inmutable (HTTP 403).
- ANULADO no se reactiva, no se reemplaza (HTTP 409).
- Solo `AUDITOR` y `SUPER_ADMIN` ven historial de REEMPLAZADOS/ANULADOS.

---

## Modelo de datos

### Enums Prisma (nuevos)

```prisma
enum rrhh_legajo_documento_estado { ACTIVO REEMPLAZADO ANULADO }
enum rrhh_legajo_vencimiento_estado { SIN_VENCIMIENTO VIGENTE POR_VENCER VENCIDO }
enum rrhh_legajo_alerta_canal { EMAIL NOTIFICACION_INTERNA AMBOS }
enum rrhh_legajo_alerta_estado { ENVIADO ERROR }
```

### 1) `rrhh_legajo_tipos_documento` (M23)

```sql
CREATE TABLE rrhh_legajo_tipos_documento (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID REFERENCES empresas(id),               -- NULL = tipo del sistema (global)
  codigo VARCHAR(30) NOT NULL,
  nombre VARCHAR(150) NOT NULL,
  descripcion TEXT,
  admite_vencimiento BOOLEAN NOT NULL DEFAULT false,
  vencimiento_obligatorio BOOLEAN NOT NULL DEFAULT false,
  es_obligatorio BOOLEAN NOT NULL DEFAULT false,
  dias_alerta_vencimiento INT,                           -- NULL = usa LEGAJO_DIAS_ALERTA_VENCIMIENTO
  permite_multiples BOOLEAN NOT NULL DEFAULT false,
  es_del_sistema BOOLEAN NOT NULL DEFAULT false,
  activo BOOLEAN NOT NULL DEFAULT true,
  creado_por UUID,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE NULLS NOT DISTINCT (empresa_id, codigo)         -- empresa_id NULL único entre sistema
);
CREATE INDEX ON rrhh_legajo_tipos_documento (empresa_id, activo);
```

**Seed sistema** (14 tipos, sección 3.2 PDF):
`CI, CONTRATO, ALTA_IPS, FOTO_PERFIL, CERTIFICADO_MED, TITULO_HABILITANTE, ANTECEDENTE_POL, ANTECEDENTE_JUD, RUC, MODIF_CONTRATO, RECIBO_SUELDO, CERT_BANCARIO, DESVINCULACION, OTRO`.

### 2) `rrhh_legajo_documentos` (M24)

```sql
CREATE TABLE rrhh_legajo_documentos (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  tipo_documento_id UUID NOT NULL REFERENCES rrhh_legajo_tipos_documento(id),
  nombre_original VARCHAR(255) NOT NULL,
  nombre_storage VARCHAR(500) NOT NULL,                  -- ruta canónica en DO Spaces
  mime_type VARCHAR(100) NOT NULL,
  extension VARCHAR(10) NOT NULL,
  tamanio_bytes BIGINT NOT NULL,
  hash_sha256 CHAR(64) NOT NULL,
  fecha_documento DATE,
  fecha_vencimiento DATE,
  estado_vencimiento rrhh_legajo_vencimiento_estado NOT NULL DEFAULT 'SIN_VENCIMIENTO',
  descripcion TEXT,
  version INT NOT NULL DEFAULT 1,
  documento_previo_id UUID REFERENCES rrhh_legajo_documentos(id),
  estado rrhh_legajo_documento_estado NOT NULL DEFAULT 'ACTIVO',
  subido_por UUID NOT NULL,
  subido_en TIMESTAMP NOT NULL DEFAULT NOW(),
  anulado_por UUID,
  anulado_en TIMESTAMP,
  motivo_anulacion TEXT
);

-- Índices (sección 6.4 PDF)
CREATE INDEX idx_rrhh_legajo_emp_tipo
  ON rrhh_legajo_documentos (empleado_id, tipo_documento_id, estado);
CREATE INDEX idx_rrhh_legajo_vencimiento
  ON rrhh_legajo_documentos (fecha_vencimiento, estado_vencimiento, empresa_id)
  WHERE estado = 'ACTIVO';
CREATE INDEX idx_rrhh_legajo_subido_en
  ON rrhh_legajo_documentos (subido_en, empresa_id);
CREATE INDEX idx_rrhh_legajo_hash
  ON rrhh_legajo_documentos (hash_sha256);
```

### 3) `rrhh_legajo_alertas_vencimiento` (M25)

```sql
CREATE TABLE rrhh_legajo_alertas_vencimiento (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  empresa_id UUID NOT NULL REFERENCES empresas(id),
  documento_id UUID NOT NULL REFERENCES rrhh_legajo_documentos(id),
  empleado_id UUID NOT NULL REFERENCES rrhh_empleados(id),
  dias_para_vencer INT NOT NULL,
  canal rrhh_legajo_alerta_canal NOT NULL,
  destinatarios JSONB NOT NULL,
  estado_envio rrhh_legajo_alerta_estado NOT NULL,
  enviado_en TIMESTAMP NOT NULL DEFAULT NOW(),
  error_detalle TEXT,
  UNIQUE (documento_id, dias_para_vencer, canal)         -- evita duplicados
);
CREATE INDEX ON rrhh_legajo_alertas_vencimiento (empresa_id, enviado_en);
```

### 4) Extensión `rrhh_permisos_presentismo` (integración M18)

```sql
ALTER TABLE rrhh_permisos_presentismo
  ADD COLUMN legajo_documento_id UUID REFERENCES rrhh_legajo_documentos(id);
CREATE INDEX ON rrhh_permisos_presentismo (legajo_documento_id);
```

---

## Almacenamiento de archivos — Estructura

Mismo bucket `envs.doSpacesBucket` (acceso por presigned URL, sin ACL público):

```
legajos/{empresa_id}/{empleado_id}/{tipo_codigo}/{anio}/{uuid_documento}.{ext}
```

Ejemplo: `legajos/emp-001/func-042/CI/2026/a3f9c1d2-....pdf`.

**`LegajosStorageService`** (`src/rrhh/services/legajos-storage.service.ts`) expone:

- `upload(buffer, key, mime): Promise<{ key, etag, size }>` — usa `PutObjectCommand` con `ACL: 'private'`.
- `presignedDownloadUrl(key, filename, expirySec): Promise<string>` — `Content-Disposition: attachment`.
- `presignedPreviewUrl(key, mime, expirySec): Promise<string>` — `Content-Disposition: inline` (solo PDF/imagen).
- `delete(key): Promise<void>` — solo para purga manual del SUPER_ADMIN; nunca llamado por el flujo automático.

Reutiliza el cliente `S3Client` ya configurado con `envs.doSpaces*` (ver [`empresas.controller.ts:318-338`](../src/empresas/empresas.controller.ts#L318-L338)).

---

## Reglas de negocio obligatorias (backend, no solo UI)

1. ANULADO no se reactiva ni reemplaza ⇒ HTTP 409 (regla PDF 16).
2. Tipos del sistema no se eliminan ni renombran ⇒ HTTP 403 (regla 17).
3. Validación MIME por magic bytes (`file-type`), no por extensión ⇒ HTTP 415 (regla 18).
4. Tamaño validado vía `FileInterceptor.limits.fileSize` antes del stream ⇒ HTTP 413 (regla 19).
5. Presigned URLs expiran (default 15 min). No se exponen URLs permanentes (regla 20).
6. Anular REEMPLAZADO solo `SUPER_ADMIN` con justificación (regla 21).
7. Cadena de versiones trazable hacia atrás vía `documento_previo_id` (regla 22).
8. Si `LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA=true`, M14 verifica completitud antes del finiquito ⇒ HTTP 422 (regla 23).
9. Job de vencimientos idempotente (regla 24).
10. Hash SHA-256 solo alerta, no bloquea (regla 25).
11. Historial REEMPLAZADO/ANULADO solo `AUDITOR` y `SUPER_ADMIN` (regla 26).
12. Purga física manual, nunca automática (regla 27).
13. RLS multi-empresa: queries siempre `WHERE empresa_id = $user.empresa_id`.
14. Reemplazo: insert nuevo `ACTIVO` + `UPDATE` del anterior a `REEMPLAZADO` en una transacción Prisma.

---

## API REST (v1, prefijo `/api/v1/rrhh/legajos`)

| Método | Ruta | Permiso |
|---|---|---|
| GET | `/tipos` | `RRHH_LEGAJOS_CONSULTA` |
| POST | `/tipos` | `RRHH_LEGAJOS_CONFIG` |
| PUT | `/tipos/:id` | `RRHH_LEGAJOS_CONFIG` |
| DELETE | `/tipos/:id` | `RRHH_LEGAJOS_CONFIG` |
| GET | `/empleados/:empleadoId` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/empleados/:empleadoId/completitud` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/empleados/:empleadoId/historial` | `RRHH_LEGAJOS_AUDITORIA` |
| POST | `/empleados/:empleadoId/documentos` | `RRHH_LEGAJOS_CARGA` |
| GET | `/documentos/:documentoId` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/documentos/:documentoId/descargar` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/documentos/:documentoId/preview` | `RRHH_LEGAJOS_CONSULTA` |
| PUT | `/documentos/:documentoId/reemplazar` | `RRHH_LEGAJOS_CARGA` |
| PUT | `/documentos/:documentoId/anular` | `RRHH_LEGAJOS_ANULAR` |
| GET | `/consulta` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/consulta/exportar` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/dashboard/vencimientos` | `RRHH_LEGAJOS_CONSULTA` |
| GET | `/dashboard/legajos-incompletos` | `RRHH_LEGAJOS_CONSULTA` |
| POST | `/jobs/actualizar-estados-vencimiento` | `RRHH_LEGAJOS_CONFIG` |

Todos: `@UseGuards(AuthGuard('jwt'), PermissionGuard)` + `@RequirePermission('RRHH', '...')`.

`POST /empleados/:empleadoId/documentos` usa `@UseInterceptors(FileInterceptor('archivo', { limits: { fileSize } }))` (mismo patrón que `marcaciones.controller.ts:68`).

---

## Permisos nuevos (sembrados en `seed.service.ts`)

| Código | Descripción |
|---|---|
| `RRHH_LEGAJOS_CONSULTA` | Ver legajos y dashboard |
| `RRHH_LEGAJOS_CARGA` | Cargar y reemplazar documentos |
| `RRHH_LEGAJOS_ANULAR` | Anular documentos ACTIVOS |
| `RRHH_LEGAJOS_CONFIG` | Gestionar tipos personalizados y ejecutar jobs |
| `RRHH_LEGAJOS_AUDITORIA` | Ver historial REEMPLAZADO/ANULADO |

Asignación a roles existentes:

| Rol | Permisos |
|---|---|
| `SUPER_ADMIN` | Todos |
| `GERENTE_RRHH` | CONSULTA + CARGA + ANULAR + CONFIG |
| `OPERADOR_RRHH` | CONSULTA + CARGA |
| `SUPERVISOR` | CONSULTA (limitada a su equipo) |
| `EMPLEADO` | CONSULTA (solo propios) |
| `AUDITOR` | CONSULTA + AUDITORIA (solo lectura) |

---

## Frontend

### Estructura de pestañas

Nueva pestaña padre **Legajos** en `frontend/src/components/templates/RRHHTemplate.jsx` con sub-tabs:

1. **Legajo por Funcionario** (`LegajoFuncionarioTab.jsx`) — selector buscable de empleado → panel de completitud + listado por tipo + acciones (Descargar, Ver, Reemplazar, Anular).
2. **Dashboard de Vencimientos** (`LegajosDashboardTab.jsx`) — vistas: Vencidos, Por vencer, Legajos incompletos. Filtros por sucursal/departamento/tipo + exportación Excel.
3. **Consulta Avanzada** (`LegajosConsultaTab.jsx`) — todos los filtros de sección 6.1 PDF, 3 vistas de resultado (Funcionario / Documento / Tipos faltantes), exportación Excel + PDF.
4. **Catálogo de Tipos** (`LegajosTiposTab.jsx`) — CRUD de tipos personalizados; tipos del sistema visibles pero no editables.

Adicionalmente: en `frontend/src/components/rrhh/EmpleadosTab.jsx` `RowActions`, agregar botón **"Ver legajo"** que navega al sub-tab **Legajo por Funcionario** con el empleado preseleccionado.

### Componentes nuevos

Bajo `frontend/src/components/rrhh/legajos/`:

- `LegajoFuncionarioTab.jsx`
- `LegajosDashboardTab.jsx`
- `LegajosConsultaTab.jsx`
- `LegajosTiposTab.jsx`
- `LegajoUploadDialog.jsx` — formulario de carga con dropzone, validación cliente y barra de progreso.
- `LegajoVersionHistoryDialog.jsx` — solo para AUDITOR/SUPER_ADMIN.
- `LegajoCompletitudPanel.jsx` — indicador visual de tipos obligatorios presentes/faltantes/vencidos.
- `LegajoDocumentRow.jsx` — fila reutilizable con acciones.
- `LegajoPdfExport.jsx` — generador PDF con `@react-pdf/renderer`.

### Servicio API

Nuevo `frontend/src/api/rrhh-legajos.service.js` (mismo patrón que `rrhh-presentismo.service.js`):

```js
// métodos: listarTipos, crearTipo, actualizarTipo, eliminarTipo,
// obtenerLegajo, obtenerCompletitud, obtenerHistorial,
// subirDocumento (FormData), obtenerDocumento, descargarUrl, previewUrl,
// reemplazarDocumento, anularDocumento,
// consultar, exportar, dashboardVencimientos, dashboardIncompletos,
// ejecutarJobVencimientos.
```

### Reglas UX aplicadas (PROJECT_STANDARDS + memorias)

- **MonedaInput**: no aplica.
- **Selectores buscables**: `Autocomplete`/`SearchableSelect` para empleado, tipo, sucursal, departamento (regla `feedback_selectores_buscables`).
- **Diálogos**: `ConfirmDialog`+`useConfirmDialog` para anular/reemplazar; `PromptDialog` para motivo de anulación. Nunca `window.confirm/alert/prompt`.
- **Feedback inmediato**: toasts `notistack` para éxito; `InlineValidationBanner` para errores de upload.
- **Empty states**: `RRHHEmptyState` en cada sub-tab.
- **Guía contextual**: `RRHHGuia` con contenido nuevo en `rrhhGuias.js` (sección `legajos*`).
- **Prereq checklist**: `RRHHPrereqChecklist` lista `Tipos del sistema sembrados`, `Empleados activos`, `Bucket DO Spaces accesible`.
- **Tour**: 4 pasos nuevos en `rrhhTour.js` (uno por sub-tab) + slide 5 en `RRHHOnboardingDialog`.
- **Responsive**: tabla con virtualización para >500 docs; vista compacta en `<600px`.
- **Accesibilidad**: `aria-describedby` en inputs de upload; focus-visible.
- **Drag & drop**: dropzone en `LegajoUploadDialog` (sin nueva dep — usar HTML5 nativo + MUI styling).

---

## Integración con módulos existentes

| Módulo | Punto | Detalle |
|---|---|---|
| **M01 — Empleados** | `foto_url` | Seed `rrhh-legajos-foto-migration.seed.ts` recorre empleados con `foto_url` y crea documento `FOTO_PERFIL` ACTIVO referenciando misma URL (sin re-subida). Nuevos uploads de foto pasan por el flujo del legajo y actualizan ambos campos. |
| **M14 — Desvinculación** | [`desvinculaciones.service.ts`](../src/rrhh/services/desvinculaciones.service.ts) | En `aprobar()` y `pagar()`: si `LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA=true`, llamar a `LegajosService.verificarCompletitud(empleadoId)`. Bloquea con HTTP 422 + detalle. Si `false`, devuelve warning en `advertencias[]` (patrón existente). |
| **M15 — Parámetros** | `rrhh_parametros_sistema` | 7 claves nuevas categoría `LEGAJOS`, editables desde `ParametrosTab.jsx`. |
| **M18 — Permisos Presentismo** | `legajo_documento_id` | UI del permiso muestra dropdown opcional de documentos del legajo (filtrados por tipo `CERTIFICADO_MED` por default). Al cargar permiso médico ⇒ link al legajo del empleado. |
| **AI Dashboard** | [`schema-context.ts`](../src/ai-dashboard/chat/schema-context.ts) | Nueva sección `// ── 12. RRHH / LEGAJOS ─` con 3 tablas + 5 ejemplos SQL (docs vencidos del mes, legajos incompletos, docs por tipo, top empleados con más versiones, alertas enviadas última semana). |
| **Auditoría** | `@Auditable('rrhh_legajo_*')` | Cada entidad nueva auditable. |
| **Mail** | [`src/mail/templates/`](../src/mail/templates/) | Template `legajo-vencimiento.hbs` con tabla de documentos por vencer agrupados por empleado. Nuevo método `MailService.sendLegajoVencimientoAlert()`. |
| **Queues** | [`src/queues/queues.module.ts`](../src/queues/queues.module.ts) | Nueva cola `legajos-queue` con processor `legajos-vencimiento.processor.ts`. Job repeatable cron `0 9 * * *` (09:00 diario). |
| **Tours frontend** | `rrhhTour.js` | 4 pasos nuevos + slide 5 `RRHHOnboardingDialog`. |

---

## Plan de implementación

### Fase 1 — Infraestructura y datos (Semanas 1–3)

- Migración `prisma/migrations/20260601_rrhh_legajos_base/migration.sql` con enums + 3 tablas + índices (idempotente, `IF NOT EXISTS` y `DO $$ ... duplicate_object ...`).
- Migración `prisma/migrations/20260602_rrhh_permisos_presentismo_legajo/migration.sql` para `legajo_documento_id`.
- Seed de 14 tipos del sistema en `seed.service.ts` (idempotente por `codigo`).
- Seed de 7 parámetros nuevos categoría `LEGAJOS` en `rrhh_parametros_sistema`.
- Seed de 5 permisos nuevos asignados a roles existentes.
- `src/rrhh/services/legajos-storage.service.ts` con upload/presigned URLs/delete.
- `src/rrhh/services/legajos-mime.validator.ts` usando `file-type` (nueva dep).
- Variable env `LEGAJO_PRESIGNED_URL_EXPIRY_SEC` agregada al schema Joi de `src/config/envs.ts`.
- AI Dashboard: agregar área `RRHH / Legajos` a `schema-context.ts`.

### Fase 2 — Catálogo y carga de documentos (Semanas 4–6)

- `legajos-tipos.controller.ts` + `legajos-tipos.service.ts` con CRUD y validaciones (`es_del_sistema` bloqueado).
- `legajos-documentos.controller.ts` + `legajos-documentos.service.ts`:
  - Endpoint multipart de carga (FileInterceptor + validador MIME + cálculo hash en streaming + insert + lógica de versionado en transacción).
  - Endpoint de descarga/preview con presigned URLs.
  - Endpoint de reemplazo (idempotente: detecta duplicado por hash + `tipo_documento_id` + `empleado_id`).
  - Endpoint de anulación con motivo obligatorio.
- DTOs en `src/rrhh/dto/legajos.dto.ts` (validados con class-validator + class-transformer ya en stack).
- Tests unitarios: `legajos-documentos.service.spec.ts` y `legajos-tipos.service.spec.ts` con mock de `LegajosStorageService` (cobertura objetivo ≥ 80%).
- Pantalla `LegajoFuncionarioTab.jsx` + `LegajoUploadDialog.jsx` + `LegajoDocumentRow.jsx` + `LegajoCompletitudPanel.jsx`.
- Pantalla `LegajosTiposTab.jsx`.
- Botón "Ver legajo" en `EmpleadosTab.jsx` RowActions.

### Fase 3 — Vencimientos y alertas (Semanas 7–9)

- `src/queues/processors/legajos-vencimiento.processor.ts` con job repeatable cron `0 9 * * *`.
- Lógica del job: recalcula `estado_vencimiento` para todos los `ACTIVO` con `fecha_vencimiento` no nula; emite alertas escalonadas según `LEGAJO_DIAS_ANTICIPACION_EMAIL`; registra en `rrhh_legajo_alertas_vencimiento`.
- Template `src/mail/templates/legajo-vencimiento.hbs` + método `MailService.sendLegajoVencimientoAlert()`.
- Endpoint `POST /jobs/actualizar-estados-vencimiento` para ejecución manual.
- Endpoints `GET /dashboard/vencimientos` y `GET /dashboard/legajos-incompletos`.
- Pantalla `LegajosDashboardTab.jsx` (3 vistas + filtros + exportación Excel).
- Tests del job con Jest `useFakeTimers` (cobertura ≥ 80%).

### Fase 4 — Consultas, reportes e integraciones (Semanas 10–13)

- Endpoint `GET /consulta` con todos los filtros (sección 6.1 PDF) y paginación cursor-based (max 100/página).
- Endpoint `GET /consulta/exportar?formato=excel|pdf` con `exceljs` (backend) — el PDF se genera en frontend con `@react-pdf/renderer` desde el resultado de `/consulta`.
- Pantalla `LegajosConsultaTab.jsx` con las 3 vistas de resultado + exportación.
- Integración M14: ajuste de [`desvinculaciones.service.ts`](../src/rrhh/services/desvinculaciones.service.ts) (`aprobar()`, `pagar()`) — llamada a `LegajosService.verificarCompletitud()`.
- Integración M18: actualizar UI de `PermisosPresentismoTab.jsx` con selector de documento del legajo (filtro default `CERTIFICADO_MED`).
- Seed `rrhh-legajos-foto-migration.seed.ts` ejecutable manualmente por empresa.
- `rrhhTour.js`: 4 pasos nuevos.
- `RRHHOnboardingDialog`: slide 5 "Legajos".
- `rrhhGuias.js`: contenido nuevo sección `legajos*`.

### Fase 5 — QA y rollout (Semanas 14–16)

- Plan de pruebas (sección siguiente) ejecutado en empresa Demo RRHH (`pnpm start:dev` + `pnpm dev`).
- Documento de evidencias en `backend/docs/qa-legajos-evidencia-YYYY-MM-DD.md`.
- Pruebas de seguridad: presigned URL expirada ⇒ 403; usuario sin permiso ⇒ 403; archivo .exe renombrado .pdf ⇒ 415; multi-empresa (Empresa A no ve docs de Empresa B).
- Prueba de carga: subir archivo 20 MB y verificar tiempo de upload/preview < 5 s en LAN.
- Documentación Swagger con ejemplos por endpoint (visible en `/docs`).
- Smoke test producción tras deploy + capacitación al equipo de RRHH.

---

## Plan de pruebas — modo dev

> Ejecutar con `pnpm start:dev` (backend) + `pnpm dev` (frontend) sobre **Demo RRHH** con 20+ empleados activos. Mensajes funcionales (no stack trace) ante fallos.

### FASE 1 — Catálogo de tipos (M23)

1.1. **Listar tipos del sistema**: GET `/tipos` devuelve 14 tipos con `es_del_sistema=true`.
1.2. **Crear tipo personalizado**: `Acuerdo de confidencialidad` con `admite_vencimiento=true`. Persiste con `es_del_sistema=false`.
1.3. **Intentar editar tipo del sistema**: PUT `/tipos/:id_CI` ⇒ HTTP 403 "Tipo del sistema no editable".
1.4. **Intentar eliminar tipo del sistema**: DELETE `/tipos/:id_CI` ⇒ HTTP 403.
1.5. **Desactivar tipo personalizado**: DELETE `/tipos/:id_personalizado` ⇒ `activo=false`. Documentos ya cargados con ese tipo siguen accesibles.

### FASE 2 — Carga, versionado y anulación (M24)

2.1. **Cargar PDF válido < 20 MB**: Upload de contrato PDF tipo `CONTRATO`. Persiste con hash SHA-256, ruta canónica en storage, `estado=ACTIVO`, `version=1`.
2.2. **Cargar segundo CONTRATO**: el anterior pasa a `REEMPLAZADO`, nuevo apunta a él vía `documento_previo_id`, `version=2`.
2.3. **Anular ACTIVO**: PUT `/anular` con motivo. Estado `ANULADO`. El archivo permanece en storage. No aparece en listado normal del legajo.
2.4. **Intentar anular REEMPLAZADO** (con `OPERADOR_RRHH`): HTTP 403. Con `SUPER_ADMIN` + motivo: éxito.
2.5. **Re-anular ANULADO**: HTTP 409.
2.6. **MIME inválido**: subir `.exe` con extensión `.pdf` (magic bytes ejecutable) ⇒ HTTP 415 con mensaje "Tipo de archivo no permitido".
2.7. **Tamaño excedido**: subir archivo 25 MB con `LEGAJO_TAMANIO_MAX_ARCHIVO_MB=20` ⇒ HTTP 413 antes del stream completo.
2.8. **Vencimiento obligatorio**: cargar tipo `CI` sin `fecha_vencimiento` ⇒ HTTP 400 "fecha_vencimiento requerida para CI".
2.9. **Hash duplicado**: subir mismo archivo dos veces para mismo empleado+tipo ⇒ alerta `documento_duplicado_detectado=true` en response, **no bloquea**.
2.10. **Presigned URL expira**: solicitar URL de preview, esperar `LEGAJO_PRESIGNED_URL_EXPIRY_SEC+10` segundos, intentar acceder ⇒ 403 desde DO Spaces.
2.11. **Empleado sin acceso a legajo ajeno**: usuario `EMPLEADO X` solicita `/empleados/Y/...` ⇒ HTTP 403.

### FASE 3 — Vencimientos y alertas (M25)

3.1. **Estado calculado en carga**: subir doc tipo `CI` con `fecha_vencimiento = hoy + 100 días`, default `dias_alerta=30` ⇒ `estado_vencimiento=VIGENTE`.
3.2. **Por vencer**: con `fecha_vencimiento = hoy + 20 días` ⇒ `estado_vencimiento=POR_VENCER`.
3.3. **Vencido**: `fecha_vencimiento = ayer` ⇒ `estado_vencimiento=VENCIDO`.
3.4. **Job idempotente**: POST `/jobs/actualizar-estados-vencimiento` dos veces seguidas ⇒ mismos contadores; no genera alertas duplicadas (constraint UNIQUE).
3.5. **Alerta escalonada**: documento con vencimiento en 30 días, ejecutar job ⇒ se registra alerta en `rrhh_legajo_alertas_vencimiento` con `dias_para_vencer=30`, email enviado. Re-ejecutar mismo día: no se duplica.
3.6. **Dashboard vencidos**: GET `/dashboard/vencimientos` lista docs `VENCIDO` agrupados por funcionario.
3.7. **Legajos incompletos**: empleado sin `CI` (obligatorio) ⇒ aparece en `/dashboard/legajos-incompletos`.

### FASE 4 — Consultas y reportes (M26)

4.1. **Consulta por funcionario**: filtro `empleado_id=X` ⇒ solo docs de X.
4.2. **Consulta por estado de vencimiento**: `estado_vencimiento=POR_VENCER` ⇒ lista correcta.
4.3. **Consulta combinada**: tipo=`CI` + sucursal=Matriz + `fecha_vence_hasta=fin_de_mes` ⇒ resultado consistente.
4.4. **Paginación**: con >100 resultados, response devuelve `cursor_next` y la siguiente página continúa sin solaparse.
4.5. **Exportación Excel**: GET `/consulta/exportar?formato=excel` ⇒ xlsx con headers de empresa, mismas filas que la UI.
4.6. **Exportación PDF**: frontend `LegajoPdfExport` genera PDF formateado con `@react-pdf/renderer` desde el resultado.

### FASE 5 — Integraciones (M14, M18)

5.1. **Desvinculación con legajo incompleto + `BLOQUEA=false`**: aprobar desvinculación ⇒ éxito con `advertencias[]` listando obligatorios faltantes/vencidos.
5.2. **Desvinculación con legajo incompleto + `BLOQUEA=true`**: aprobar ⇒ HTTP 422 con detalle. Cargar los faltantes ⇒ aprobar funciona.
5.3. **Permiso médico con legajo_documento_id**: crear permiso `JORNADA_COMPLETA` justificación `MEDICA` y adjuntar doc `CERTIFICADO_MED` del legajo. Persiste el link.
5.4. **Migración de fotos**: ejecutar seed `rrhh-legajos-foto-migration.seed.ts` ⇒ cada empleado con `foto_url` tiene doc `FOTO_PERFIL` ACTIVO.

### FASE 6 — Seguridad y multi-empresa

6.1. **Usuario sin `RRHH_LEGAJOS_CARGA`** intenta POST de documento ⇒ HTTP 403.
6.2. **Usuario sin `RRHH_LEGAJOS_AUDITORIA`** intenta GET historial ⇒ HTTP 403.
6.3. **Aislamiento multi-empresa**: usuario Empresa A solicita `/empleados/:id` con un `id` de Empresa B ⇒ HTTP 404 (no expone existencia).
6.4. **Auditoría**: cada carga/reemplazo/anulación queda en `audit_logs` con `entity_type='rrhh_legajo_documentos'`.

### FASE 7 — UX y onboarding

7.1. **Onboarding actualizado**: con localStorage limpio, entrar a RRHH ⇒ slide 5 "Legajos" visible.
7.2. **Tour cubre Legajos**: tour overview visita las 4 sub-tabs.
7.3. **Empty states**: empleado sin documentos ⇒ `RRHHEmptyState` con CTA "Cargar primer documento".
7.4. **Diálogos no nativos**: anular/reemplazar usan `ConfirmDialog` y `PromptDialog`. Ninguno usa `window.confirm/alert/prompt`.
7.5. **Selectores buscables**: dropdown de empleado/tipo/sucursal usan `Autocomplete`/`SearchableSelect` con búsqueda por texto.
7.6. **Responsive**: viewport `<600px` ⇒ tabla colapsa a vista compacta; dropzone se adapta.
7.7. **Drag & drop**: arrastrar archivo al dropzone ⇒ formulario se rellena con nombre y tipo MIME detectado.

### FASE 8 — AI Dashboard

8.1. **Pregunta**: "¿Cuántos legajos están incompletos este mes?" ⇒ IA usa área `RRHH / Legajos`, devuelve cifra correcta.
8.2. **Pregunta**: "Lista documentos vencidos por funcionario" ⇒ IA genera SQL contra `rrhh_legajo_documentos`.
8.3. **Pregunta**: "¿Cuántas alertas se enviaron la última semana?" ⇒ IA consulta `rrhh_legajo_alertas_vencimiento`.

---

## Verificación end-to-end (golden path)

1. Levantar backend (`pnpm start:dev`) y frontend (`pnpm dev`). Confirmar conexión a DO Spaces y Redis en logs.
2. Login con `SUPER_ADMIN` de Demo RRHH. Verificar permisos `RRHH_LEGAJOS_*` visibles en su rol.
3. Ir a **RRHH › Legajos › Catálogo de Tipos**: confirmar 14 tipos del sistema sembrados.
4. Crear tipo personalizado `Acuerdo NDA` y verificar que aparece editable.
5. Ir a **RRHH › Empleados**, en cualquier empleado activo click **"Ver legajo"** ⇒ navega a sub-tab **Legajo por Funcionario** con empleado preseleccionado.
6. Cargar 3 documentos: `CI` con vencimiento +180 días, `CONTRATO` sin vencimiento, `CERTIFICADO_MED` con vencimiento ayer (VENCIDO).
7. Confirmar:
   - Panel de completitud muestra `CI ✓`, `CONTRATO ✓`, `ALTA_IPS ✗ (faltante)`.
   - `CERTIFICADO_MED` aparece con badge rojo `VENCIDO`.
   - Click `Ver` en CI abre preview inline en el navegador (presigned URL).
   - Click `Descargar` baja el archivo con nombre original.
8. Reemplazar `CONTRATO` con un nuevo PDF ⇒ versión 2 ACTIVA, versión 1 visible solo desde "Historial" (con `SUPER_ADMIN`).
9. Anular `CERTIFICADO_MED` con motivo ⇒ desaparece del listado normal.
10. Ir a **Dashboard de Vencimientos**: aparece el empleado en "Legajos incompletos" (falta ALTA_IPS).
11. Ir a **Consulta Avanzada**: filtrar por sucursal Matriz + estado `VENCIDO` ⇒ resultado coherente. Exportar a Excel y PDF ⇒ ambos archivos correctos.
12. Ir a **RRHH › Presentismo › Permisos**: crear permiso médico para mismo empleado y adjuntar nuevo `CERTIFICADO_MED` del legajo ⇒ link persiste.
13. Ir a **RRHH › Desvinculaciones**: intentar aprobar desvinculación del mismo empleado con `LEGAJO_COMPLETITUD_OBLIGATORIO_BLOQUEA=true` ⇒ HTTP 422 con detalle. Cambiar param a `false` ⇒ aprueba con warning.
14. Ejecutar manualmente POST `/jobs/actualizar-estados-vencimiento` ⇒ verificar que `rrhh_legajo_alertas_vencimiento` registró la alerta del CERTIFICADO_MED (si seguía ACTIVO) y que se envió email.
15. Consultar AI Dashboard: "¿Cuántos documentos vencidos hay este mes?" ⇒ respuesta correcta.

Todo el flujo debe ejecutarse sin errores en consola del backend, sin warnings en consola del navegador y sin uso de `window.confirm/alert/prompt`.

---

## Riesgos y mitigaciones

- **Volumen de archivos grande**: 100 empleados × 14 docs ≈ 1 400 docs por empresa. Mitigar con paginación, índices BTREE en `(empleado_id, tipo_documento_id, estado)` y consultas filtradas por `estado=ACTIVO` (índice parcial).
- **Bucket DO Spaces fuera de cuota**: alerta proactiva si tamaño total > 80% del plan; cifras en dashboard admin (fuera de alcance v1, queda como follow-up).
- **MIME spoof**: validación con `file-type` (magic bytes), no solo extensión. Tests dedicados (PRUEBA 2.6).
- **Foto perfil duplicada**: el seed de migración es idempotente; verifica existencia de doc `FOTO_PERFIL` con `hash_sha256` calculado antes de insertar.
- **Tope de tamaño de Multer**: el `FileInterceptor` corta antes de leer el buffer completo (HTTP 413) — no hay riesgo de OOM.
- **Presigned URL leak**: expiry corto (15 min default); auditar accesos en `audit_logs` (entity `rrhh_legajo_documentos`).
- **Permisos sembrados mal**: integrar al `seed.service.ts` con idempotencia por `codigo`; documentar en QA Fase 6 que un nuevo SUPER_ADMIN tras `pnpm seed` ve los permisos.
- **Job de vencimientos no corre**: BullMQ requiere Redis estable; agregar monitoreo en `/queues` (Bull Board ya disponible) y alerta si no se ejecutó en 25h.
- **Compatibilidad `foto_url`**: dejar el campo en `rrhh_empleados` y actualizarlo cuando se cargue/reemplace un `FOTO_PERFIL` para no romper componentes que lo consumen directo.

---

## Entregables mínimos de v1 Legajos

- 3 tablas nuevas + 1 extensión de `rrhh_permisos_presentismo` aplicadas con migraciones idempotentes.
- 4 enums Prisma nuevos.
- 18 endpoints REST documentados en Swagger.
- 4 sub-tabs frontend con `RRHHGuia`, `RRHHEmptyState`, `RRHHPrereqChecklist`, tour y onboarding.
- 14 tipos del sistema sembrados + 5 permisos asignados a 6 roles.
- 7 parámetros nuevos en `rrhh_parametros_sistema` categoría `LEGAJOS`.
- Job BullMQ diario de vencimientos + 1 template Handlebars de alerta por email.
- Integración bidireccional con M14 (Desvinculación) y M18 (Permisos Presentismo).
- Migración de fotos existentes (`foto_url` → documento `FOTO_PERFIL`).
- Área `RRHH / Legajos` registrada en AI Dashboard con 5 ejemplos SQL.
- Plan de pruebas ejecutado en modo dev con evidencias en `docs/qa-legajos-evidencia-YYYY-MM-DD.md`.
- Tests unitarios con cobertura ≥ 80% en services nuevos.
- `LegajosStorageService` reusable (abstrae DO Spaces) — base para futuros módulos que requieran storage.

---

## Anexo — Checklist de cumplimiento PROJECT_STANDARDS

- [ ] **MonedaInput**: no aplica.
- [ ] **Enums en código y BD**: 4 enums Prisma declarados.
- [ ] **Navegación clara**: sub-tabs ordenadas (legajo individual → dashboard → consulta → catálogo).
- [ ] **Minimizar fricción**: botón "Ver legajo" en `EmpleadosTab.jsx` preselecciona empleado; upload con drag & drop.
- [ ] **Interfaces limpias**: panel de completitud visual, badges de color por estado de vencimiento.
- [ ] **Consistencia**: reutiliza `_shared` de RRHH y `SearchableSelect`/`MonedaInput`/`ConfirmDialog` existentes.
- [ ] **Ayuda contextual**: `RRHHGuia` con casos reales (renovar CI, certificado médico, contrato).
- [ ] **Ejemplos prácticos**: `rrhhGuias.js` con flujo "subir CI con vencimiento" y "anular documento erróneo".
- [ ] **Feedback inmediato**: barra de progreso en upload + toast + `InlineValidationBanner`.
- [ ] **Responsive**: tabla con virtualización, vista compacta en móvil.
- [ ] **Accesibilidad**: `aria-describedby` en inputs; focus-visible en dropzone.
- [ ] **Velocidad**: índices BTREE + presigned URLs (no proxy de descarga por el backend).
- [ ] **Plan de pruebas en modo dev**: documentado.
- [ ] **AI Dashboard**: área `RRHH / Legajos` registrada en `schema-context.ts`.

---

## Archivos críticos a crear/modificar

### Backend (NestJS)

**Nuevos**:
- `backend/prisma/migrations/20260601_rrhh_legajos_base/migration.sql`
- `backend/prisma/migrations/20260602_rrhh_permisos_presentismo_legajo/migration.sql`
- `backend/src/rrhh/controllers/legajos-tipos.controller.ts`
- `backend/src/rrhh/controllers/legajos-documentos.controller.ts`
- `backend/src/rrhh/controllers/legajos-consulta.controller.ts`
- `backend/src/rrhh/services/legajos-tipos.service.ts`
- `backend/src/rrhh/services/legajos-documentos.service.ts`
- `backend/src/rrhh/services/legajos-consulta.service.ts`
- `backend/src/rrhh/services/legajos-storage.service.ts`
- `backend/src/rrhh/services/legajos-mime.validator.ts`
- `backend/src/rrhh/services/legajos-completitud.service.ts`
- `backend/src/rrhh/dto/legajos.dto.ts`
- `backend/src/rrhh/seeds/legajos-tipos-sistema.seed.ts`
- `backend/src/rrhh/seeds/legajos-foto-migration.seed.ts`
- `backend/src/queues/processors/legajos-vencimiento.processor.ts`
- `backend/src/mail/templates/legajo-vencimiento.hbs`
- Tests `.spec.ts` correspondientes para cada service.

**Modificados**:
- [`backend/prisma/schema.prisma`](../prisma/schema.prisma) — 3 modelos nuevos + 4 enums + campo en `rrhh_permisos_presentismo`.
- [`backend/src/rrhh/rrhh.module.ts`](../src/rrhh/rrhh.module.ts) — registrar nuevos controllers/services.
- [`backend/src/rrhh/services/desvinculaciones.service.ts`](../src/rrhh/services/desvinculaciones.service.ts) — hook de verificación de completitud en `aprobar()`/`pagar()`.
- [`backend/src/rrhh/services/seed.service.ts`](../src/rrhh/services/seed.service.ts) — sembrar 14 tipos sistema + 5 permisos + 7 parámetros.
- [`backend/src/queues/queues.module.ts`](../src/queues/queues.module.ts) — registrar `legajos-queue`.
- [`backend/src/mail/mail.service.ts`](../src/mail/mail.service.ts) — método `sendLegajoVencimientoAlert()`.
- [`backend/src/ai-dashboard/chat/schema-context.ts`](../src/ai-dashboard/chat/schema-context.ts) — sección `RRHH / Legajos`.
- [`backend/src/config/envs.ts`](../src/config/envs.ts) — `LEGAJO_PRESIGNED_URL_EXPIRY_SEC`.
- `backend/package.json` — dep nueva: `file-type@^19`.

### Frontend (React/Vite/JSX)

**Nuevos**:
- `frontend/src/components/rrhh/legajos/LegajoFuncionarioTab.jsx`
- `frontend/src/components/rrhh/legajos/LegajosDashboardTab.jsx`
- `frontend/src/components/rrhh/legajos/LegajosConsultaTab.jsx`
- `frontend/src/components/rrhh/legajos/LegajosTiposTab.jsx`
- `frontend/src/components/rrhh/legajos/LegajoUploadDialog.jsx`
- `frontend/src/components/rrhh/legajos/LegajoVersionHistoryDialog.jsx`
- `frontend/src/components/rrhh/legajos/LegajoCompletitudPanel.jsx`
- `frontend/src/components/rrhh/legajos/LegajoDocumentRow.jsx`
- `frontend/src/components/rrhh/legajos/LegajoPdfExport.jsx`
- `frontend/src/api/rrhh-legajos.service.js`

**Modificados**:
- `frontend/src/components/templates/RRHHTemplate.jsx` — agregar tab padre "Legajos".
- `frontend/src/components/rrhh/EmpleadosTab.jsx` — botón "Ver legajo" en `RowActions`.
- `frontend/src/components/rrhh/presentismo/PermisosPresentismoTab.jsx` — selector `legajo_documento_id`.
- `frontend/src/tours/definitions/rrhhTour.js` — 4 pasos nuevos.
- `frontend/src/components/rrhh/_shared/RRHHOnboardingDialog.jsx` — slide 5 "Legajos".
- `frontend/src/components/rrhh/_shared/rrhhGuias.js` — sección `legajos*`.
- `frontend/package.json` — dep nueva: `@react-pdf/renderer`.
