window.AdminOvertime = function({ globalUsers = [], isReadOnly = false, showToast, refreshData, currentUser }) { const [subTab, setSubTab] = React.useState('reentries'); // 'reentries' | 'balances' | 'history' const [loading, setLoading] = React.useState(true); const [overtimeLogs, setOvertimeLogs] = React.useState([]); const [userBalances, setUserBalances] = React.useState([]); const [usagesHistory, setUsagesHistory] = React.useState([]); // Filtros const [statusFilter, setStatusFilter] = React.useState('pending'); // 'pending' | 'approved' | 'rejected' | 'all' const [userFilter, setUserFilter] = React.useState('all'); // Modales const [reviewingLog, setReviewingLog] = React.useState(null); const [reviewHours, setReviewHours] = React.useState(''); const [reviewDestination, setReviewDestination] = React.useState('payroll'); // 'payroll' | 'compensated_time' | 'mixed' | 'rejected' const [reviewPayrollHours, setReviewPayrollHours] = React.useState(''); const [reviewCompensatedHours, setReviewCompensatedHours] = React.useState(''); const [adminNotes, setAdminNotes] = React.useState(''); const [isSavingReview, setIsSavingReview] = React.useState(false); // Modal Otorgar Permiso por Tiempo Compensado const [grantingUser, setGrantingUser] = React.useState(null); const [grantDate, setGrantDate] = React.useState(window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]); const [grantHours, setGrantHours] = React.useState('8'); const [grantReason, setGrantReason] = React.useState('Permiso Remunerado por Tiempo Compensado'); const [isSavingGrant, setIsSavingGrant] = React.useState(false); // Modal Ajuste Manual de Saldo const [adjustingUser, setAdjustingUser] = React.useState(null); const [newBalanceInput, setNewBalanceInput] = React.useState(''); const [adjustNoteInput, setAdjustNoteInput] = React.useState(''); const [isSavingAdjust, setIsSavingAdjust] = React.useState(false); const showToastRef = React.useRef(showToast); React.useEffect(() => { showToastRef.current = showToast; }, [showToast]); const loadOvertimeData = React.useCallback(async () => { setLoading(true); try { const res = await window.apiCall('get_overtime_logs', {}); if (res && res.success) { setOvertimeLogs(res.overtimeLogs || []); setUserBalances(res.userBalances || []); setUsagesHistory(res.usagesHistory || []); } else { if (showToastRef.current) showToastRef.current(res?.error || "Error al cargar datos de horas extras."); } } catch (e) { if (showToastRef.current) showToastRef.current("Error de conexión al obtener horas extras."); } finally { setLoading(false); } }, []); React.useEffect(() => { loadOvertimeData(); }, [loadOvertimeData]); // Calcular Estadísticas KPIs const stats = React.useMemo(() => { const pendingCount = overtimeLogs.filter(o => o.status === 'pending').length; const pendingHours = overtimeLogs.filter(o => o.status === 'pending').reduce((sum, o) => sum + Number(o.raw_hours || 0), 0); const approvedPayrollHours = overtimeLogs.filter(o => o.status === 'approved').reduce((sum, o) => sum + Number(o.payroll_hours || 0), 0); const totalCompensatedBalance = userBalances.reduce((sum, u) => sum + Number(u.compensated_time_hours || 0), 0); const totalUsagesHours = usagesHistory.reduce((sum, u) => sum + Number(u.hours_used || 0), 0); // Detectar si hay logs con más de 3 horas diarias para alerta legal Art. 62 const excessLogs = overtimeLogs.filter(o => Number(o.raw_hours || o.approved_hours || 0) > 3); return { pendingCount, pendingHours, approvedPayrollHours, totalCompensatedBalance, totalUsagesHours, hasExcess: excessLogs.length > 0, excessLogs }; }, [overtimeLogs, userBalances, usagesHistory]); // Filtrar logs de horas extras const filteredLogs = React.useMemo(() => { return overtimeLogs.filter(log => { if (statusFilter !== 'all' && log.status !== statusFilter) return false; if (userFilter !== 'all' && Number(log.user_id) !== Number(userFilter)) return false; return true; }); }, [overtimeLogs, statusFilter, userFilter]); // Abrir Modal de Revisión const handleOpenReview = (log) => { setReviewingLog(log); const initialApproved = Number(log.approved_hours || log.raw_hours || 0); setReviewHours(initialApproved.toString()); const dest = log.destination !== 'pending' ? log.destination : 'payroll'; setReviewDestination(dest); setReviewPayrollHours(log.payroll_hours ? log.payroll_hours.toString() : (dest === 'payroll' ? initialApproved.toString() : '0')); setReviewCompensatedHours(log.compensated_hours ? log.compensated_hours.toString() : (dest === 'compensated_time' ? initialApproved.toString() : '0')); setAdminNotes(log.admin_notes || ''); }; // Cambiar horas aprobadas y auto-ajustar destinación const handleHoursChange = (val) => { setReviewHours(val); const h = parseFloat(val) || 0; if (reviewDestination === 'payroll') { setReviewPayrollHours(h.toString()); setReviewCompensatedHours('0'); } else if (reviewDestination === 'compensated_time') { setReviewCompensatedHours(h.toString()); setReviewPayrollHours('0'); } else if (reviewDestination === 'mixed') { setReviewPayrollHours((h / 2).toString()); setReviewCompensatedHours((h / 2).toString()); } }; const handleDestinationChange = (dest) => { setReviewDestination(dest); const h = parseFloat(reviewHours) || 0; if (dest === 'payroll') { setReviewPayrollHours(h.toString()); setReviewCompensatedHours('0'); } else if (dest === 'compensated_time') { setReviewCompensatedHours(h.toString()); setReviewPayrollHours('0'); } else if (dest === 'mixed') { setReviewPayrollHours((h / 2).toString()); setReviewCompensatedHours((h / 2).toString()); } else if (dest === 'rejected') { setReviewPayrollHours('0'); setReviewCompensatedHours('0'); } }; // Guardar Aprobación const handleSaveReview = async (isReject = false) => { if (isReadOnly) return alert("Modo supervisión: no tienes permisos para realizar esta acción."); if (!reviewingLog) return; setIsSavingReview(true); try { const finalStatus = isReject ? 'rejected' : 'approved'; const finalDest = isReject ? 'rejected' : reviewDestination; const finalApprHours = isReject ? 0 : (parseFloat(reviewHours) || 0); const finalPayHours = isReject ? 0 : (parseFloat(reviewPayrollHours) || 0); const finalCompHours = isReject ? 0 : (parseFloat(reviewCompensatedHours) || 0); const res = await window.apiCall('approve_overtime_log', { id: reviewingLog.id, status: finalStatus, approved_hours: finalApprHours, destination: finalDest, payroll_hours: finalPayHours, compensated_hours: finalCompHours, admin_notes: adminNotes, admin_user_id: currentUser?.id }); if (res && res.success) { if (res.is_rolled_over) { alert(`ℹ️ Arrastre a Quincena Abierta:\n\n${res.message || 'Las horas extras aprobadas correspondían a un período oficialmente pagado. Se asignaron automáticamente a la quincena abierta actual.'}`); if (showToast) showToast("ℹ️ Horas extras asignadas como arrastre a la quincena actual"); } else if (showToast) { showToast(isReject ? "Registro de horas extras rechazado." : "¡Horas extras aprobadas y distribuidas exitosamente!"); } setReviewingLog(null); await loadOvertimeData(); if (refreshData) await refreshData(); } else { alert("Error al guardar revisión: " + (res?.error || "Error desconocido")); } } catch (e) { alert("Error de conexión al guardar revisión."); } finally { setIsSavingReview(false); } }; // Guardar Permiso por Tiempo Compensado const handleSaveGrantLeave = async () => { if (isReadOnly) return alert("Modo supervisión: no tienes permisos."); if (!grantingUser) return; const hrs = parseFloat(grantHours) || 0; if (hrs <= 0) return alert("Ingresa un número de horas válido."); setIsSavingGrant(true); try { const res = await window.apiCall('grant_compensated_time_leave', { user_id: grantingUser.id, date: grantDate, hours_used: hrs, reason: grantReason, admin_user_id: currentUser?.id }); if (res && res.success) { if (showToast) showToast(`¡Permiso concedido! Se descontaron ${hrs} hrs del saldo de ${grantingUser.name}.`); setGrantingUser(null); await loadOvertimeData(); if (refreshData) await refreshData(); } else { alert("Error al otorgar permiso: " + (res?.error || "Error desconocido")); } } catch (e) { alert("Error de conexión al registrar permiso."); } finally { setIsSavingGrant(false); } }; // Guardar Ajuste Manual de Saldo const handleSaveAdjustBalance = async () => { if (isReadOnly) return alert("Modo supervisión: no tienes permisos."); if (!adjustingUser) return; const val = parseFloat(newBalanceInput); if (isNaN(val) || val < 0) return alert("Ingresa un saldo en horas válido (mínimo 0)."); setIsSavingAdjust(true); try { const res = await window.apiCall('adjust_user_compensated_time', { user_id: adjustingUser.id, new_balance: val, note: adjustNoteInput || 'Ajuste Manual por Administración', admin_user_id: currentUser?.id }); if (res && res.success) { if (showToast) showToast(`Saldo de tiempo compensado actualizado a ${val} hrs.`); setAdjustingUser(null); await loadOvertimeData(); if (refreshData) await refreshData(); } else { alert("Error al ajustar saldo: " + (res?.error || "Error desconocido")); } } catch (e) { alert("Error de conexión al ajustar saldo."); } finally { setIsSavingAdjust(false); } }; return (
Auditoría de reingresos, aprobación flexible a planilla o bolsa de descansos remunerados.
Reingresos pendientes de aprobación
Liquidables a tarifa doble (100% recargo)
~{(stats.totalCompensatedBalance / 8).toFixed(1)} días libres para personal
Otorgados sin afectar vacaciones ni salario
Se detectaron {stats.excessLogs.length} registro(s) con más de 3 horas extraordinarias en una jornada diaria. El estándar legal de Nicaragua establece un máximo de 3 horas por día (máx. 9 semanales), salvo acuerdo mutuo voluntario entre el empleador y el colaborador o emergencias operativas. El sistema permite aprobar y pagar la totalidad de las horas registradas si existe dicho acuerdo.
No hay registros de horas extras en esta categoría.
Los reingresos extraordinarios y jornadas en días libres aparecerán aquí automáticamente.
Entrada: {log.entry_time || '---'} • Salida: {log.exit_time || 'En curso...'}
Motivo: {log.justification || 'Reingreso Extraordinario'}
| Colaborador | Cargo | Saldo Acumulado (Horas) | Equivalente Días Libres | Acción |
|---|---|---|---|---|
|
{u.avatar_url ? (
{(u.name || 'U')[0].toUpperCase()}
)}
{u.name}
|
{u.position_title || 'Colaborador'} | 0 ? 'bg-indigo-50 text-indigo-700 border border-indigo-200' : 'bg-slate-100 text-slate-400'}`}> {hrs.toFixed(1)} hrs | {days} días |
| Fecha Descanso | Colaborador | Horas Descontadas | Motivo / Justificación | Autorizado por |
|---|---|---|---|---|
| No hay registro de descansos compensados otorgados aún. | ||||
| {window.formatDateToNIC ? window.formatDateToNIC(u.date_str) : u.date_str} | {u.user_name} | -{u.hours_used} hrs | {u.reason} | {u.approved_by_name || 'Administración'} |
{reviewingLog.user_name} • {reviewingLog.date_str}
{grantingUser.name}
Este permiso otorgado descontará del saldo de tiempo compensado. NO tocará el salario de planilla ni restará del saldo de vacaciones acumuladas.
{adjustingUser.name}