erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-006-reportes-asistencia.md

713 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# US-HR-006: Reportes de Asistencia
**Epic:** MAI-007 - RRHH, Asistencias y Nómina
**RF:** No aplica (funcionalidad de reportes)
**ET:** No aplica
**Tipo:** Historia de Usuario
**Prioridad:** Media
**Story Points:** 5
**Sprint:** 11
**Estado:** 📋 Pendiente
**Última actualización:** 2025-11-17
---
## 📖 Historia de Usuario
**Como** Gerente de RRHH, Residente de Obra o Director
**Quiero** generar reportes detallados de asistencia con filtros y exportación
**Para** analizar patrones de ausentismo, detectar problemas de puntualidad, tomar decisiones informadas sobre el personal y generar reportes para auditorías
---
## 🎯 Criterios de Aceptación
### CA-1: Reporte Diario de Asistencia 📅
**Dado que** soy Residente de Obra o Gerente de RRHH
**Cuando** accedo a "Reportes" > "Asistencia Diaria"
**Entonces** puedo:
1. **Seleccionar Filtros:**
- **Fecha:** 2025-11-17 (default: hoy)
- **Obra:** "Casa Modelo Norte" o "Todas las obras"
- **Cuadrilla:** "Albañilería A" o "Todas las cuadrillas"
2. **Ver Resumen del Día:**
```
┌─────────────────────────────────────┐
│ Resumen de Asistencia - 17/11/2025 │
├─────────────────────────────────────┤
│ ✅ Presentes: 45 (86%) │
│ ❌ Ausentes: 7 (14%) │
│ ⏰ Retardos: 5 (11% de presentes) │
│ 🏥 Incapacidades: 0 │
│ 📝 Permisos: 0 │
│ Total empleados: 52 │
└─────────────────────────────────────┘
```
3. **Ver Tabla Detallada:**
| Empleado | Cuadrilla | Check-In | Check-Out | Horas | Estado | GPS |
|----------|-----------|----------|-----------|-------|--------|-----|
| Juan Pérez | Albañilería A | 07:15 | 17:30 | 10:15 | ✅ Presente | ✓ |
| María López | Albañilería A | 07:45 | 17:35 | 09:50 | ⏰ Retardo | ✓ |
| Carlos Ruiz | Electricidad | - | - | 0:00 | ❌ Ausente | - |
4. **Indicadores Visuales:**
- **Verde ✅:** Presente y puntual (check-in antes de 7:30 AM)
- **Amarillo ⏰:** Retardo (check-in después de 7:30 AM)
- **Rojo ❌:** Ausente (sin check-in)
- **Azul 🏥:** Incapacidad médica
- **Morado 📝:** Permiso autorizado
5. **Acciones:**
- Exportar a PDF: "Asistencia_17Nov2025.pdf"
- Exportar a Excel: "Asistencia_17Nov2025.xlsx"
- Enviar por email
- Imprimir
**Y** el reporte debe actualizarse en tiempo real conforme se registran asistencias
### CA-2: Reporte Semanal de Asistencia 📊
**Dado que** necesito analizar una semana completa
**Cuando** selecciono "Reporte Semanal"
**Entonces** veo:
1. **Seleccionar Semana:**
- Semana del: 13/11/2025 al 17/11/2025 (L-V)
- Selector de semana con navegación ← →
2. **Vista de Tabla Resumen:**
| Empleado | Lun | Mar | Mié | Jue | Vie | Total | % Asist. |
|----------|-----|-----|-----|-----|-----|-------|----------|
| Juan Pérez | ✅ | ✅ | ✅ | ✅ | ✅ | 5/5 | 100% |
| María López | ✅ | ⏰ | ✅ | ✅ | ❌ | 4/5 | 80% |
| Carlos Ruiz | ✅ | ✅ | 🏥 | 🏥 | 🏥 | 2/5 | 40% |
3. **Gráfica de Asistencia Semanal:**
- Gráfica de barras por día:
```
50 ┤ ██
40 ┤ ██ ██
30 ┤ ██ ██ ██ ██
20 ┤ ██ ██ ██ ██ ██
10 ┤ ██ ██ ██ ██ ██
0 ┴──────────────────────
Lun Mar Mié Jue Vie
```
- Leyenda: Verde (presentes), Amarillo (retardos), Rojo (ausencias)
4. **Top 5 Ausencias:**
- Lista de empleados con más ausencias en la semana
- Sugerencia de seguimiento
### CA-3: Reporte Mensual de Asistencia 📆
**Dado que** necesito un análisis mensual
**Cuando** selecciono "Reporte Mensual"
**Entonces** veo:
1. **Seleccionar Mes:**
- Mes: Noviembre 2025
- Días laborales: 21 (excluyendo sábados y domingos)
2. **Resumen Mensual:**
```
┌──────────────────────────────────────┐
│ Noviembre 2025 - Asistencia General │
├──────────────────────────────────────┤
│ Promedio de asistencia diaria: 89% │
│ Total días-hombre: 978 │
│ Total ausencias: 124 │
│ Total retardos: 87 │
│ Día con más ausencias: 15/11 (12) │
│ Día con más retardos: 18/11 (9) │
└──────────────────────────────────────┘
```
3. **Tabla por Empleado:**
| Empleado | Días Lab. | Presentes | Ausencias | Retardos | % Asist. | Estatus |
|----------|-----------|-----------|-----------|----------|----------|---------|
| Juan Pérez | 21 | 21 | 0 | 1 | 100% | 🟢 Excelente |
| María López | 21 | 18 | 3 | 5 | 85.7% | 🟡 Regular |
| Carlos Ruiz | 21 | 15 | 6 | 2 | 71.4% | 🔴 Atención |
4. **Clasificación de Estatus:**
- 🟢 **Excelente:** ≥ 95% asistencia
- 🟡 **Regular:** 80-94% asistencia
- 🔴 **Atención requerida:** < 80% asistencia
5. **Gráfica de Tendencia:**
- Línea de % de asistencia por día del mes
- Identificar patrones (ej: lunes con más ausencias)
### CA-4: Reporte de Ausentismo 📉
**Dado que** necesito analizar patrones de ausentismo
**Cuando** accedo a "Reporte de Ausentismo"
**Entonces** puedo:
1. **Ver Estadísticas Generales:**
- Tasa de ausentismo: 11.2%
- Promedio de industria: 8-10% (benchmark)
- Tendencia: +2.5% vs mes anterior
2. **Ausentismo por Tipo:**
```
Faltas injustificadas: 45% (56 días)
Incapacidades médicas: 35% (44 días)
Permisos autorizados: 15% (19 días)
Faltas justificadas: 5% (6 días)
```
3. **Ausentismo por Día de la Semana:**
- Gráfica de pastel o barras:
- Lunes: 35% (mayor ausentismo)
- Viernes: 25%
- Martes: 15%
- Miércoles: 13%
- Jueves: 12%
4. **Empleados con Mayor Ausentismo:**
| Empleado | Total Ausencias | Último Mes | Tendencia |
|----------|-----------------|------------|-----------|
| Carlos Ruiz | 12 | 6 | Incrementando |
| Pedro Gómez | 9 | 3 | Estable |
| Luis Martínez | 7 | 2 | Mejorando |
5. **Acciones Sugeridas:**
- 🔴 **Alerta:** 3 empleados con > 10 ausencias en 30 días
- Botón: "Generar plan de seguimiento"
- Botón: "Agendar reunión con empleado"
### CA-5: Reporte de Puntualidad ⏰
**Dado que** necesito monitorear puntualidad
**Cuando** acceso a "Reporte de Puntualidad"
**Entonces** veo:
1. **Configuración de Horarios:**
- Hora de entrada esperada: 7:30 AM (configurable)
- Tolerancia: 15 minutos (configurable)
- Retardo > 15 min: Se marca como retardo
2. **Resumen de Puntualidad:**
```
┌────────────────────────────────────┐
│ Puntualidad - Noviembre 2025 │
├────────────────────────────────────┤
│ ✅ Puntuales: 891 (91%) │
│ ⏰ Retardos: 87 (9%) │
│ Promedio de minutos de retardo: 22 │
│ Mayor retardo: 1h 15min │
└────────────────────────────────────┘
```
3. **Tabla de Retardos:**
| Fecha | Empleado | Esperado | Real | Minutos Retardo | Motivo |
|-------|----------|----------|------|-----------------|--------|
| 15/11 | María López | 07:30 | 07:45 | 15 | Tráfico |
| 15/11 | Juan García | 07:30 | 08:45 | 75 | - |
4. **Empleados con Más Retardos:**
| Empleado | Total Retardos | Promedio Min. | Estatus |
|----------|----------------|---------------|---------|
| María López | 12 | 18 min | 🟡 Advertencia |
| Juan García | 8 | 45 min | 🔴 Crítico |
5. **Gráfica de Distribución:**
- Histograma de minutos de retardo:
```
40 ┤ ██
30 ┤ ██ ██
20 ┤ ██ ██ ██
10 ┤ ██ ██ ██ ██
0 ┴────────────────────
0-15 15-30 30-60 >60
min min min min
```
### CA-6: Reporte por Obra 🏗️
**Dado que** soy Residente de Obra
**Cuando** genero reporte de mi obra específica
**Entonces** veo:
1. **Selección:**
- Obra: Casa Modelo Residencial Norte
- Periodo: Última semana / Último mes / Personalizado
2. **Dashboard de la Obra:**
```
┌────────────────────────────────────┐
│ Casa Modelo Residencial Norte │
├────────────────────────────────────┤
│ Empleados asignados: 24 │
│ Promedio asistencia: 92% │
│ Cuadrillas activas: 3 │
│ Total días-hombre (mes): 504 │
└────────────────────────────────────┘
```
3. **Asistencia por Cuadrilla:**
| Cuadrilla | Empleados | Presentes Hoy | % Asist. Mes |
|-----------|-----------|---------------|--------------|
| Albañilería A | 10 | 9 | 95% |
| Electricidad | 8 | 8 | 88% |
| Plomería | 6 | 5 | 90% |
4. **Comparación con Otras Obras:**
- Gráfica de barras comparando % de asistencia
- Posición: 2° de 5 obras activas
### CA-7: Reporte de Incidencias 📝
**Dado que** necesito rastrear incidencias
**Cuando** accedo a "Reporte de Incidencias"
**Entonces** veo:
1. **Tipos de Incidencias:**
- Incapacidades médicas: 44 (IMSS)
- Permisos personales: 19
- Faltas justificadas: 6
- Accidentes de trabajo: 2
2. **Tabla de Incidencias:**
| Fecha | Empleado | Tipo | Días | Documentado | Aprobado |
|-------|----------|------|------|-------------|----------|
| 10/11 | Juan Pérez | Incapacidad | 3 | ✅ | ✅ |
| 12/11 | María López | Permiso | 1 | ✅ | ✅ |
| 15/11 | Carlos Ruiz | Falta Just. | 1 | ❌ | ⏳ |
3. **Filtros:**
- Por tipo de incidencia
- Por estado: Pendiente, Aprobado, Rechazado
- Por fecha
4. **Documentos Adjuntos:**
- Ver/descargar documentos de respaldo (recetas médicas, etc.)
### CA-8: Exportación de Reportes 📤
**Dado que** genero cualquier reporte
**Cuando** hago clic en "Exportar"
**Entonces** puedo:
1. **Seleccionar Formato:**
- 📄 **PDF:** Para impresión o firma
- 📊 **Excel (.xlsx):** Para análisis adicional
- 📋 **CSV:** Para importar a otros sistemas
2. **Exportación PDF:**
- Header con logo de la empresa
- Título del reporte y periodo
- Tablas con formato profesional
- Footer con:
- Fecha de generación
- Usuario que generó
- Página X de Y
- Tamaño: Carta (8.5" × 11")
3. **Exportación Excel:**
- Hoja "Resumen" con métricas
- Hoja "Detalle" con datos completos
- Formato de celdas (moneda, porcentajes, fechas)
- Fórmulas incluidas
- Filtros automáticos en headers
4. **Envío por Email:**
- Modal: "Enviar Reporte por Email"
- Destinatarios: rrhh@constructora.com (editable)
- Asunto: "Reporte de Asistencia - Noviembre 2025"
- Mensaje personalizable
- Adjuntar archivo en formato seleccionado
### CA-9: Dashboard Ejecutivo 📈
**Dado que** soy Director
**Cuando** accedo a "Dashboard Ejecutivo de RRHH"
**Entonces** veo:
1. **KPIs Principales:**
```
┌───────────────┬───────────────┬───────────────┐
│ Tasa Asist. │ Ausentismo │ Puntualidad │
│ 89% │ 11% │ 91% │
│ ↑ +2% vs ant. │ ↓ -1.5% │ ↑ +3% │
└───────────────┴───────────────┴───────────────┘
```
2. **Gráficas de Tendencia:**
- Línea de asistencia últimos 6 meses
- Comparación año actual vs año anterior
3. **Top Performers:**
- Obras con mejor asistencia
- Cuadrillas con mejor puntualidad
- Empleados destacados (100% asistencia)
4. **Alertas Críticas:**
- 3 empleados con riesgo de deserción (> 15 ausencias)
- 2 obras con asistencia < 80%
- 1 cuadrilla con 40% de retardos
### CA-10: Permisos por Rol 🔐
**Roles y Permisos:**
| Reporte | Director | Engineer | Resident | HR | Finance |
|---------|----------|----------|----------|-----|---------|
| Reporte Diario | | | | | |
| Reporte Semanal/Mensual | | | (solo su obra) | | |
| Reporte de Ausentismo | | | | | |
| Reporte de Puntualidad | | | (solo su obra) | | |
| Dashboard Ejecutivo | | | | | |
| Exportar a PDF/Excel | | | | | |
---
## 🔧 Detalles Técnicos
### Servicio de Reportes
```typescript
// attendance-reports.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Between } from 'typeorm';
@Injectable()
export class AttendanceReportsService {
/**
* Generar reporte diario de asistencia
*/
async getDailyReport(
constructoraId: string,
date: Date,
filters?: { workId?: string; crewId?: string }
) {
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
const endOfDay = new Date(date.setHours(23, 59, 59, 999));
// Query de asistencias del día
const query = this.attendanceRepo
.createQueryBuilder('att')
.leftJoinAndSelect('att.employee', 'emp')
.leftJoinAndSelect('emp.crewMemberships', 'crew')
.where('att.workDate = :date', { date: startOfDay })
.andWhere('emp.constructoraId = :constructoraId', { constructoraId });
if (filters?.workId) {
query.andWhere('att.workId = :workId', { workId: filters.workId });
}
if (filters?.crewId) {
query.andWhere('crew.crewId = :crewId', { crewId: filters.crewId });
}
const attendances = await query.getMany();
// Calcular estadísticas
const total = attendances.length;
const present = attendances.filter(a => a.type === 'check_in').length;
const late = attendances.filter(a => {
if (a.type !== 'check_in') return false;
const checkInTime = new Date(a.timestamp).getHours() * 60 +
new Date(a.timestamp).getMinutes();
const expectedTime = 7 * 60 + 30; // 7:30 AM
return checkInTime > expectedTime + 15; // Más de 15 min tarde
}).length;
return {
date: startOfDay,
summary: {
total,
present,
absent: total - present,
late,
presentPercentage: (present / total) * 100,
},
details: attendances.map(att => ({
employeeId: att.employeeId,
employeeName: att.employee.fullName,
crewName: att.employee.crewMemberships[0]?.crew?.name,
checkIn: att.type === 'check_in' ? att.timestamp : null,
checkOut: att.type === 'check_out' ? att.timestamp : null,
hoursWorked: this.calculateHours(att),
status: this.getAttendanceStatus(att),
gpsValidated: !att.locationWarning,
})),
};
}
/**
* Generar reporte mensual
*/
async getMonthlyReport(
constructoraId: string,
month: number,
year: number
) {
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
// Query complejo con agregaciones
const result = await this.db.query(`
SELECT
e.id,
e.employee_code,
CONCAT(e.first_name, ' ', e.last_name) as full_name,
COUNT(DISTINCT att.work_date) FILTER (WHERE att.type = 'check_in') as days_present,
$1 - COUNT(DISTINCT att.work_date) FILTER (WHERE att.type = 'check_in') as days_absent,
COUNT(*) FILTER (
WHERE att.type = 'check_in'
AND EXTRACT(HOUR FROM att.timestamp) * 60 + EXTRACT(MINUTE FROM att.timestamp) > 450
) as late_count,
ROUND(
COUNT(DISTINCT att.work_date) FILTER (WHERE att.type = 'check_in')::numeric / $1 * 100,
1
) as attendance_percentage
FROM hr.employees e
LEFT JOIN hr.attendance_records att ON att.employee_id = e.id
AND att.work_date >= $2
AND att.work_date <= $3
WHERE e.constructora_id = $4
GROUP BY e.id
ORDER BY attendance_percentage DESC
`, [this.getWorkDays(month, year), startDate, endDate, constructoraId]);
return {
month,
year,
workDays: this.getWorkDays(month, year),
employees: result.rows,
summary: {
averageAttendance: result.rows.reduce((sum, emp) =>
sum + parseFloat(emp.attendance_percentage), 0) / result.rows.length,
totalAbsences: result.rows.reduce((sum, emp) =>
sum + parseInt(emp.days_absent), 0),
totalLate: result.rows.reduce((sum, emp) =>
sum + parseInt(emp.late_count), 0),
},
};
}
private getAttendanceStatus(attendance: AttendanceRecord): string {
if (!attendance) return 'absent';
const checkInTime = new Date(attendance.timestamp).getHours() * 60 +
new Date(attendance.timestamp).getMinutes();
const expectedTime = 7 * 60 + 30;
if (checkInTime <= expectedTime + 15) return 'present';
return 'late';
}
}
```
### Generación de PDF
```typescript
// pdf-generator.service.ts
import { Injectable } from '@nestjs/common';
import * as PDFDocument from 'pdfkit';
@Injectable()
export class PDFGeneratorService {
async generateAttendanceReport(data: any): Promise<Buffer> {
const doc = new PDFDocument({ size: 'LETTER', margin: 50 });
const buffers = [];
doc.on('data', buffers.push.bind(buffers));
// Header
doc
.fontSize(20)
.text('Reporte de Asistencia', { align: 'center' })
.moveDown();
doc
.fontSize(12)
.text(`Periodo: ${this.formatDate(data.startDate)} - ${this.formatDate(data.endDate)}`)
.text(`Generado: ${new Date().toLocaleString('es-MX')}`)
.moveDown();
// Summary
doc
.fontSize(14)
.text('Resumen', { underline: true })
.moveDown(0.5);
doc
.fontSize(10)
.text(`Total empleados: ${data.summary.total}`)
.text(`Presentes: ${data.summary.present} (${data.summary.presentPercentage.toFixed(1)}%)`)
.text(`Ausentes: ${data.summary.absent}`)
.text(`Retardos: ${data.summary.late}`)
.moveDown();
// Table
this.generateTable(doc, data.details);
// Footer
const pages = doc.bufferedPageRange();
for (let i = 0; i < pages.count; i++) {
doc.switchToPage(i);
doc
.fontSize(8)
.text(
`Página ${i + 1} de ${pages.count}`,
50,
doc.page.height - 50,
{ align: 'center' }
);
}
doc.end();
return new Promise((resolve) => {
doc.on('end', () => resolve(Buffer.concat(buffers)));
});
}
private generateTable(doc: any, data: any[]) {
const tableTop = doc.y;
const col1 = 50;
const col2 = 150;
const col3 = 250;
const col4 = 350;
const col5 = 450;
// Headers
doc
.fontSize(9)
.text('Empleado', col1, tableTop, { bold: true })
.text('Check-In', col2, tableTop)
.text('Check-Out', col3, tableTop)
.text('Horas', col4, tableTop)
.text('Estado', col5, tableTop);
doc.moveDown();
// Rows
data.forEach((row) => {
const y = doc.y;
doc
.fontSize(8)
.text(row.employeeName, col1, y, { width: 90 })
.text(row.checkIn ? this.formatTime(row.checkIn) : '-', col2, y)
.text(row.checkOut ? this.formatTime(row.checkOut) : '-', col3, y)
.text(row.hoursWorked || '0:00', col4, y)
.text(this.translateStatus(row.status), col5, y);
doc.moveDown(0.5);
});
}
}
```
---
## 🧪 Casos de Prueba
### TC-REP-001: Reporte Diario ✅
**Precondiciones:**
- Fecha: 17/11/2025
- 45 empleados presentes, 7 ausentes
**Pasos:**
1. Ir a "Reportes" > "Asistencia Diaria"
2. Seleccionar fecha 17/11/2025
**Resultado esperado:**
- Resumen muestra:
- Presentes: 45 (86%)
- Ausentes: 7 (14%)
- Tabla con 52 empleados
- Exportar a PDF funcional
### TC-REP-002: Reporte Mensual ✅
**Precondiciones:**
- Noviembre 2025, 21 días laborales
**Pasos:**
1. Generar reporte mensual
**Resultado esperado:**
- Promedio asistencia calculado correctamente
- Tabla ordenada por % asistencia DESC
- Clasificación de estatus visible
---
## 📦 Dependencias
-**US-HR-002:** Asistencias registradas
-**US-HR-001:** Empleados y cuadrillas
### Librerías
```json
{
"pdfkit": "^0.14.0",
"exceljs": "^4.4.0",
"recharts": "^2.10.3"
}
```
---
## ⚠️ Riesgos
### R-1: Rendimiento con Datos Históricos
**Descripción:** Reportes anuales pueden ser lentos
**Impacto:** Medio
**Probabilidad:** Media
**Mitigación:**
- Paginación en reportes
- Índices en tablas
- Cache de reportes frecuentes
---
## 📊 Métricas de Éxito
- ✅ Reportes generan en < 5 segundos
- 100% de exportaciones exitosas
- 80% de gerentes usan reportes semanalmente
---
## 📋 Checklist de Implementación
### Backend
- [ ] Implementar AttendanceReportsService
- [ ] Crear queries de agregación
- [ ] Implementar PDFGeneratorService
- [ ] Implementar ExcelGeneratorService
- [ ] Crear endpoints de reportes
### Frontend
- [ ] Crear páginas de reportes
- [ ] Implementar gráficas con Recharts
- [ ] Crear componente de filtros
- [ ] Implementar exportación
---
**Fecha de creación:** 2025-11-17
**Versión:** 1.0