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 = `
| Fecha | Motivo | Horas | Recargo |
|---|---|---|---|
| ${otLog.date_str} ${isRolledOver ? '(Arrastre corte anterior)' : ''} | ${otLog.justification || 'Reingreso Extraordinario'} | +${otLog.payroll_hours}h | 100% Doble |
| ${(parseFloat(emp.compensatedTimeHours || emp.compensated_time_hours || 0) > 0) ? ` | ` : ' | '} |
|
Percepciones (Ingresos)
|
Deducciones de Ley y Otras
|
| NETO LÍQUIDO A RECIBIR: Monto cancelado en moneda nacional (Córdobas) | C$ ${netPayable} |
| Elaborado por / RRHH ${settings?.companyName || 'Empresa Empleadora'} | Recibí Conforme ${emp.name} |
Cálculo quincenal, retenciones de ley (INSS, IR), horas extras y adelantos
{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 && (Base + Extras + Comisiones
INSS 7% (C$ {totalsSummary.inssLab.toFixed(0)}) + IR DGI
Deducción de adelantos: -C$ {totalsSummary.loans.toFixed(2)}
) : (Sin deducciones de adelanto
)}INSS ({activeWorkersList.length >= 50 ? '22.5%' : '21.5%'}) + INATEC 2%
| Colaborador | Asistencia & Séptimo Día | Variables & Préstamos | Estado de Pago | Neto a Recibir | Acciones |
|---|---|---|---|---|---|
|
{u.avatarUrl ? (
{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 })} |
{/* ASISTENCIA */}
{/* VARIABLES */} |
{/* TOOLTIP DE MICRO-INTERACCIÓN ESTILIZADO */}
{loanInfo && (
Adelanto: -C$ {loanInfo.deduction}
)}
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. )} |
{/* ESTADO PAGO */}
{(() => { const itemPayment = payrollClosureStatus?.item_payments?.[u.id]; const isUserPaid = itemPayment ? (itemPayment.status === 'paid') : (payrollClosureStatus?.closed && !payrollClosureStatus?.pending_user_ids?.includes(u.id)); return isUserPaid ? ( ) : ( ); })()} | {/* NETO */}
C$ {netPayable}Bruto: C$ {calc.grossSalary} |
{/* ACCIONES */}
|
Deducciones quincenales amortizadas automáticamente en nómina.
Código del Trabajo de la República de Nicaragua (Ley No. 185)
| 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} |
Cálculo legal según el Código del Trabajo de Nicaragua (Ley 185).
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}
Configura las fechas de corte en que el sistema consolida la asistencia e incidencias para tu empresa.
Período de Nómina: {startDate} al {endDate} ({periodDaysCount} días) • Régimen: {calc.isCuotaFija ? 'Cuota Fija' : 'Régimen General'}
Período Evaluado: {startDate} al {endDate}
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.
Generador de transferencias y pagos ACH (BAC, BANPRO, LAFISE y Multibanco) para el ciclo {startDate} al {endDate}
| 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 )} |
Período: {startDate} al {endDate}
Al confirmar, las colillas de pago se publicarán inmediatamente en el portal de autoservicio de los colaboradores.
Registro de pagos procesados y cuentas bancarias de la empresa utilizadas
No hay registros históricos de cierres de planilla para mostrar.
) : ( (payrollClosureStatus?.all_closures || [payrollClosureStatus?.closure].filter(Boolean)).map((c, idx) => (