const { useState, useMemo, useEffect } = React; window.AdminPlanilla = function({ globalUsers = [], absences = [], logs = [], vacations = [], settings = {}, refreshData }) { const [activeTab, setActiveTab] = useState('payroll'); const [periodFilter, setPeriodFilter] = useState('current_fortnight'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); // Variables guardadas en base de datos por colaborador const [payrollVariables, setPayrollVariables] = useState({}); const [savingVarId, setSavingVarId] = useState(null); // Modal de Calendario de Asistencia const [calendarModalUser, setCalendarModalUser] = useState(null); // Configuración de cortes const [cutStartDayQ1, setCutStartDayQ1] = useState(settings?.cutStartDayQ1 || 1); const [cutEndDayQ1, setCutEndDayQ1] = useState(settings?.cutEndDayQ1 || 15); const [cutStartDayQ2, setCutStartDayQ2] = useState(settings?.cutStartDayQ2 || 16); const [cutEndDayQ2, setCutEndDayQ2] = useState(settings?.cutEndDayQ2 || 30); const [isSavingConfig, setIsSavingConfig] = useState(false); // Estado Finiquito const [selectedSeveranceUser, setSelectedSeveranceUser] = useState(null); const [sevTermDate, setSevTermDate] = useState(window.getNICDate()); const [sevReason, setSevReason] = useState('despido_sin_causa_art45'); const [sevAvgVariable, setSevAvgVariable] = useState(0); const [sevVacDaysPending, setSevVacDaysPending] = useState(0); const todayStr = window.getNICDate(); const [curYear, curMonth, curDay] = todayStr.split('-').map(Number); const applyPeriodPreset = (preset) => { setPeriodFilter(preset); const y = curYear; const m = curMonth; if (preset === 'today_accumulated') { const qStart = curDay <= 15 ? 1 : 16; setStartDate(`${y}-${String(m).padStart(2,'0')}-${String(qStart).padStart(2,'0')}`); setEndDate(todayStr); } 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}`); } else if (preset === 'last_month') { 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')}-01`); setEndDate(`${prevY}-${String(prevM).padStart(2,'0')}-${lastDay}`); } }; useEffect(() => { applyPeriodPreset('current_fortnight'); }, []); // Cargar variables guardadas en BD cada vez que cambian las fechas de corte const loadPayrollVariablesFromDB = async () => { if (!startDate || !endDate) return; try { const res = await window.apiCall('get_payroll_items', { period_start: startDate, period_end: endDate }); if (res && res.success && res.items) { const map = {}; res.items.forEach(it => { map[it.user_id] = { overtime: parseFloat(it.overtime_hours || 0), holidays: parseFloat(it.holidays_worked || 0), commissions: parseFloat(it.commissions || 0), bonuses: parseFloat(it.bonuses || 0) }; }); setPayrollVariables(map); } } catch (e) {} }; useEffect(() => { loadPayrollVariablesFromDB(); }, [startDate, endDate]); const periodDaysCount = useMemo(() => { if (!startDate || !endDate) return 15; return window.getDaysDiff(startDate, endDate); }, [startDate, endDate]); // Guardar variable en base de datos al cambiar valor const handleSaveVariable = async (userId, field, value) => { const numVal = parseFloat(value) || 0; const current = payrollVariables[userId] || { overtime: 0, holidays: 0, commissions: 0, bonuses: 0 }; const updated = { ...current, [field]: numVal }; setPayrollVariables(prev => ({ ...prev, [userId]: updated })); setSavingVarId(userId); await window.apiCall('save_payroll_item', { user_id: userId, period_start: startDate, period_end: endDate, overtime_hours: updated.overtime || 0, holidays_worked: updated.holidays || 0, commissions: updated.commissions || 0, bonuses: updated.bonuses || 0 }); setTimeout(() => setSavingVarId(null), 800); }; const activeWorkersList = useMemo(() => { return (globalUsers || []).filter(u => u.status === 'activo'); }, [globalUsers]); const processedPayroll = useMemo(() => { return activeWorkersList.map(u => { const vars = payrollVariables[u.id] || {}; const calc = window.calculatePayrollRowAdv(u, { payrollPeriod: periodDaysCount > 20 ? 'mensual' : 'quincenal', periodDays: periodDaysCount, startDate, endDate, totalActiveWorkers: activeWorkersList.length, absences, logs, vacations, overtimeHours: vars.overtime || 0, holidaysWorkedManual: vars.holidays || 0, commissions: vars.commissions || 0, bonuses: vars.bonuses || 0, inssPatronalRateCustom: settings?.inssPatronalRate || null }); return { user: u, calc, vars }; }); }, [activeWorkersList, payrollVariables, periodDaysCount, startDate, endDate, absences, logs, vacations, settings]); const totalsSummary = useMemo(() => { let gross = 0, inssLab = 0, ir = 0, net = 0, inssPat = 0, inatec = 0, totalCost = 0; processedPayroll.forEach(p => { gross += parseFloat(p.calc.grossSalary); inssLab += parseFloat(p.calc.inssLaboral); ir += parseFloat(p.calc.irDGI); net += parseFloat(p.calc.netSalary); inssPat += parseFloat(p.calc.inssPatronal); inatec += parseFloat(p.calc.inatecPatronal); totalCost += parseFloat(p.calc.totalCostEmployer); }); return { gross, inssLab, ir, net, inssPat, inatec, totalCost }; }, [processedPayroll]); const severanceResult = useMemo(() => { if (!selectedSeveranceUser) return null; return window.calculateSeveranceNI({ user: selectedSeveranceUser, hiringDate: selectedSeveranceUser.hiringDate || window.getNICDate(), terminationDate: sevTermDate, reason: sevReason, monthlyBaseSalary: parseFloat(selectedSeveranceUser.baseSalary || 0), averageVariableSalary: parseFloat(sevAvgVariable || 0), vacationDaysPending: parseFloat(sevVacDaysPending || 0) }); }, [selectedSeveranceUser, sevTermDate, sevReason, sevAvgVariable, sevVacDaysPending]); const handleOpenSeveranceForUser = (u) => { setSelectedSeveranceUser(u); const vacBal = parseFloat(window.calculateVacationBalance(u, settings?.vacationRate || 2.5, vacations)); setSevVacDaysPending(vacBal); setSevAvgVariable(0); setSevTermDate(window.getNICDate()); setActiveTab('severance'); }; const handleSaveCutConfig = async (e) => { e.preventDefault(); setIsSavingConfig(true); const res = await window.apiCall('update_settings', { cut_start_q1: parseInt(cutStartDayQ1), cut_end_q1: parseInt(cutEndDayQ1), cut_start_q2: parseInt(cutStartDayQ2), cut_end_q2: parseInt(cutEndDayQ2) }); setIsSavingConfig(false); if (res && res.success) { alert("Ventanas de corte actualizadas."); if (refreshData) await refreshData(); } }; const exportCSVReport = () => { const headers = [ "ID", "Empleado", "Correo", "Salario Base Mensual (C$)", "Días Corte", "Faltas Injust.", "Días Deducidos", "Salario Período", "Horas Extras (C$)", "Feriados (C$)", "Comisiones/Bonos (C$)", "Salario Bruto (C$)", "INSS Laboral 7%", "IR DGI", "Neto a Pagar (C$)", `INSS Patronal (${activeWorkersList.length >= 20 ? '22.5%' : '21.5%'})`, "INATEC 2%", "Costo Empresa (C$)" ]; const rows = processedPayroll.map(p => [ p.user.id, p.user.name, p.user.email, p.user.baseSalary || 0, periodDaysCount, p.calc.unjustifiedCount, p.calc.deductedDays, p.calc.basePeriodPay, p.calc.overtimePay, p.calc.holidayPay, p.calc.variablePay, p.calc.grossSalary, p.calc.inssLaboral, p.calc.irDGI, p.calc.netSalary, p.calc.inssPatronal, p.calc.inatecPatronal, p.calc.totalCostEmployer ]); window.exportToCSV(`Planilla_${startDate}_al_${endDate}`, headers, rows); }; return (
Auditoría Real de Bitácora • Deducción Séptimo Día • Horas Extras • Finiquitos
INSS ({activeWorkersList.length >= 20 ? '22.5%' : '21.5%'}) + INATEC 2%
| Colaborador | Salario Base | Auditoría Asistencia | Horas Extras | Comisiones / Bonos | Total Bruto | INSS (7%) | IR DGI | Neto | Cargas Patronales | Acciones |
|---|---|---|---|---|---|---|---|---|---|---|
|
{u.name} @{u.alias} • {u.positionTitle || u.role} |
C$ {parseFloat(u.baseSalary || 0).toFixed(2)} |
handleSaveVariable(u.id, 'overtime', e.target.value)}
className="w-14 p-1.5 bg-slate-50 border rounded-lg text-center font-black text-xs outline-none focus:border-indigo-600"
/>
h = C${calc.overtimePay}
|
handleSaveVariable(u.id, 'commissions', e.target.value)}
className="w-20 p-1.5 bg-slate-50 border rounded-lg text-center font-black text-xs outline-none focus:border-indigo-600"
/>
|
C$ {calc.grossSalary} | - C$ {calc.inssLaboral} | - C$ {calc.irDGI} | C$ {calc.netSalary} |
INSS: C$ {calc.inssPatronal} INATEC: C$ {calc.inatecPatronal} |
Auditoría de Asistencia del Período ({startDate} al {endDate})
Cálculo de Prestaciones Sociales según el Código del Trabajo de Nicaragua.
Selecciona un colaborador para calcular su finiquito legal.
Ingreso: {window.formatDateToNIC(selectedSeveranceUser.hiringDate)} • Salida: {window.formatDateToNIC(sevTermDate)}
Antigüedad Total: {severanceResult.yearsWorkedText} ({severanceResult.totalDaysWorked} días laborados)
1. Vacaciones No Gozadas (Art. 76 CT)
{severanceResult.vacationDaysPending} días pendientes × C$ {severanceResult.dailyRate} (Deduce INSS e IR)
C$ {severanceResult.vacationGross}
- C$ {severanceResult.vacationInssLaboral} (INSS) - C$ {severanceResult.vacationIR} (IR)
2. Décimo Tercer Mes / Aguinaldo (Art. 93 CT)
{severanceResult.aguinaldoDays} días proporcionales • Exento de INSS e IR
C$ {severanceResult.aguinaldoGross}
3. Indemnización por Antigüedad (Art. 45 CT)
{sevReason === 'despido_sin_causa_art45' ? '1 mes/año (1-3 años) + 20 días/año posterior (Tope 5 meses) • Exento' : 'No aplica por causa de retiro'}
C$ {severanceResult.indemnityGross}
Total Neto a Entregar
Bruto: C$ {severanceResult.totalGross} • Retenciones: C$ {severanceResult.totalDeductions}
Configura los días de corte en que el sistema consolida las asistencias, incidencias y extras para la planilla de tu empresa.