const { useState, useMemo } = React; window.AdminAusencias = function({ globalUsers = [], absences = [], logs = [], vacations = [], settings = {}, saveAbsence, confirmAction, isReadOnly = false, showToast, refreshData }) { const todayStr = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]; const [curYear, curMonth, curDay] = todayStr.split('-').map(Number); // Filtros de período const [periodPreset, setPeriodPreset] = useState('current_fortnight'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [filterUser, setFilterUser] = useState(''); const [filterCategory, setFilterCategory] = useState('ALL'); // 'ALL', 'unresolved', 'injustificada', 'justificada' // Modal de justificación / resolución rápida de inasistencias const [targetAbsence, setTargetAbsence] = useState(null); const [selectedType, setSelectedType] = useState('injustificada'); const [absenceNotes, setAbsenceNotes] = useState(''); const [isProcessing, setIsProcessing] = useState(false); // Modal para registrar permiso anticipado manual const [showManualModal, setShowManualModal] = useState(false); const [manualUserId, setManualUserId] = useState(''); const [manualDate, setManualDate] = useState(todayStr); const [manualType, setManualType] = useState('justificada'); const [manualNotes, setManualNotes] = useState(''); const applyPeriodPreset = (preset) => { setPeriodPreset(preset); const y = curYear; const m = curMonth; if (preset === 'today') { setStartDate(todayStr); setEndDate(todayStr); } else if (preset === 'this_week') { const curr = new Date(); const dayOfWeek = curr.getDay(); const diffToMon = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; const mon = new Date(curr.getFullYear(), curr.getMonth(), curr.getDate() + diffToMon); const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6); const fmt = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`; setStartDate(fmt(mon)); setEndDate(fmt(sun)); } else if (preset === 'current_fortnight') { if (curDay <= 15) { setStartDate(`${y}-${String(m).padStart(2,'0')}-01`); setEndDate(`${y}-${String(m).padStart(2,'0')}-15`); } else { const lastDay = new Date(y, m, 0).getDate(); setStartDate(`${y}-${String(m).padStart(2,'0')}-16`); setEndDate(`${y}-${String(m).padStart(2,'0')}-${lastDay}`); } } else if (preset === 'last_fortnight') { if (curDay <= 15) { const prevM = m === 1 ? 12 : m - 1; const prevY = m === 1 ? y - 1 : y; const lastDay = new Date(prevY, prevM, 0).getDate(); setStartDate(`${prevY}-${String(prevM).padStart(2,'0')}-16`); setEndDate(`${prevY}-${String(prevM).padStart(2,'0')}-${lastDay}`); } else { setStartDate(`${y}-${String(m).padStart(2,'0')}-01`); setEndDate(`${y}-${String(m).padStart(2,'0')}-15`); } } else if (preset === 'current_month') { const lastDay = new Date(y, m, 0).getDate(); setStartDate(`${y}-${String(m).padStart(2,'0')}-01`); setEndDate(`${y}-${String(m).padStart(2,'0')}-${lastDay}`); } }; React.useEffect(() => { applyPeriodPreset('current_fortnight'); }, []); // MOTOR AUTOMÁTICO DE DETECCIÓN DE INASISTENCIAS // Evalúa cada día del rango contra cada empleado activo const detectedInattendances = useMemo(() => { if (!startDate || !endDate) return []; const dateRange = window.getDatesInRange(startDate, endDate); const results = []; const activeWorkers = (globalUsers || []).filter(u => u.status === 'activo'); dateRange.forEach(dateStr => { // No evaluar días que aún no han ocurrido if (dateStr > todayStr) return; // Exentar Días Feriados de la Empresa (Nicaragua) if (settings?.holidays && Array.isArray(settings.holidays)) { const isHoliday = settings.holidays.some(h => h.date === dateStr); if (isHoliday) return; } activeWorkers.forEach(user => { const isOffDay = window.isUserOffDay(user.scheduleDays, dateStr); // Si le tocaba descansar, no cuenta como falta if (isOffDay) return; // Si está en período de vacaciones aprobadas const onVacation = (vacations || []).some(v => Number(v.userId) === Number(user.id) && v.status === 'approved' && v.startDate <= dateStr && (v.endDate || v.startDate) >= dateStr && !(v.revokedDates || []).includes(dateStr) ); if (onVacation) return; // Verificar si tuvo alguna marca real en shift_logs const userLogs = (logs || []).filter(l => Number(l.userId) === Number(user.id) && l.date === dateStr); const hasPunches = userLogs.length > 0; // Buscar si ya existe una resolución formal en shift_absences const formalAbsence = (absences || []).find(a => Number(a.userId) === Number(user.id) && a.date === dateStr); // Si no tuvo marcas, es una inasistencia detectada if (!hasPunches) { results.push({ id: formalAbsence ? formalAbsence.id : `auto_${user.id}_${dateStr}`, isFormal: Boolean(formalAbsence), userId: user.id, userName: user.name, alias: user.alias, avatarUrl: user.avatarUrl, date: dateStr, baseSalary: user.baseSalary || 0, type: formalAbsence ? formalAbsence.type : 'pendiente', notes: formalAbsence ? formalAbsence.notes : '', isUnresolved: !formalAbsence }); } }); }); return results.sort((a,b) => b.date.localeCompare(a.date)); }, [startDate, endDate, globalUsers, logs, absences, vacations, todayStr]); // Filtrado de la bandeja const filteredList = useMemo(() => { return detectedInattendances.filter(item => { if (filterUser && !item.userName.toLowerCase().includes(filterUser.toLowerCase())) return false; if (filterCategory === 'unresolved' && !item.isUnresolved) return false; if (filterCategory === 'injustificada' && item.type !== 'injustificada') return false; if (filterCategory === 'justificada' && (item.type !== 'justificada' && item.type !== 'sin_goce')) return false; return true; }); }, [detectedInattendances, filterUser, filterCategory]); // Métricas para KPI Cards const kpiSummary = useMemo(() => { const total = detectedInattendances.length; const unresolved = detectedInattendances.filter(i => i.isUnresolved).length; const unexcused = detectedInattendances.filter(i => i.type === 'injustificada').length; const excused = detectedInattendances.filter(i => i.type === 'justificada' || i.type === 'sin_goce').length; return { total, unresolved, unexcused, excused }; }, [detectedInattendances]); // Guardar resolución de inasistencia (conecta con Vacaciones y Planilla) const handleApplyResolution = async (e) => { e.preventDefault(); if (!targetAbsence) return; setIsProcessing(true); try { const uId = Number(targetAbsence.userId); // 1. Si se eligió deducir de vacaciones acumuladas if (selectedType === 'vacaciones') { const resVac = await window.apiCall('deduct_vacation_from_audit', { userId: uId, days: 1.0, date: targetAbsence.date, reason: `Deducción por ausencia el ${window.formatDateToNIC(targetAbsence.date)} (${absenceNotes || 'Compensado con Vacaciones'})` }); if (!resVac || !resVac.success) { setIsProcessing(false); alert("Error al deducir de vacaciones: " + (resVac?.error || "")); return; } } // 2. Guardar en shift_absences const dbType = selectedType === 'vacaciones' ? 'justificada' : selectedType; const finalNote = selectedType === 'vacaciones' ? `Compensado con 1 día de Vacaciones de Ley${absenceNotes ? ` • ${absenceNotes}` : ''}` : absenceNotes; const resAbs = await window.apiCall('save_absence', { userId: uId, user_id: uId, date: targetAbsence.date, type: dbType, notes: finalNote }); if (resAbs && resAbs.success) { if (showToast) showToast("Inasistencia clasificada y guardada exitosamente."); setTargetAbsence(null); setAbsenceNotes(''); if (refreshData) await refreshData(); } else { alert("Error al guardar: " + (resAbs?.error || "")); } } catch (err) { alert("Error de conexión: " + err.message); } finally { setIsProcessing(false); } }; // Crear permiso anticipado manual const handleCreateManualAbsence = async (e) => { e.preventDefault(); if (!manualUserId) return alert("Selecciona un colaborador."); setIsProcessing(true); const uId = Number(manualUserId); const res = await window.apiCall('save_absence', { userId: uId, user_id: uId, date: manualDate, type: manualType, notes: manualNotes }); setIsProcessing(false); if (res && res.success) { if (showToast) showToast("Permiso anticipado registrado con éxito."); setShowManualModal(false); setManualNotes(''); if (refreshData) await refreshData(); } else { alert("Error: " + (res?.error || "No se pudo registrar")); } }; // Eliminar / Revertir ausencia formal const handleDeleteFormalAbsence = async (absenceId, userName) => { if (isReadOnly) return; confirmAction(`¿Revertir la resolución de inasistencia de ${userName}? Volverá a quedar pendiente de justificar.`, async () => { const res = await window.apiCall('delete_absence', { id: absenceId }); if (res && res.success) { if (showToast) showToast("Resolución revertida."); if (refreshData) await refreshData(); } }); }; return (
{/* CABECERA PRINCIPAL Y PRESETS */}

Bitácora y Control de Ausencias

Detección proactiva de inasistencias • Aplicación Art. 64 CT (Séptimo Día) • Deducción a Vacaciones

{!isReadOnly && ( )}
{/* FILTROS SECUNDARIOS */}
setFilterUser(e.target.value)} placeholder="Buscar colaborador por nombre..." className="w-full p-3 bg-slate-50 border rounded-xl font-bold text-xs outline-none focus:border-indigo-500" />
Rango: {window.formatDateToNIC(startDate)} al {window.formatDateToNIC(endDate)}
{/* KPI METRIC CARDS */}
Inasistencias Totales

{kpiSummary.total}

Días laborales sin marcaje

Por Justificar / Resolver

{kpiSummary.unresolved}

Requieren acción administrativa

Faltas Injustificadas

{kpiSummary.unexcused}

Deducen doble día (Séptimo Día)

Justificadas / Vacaciones

{kpiSummary.excused}

Con soporte o compensadas

{/* BANDEJA INTELIGENTE DE AUSENCIAS DETECTADAS */}

Bitácora Operativa de Inasistencias ({filteredList.length})

Auditoría cruzada en tiempo real contra turnos de contrato
{filteredList.length === 0 ? (

Asistencia Completa

No se detectaron inasistencias en el período y filtros seleccionados.

) : ( filteredList.map((item, idx) => { const dailyCost = (parseFloat(item.baseSalary) / 30).toFixed(2); const doubleCost = (parseFloat(dailyCost) * 2).toFixed(2); return (
{item.avatarUrl ? ( {item.userName} ) : (
{item.userName ? item.userName[0].toUpperCase() : 'U'}
)}

{item.userName}

@{item.alias} • Base: C$ {parseFloat(item.baseSalary).toLocaleString('en-US', { minimumFractionDigits: 2 })}

Costo Día: C$ {dailyCost} | Séptimo Día (x2): C$ {doubleCost}
{/* FECHA Y ESTADO DETECTADO */}
Fecha de Inasistencia {window.formatDateToNIC(item.date)} ({window.getDayOfWeekCode(item.date).toUpperCase()}) 0 marcajes registrados en jornada regular
{/* CONDICIÓN Y NOTA */}
Resolución Legal {item.isUnresolved ? (
Sin Resolver (Pendiente)
) : item.type === 'injustificada' ? (
Falta Injustificada (-2 días Art. 64) {item.notes &&

"{item.notes}"

}
) : item.type === 'sin_goce' ? (
Permiso Sin Goce (-1 día salario) {item.notes &&

"{item.notes}"

}
) : (
Permiso Justificado / Compensado {item.notes &&

"{item.notes}"

}
)}
{/* BOTÓN DE RESOLUCIÓN RÁPIDA */} {!isReadOnly && (
{item.isFormal && ( )}
)}
); }) )}
{/* MODAL CLASIFICAR / RESOLVER INASISTENCIA */} {targetAbsence && (
setTargetAbsence(null)}>
e.stopPropagation()}>

Resolución de Inasistencia

{targetAbsence.userName} • Fecha: {window.formatDateToNIC(targetAbsence.date)}

{selectedType === 'injustificada' && (
Efecto en Nómina: Se aplicará la ley laboral nicaragüense. El colaborador sufrirá la deducción del día no laborado más el descanso proporcional (2 días en total).
)} {selectedType === 'vacaciones' && (
Efecto en Vacaciones: Se insertará un registro en el módulo de Vacaciones restando 1 día del acumulado. En nómina recibirá su salario quincenal completo.
)}