Reportes de Recursos Humanos
Import React, { useState, useEffect } from "react"; import DatePicker from "react datepicker"; import "react datepicker/dist/react datepicker.css"; import Swal from "sweetalert2"; import config from "../../config/config"; import WorkedHoursReport from "./WorkedHoursReport"; import PunctualityReport from "./PunctualityReport"; import AttendanceReport from "./AttendanceReport"; import AbsenceReport from "./AbsenceReport"; import ExtraHoursReport from "./ExtraHoursReport"; const ReportsTable = () => { const [workedHoursData, setWorkedHoursData] = useState([]); const [punctualityData, setPunctualityData] = useState([]); const [attendanceData, setAttendanceData] = useState([]); const [absenceData, setAbsenceData] = useState([]); const [extraHoursData, setExtraHoursData] = useState([]); // Filtros const [workedHoursFilters, setWorkedHoursFilters] = useState({ fromDate: null, toDate: null, }); const [punctualityFilters, setPunctualityFilters] = useState({ state: "", rut: "", fromDate: null, toDate: null, }); const [attendanceFilters, setAttendanceFilters] = useState({ state: "", rut: "", fromDate: null, toDate: null, }); const [absenceFilters, setAbsenceFilters] = useState({ type: "", justification: "", rut: "", fromDate: null, toDate: null, }); const [extraHoursFilters, setExtraHoursFilters] = useState({ extra: "", fromDate: null, toDate: null, }); useEffect(() => { fetchData(); }, []); const fetchData = () => { fetch(`${config.apiUrl}/reports/worked hours`) .then((res) => res.json()) .then(setWorkedHoursData); fetch(`${config.apiUrl}/reports/punctuality`) .then((res) => res.json()) .then(setPunctualityData); fetch(`${config.apiUrl}/reports/attendance`) .then((res) => res.json()) .then(setAttendanceData); fetch(`${config.apiUrl}/reports/absence`) .then((res) => res.json()) .then(setAbsenceData); fetch(`${config.apiUrl}/reports/extra hours`) .then((res) => res.json()) .then(setExtraHoursData); }; const handleFilter = (endpoint, filters, setData) => { const params = { ...filters }; if (filters.fromDate) { params.fromDate = filters.fromDate.toISOString().split("T")[0]; } if (filters.toDate) { params.toDate = filters.toDate.toISOString().split("T")[0]; } let query = Object.entries(params) .filter(([_, value]) => value) .map(([key, value]) => `${key}=${value}`) .join("&"); fetch(`${config.apiUrl}/reports/${endpoint}?${query}`) .then((res) => res.json()) .then(setData); }; const downloadCSV = (data, filename) => { if (!data || data.length === 0) { Swal.fire("No hay datos", "No hay datos para descargar.", "warning"); return; } const keys = Object.keys(data[0]); const header = keys.join(","); const rows = data.map((item) => keys .map((key) => { const val = item[key] !== null && item[key] !== undefined ? item[key].toString() : ""; return `"${val.replace(/"/g, '""')}"`; }) .join(",") ); const csvContent = [header, ...rows].join("\n"); const blob = new Blob([csvContent], { type: "text/csv;charset=utf 8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.setAttribute("download", filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; // Función para obtener el nombre completo según los campos disponibles const getFullName = (entry) => { if (entry.employee_name) { return entry.employee_name; } if (entry.name_employee && entry.lastname_employee) { return `${entry.name_employee} ${entry.lastname_employee}`; } return ""; }; return ( <div className="p 6 bg transparent min h screen"> <div className="max w 7xl mx auto space y 8"> <h3 className="text 3xl font bold text [#07143d] mb 6 text center"> Reportes de RRHH </h3> <div className="grid grid cols 1 lg:grid cols 2 gap 8"> {/* Reporte de Horas Trabajadas */} <div className="col span 1"> <div className="h [400px] bg transparent rounded lg p 4 overflow y auto"> <WorkedHoursReport data={workedHoursData} filters={workedHoursFilters} setFilters={setWorkedHoursFilters} handleFilter={handleFilter} downloadCSV={downloadCSV} fetchData={fetchData} getFullName={getFullName} setData={setWorkedHoursData} // Se pasa el setter /> </div> </div> {/* Reporte de Puntualidad */} <div className="col span 1"> <div className="h [400px] bg transparent rounded lg p 4 overflow y auto"> <PunctualityReport data={punctualityData} filters={punctualityFilters} setFilters={setPunctualityFilters} handleFilter={handleFilter} downloadCSV={downloadCSV} fetchData={fetchData} getFullName={getFullName} setData={setPunctualityData} // Se pasa el setter /> </div> </div> {/* Reporte de Ausencias */} <div className="col span 1 lg:col span 2"> <div className="h [400px] bg transparent rounded lg p 4 overflow y auto"> <AbsenceReport data={absenceData} filters={absenceFilters} setFilters={setAbsenceFilters} handleFilter={handleFilter} downloadCSV={downloadCSV} fetchData={fetchData} getFullName={getFullName} setData={setAbsenceData} // Se pasa el setter /> </div> </div> {/* Reporte de Asistencia */} <div className="col span 1"> <div className="h [400px] bg transparent rounded lg p 4 overflow y auto"> <AttendanceReport data={attendanceData} filters={attendanceFilters} setFilters={setAttendanceFilters} handleFilter={handleFilter} downloadCSV={downloadCSV} fetchData={fetchData} getFullName={getFullName} setData={setAttendanceData} // Se pasa el setter /> </div> </div> {/* Reporte de Horas Extras */} <div className="col span 1"> <div className="h [400px] bg transparent rounded lg p 4 overflow y auto"> <ExtraHoursReport data={extraHoursData} filters={extraHoursFilters} setFilters={setExtraHoursFilters} handleFilter={handleFilter} downloadCSV={downloadCSV} fetchData={fetchData} getFullName={getFullName} setData={setExtraHoursData} // Se pasa el setter /> </div> </div> </div> </div> </div> ); }; export default ReportsTable; Mejora la distribución de elemntos de este componente, no me gusta como distribuye los reportes, y como les maneja su altura máxima. ejemplo de uno de los reportes dentro del componente padre: import React, { useState } from "react"; import { FiDownload, FiFilter, FiX, FiLoader, FiCopy } from "react icons/fi"; import DatePicker from "react datepicker"; import "react datepicker/dist/react datepicker.css"; const AttendanceReport = ({ data, filters, setFilters, handleFilter, downloadCSV, fetchData, getFullName, setData, }) => { const [loading, setLoading] = useState(false); const endpoint = "attendance"; const applyFilter = () => { setLoading(true); setTimeout(() => { setLoading(false); handleFilter(endpoint, filters, setData); }, 500); }; const clearFilters = () => { setFilters({ state: "", rut: "", fromDate: null, toDate: null }); fetchData(); }; const handleCopyToClipboard = (entry) => { const fullName = getFullName(entry); const textToCopy = `RUT: ${entry.rut_employee}\nNombre: ${fullName}\nEstado: ${entry.attendance_status}\nFecha: ${entry.work_date}`; navigator.clipboard.writeText(textToCopy) .then(() => alert("Información copiada al portapapeles")) .catch(err => console.error("Error al copiar:", err)); }; return ( <div className="min h screen bg gray 50 p 4 md:p 6 lg:p 8"> <div className="mx auto max w 7xl"> <h1 className="mb 8 text 2xl md:text 3xl font bold text gray 800"> Reporte de Asistencia Diaria </h1> <div className="bg white rounded lg shadow md p 4 md:p 6 mb 6"> <div className="grid grid cols 1 md:grid cols 2 lg:grid cols 4 gap 4"> <div className="space y 2"> <label className="block text sm font medium text gray 700"> Desde </label> <DatePicker selected={filters.fromDate} onChange={(date) => setFilters((prev) => ({ ...prev, fromDate: date }))} dateFormat="yyyy MM dd" className="w full px 3 py 2 border border gray 300 rounded md focus:ring 2 focus:ring blue 500 focus:border blue 500" /> </div> <div className="space y 2"> <label className="block text sm font medium text gray 700"> Hasta </label> <DatePicker selected={filters.toDate} onChange={(date) => setFilters((prev) => ({ ...prev, toDate: date }))} dateFormat="yyyy MM dd" className="w full px 3 py 2 border border gray 300 rounded md focus:ring 2 focus:ring blue 500 focus:border blue 500" /> </div> <div className="space y 2"> <label className="block text sm font medium text gray 700"> RUT </label> <input type="text" value={filters.rut} onChange={(e) => setFilters((prev) => ({ ...prev, rut: e.target.value }))} placeholder="RUT" className="w full px 3 py 2 border border gray 300 rounded md focus:ring 2 focus:ring blue 500 focus:border blue 500" /> </div> <div className="space y 2"> <label className="block text sm font medium text gray 700"> Estado </label> <select value={filters.state} onChange={(e) => setFilters((prev) => ({ ...prev, state: e.target.value }))} className="w full px 3 py 2 border border gray 300 rounded md focus:ring 2 focus:ring blue 500 focus:border blue 500" > <option value="">Sin Filtro</option> <option value="Presente">Presente</option> <option value="Ausente">Ausente</option> </select> </div> </div> <div className="mt 4 flex flex wrap gap 3"> <button onClick={applyFilter} disabled={loading} className="inline flex items center px 4 py 2 bg blue 600 hover:bg blue 700 text white font medium rounded md transition colors focus:outline none focus:ring 2 focus:ring offset 2 focus:ring blue 500 disabled:opacity 50" aria label="Aplicar filtros" > {loading ? ( <FiLoader className="animate spin mr 2" /> ) : ( <FiFilter className="mr 2" /> )} Filtrar </button> <button onClick={clearFilters} className="inline flex items center px 4 py 2 bg gray 200 hover:bg gray 300 text gray 700 font medium rounded md transition colors focus:outline none focus:ring 2 focus:ring offset 2 focus:ring gray 500" aria label="Limpiar filtros" > <FiX className="mr 2" /> Limpiar </button> <button onClick={() => downloadCSV(data, "asistencia_diaria.csv")} className="inline flex items center px 4 py 2 bg green 600 hover:bg green 700 text white font medium rounded md transition colors focus:outline none focus:ring 2 focus:ring offset 2 focus:ring green 500" aria label="Exportar a CSV" > <FiDownload className="mr 2" /> Exportar CSV </button> </div> </div> <div className="bg white rounded lg shadow md overflow hidden"> <div className="overflow x auto"> <table className="min w full divide y divide gray 200"> <thead className="bg gray 50"> <tr> <th className="px 6 py 3 text left text xs font medium text gray 500 uppercase tracking wider"> RUT </th> <th className="px 6 py 3 text left text xs font medium text gray 500 uppercase tracking wider"> Nombre Completo </th> <th className="px 6 py 3 text left text xs font medium text gray 500 uppercase tracking wider"> Estado </th> <th className="px 6 py 3 text left text xs font medium text gray 500 uppercase tracking wider"> Fecha </th> <th className="px 6 py 3 text left text xs font medium text gray 500 uppercase tracking wider"> Acciones </th> </tr> </thead> <tbody className="bg white divide y divide gray 200"> {loading ? ( <tr> <td colSpan="5" className="px 6 py 4 text center text gray 500"> <FiLoader className="animate spin inline mr 2" /> Cargando... </td> </tr> ) : data.length === 0 ? ( <tr> <td colSpan="5" className="px 6 py 4 text center text gray 500"> No hay datos disponibles </td> </tr> ) : ( data.map((entry, index) => { const fullName = getFullName(entry); const textToCopy = `RUT: ${entry.rut_employee}\nNombre: ${fullName}\nEstado: ${entry.attendance_status}\nFecha: ${entry.work_date}`; return ( <tr key={index} className={index % 2 === 0 ? "bg white" : "bg gray 50"}> <td className="px 6 py 4 whitespace nowrap text sm text gray 700"> {entry.rut_employee} </td> <td className="px 6 py 4 whitespace nowrap text sm text gray 700"> {fullName} </td> <td className="px 6 py 4 whitespace nowrap text sm text gray 700"> {entry.attendance_status} </td> <td className="px 6 py 4 whitespace nowrap text sm text gray 700"> {entry.work_date} </td> <td className="px 6 py 4 whitespace nowrap text sm text gray 700"> <button onClick={() => { navigator.clipboard.writeText(textToCopy) .then(() => alert("Información copiada al portapapeles")) .catch(err => console.error("Error al copiar:", err)); }} className="text blue 600 hover:text blue 800" aria label="Copiar al portapapeles" > <FiCopy className="w 5 h 5" /> </button> </td> </tr> ); }) )} </tbody> </table> </div> </div> </div> </div> ); }; export default AttendanceReport; para que sepas mas menos como estructurar.
