22 KiB
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:
-
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"
-
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 │ └─────────────────────────────────────┘ -
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 - -
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
-
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:
-
Seleccionar Semana:
- Semana del: 13/11/2025 al 17/11/2025 (L-V)
- Selector de semana con navegación ← →
-
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% -
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)
- Gráfica de barras por día:
-
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:
-
Seleccionar Mes:
- Mes: Noviembre 2025
- Días laborales: 21 (excluyendo sábados y domingos)
-
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) │ └──────────────────────────────────────┘ -
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 -
Clasificación de Estatus:
- 🟢 Excelente: ≥ 95% asistencia
- 🟡 Regular: 80-94% asistencia
- 🔴 Atención requerida: < 80% asistencia
-
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:
-
Ver Estadísticas Generales:
- Tasa de ausentismo: 11.2%
- Promedio de industria: 8-10% (benchmark)
- Tendencia: ↑ +2.5% vs mes anterior
-
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) -
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%
- Gráfica de pastel o barras:
-
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 -
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:
-
Configuración de Horarios:
- Hora de entrada esperada: 7:30 AM (configurable)
- Tolerancia: 15 minutos (configurable)
- Retardo > 15 min: Se marca como retardo
-
Resumen de Puntualidad:
┌────────────────────────────────────┐ │ Puntualidad - Noviembre 2025 │ ├────────────────────────────────────┤ │ ✅ Puntuales: 891 (91%) │ │ ⏰ Retardos: 87 (9%) │ │ Promedio de minutos de retardo: 22 │ │ Mayor retardo: 1h 15min │ └────────────────────────────────────┘ -
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 - -
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 -
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
- Histograma de minutos de retardo:
CA-6: Reporte por Obra 🏗️
Dado que soy Residente de Obra Cuando genero reporte de mi obra específica Entonces veo:
-
Selección:
- Obra: Casa Modelo Residencial Norte
- Periodo: Última semana / Último mes / Personalizado
-
Dashboard de la Obra:
┌────────────────────────────────────┐ │ Casa Modelo Residencial Norte │ ├────────────────────────────────────┤ │ Empleados asignados: 24 │ │ Promedio asistencia: 92% │ │ Cuadrillas activas: 3 │ │ Total días-hombre (mes): 504 │ └────────────────────────────────────┘ -
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% -
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:
-
Tipos de Incidencias:
- Incapacidades médicas: 44 (IMSS)
- Permisos personales: 19
- Faltas justificadas: 6
- Accidentes de trabajo: 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 ❌ ⏳ -
Filtros:
- Por tipo de incidencia
- Por estado: Pendiente, Aprobado, Rechazado
- Por fecha
-
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:
-
Seleccionar Formato:
- 📄 PDF: Para impresión o firma
- 📊 Excel (.xlsx): Para análisis adicional
- 📋 CSV: Para importar a otros sistemas
-
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")
-
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
-
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:
-
KPIs Principales:
┌───────────────┬───────────────┬───────────────┐ │ Tasa Asist. │ Ausentismo │ Puntualidad │ │ 89% │ 11% │ 91% │ │ ↑ +2% vs ant. │ ↓ -1.5% │ ↑ +3% │ └───────────────┴───────────────┴───────────────┘ -
Gráficas de Tendencia:
- Línea de asistencia últimos 6 meses
- Comparación año actual vs año anterior
-
Top Performers:
- Obras con mejor asistencia
- Cuadrillas con mejor puntualidad
- Empleados destacados (100% asistencia)
-
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
// 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
// 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:
- Ir a "Reportes" > "Asistencia Diaria"
- 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:
- 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
{
"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