const { useState, useMemo, useEffect } = React; window.AdminPlanilla = function({ globalUsers = [], absences = [], logs = [], vacations = [], settings = {}, refreshData }) { const [activeTab, setActiveTab] = useState(() => { const saved = localStorage.getItem('shift_planilla_subtab'); return ['payroll', 'loans', 'severance', 'cut_config'].includes(saved) ? saved : 'payroll'; }); useEffect(() => { try { localStorage.setItem('shift_planilla_subtab', activeTab); } catch (e) {} }, [activeTab]); // Filtros de período 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 [activeLoans, setActiveLoans] = useState([]); const [allOvertimeLogs, setAllOvertimeLogs] = useState([]); const [payrollClosureStatus, setPayrollClosureStatus] = useState(null); const [isTogglingClosure, setIsTogglingClosure] = useState(false); // Modales y Drawers const [selectedDetailEmployee, setSelectedDetailEmployee] = useState(null); // Drawer lateral con colilla const [editingAdjustmentUser, setEditingAdjustmentUser] = useState(null); // Modal de horas extras / bonos const [calendarModalUser, setCalendarModalUser] = useState(null); // Modal calendario auditoría const [showLoanModal, setShowLoanModal] = useState(false); // Modal nuevo préstamo const [showBankModal, setShowBankModal] = useState(false); // Modal dispersión bancaria masiva const [showDisbursementModal, setShowDisbursementModal] = useState(false); // Modal de selección de cuenta para pago de planilla const [showClosureAuditModal, setShowClosureAuditModal] = useState(false); // Modal de auditoría e historial de cierres const [selectedPaymentAccount, setSelectedPaymentAccount] = useState('Efectivo / Caja Chica'); const [disbursementNotes, setDisbursementNotes] = useState(''); const [showExportMenu, setShowExportMenu] = useState(false); // Dropdown exportación y reportes const [bankFormat, setBankFormat] = useState('bac'); // 'bac' | 'banpro' | 'lafise' | 'multibank' const [companyDebitAcc, setCompanyDebitAcc] = useState(settings?.companyDebitAccount || ''); useEffect(() => { if (settings?.companyDebitAccount) { setCompanyDebitAcc(settings.companyDebitAccount); } }, [settings?.companyDebitAccount]); // Campos del formulario de ajuste rápido const [adjOvertime, setAdjOvertime] = useState(''); const [adjCommissions, setAdjCommissions] = useState(''); const [adjBonuses, setAdjBonuses] = useState(''); // Campos préstamo / adelanto const [loanUserId, setLoanUserId] = useState(''); const [loanAmount, setLoanAmount] = useState(''); const [loanInstallments, setLoanInstallments] = useState(2); const [loanNotes, setLoanNotes] = useState('Adelanto de Salario'); // Configuración 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); // Liquidación 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'); }, []); // State for initial asynchronous data loading const [isLoadingData, setIsLoadingData] = useState(true); const loadData = async () => { if (!startDate || !endDate) return; setIsLoadingData(true); try { const [resVars, resLoans, resClosure, resOvertime] = await Promise.all([ window.apiCall('get_payroll_items', { period_start: startDate, period_end: endDate }), window.apiCall('get_loans', {}), window.apiCall('get_payroll_closure_status', { period_start: startDate, period_end: endDate }), window.apiCall('get_overtime_logs', {}) ]); if (resVars && resVars.success && resVars.items) { const map = {}; resVars.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); } if (resLoans && resLoans.success) { setActiveLoans(resLoans.loans || []); } if (resOvertime && resOvertime.success) { setAllOvertimeLogs(resOvertime.overtimeLogs || []); } if (resClosure && resClosure.success) { setPayrollClosureStatus(resClosure); } else { setPayrollClosureStatus(null); } } catch (e) { } finally { setIsLoadingData(false); } }; const handleTogglePayrollClosure = async (shouldClose, payAccount = null, customNotes = null) => { if (shouldClose && !payAccount) { setShowDisbursementModal(true); return; } if (!shouldClose) { const unlockReason = prompt(`ATENCIÓN: La planilla del período ${startDate} al ${endDate} está OFICIALMENTE PAGADA Y BLOQUEADA.\n\nPara desprotegerla y regresar a modo borrador, debes ingresar el motivo de reapertura para el registro de auditoría:`); if (!unlockReason || !unlockReason.trim()) { alert("Acción cancelada: Se requiere una justificación válida para reabrir una planilla pagada."); return; } customNotes = `Reapertura autorizada. Motivo: ${unlockReason.trim()}`; } setIsTogglingClosure(true); try { const targetAccount = payAccount || selectedPaymentAccount || 'Efectivo / Caja Chica'; const fullNotes = shouldClose ? `Pago realizado mediante: ${targetAccount}${customNotes || disbursementNotes ? ' - ' + (customNotes || disbursementNotes) : ''}` : (customNotes || 'Reabierta para correcciones'); const res = await window.apiCall('toggle_payroll_closure', { period_start: startDate, period_end: endDate, status: shouldClose ? 'paid' : 'draft', total_net_disbursed: totalsSummary?.net || 0, total_gross_disbursed: totalsSummary?.gross || 0, notes: fullNotes }); if (res && res.success) { setShowDisbursementModal(false); await loadData(); } else { alert(res?.error || 'No se pudo actualizar el estado de cierre de la planilla'); } } catch (err) { alert('Error de conexión al cambiar el estado de la planilla'); } finally { setIsTogglingClosure(false); } }; const handleToggleUserPayment = async (uId, currentStatus) => { if (payrollClosureStatus?.closed) { alert("Planilla Bloqueada: Esta planilla está oficialmente pagada y cerrada. Para modificar pagos individuales, haz clic en 'Reabrir Planilla' e ingresa la justificación de auditoría."); return; } const newStatus = currentStatus === 'paid' ? 'pending' : 'paid'; const confirmMsg = newStatus === 'paid' ? '¿Estás seguro de marcar el pago de este colaborador como PAGADO?\n\nAl marcarlo como pagado, su colilla de este período se publicará inmediatamente en su portal de autoservicio.' : '¿Estás seguro de cambiar el estado a PENDIENTE DE PAGO?\n\nLa colilla dejará de ser visible para el trabajador hasta que autorices su pago.'; if (!confirm(confirmMsg)) return; setIsTogglingClosure(true); try { const res = await window.apiCall('toggle_user_payroll_payment', { period_start: startDate, period_end: endDate, user_id: uId, status: newStatus, notes: newStatus === 'paid' ? 'Pago individual autorizado por administración' : 'Pendiente de dispersión' }); if (res && res.success) { await loadData(); } else { alert(res?.error || 'No se pudo actualizar el pago del colaborador.'); } } catch (err) { alert('Error de conexión al actualizar pago del colaborador'); } finally { setIsTogglingClosure(false); } }; const handleBulkUserPayment = async (statusToSet) => { if (payrollClosureStatus?.closed) { alert("Planilla Bloqueada: Esta planilla se encuentra oficialmente procesada y PAGADA. Para modificar pagos en bloque, haz clic en 'Reabrir Planilla' e ingresa el motivo de desprotección."); return; } const allUserIds = activeWorkersList.map(w => w.id); if (allUserIds.length === 0) return; const confirmMsg = statusToSet === 'paid' ? `¿Estás seguro de MARCAR COMO PAGADOS A TODOS (${allUserIds.length}) los colaboradores para el período ${startDate} al ${endDate}?` : `¿Estás seguro de MARCAR PENDIENTES DE PAGO A TODOS los colaboradores para el período ${startDate} al ${endDate}?`; if (!confirm(confirmMsg)) return; setIsTogglingClosure(true); try { const res = await window.apiCall('toggle_user_payroll_payment', { period_start: startDate, period_end: endDate, user_ids: allUserIds, status: statusToSet, notes: statusToSet === 'paid' ? 'Pago masivo autorizado por administración' : 'Marcar pendientes en bloque' }); if (res && res.success) { await loadData(); } else { alert(res?.error || 'No se pudo procesar la actualización en bloque.'); } } catch (e) { alert('Error de conexión.'); } finally { setIsTogglingClosure(false); } }; const [isSendingPayslips, setIsSendingPayslips] = useState(false); const handleSendPayslipNotifications = async (targetUserIds = null, channel = 'all') => { const ids = targetUserIds ? (Array.isArray(targetUserIds) ? targetUserIds : [targetUserIds]) : activeWorkersList.map(w => w.id); if (!ids.length) { alert('No hay colaboradores seleccionados.'); return; } const payrollDataMap = {}; processedPayroll.forEach(p => { if (p.user && p.user.id) { const totalDeds = ( parseFloat(p.calc?.absenceDeduction || 0) + parseFloat(p.calc?.seventhDayDeduction || 0) + parseFloat(p.calc?.unpaidLeaveDeduction || 0) + parseFloat(p.calc?.earlyDepartureDeduction || 0) + parseFloat(p.calc?.inssLaboral || 0) + parseFloat(p.calc?.irDGI || 0) + parseFloat(p.loanInfo?.deduction || 0) ); payrollDataMap[p.user.id] = { netPayable: p.netPayable, scheduledPeriodPay: p.calc?.scheduledPeriodPay || '0.00', basePeriodPay: p.calc?.basePeriodPay || '0.00', overtimePay: p.calc?.overtimePay || '0.00', overtimeHours: p.vars?.overtime || 0, holidayPay: p.calc?.holidayPay || '0.00', holidaysWorked: p.calc?.effectiveHolidaysWorked || 0, commissions: p.vars?.commissions || 0, bonuses: p.vars?.bonuses || 0, grossSalary: p.calc?.grossSalary || '0.00', unjustifiedCount: p.calc?.unjustifiedCount || 0, absenceDeduction: p.calc?.absenceDeduction || '0.00', seventhDayDeduction: p.calc?.seventhDayDeduction || '0.00', unpaidLeaveCount: p.calc?.justifiedNoPayCount || 0, unpaidLeaveDeduction: p.calc?.unpaidLeaveDeduction || '0.00', earlyDepartureDeduction: p.calc?.earlyDepartureDeduction || '0.00', deductedMinutes: p.calc?.totalDeductedMinutes || 0, inssLaboral: p.calc?.inssLaboral || '0.00', irDGI: p.calc?.irDGI || '0.00', loanDeduction: p.loanInfo?.deduction || 0, loanNotes: p.loanInfo?.notes || 'Préstamo / Adelanto', totalDeductions: totalDeds.toFixed(2) }; } }); const channelName = channel === 'whatsapp' ? 'WhatsApp' : (channel === 'email' ? 'Correo Electrónico' : 'WhatsApp y Correo'); const confirmMsg = `¿Deseas enviar las colillas oficiales por ${channelName} a ${ids.length} colaborador(es) para el período ${startDate} al ${endDate}?`; if (!confirm(confirmMsg)) return; setIsSendingPayslips(true); try { const res = await window.apiCall('send_payslip_notification', { period_start: startDate, period_end: endDate, user_ids: ids, channel: channel, payroll_data: payrollDataMap }); if (res && res.success && res.dispatched_count > 0) { let msg = `✅ Se despacharon las colillas por ${channelName} (${res.dispatched_count} envíos confirmados).`; const waErr = res?.results?.find(r => r.whatsapp && !r.whatsapp.success)?.whatsapp?.error; const waFallback = res?.results?.find(r => r.whatsapp && r.whatsapp.is_fallback_text); const waSuccessTpl = res?.results?.find(r => r.whatsapp && r.whatsapp.used_template); if (waSuccessTpl) { msg += `\n\n✨ Plantilla Meta WhatsApp entregada con éxito: "${waSuccessTpl.whatsapp.used_template}"`; } if (waErr) { msg += `\n\n⚠️ NOTA SOBRE WHATSAPP:\n${waErr}`; } else if (waFallback) { msg += `\n\n⚠️ ATENCIÓN SOBRE PLANTILLA DE META:\nMeta rechazó la plantilla por lo que se envió como texto directo de respaldo.\nDetalle Meta: ${waFallback.whatsapp.template_error || 'Comprobar nombre/parámetros en Meta'}`; } alert(msg); } else { const errMsg = res?.error || (res?.results?.[0]?.whatsapp?.error || 'No se pudieron enviar las colillas.'); alert(`⚠️ ATENCIÓN EN ENVÍO:\n\n${errMsg}`); } } catch (e) { alert('Error de conexión al enviar notificaciones.'); } finally { setIsSendingPayslips(false); } }; useEffect(() => { loadData(); }, [startDate, endDate]); const periodDaysCount = useMemo(() => { if (!startDate || !endDate) return 15; return window.getDaysDiff(startDate, endDate); }, [startDate, endDate]); const openAdjustmentModal = (u, currentVars) => { if (payrollClosureStatus?.closed) { alert("Esta planilla ya fue marcada como PAGADA y sus valores están congelados por auditoría.\n\nSi necesitas modificar horas extras, comisiones o bonos de este período, haz clic en el botón 'Reabrir' primero."); return; } setEditingAdjustmentUser(u); setAdjOvertime(currentVars.overtime || ''); setAdjCommissions(currentVars.commissions || ''); setAdjBonuses(currentVars.bonuses || ''); }; const handleSaveAdjustmentModal = async (e) => { e.preventDefault(); if (!editingAdjustmentUser) return; const uId = editingAdjustmentUser.id; const updated = { overtime: parseFloat(adjOvertime) || 0, commissions: parseFloat(adjCommissions) || 0, bonuses: parseFloat(adjBonuses) || 0, holidays: (payrollVariables[uId]?.holidays) || 0 }; setPayrollVariables(prev => ({ ...prev, [uId]: updated })); setEditingAdjustmentUser(null); await window.apiCall('save_payroll_item', { user_id: uId, period_start: startDate, period_end: endDate, overtime_hours: updated.overtime, holidays_worked: updated.holidays, commissions: updated.commissions, bonuses: updated.bonuses }); }; const handleCreateLoan = async (e) => { e.preventDefault(); if (!loanUserId || !loanAmount) return; const res = await window.apiCall('save_loan', { user_id: loanUserId, total_amount: parseFloat(loanAmount), total_installments: parseInt(loanInstallments), notes: loanNotes, start_date: startDate || window.getNICDate() }); if (res && res.success) { setShowLoanModal(false); setLoanAmount(''); setLoanNotes('Adelanto de Salario'); loadData(); } }; 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, settings: settings || {} }); // Préstamo activo const userLoan = activeLoans.find(l => Number(l.user_id) === Number(u.id) && l.status === 'active' && parseFloat(l.remaining_balance) > 0); let loanDeduction = 0; let loanInfo = null; if (userLoan) { const installment = parseFloat(userLoan.installment_amount); const remaining = parseFloat(userLoan.remaining_balance); loanDeduction = Math.min(installment, remaining); loanInfo = { id: userLoan.id, deduction: loanDeduction, pendingAfter: Math.max(0, remaining - loanDeduction), total: parseFloat(userLoan.total_amount), installmentAmount: installment, notes: userLoan.notes }; } // Horas extras auditadas y aprobadas para este colaborador en el período (incluyendo arrastres post-cierre) const userOvertimeLogs = (allOvertimeLogs || []).filter(o => Number(o.user_id) === Number(u.id) && o.status === 'approved' && (o.destination === 'payroll' || o.destination === 'mixed') && parseFloat(o.payroll_hours || 0) > 0 && ( (o.payroll_period_start && o.payroll_period_start === startDate) || (!o.payroll_period_start && o.date_str >= startDate && o.date_str <= endDate) ) ); const netPayable = Math.max(0, parseFloat(calc.netSalary) - loanDeduction); return { user: u, calc, vars, loanInfo, netPayable: netPayable.toFixed(2), userOvertimeLogs }; }); }, [activeWorkersList, payrollVariables, activeLoans, allOvertimeLogs, periodDaysCount, startDate, endDate, absences, logs, vacations, settings]); const totalsSummary = useMemo(() => { let gross = 0, inssLab = 0, ir = 0, net = 0, loans = 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.netPayable); loans += p.loanInfo ? p.loanInfo.deduction : 0; inssPat += parseFloat(p.calc.inssPatronal); inatec += parseFloat(p.calc.inatecPatronal); totalCost += parseFloat(p.calc.totalCostEmployer); }); return { gross, inssLab, ir, net, loans, 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 exitosamente."); if (refreshData) await refreshData(); } }; const exportCSVReport = () => { const headers = [ "ID", "Empleado", "Correo", "Salario Base Mensual (C$)", "Días Período", "Faltas Injust.", "Días Deducidos", "Salario Período", "Horas Extras (C$)", "Feriados (C$)", "Comisiones/Bonos (C$)", "Salario Bruto (C$)", "Salida Anticipada (C$)", "INSS Laboral 7%", "IR DGI", "Adelantos Deducidos (C$)", "Neto a Pagar (C$)", `INSS Patronal (${activeWorkersList.length >= 50 ? '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.earlyDepartureDeduction || 0, p.calc.inssLaboral, p.calc.irDGI, p.loanInfo ? p.loanInfo.deduction : 0, p.netPayable, p.calc.inssPatronal, p.calc.inatecPatronal, p.calc.totalCostEmployer ]); window.exportToCSV(`Planilla_${startDate}_al_${endDate}`, headers, rows); }; const downloadBankDisbursementFile = () => { const cleanStr = (s) => (s || '').toString().trim().replace(/,/g, ' '); const cleanId = (s) => (s || '').toString().replace(/[^a-zA-Z0-9]/g, ''); // Desinfección estricta para dispersión ACH bancaria (BAC, BANPRO, LAFISE) // Elimina tildes, diacríticos y caracteres no ASCII para evitar rechazos en portales bancarios const sanitizeForBank = (s) => { return (s || '') .toString() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/ñ/g, 'n') .replace(/Ñ/g, 'N') .replace(/[^a-zA-Z0-9\s\.\-]/g, '') .trim(); }; if (bankFormat === 'bac') { // BAC Credomatic Conexión Empresarial const debitAccount = cleanId(companyDebitAcc) || 'CUENTA_EMPRESA'; const headers = ['Cuenta Debito', 'Tipo Cuenta Debito', 'Cuenta Credito', 'Tipo Cuenta Credito', 'Moneda', 'Monto', 'Referencia', 'Cedula', 'Beneficiario']; const validRows = []; processedPayroll.forEach(p => { const u = p.user; const accNum = u.bank_account_number || u.bankAccountNumber; if (!accNum) return; const accType = ((u.bank_account_type || u.bankAccountType || '').toLowerCase() === 'corriente') ? 'COR' : 'AHO'; const ref = `Planilla ${startDate} al ${endDate}`; validRows.push([ debitAccount, 'COR', cleanId(accNum), accType, 'NIO', p.netPayable, sanitizeForBank(ref), cleanId(u.idCard || u.id_card), sanitizeForBank(u.name) ]); }); if (validRows.length === 0) { alert("Ningún colaborador de la nómina tiene configurada una cuenta bancaria."); return; } const csvContent = [headers.join(','), ...validRows.map(r => r.join(','))].join('\r\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Dispersion_BAC_${startDate}_al_${endDate}.csv`; a.click(); URL.revokeObjectURL(url); } else if (bankFormat === 'banpro') { // BANPRO Promerica Formato ACH Nómina const headers = ['Cuenta Destino', 'Moneda', 'Monto', 'Concepto', 'Nombre Empleado', 'Cedula']; const validRows = []; processedPayroll.forEach(p => { const u = p.user; const accNum = u.bank_account_number || u.bankAccountNumber; if (!accNum) return; validRows.push([ cleanId(accNum), 'COR', p.netPayable, sanitizeForBank(`Pago Planilla ${startDate} al ${endDate}`), sanitizeForBank(u.name), cleanId(u.idCard || u.id_card) ]); }); if (validRows.length === 0) { alert("Ningún colaborador de la nómina tiene configurada una cuenta bancaria."); return; } const txtContent = [headers.join(';'), ...validRows.map(r => r.join(';'))].join('\r\n'); const blob = new Blob([txtContent], { type: 'text/plain;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Dispersion_BANPRO_${startDate}_al_${endDate}.txt`; a.click(); URL.revokeObjectURL(url); } else if (bankFormat === 'lafise') { // LAFISE Bancentro Nómina const headers = ['Numero Cuenta', 'Nombre Beneficiario', 'Monto', 'Identificacion', 'Concepto']; const validRows = []; processedPayroll.forEach(p => { const u = p.user; const accNum = u.bank_account_number || u.bankAccountNumber; if (!accNum) return; validRows.push([ cleanId(accNum), sanitizeForBank(u.name), p.netPayable, cleanId(u.idCard || u.id_card), sanitizeForBank(`Nomina ${startDate} al ${endDate}`) ]); }); if (validRows.length === 0) { alert("Ningún colaborador de la nómina tiene configurada una cuenta bancaria."); return; } const csvContent = [headers.join(','), ...validRows.map(r => r.join(','))].join('\r\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Dispersion_LAFISE_${startDate}_al_${endDate}.csv`; a.click(); URL.revokeObjectURL(url); } else { // Consolidado Multibanco (Excel / CSV con todos los colaboradores) // Se incluye BOM UTF-8 (\uFEFF) para que Microsoft Excel en Windows abra correctamente acentos y caracteres latinos const headers = ['ID', 'Colaborador', 'Cédula', 'Banco Destino', 'Número de Cuenta', 'Tipo de Cuenta', 'Monto Neto a Pagar (C$)', 'Modalidad de Pago']; const rows = processedPayroll.map(p => { const u = p.user; const accNum = u.bank_account_number || u.bankAccountNumber; const bank = u.bank_name || u.bankName || 'Sin banco'; const accType = u.bank_account_type || u.bankAccountType || 'ahorro'; const modality = accNum ? 'Transferencia Bancaria' : 'Pago Efectivo / Cheque Requerido'; return [ u.id, cleanStr(u.name), cleanStr(u.idCard || u.id_card || 'N/A'), cleanStr(bank), accNum ? cleanStr(accNum) : 'SIN CUENTA', accType, p.netPayable, modality ]; }); const csvContent = [ headers.map(h => `"${h.replace(/"/g, '""')}"`).join(','), ...rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')) ].join('\r\n'); const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Planilla_Consolidado_Multibanco_${startDate}_al_${endDate}.csv`; a.click(); URL.revokeObjectURL(url); } }; const renderPayslipHtml = (item) => { const emp = item.user; const calc = item.calc; const vars = item.vars || {}; const loan = item.loanInfo; const netPayable = item.netPayable; const logoHtml = settings?.logoUrl ? `` : ''; let earningsRows = ` Salario Base Quincenal (${periodDaysCount} d): C$ ${calc.scheduledPeriodPay || calc.basePeriodPay} `; if (vars.overtime > 0) { earningsRows += ` Horas Extras (${vars.overtime}h al 100%): + C$ ${calc.overtimePay} `; } if (vars.commissions > 0) { earningsRows += ` Comisiones por Ventas: + C$ ${parseFloat(vars.commissions).toFixed(2)} `; } if (vars.bonuses > 0) { earningsRows += ` Incentivo / Bono por Meta: + C$ ${parseFloat(vars.bonuses).toFixed(2)} `; } const totalDevengado = ( parseFloat(calc.scheduledPeriodPay || calc.basePeriodPay) + parseFloat(calc.overtimePay || 0) + parseFloat(calc.variablePay || 0) ).toFixed(2); let deductionRows = ''; let sumDeductions = 0; if (calc.unjustifiedCount > 0) { deductionRows += ` Ausencia Injustificada (${calc.unjustifiedCount} d): - C$ ${calc.absenceDeduction} Deducción 7mo Día (Art. 64 CT): - C$ ${calc.seventhDayDeduction} `; sumDeductions += (parseFloat(calc.absenceDeduction) + parseFloat(calc.seventhDayDeduction)); } if (calc.justifiedNoPayCount > 0) { deductionRows += ` Permiso Sin Goce (${calc.justifiedNoPayCount} d): - C$ ${calc.unpaidLeaveDeduction} `; sumDeductions += parseFloat(calc.unpaidLeaveDeduction); } if (parseFloat(calc.earlyDepartureDeduction || 0) > 0) { const hrsText = (calc.totalDeductedMinutes >= 60) ? `${(calc.totalDeductedMinutes / 60).toFixed(1)} hrs` : `${calc.totalDeductedMinutes} min`; deductionRows += ` Salida Anticipada (${hrsText}): - C$ ${calc.earlyDepartureDeduction} `; sumDeductions += parseFloat(calc.earlyDepartureDeduction); } if (parseFloat(calc.inssLaboral) > 0) { deductionRows += ` INSS Laboral (7.0%): - C$ ${calc.inssLaboral} `; sumDeductions += parseFloat(calc.inssLaboral); } if (parseFloat(calc.irDGI) > 0) { deductionRows += ` IR DGI (Ley 822): - C$ ${calc.irDGI} `; sumDeductions += parseFloat(calc.irDGI); } if (loan) { deductionRows += ` ${loan.notes || 'Adelanto Salarial'}: - C$ ${loan.deduction.toFixed(2)} `; sumDeductions += loan.deduction; } if (!deductionRows) { deductionRows = ` Sin deducciones en este período. `; } const loanNoteHtml = loan ? `
Nota: Descuento por adelanto aplicado en este corte. Saldo pendiente restante: C$ ${loan.pendingAfter.toFixed(2)}.
` : ''; let overtimeBreakdownHtml = ''; if (item.userOvertimeLogs && item.userOvertimeLogs.length > 0) { overtimeBreakdownHtml = `
Bitácora Auditada de Horas Extras (${vars.overtime || 0} hrs tot.)
${item.userOvertimeLogs.map(otLog => { const isRolledOver = otLog.date_str < startDate; return ` `; }).join('')}
Fecha Motivo Horas Recargo
${otLog.date_str} ${isRolledOver ? '(Arrastre corte anterior)' : ''} ${otLog.justification || 'Reingreso Extraordinario'} +${otLog.payroll_hours}h 100% Doble
`; } return `
${logoHtml}
${settings?.companyName || 'EMPRESA'}
COMPROBANTE DE PAGO DE SALARIO
Período de Nómina: ${startDate} al ${endDate} (${periodDaysCount} días) • Régimen: ${calc.isCuotaFija ? 'Cuota Fija' : 'Régimen General'}
${(parseFloat(emp.compensatedTimeHours || emp.compensated_time_hours || 0) > 0) ? ` ` : ''}
Colaborador: ${emp.name} Cargo / Puesto: ${emp.positionTitle || emp.role}
Cédula / No. INSS: ${emp.idCard || '---'} / ${emp.inssNumber || 'N/A'} Salario Base Mensual: C$ ${parseFloat(emp.baseSalary || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
Vacaciones Acumuladas: ${((parseFloat(emp.vacationDaysAccrued || 0) - parseFloat(emp.vacationDaysUsed || 0)) || 0).toFixed(1)} días Tiempo Compensado Acumulado: ${parseFloat(emp.compensatedTimeHours || emp.compensated_time_hours || 0).toFixed(1)} hrs
Percepciones (Ingresos)
${earningsRows}
Total Devengado: C$ ${totalDevengado}
${overtimeBreakdownHtml}
Deducciones de Ley y Otras
${deductionRows}
Total Deducciones: - C$ ${sumDeductions.toFixed(2)}
NETO LÍQUIDO A RECIBIR: Monto cancelado en moneda nacional (Córdobas) C$ ${netPayable}
${loanNoteHtml}
Elaborado por / RRHH ${settings?.companyName || 'Empresa Empleadora'}
Recibí Conforme ${emp.name}
`; }; const getPrintablePayslipDocumentHtml = (payslipsInnerHtml, title) => ` ${title} ${payslipsInnerHtml} `; const printDocumentViaIframe = (fullHtml) => { const printIframe = document.createElement('iframe'); printIframe.style.position = 'fixed'; printIframe.style.right = '0'; printIframe.style.bottom = '0'; printIframe.style.width = '0'; printIframe.style.height = '0'; printIframe.style.border = '0'; document.body.appendChild(printIframe); const doc = printIframe.contentWindow.document; doc.open(); doc.write(fullHtml); doc.close(); setTimeout(() => { printIframe.contentWindow.focus(); printIframe.contentWindow.print(); setTimeout(() => { if (document.body.contains(printIframe)) { document.body.removeChild(printIframe); } }, 1000); }, 400); }; const handlePrintBatchPayslips = () => { if (!processedPayroll || processedPayroll.length === 0) { alert("No hay colaboradores calculados en este período."); return; } const payslipsHtml = processedPayroll.map(item => renderPayslipHtml(item)).join('\n'); const batchFileName = `COLILLAS-EN-BLOQUE-${startDate}-al-${endDate}`; printDocumentViaIframe(getPrintablePayslipDocumentHtml(payslipsHtml, batchFileName)); }; return (
{/* ENCABEZADO Y TABS */}

Centro de Nómina y Planilla

Cálculo quincenal, retenciones de ley (INSS, IR), horas extras y adelantos

{/* TAB 1: VISTA NÓMINA */} {activeTab === 'payroll' && (
{/* BARRA DE FILTROS Y ACCIONES UNIFICADA */}
{/* PRESETS */}
{/* ACCIONES UNIFICADAS */}
{/* MENÚ DESPLEGABLE DE REPORTES Y DESCARGAS */}
{showExportMenu && (
)}
{/* FECHAS EVALUADAS */}
Ciclo Evaluado: { setStartDate(e.target.value); setPeriodFilter('custom'); }} className="p-2 bg-slate-50 border rounded-xl font-bold text-xs outline-none" /> al { setEndDate(e.target.value); setPeriodFilter('custom'); }} className="p-2 bg-slate-50 border rounded-xl font-bold text-xs outline-none" /> {periodDaysCount} Día(s) Programados
{/* DETALLE Y ESTADO POR QUINCENAS AL FILTRAR POR MES O ACUMULADO */} {periodDaysCount > 15 && (
Estado Quincenal del Mes Evaluado:
{(() => { const dateParts = (startDate || '').split('-').map(Number); if (dateParts.length < 2) return null; const y = dateParts[0]; const m = dateParts[1]; const lastDay = new Date(y, m, 0).getDate(); const q1Start = `${y}-${String(m).padStart(2,'0')}-01`; const q1End = `${y}-${String(m).padStart(2,'0')}-15`; const q2Start = `${y}-${String(m).padStart(2,'0')}-16`; const q2End = `${y}-${String(m).padStart(2,'0')}-${lastDay}`; const q1Closed = payrollClosureStatus?.sub_closures?.some(c => c.period_start === q1Start && c.status === 'paid'); const q2Closed = payrollClosureStatus?.sub_closures?.some(c => c.period_start === q2Start && c.status === 'paid'); return ( <> ); })()}
)} {/* BANNER DE ESTADO DE CIERRE Y PUBLICACION DE COLILLAS */}
{payrollClosureStatus?.closed ? ( ) : (payrollClosureStatus?.isPartial ? ( ) : ( ))}

{payrollClosureStatus?.closed ? 'Planilla Pagada & Publicada (Colillas Visibles)' : (payrollClosureStatus?.isPartial ? `Pago Parcial en Proceso (${payrollClosureStatus?.paid_count || 0} de ${payrollClosureStatus?.total_active_users || 0} Colaboradores)` : 'Planilla en Preparacion (Modo Borrador)')}

{payrollClosureStatus?.closed ? 'Oficialmente Pagada' : (payrollClosureStatus?.isPartial ? 'Pago Parcial' : 'Borrador Interno')}

{payrollClosureStatus?.closed ? `Marcada como pagada por ${payrollClosureStatus.closure?.closed_by_name || 'Administración'} el ${payrollClosureStatus.closure?.paid_at ? new Date(payrollClosureStatus.closure.paid_at).toLocaleDateString() : 'reciente'}. Los colaboradores pueden ver y descargar su colilla oficial en su portal.` : (payrollClosureStatus?.isPartial ? `Hay ${payrollClosureStatus?.paid_count} colaboradores pagados y ${payrollClosureStatus?.pending_count} pendientes. Solo los colaboradores pagados pueden ver su colilla.` : 'Las colillas de este período están protegidas y ocultas para los colaboradores en su autoservicio hasta que confirmes el pago de nómina.')}

{payrollClosureStatus?.closed && (
Registro de Auditoría de Pago: {payrollClosureStatus.closure?.notes || 'Efectivo / Caja Chica'}
)}
{!payrollClosureStatus?.closed ? ( <> ) : (
Planilla Bloqueada
)} {payrollClosureStatus?.closed && ( )}
{/* HERO KPI CARDS */}
Ingresos Brutos (TIG)

{isLoadingData ? ( ) : ( `C$ ${totalsSummary.gross.toLocaleString('en-US', { minimumFractionDigits: 2 })}` )}

Base + Extras + Comisiones

Retenciones de Ley

{isLoadingData ? ( ) : ( `- C$ ${(totalsSummary.inssLab + totalsSummary.ir).toLocaleString('en-US', { minimumFractionDigits: 2 })}` )}

INSS 7% (C$ {totalsSummary.inssLab.toFixed(0)}) + IR DGI

Total Neto a Desembolsar

{isLoadingData ? ( ) : ( `C$ ${totalsSummary.net.toLocaleString('en-US', { minimumFractionDigits: 2 })}` )}

{totalsSummary.loans > 0 ? (

Deducción de adelantos: -C$ {totalsSummary.loans.toFixed(2)}

) : (

Sin deducciones de adelanto

)}
Costo Patronal Total

{isLoadingData ? ( ) : ( `C$ ${totalsSummary.totalCost.toLocaleString('en-US', { minimumFractionDigits: 2 })}` )}

INSS ({activeWorkersList.length >= 50 ? '22.5%' : '21.5%'}) + INATEC 2%

{/* LISTA ESTRUCTURADA DE COLABORADORES (GRID DE NÓMINA) */}
{isLoadingData ? ( [1, 2, 3].map((n) => ( )) ) : ( processedPayroll.map(({ user: u, calc, vars, loanInfo, netPayable, userOvertimeLogs }) => ( {/* COLABORADOR */} {/* ASISTENCIA */} {/* VARIABLES */} {/* ESTADO PAGO */} {/* NETO */} {/* ACCIONES */} )))}
Colaborador Asistencia & Séptimo Día Variables & Préstamos Estado de Pago Neto a Recibir Acciones
{u.avatarUrl ? ( {u.name} ) : (
{u.name ? u.name[0].toUpperCase() : 'U'}
)}

{u.name}

@{u.alias} • {u.positionTitle || u.role}

Base: C$ {parseFloat(u.baseSalary || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
{/* TOOLTIP DE MICRO-INTERACCIÓN ESTILIZADO */}
Desglose de Extras & Variables Clic para editar
{(vars.overtime > 0 || vars.commissions > 0 || vars.bonuses > 0) ? (
Horas Extras ({vars.overtime || 0} hrs): C$ {parseFloat(calc.overtimePay || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
{vars.commissions > 0 && (
Comisiones: C$ {parseFloat(vars.commissions || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
)} {vars.bonuses > 0 && (
Bonos / Incentivos: C$ {parseFloat(vars.bonuses || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
)}
Total Percepciones Extra: C$ {(parseFloat(calc.overtimePay || 0) + parseFloat(vars.commissions || 0) + parseFloat(vars.bonuses || 0)).toLocaleString('en-US', { minimumFractionDigits: 2 })}
) : (

Sin variables registradas en este período. Haz clic para asignar Horas Extras, Comisiones o Bonos a este colaborador.

)}
{loanInfo && ( Adelanto: -C$ {loanInfo.deduction} )}
{(() => { const itemPayment = payrollClosureStatus?.item_payments?.[u.id]; const isUserPaid = itemPayment ? (itemPayment.status === 'paid') : (payrollClosureStatus?.closed && !payrollClosureStatus?.pending_user_ids?.includes(u.id)); return isUserPaid ? ( ) : ( ); })()}

C$ {netPayable}

Bruto: C$ {calc.grossSalary}
)} {/* TAB 2: PRÉSTAMOS Y ADELANTOS */} {activeTab === 'loans' && (

Control de Adelantos y Préstamos

Deducciones quincenales amortizadas automáticamente en nómina.

{activeLoans.length === 0 ? (
No hay préstamos ni adelantos activos registrados.
) : ( activeLoans.map(l => { const total = parseFloat(l.total_amount || 0); const rem = parseFloat(l.remaining_balance || 0); const paid = Math.max(0, total - rem); const pct = total > 0 ? Math.round((paid / total) * 100) : 100; return (

{l.user_name}

{l.notes || 'Adelanto'}
Amortizado: {pct}% (C$ {paid.toFixed(2)} de C$ {total.toFixed(2)})
Cuota por Quincena C$ {parseFloat(l.installment_amount).toFixed(2)}
Saldo Pendiente C$ {rem.toFixed(2)}
); }) )}
)} {/* TAB 3: MOTOR DE LIQUIDACIÓN / FINIQUITO */} {/* TAB 3: MOTOR DE LIQUIDACIÓN / FINIQUITO */} {activeTab === 'severance' && (() => { const handlePrintSeveranceDocument = () => { if (!selectedSeveranceUser || !severanceResult) return; const emp = selectedSeveranceUser; const res = severanceResult; const fileName = `FINIQUITO_LEGAL_${emp.name.replace(/\s+/g, '_')}_${sevTermDate}`; const printIframe = document.createElement('iframe'); printIframe.style.position = 'fixed'; printIframe.style.right = '0'; printIframe.style.bottom = '0'; printIframe.style.width = '0'; printIframe.style.height = '0'; printIframe.style.border = '0'; document.body.appendChild(printIframe); const logoHtml = settings?.logoUrl ? `` : ''; let reasonLabel = 'Despido Injustificado (Art. 45 Código del Trabajo)'; if (sevReason === 'renuncia_con_preaviso') reasonLabel = 'Renuncia Voluntaria con Preaviso de Ley (Art. 43/45 CT)'; else if (sevReason === 'abandono_trabajo') reasonLabel = 'Abandono de Trabajo / Renuncia Inmediata (Sin Indemnización)'; else if (sevReason === 'despido_con_causa') reasonLabel = 'Despido con Causa Justificada (Art. 48 CT - Sin Indemnización)'; const doc = printIframe.contentWindow.document; doc.open(); doc.write(` ${fileName}
${logoHtml}
${settings?.companyName || 'CLEANDENT'}
FINIQUITO Y LIQUIDACIÓN LABORAL OFICIAL

Código del Trabajo de la República de Nicaragua (Ley No. 185)

Colaborador: ${emp.name} Cédula de Identidad: ${emp.idCard || '---'}
Fecha de Ingreso: ${window.formatDateToNIC(emp.hiringDate)} Último Día Laborado: ${window.formatDateToNIC(sevTermDate)}
Antigüedad Acumulada: ${res.yearsWorkedText} (${res.totalDaysWorked} días) Salario Base Mensual: C$ ${parseFloat(res.effectiveMonthlySalary).toLocaleString('en-US', { minimumFractionDigits: 2 })}
Causa o Motivo de Separación: ${reasonLabel}
${parseFloat(res.vacationIR) > 0 ? ` ` : ''}
Concepto de Liquidación Detalle Legal Monto
1. Vacaciones Pendientes ${res.vacationDaysPending} días acumulados a razón de C$ ${res.dailyRate}/día C$ ${res.vacationGross}
2. Décimo Tercer Mes (Aguinaldo) ${res.aguinaldoDays} días proporcionales (Art. 93 CT) • Exento C$ ${res.aguinaldoGross}
3. Indemnización por Antigüedad ${res.appliesIndemnity ? 'Escala Art. 45 CT (Tope 5 meses) • Exento' : 'No aplica por motivo de egreso'} C$ ${res.indemnityGross}
Total Ingresos Brutos: C$ ${res.totalGross}
(-) Deducción INSS Laboral (7% Vacaciones) Art. 16 Ley de Seguridad Social - C$ ${res.vacationInssLaboral}
(-) Retención IR DGI (Vacaciones) Ley 822 de Concertación Tributaria - C$ ${res.vacationIR}
Total Deducciones: - C$ ${res.totalDeductions}
TOTAL NETO A RECIBIR (FINIQUITO): Saldo cancelado a entera satisfacción sin reclamo posterior C$ ${res.totalNetToPay}

El suscrito trabajador declara haber recibido de conformidad el monto total detallado en el presente documento, en concepto de pago final de todas sus prestaciones sociales y derechos laborales derivados de su contrato de trabajo, no teniendo reclamo alguno de ninguna naturaleza pendiente contra la empresa empleadora.

Elaborado por / Empleador ${settings?.companyName || 'Empresa'}
Recibí Conforme / Trabajador ${emp.name}
`); doc.close(); setTimeout(() => { printIframe.contentWindow.focus(); printIframe.contentWindow.print(); setTimeout(() => { if (document.body.contains(printIframe)) { document.body.removeChild(printIframe); } }, 1000); }, 400); }; return (
{/* PANEL IZQUIERDO: CONFIGURACIÓN DE PARÁMETROS */}

Parámetros de Finiquito

Cálculo legal según el Código del Trabajo de Nicaragua (Ley 185).

setSevTermDate(e.target.value)} className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-medium text-xs" />
setSevVacDaysPending(e.target.value)} className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-medium text-xs" />
setSevAvgVariable(e.target.value)} placeholder="0.00" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-medium text-xs" /> Deja en 0 si devengó salario fijo ordinario.
{/* PANEL DERECHO: VISTA PREVIA Y DESGLOSE */}
{!selectedSeveranceUser || !severanceResult ? (
Selecciona un colaborador para calcular su finiquito legal.
) : (

{selectedSeveranceUser.name}

Ingreso: {window.formatDateToNIC(selectedSeveranceUser.hiringDate)} • Salida: {window.formatDateToNIC(sevTermDate)}

Antigüedad Acumulada: {severanceResult.yearsWorkedText} ({severanceResult.totalDaysWorked} días laborados)

1. Vacaciones Pendientes (Art. 76 CT)

{severanceResult.vacationDaysPending} días acumulados × C$ {severanceResult.dailyRate} (Gravable INSS e IR)

C$ {severanceResult.vacationGross}

- C$ {severanceResult.vacationInssLaboral} (INSS) - C$ {severanceResult.vacationIR} (IR)

2. Aguinaldo Proporcional (Art. 93 CT)

{severanceResult.aguinaldoDays} días proporcionales de ciclo • Exento de INSS e IR

C$ {severanceResult.aguinaldoGross}

3. Indemnización por Antigüedad (Art. 45 CT)

{severanceResult.appliesIndemnity ? 'Escala progresiva Art. 45 CT (1 mes/año 1-3 años + 20 d/año posterior • Tope 5 meses) • Exento' : 'No aplica por motivo de egreso (Sin preaviso / Con causa)'}

C$ {severanceResult.indemnityGross}

Total Neto a Desembolsar

Bruto: C$ {severanceResult.totalGross} • Retenciones: C$ {severanceResult.totalDeductions}

C$ {severanceResult.totalNetToPay}

)}
); })()} {/* TAB 4: VENTANA DE CORTES */} {activeTab === 'cut_config' && (

Ventana de Cortes de Asistencia

Configura las fechas de corte en que el sistema consolida la asistencia e incidencias para tu empresa.

Primera Quincena

setCutStartDayQ1(e.target.value)} className="w-full p-3 bg-white border border-slate-200/80 rounded-xl font-medium text-xs" />
setCutEndDayQ1(e.target.value)} className="w-full p-3 bg-white border border-slate-200/80 rounded-xl font-medium text-xs" />

Segunda Quincena

setCutStartDayQ2(e.target.value)} className="w-full p-3 bg-white border border-slate-200/80 rounded-xl font-medium text-xs" />
setCutEndDayQ2(e.target.value)} className="w-full p-3 bg-white border border-slate-200/80 rounded-xl font-medium text-xs" />
)} {/* SLIDE-OVER DRAWER: COLILLA DETALLADA DE NÓMINA */} {selectedDetailEmployee && (() => { const emp = selectedDetailEmployee.user; const calc = selectedDetailEmployee.calc; const vars = selectedDetailEmployee.vars || {}; const loan = selectedDetailEmployee.loanInfo; const empName = (emp.name || 'Empleado').trim(); const fileName = `COMPROBANTE-SALARIO - ${empName} - ${startDate} al ${endDate}`; const handlePrintCleanPayslip = () => { const payslipHtml = renderPayslipHtml(selectedDetailEmployee); printDocumentViaIframe(getPrintablePayslipDocumentHtml(payslipHtml, fileName)); }; const totalDeductionsDisplay = ( parseFloat(calc.inssLaboral || 0) + parseFloat(calc.irDGI || 0) + parseFloat(calc.absenceDeduction || 0) + parseFloat(calc.seventhDayDeduction || 0) + parseFloat(calc.earlyDepartureDeduction || 0) + (loan ? loan.deduction : 0) ).toFixed(2); return (
setSelectedDetailEmployee(null)}>
e.stopPropagation()}> {/* BOTONES SUPERIORES */}
Recibo Oficial de Nómina
{/* COMPROBANTE OFICIAL EN PANTALLA */}
{/* MEMBRETE */}
{settings?.logoUrl && ( Logo )}

{settings?.companyName || 'EMPRESA'}

COMPROBANTE DE PAGO DE SALARIO

Período de Nómina: {startDate} al {endDate} ({periodDaysCount} días) • Régimen: {calc.isCuotaFija ? 'Cuota Fija' : 'Régimen General'}

{/* DATOS DEL COLABORADOR */}
Colaborador: {emp.name}
Cargo / Puesto: {emp.positionTitle || emp.role}
Cédula / No. INSS: {emp.idCard || '---'} / {emp.inssNumber || 'N/A'}
Salario Base Mensual: C$ {parseFloat(emp.baseSalary || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}
{/* TABLA DOBLE COLUMNA: PERCEPCIONES VS DEDUCCIONES */}
{/* COLUMNA 1: PERCEPCIONES */}
Percepciones (Ingresos)
Salario Base Quincenal ({periodDaysCount} d): C$ {calc.scheduledPeriodPay || calc.basePeriodPay}
{vars.overtime > 0 && (
Horas Extras ({vars.overtime}h al 100%): + C$ {calc.overtimePay}
)} {vars.commissions > 0 && (
Comisiones por Ventas: + C$ {parseFloat(vars.commissions).toFixed(2)}
)} {vars.bonuses > 0 && (
Incentivo / Bono por Meta: + C$ {parseFloat(vars.bonuses).toFixed(2)}
)}
Total Devengado: C$ {(parseFloat(calc.scheduledPeriodPay || calc.basePeriodPay) + parseFloat(calc.overtimePay || 0) + parseFloat(calc.variablePay || 0)).toFixed(2)}
{/* COLUMNA 2: DEDUCCIONES DE LEY, ASISTENCIA Y OTRAS */}
Deducciones de Ley y Otras
{calc.unjustifiedCount > 0 && (
Ausencia Injustificada ({calc.unjustifiedCount} d): - C$ {calc.absenceDeduction}
)} {calc.unjustifiedCount > 0 && (
Deducción 7mo Día (Art. 64 CT): - C$ {calc.seventhDayDeduction}
)} {calc.justifiedNoPayCount > 0 && (
Permiso Sin Goce ({calc.justifiedNoPayCount} d): - C$ {calc.unpaidLeaveDeduction}
)} {/* Renglón de Salida Anticipada / Minutos no laborados */} {parseFloat(calc.earlyDepartureDeduction || 0) > 0 && (
Salida Anticipada ({Math.round(calc.totalDeductedMinutes / 60 * 10) / 10} hrs / {calc.totalDeductedMinutes} min): - C$ {calc.earlyDepartureDeduction}
)} {parseFloat(calc.inssLaboral) > 0 && (
INSS Laboral (7.0%): - C$ {calc.inssLaboral}
)} {parseFloat(calc.irDGI) > 0 && (
IR DGI (Ley 822): - C$ {calc.irDGI}
)} {loan && (
{loan.notes || 'Adelanto Salarial'}: - C$ {loan.deduction.toFixed(2)}
)} {/* Total Deducciones Consolidado */}
Total Deducciones: - C$ {totalDeductionsDisplay}
{/* TOTAL NETO */}
NETO LÍQUIDO A RECIBIR: Monto cancelado en moneda nacional (Córdobas)

C$ {selectedDetailEmployee.netPayable}

{/* NOTA ADELANTO */} {loan && (
Nota: Descuento por adelanto. Saldo pendiente restante: C$ {loan.pendingAfter.toFixed(2)}.
)} {/* BLOQUE DE FIRMAS */}
Elaborado por / RRHH {settings?.companyName || 'Empresa Empleadora'}
Recibí Conforme {emp.name}
{/* INFORMACIÓN INTERNA ADMINISTRATIVA */}
Control Administrativo Interno (No se imprime)
INSS Patronal ({calc.inssPatronalRate}%): C$ {calc.inssPatronal}
INATEC Patronal (2.0%): {calc.appliesInatec && emp.appliesInss ? `C$ ${calc.inatecPatronal}` : 'Exento / No aplica'}
Costo Total Empresa: C$ {calc.totalCostEmployer}
); })()} {/* MODAL AJUSTE RÁPIDO DE HORAS EXTRAS Y BONOS CON BITÁCORA DETALLADA */} {editingAdjustmentUser && (() => { const modalUserOvertimeLogs = (allOvertimeLogs || []).filter(o => Number(o.user_id) === Number(editingAdjustmentUser.id) && o.status === 'approved' && (o.destination === 'payroll' || o.destination === 'mixed') && parseFloat(o.payroll_hours || 0) > 0 && o.date_str >= startDate && o.date_str <= endDate ); const totalOtHoursInModal = modalUserOvertimeLogs.reduce((sum, o) => sum + Number(o.payroll_hours || 0), 0); return (
setEditingAdjustmentUser(null)}>
e.stopPropagation()}>
Variables y Bitácora de Horas Extras

{editingAdjustmentUser.name}

Período Evaluado: {startDate} al {endDate}

{totalOtHoursInModal > 0 && ( Auditadas: {totalOtHoursInModal.toFixed(1)} hrs )}
setAdjOvertime(e.target.value)} placeholder="0" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-bold text-xs outline-none focus:border-slate-400" /> Se pagarán con recargo del 100% (Doble) según Art. 58 CT.
{/* BITÁCORA INTEGRADA DE HORAS EXTRAS DEL PERÍODO */}

Bitácora de Reingresos Auditados ({modalUserOvertimeLogs.length})

{modalUserOvertimeLogs.length === 0 ? (

No hay reingresos u horas extras aprobadas para planilla en esta quincena.

) : (
{modalUserOvertimeLogs.map(log => (
{log.date_str} {log.entry_time ? `(${log.entry_time} - ${log.exit_time || 'Activo'})` : ''} +{log.payroll_hours} hrs (Doble)

Motivo: {log.justification || 'Horas extras autorizadas'}

{log.admin_notes && (

Nota Admin: {log.admin_notes}

)}
))}
)}
setAdjCommissions(e.target.value)} placeholder="0.00" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-bold text-xs outline-none focus:border-slate-400" />
setAdjBonuses(e.target.value)} placeholder="0.00" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-bold text-xs outline-none focus:border-slate-400" />
); })()} {/* MODAL NUEVO ADELANTO / PRÉSTAMO */} {showLoanModal && (
setShowLoanModal(false)}>
e.stopPropagation()}>

Registrar Adelanto / Préstamo

setLoanAmount(e.target.value)} placeholder="Ej. 1000" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-bold text-xs outline-none" required />
setLoanNotes(e.target.value)} placeholder="Ej. Adelanto de salario" className="w-full p-3 bg-slate-50 border border-slate-200/80 rounded-xl font-medium text-xs outline-none" />
)} {/* MODAL AUDITORÍA DE ASISTENCIA DÍA A DÍA */} {calendarModalUser && (
setCalendarModalUser(null)}>
e.stopPropagation()}>

{calendarModalUser.user.name}

Auditoría del {startDate} al {endDate}

Legislación Laboral (Art. 64 Código del Trabajo de Nicaragua):

Cada falta injustificada deduce el día no laborado más el descanso semanal proporcional (Séptimo Día = 2 días deducidos en total). Los días futuros del ciclo no son penalizados.

{calendarModalUser.calc.audit.dayBreakdown.map((d, i) => (
{window.formatDateToNIC(d.date)} ({d.dayCode.toUpperCase()}) {d.label}
))}
)} {/* MODAL DISPERSIÓN BANCARIA MASIVA */} {showBankModal && (
setShowBankModal(false)}>
e.stopPropagation()}> {/* Cabecera */}

Dispersión Bancaria Masiva de Nómina

Generador de transferencias y pagos ACH (BAC, BANPRO, LAFISE y Multibanco) para el ciclo {startDate} al {endDate}

{/* Parámetros de Dispersión */}
setCompanyDebitAcc(e.target.value)} placeholder="Ej. 10020304050 (Configurada en Ajustes)" className="w-full p-2.5 bg-white border border-slate-200 rounded-xl font-mono text-xs text-slate-900 outline-none" />
{/* Métricas de dispersión */} {(() => { const withBank = processedPayroll.filter(p => Boolean(p.user.bank_account_number || p.user.bankAccountNumber)); const withoutBank = processedPayroll.filter(p => !Boolean(p.user.bank_account_number || p.user.bankAccountNumber)); const totalAmount = processedPayroll.reduce((acc, p) => acc + parseFloat(p.netPayable || 0), 0); const bankAmount = withBank.reduce((acc, p) => acc + parseFloat(p.netPayable || 0), 0); return (
Neto Total Planilla C$ {totalAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })}
Total a Dispersar (Bancos) C$ {bankAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })}
Con Cuenta Bancaria {withBank.length} de {processedPayroll.length} colaboradores
0 ? 'bg-amber-50 border-amber-200' : 'bg-slate-50 border-slate-200'}`}> 0 ? 'text-amber-800' : 'text-slate-500'}`}> Efectivo / Cheque 0 ? 'text-amber-800' : 'text-slate-900'}`}> {withoutBank.length} colaboradores
); })()} {/* Tabla Resumen / Previsualización */}
{processedPayroll.map(p => { const u = p.user; const accNum = u.bank_account_number || u.bankAccountNumber; const bank = u.bank_name || u.bankName; return ( ); })}
Colaborador Cédula Banco Cuenta Destino Monto Neto Estado
{u.name} {u.idCard || u.id_card || 'Sin registrar'} {bank || 'No asignado'} {accNum || 'Sin cuenta'} C$ {parseFloat(p.netPayable || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })} {accNum ? ( Listo para Dispersión ) : ( Efectivo / Cheque )}
{/* Acciones */}
{bankFormat === 'bac' && "Formato oficial para Conexión Empresarial BAC (.csv)"} {bankFormat === 'banpro' && "Formato oficial para Nómina ACH BANPRO (.txt)"} {bankFormat === 'lafise' && "Formato oficial para Nómina LAFISE Bancentro (.csv)"} {bankFormat === 'multibank' && "Consolidado universal con cuentas y efectivo (.csv)"}
)} {/* MODAL DE SELECCIÓN DE CUENTA BANCARIA PARA PAGO Y CIERRE DE PLANILLA */} {showDisbursementModal && (

Autorizar y Procesar Pago de Planilla

Período: {startDate} al {endDate}

setDisbursementNotes(e.target.value)} className="w-full p-3 bg-slate-50 border border-slate-200 rounded-xl font-medium text-xs text-slate-900 outline-none focus:border-slate-400" />
Monto Total Neto a Dispersar: C$ {(totalsSummary?.net || 0).toLocaleString('en-US', { minimumFractionDigits: 2 })}

Al confirmar, las colillas de pago se publicarán inmediatamente en el portal de autoservicio de los colaboradores.

)} {/* MODAL DE HISTORIAL Y AUDITORÍA DE CIERRES DE PLANILLA */} {showClosureAuditModal && (

Auditoría & Historial de Cierres de Planilla

Registro de pagos procesados y cuentas bancarias de la empresa utilizadas

{(!payrollClosureStatus?.all_closures || payrollClosureStatus.all_closures.length === 0) && !payrollClosureStatus?.closure ? (

No hay registros históricos de cierres de planilla para mostrar.

) : ( (payrollClosureStatus?.all_closures || [payrollClosureStatus?.closure].filter(Boolean)).map((c, idx) => (
Período: {c.period_start} al {c.period_end} Procesado por: {c.closed_by_name || 'Administración'}
{c.status === 'paid' ? 'Oficialmente Pagada' : c.status}
Cuenta / Método Registrado {c.notes || 'Efectivo / Caja Chica'}
Fecha y Hora de Cierre {c.paid_at ? (window.formatNICTimeFull ? window.formatNICTimeFull(c.paid_at) : new Date(Number(c.paid_at)).toLocaleString()) : 'Reciente'}
)) )}
)}
); };