Function bodies 54 total
normalizarDNI function · javascript · L7-L9 (3 LOC)admin.js
function normalizarDNI(raw) {
return String(raw).trim().replace(/[\n\r]/g, '').padStart(8, '0');
}initSelectBuscable function · javascript · L12-L17 (6 LOC)admin.js
function initSelectBuscable(id) {
const el = document.getElementById(id);
if (!el) return;
if (el.tomselect) { el.tomselect.sync(); return; }
new window.TomSelect(el, { allowEmptyOption: true, maxOptions: 300 });
}cargarDatosAdmin function · javascript · L59-L120 (62 LOC)admin.js
async function cargarDatosAdmin() {
const { data: { user } } = await supabase.auth.getUser();
const { data: perfil } = await supabase
.from('profiles')
.select('empresa_id, empresas(nombre, ruc)')
.eq('id', user.id)
.single();
if (perfil?.empresa_id) {
empresaAdminId = perfil.empresa_id;
empresaAdminNombre = perfil.empresas?.nombre;
empresaAdminRuc = perfil.empresas?.ruc;
document.getElementById('info-empresa-header').textContent = `🏢 ${empresaAdminNombre}`;
document.getElementById('info-empresa').innerHTML = `
<div class="info-box" style="margin-bottom:16px;">
🏢 <strong>${empresaAdminNombre}</strong> — RUC: ${empresaAdminRuc}
</div>
`;
} else {
document.getElementById('info-empresa').innerHTML = `
<div style="background:#fff3cd; padding:12px; border-radius:8px;
border-left:4px solid #ffc107; margin-bottom:15px;">
⚠️ Tu usuario no tiene empresa asignada. Contacta al superadmin.
configurarRENIEC function · javascript · L123-L183 (61 LOC)admin.js
function configurarRENIEC(idDni, idTipo, idNombres, idApellidos) {
const inputDni = document.getElementById(idDni);
if (!inputDni) return;
// Desbloquear al cambiar a CE o Pasaporte
document.getElementById(idTipo)?.addEventListener('change', () => {
const tipo = document.getElementById(idTipo).value;
const nombresEl = document.getElementById(idNombres);
const apellidosEl = document.getElementById(idApellidos);
const msgEl = document.getElementById(idDni + '-reniec-msg');
if (tipo !== 'DNI') {
nombresEl.disabled = false;
apellidosEl.disabled = false;
if (msgEl) msgEl.textContent = '';
} else {
nombresEl.disabled = true;
apellidosEl.disabled = true;
nombresEl.value = '';
apellidosEl.value = '';
}
});
inputDni.addEventListener('input', async () => {
const dni = inputDni.value.trim();
const tipo = document.getElementById(idTipo)?.value;
if (tipo !== 'DNI' || dni.length !== 8) return;
const cargarDatosDashboard function · javascript · L1452-L1467 (16 LOC)admin.js
async function cargarDatosDashboard() {
if (todosTrabajadoresDash.length) return;
const [{ data: trabajadores }, { data: cursos }] = await Promise.all([
supabase.from('profiles').select('id, nombres, apellidos, email, documento_numero, cargo')
.eq('empresa_id', empresaAdminId).eq('rol', 'trabajador').eq('activo', true).order('apellidos'),
supabase.from('cursos').select('id, titulo').eq('activo', true)
]);
todosTrabajadoresDash = trabajadores || [];
todosCursosDash = cursos || [];
const sel = document.getElementById('select-curso-dashboard');
todosCursosDash.forEach(c => {
sel.innerHTML += `<option value="${c.id}">${c.titulo}</option>`;
});
initSelectBuscable('select-curso-dashboard');
}cargarPreguntas function · javascript · L1983-L2036 (54 LOC)admin.js
async function cargarPreguntas(formularioId, tipo) {
const { data: preguntas } = await supabase
.from('preguntas').select('*, opciones_pregunta(*)')
.eq('id_formulario', formularioId).order('orden');
const cont = document.getElementById(`lista-preguntas-${formularioId}`);
if (!cont) return;
if (!preguntas?.length) {
cont.innerHTML = '<p style="color:#888;font-size:0.85rem;">Sin preguntas. Agrega la primera.</p>';
return;
}
cont.innerHTML = preguntas.map((p, i) => {
const opciones = (p.opciones_pregunta || []).sort((a, b) => a.orden - b.orden).map(o => `
<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;margin-bottom:4px;
background:${o.es_correcta ? '#d4edda' : '#f8f9fa'};border-radius:6px;font-size:0.85rem;">
<span style="flex:1;">${o.opcion}</span>
<button onclick="toggleCorrecta(${o.id},${p.id},${formularioId},'${tipo}')"
style="background:${o.es_correcta ? '#28a745' : '#e0e0e0'};color:${o.ecargarResumenCumplimiento function · javascript · L2693-L2799 (107 LOC)admin.js
async function cargarResumenCumplimiento() {
if (!empresaAdminId) return;
const [{ data: workers }, { data: cursos }] = await Promise.all([
supabase.from('profiles').select('id, nombres, apellidos, email, documento_numero, cargo').eq('empresa_id', empresaAdminId).eq('activo', true).order('apellidos'),
supabase.from('cursos').select('id, titulo, vigencia_meses').eq('activo', true),
]);
if (!workers?.length || !cursos?.length) {
document.getElementById('cumpl-kpis').innerHTML = '<p style="color:#888;">No hay datos suficientes.</p>';
document.getElementById('tabla-por-vencer').innerHTML = '';
return;
}
const emails = workers.map(w => w.email);
const { data: envios } = await supabase
.from('envios_formulario')
.select('usuario_email, id_curso, created_at, formularios(tipo)')
.in('usuario_email', emails)
.eq('aprobado', true);
// Solo tipo examen, más reciente por worker+curso
const lastSub = {};
(envios || []).filter(e => e.formulRepobility analyzer · published findings · https://repobility.com
mostrarQR function · javascript · L3304-L3320 (17 LOC)admin.js
function mostrarQR(sesion) {
const url = `${window.location.origin}/qr-asistencia.html?sesion=${sesion.id}`;
const canvas = document.getElementById('qr-canvas');
window.QRCode.toCanvas(canvas, url, {
width: 280, margin: 2,
color: { dark: '#002855', light: '#ffffff' }
});
const fecha = new Date(sesion.fecha_hora).toLocaleString('es-PE', { dateStyle: 'short', timeStyle: 'short' });
document.getElementById('qr-info-sesion').textContent =
`${sesion.lugar} · ${fecha}${sesion.expositor ? ' · ' + sesion.expositor : ''}`;
document.getElementById('panel-qr-generado').style.display = 'block';
document.getElementById('panel-asistentes-qr').style.display = 'block';
window.scrollTo({ top: document.getElementById('panel-qr-generado').offsetTop - 20, behavior: 'smooth' });
}iniciarPollingAsistentes function · javascript · L3322-L3326 (5 LOC)admin.js
function iniciarPollingAsistentes(sesionId) {
if (intervalAsistentes) clearInterval(intervalAsistentes);
refrescarAsistentes();
intervalAsistentes = setInterval(refrescarAsistentes, 8000);
}detectarColumna function · javascript · L3739-L3754 (16 LOC)admin.js
function detectarColumna(headers, textos) {
for (const texto of textos) {
const idx = headers.findIndex(h =>
String(h).trim().toLowerCase() === texto.toLowerCase()
);
if (idx !== -1) return idx;
}
// búsqueda parcial como fallback
for (const texto of textos) {
const idx = headers.findIndex(h =>
String(h).trim().toLowerCase().includes(texto.toLowerCase())
);
if (idx !== -1) return idx;
}
return -1;
}parsearFechaForms function · javascript · L3757-L3794 (38 LOC)admin.js
function parsearFechaForms(raw) {
if (!raw) return { fechaStr: '', horaStr: '', iso: null };
// SheetJS con cellDates:true entrega Date objects — caso principal
if (raw instanceof Date && !isNaN(raw.getTime())) {
const dia = String(raw.getDate()).padStart(2, '0');
const mes = String(raw.getMonth() + 1).padStart(2, '0');
const hh = String(raw.getHours()).padStart(2, '0');
const mm = String(raw.getMinutes()).padStart(2, '0');
return { fechaStr: `${dia}/${mes}/${raw.getFullYear()}`, horaStr: `${hh}:${mm}`, iso: raw.toISOString() };
}
const s = String(raw).trim();
// Formato texto M/D/YY H:MM:SS (Forms exportado como CSV o texto)
const m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})\s+(\d{1,2}):(\d{2})/);
if (m) {
const anio = m[3].length === 2 ? 2000 + parseInt(m[3]) : parseInt(m[3]);
const mes = m[1].padStart(2, '0');
const dia = m[2].padStart(2, '0');
const hh = m[4].padStart(2, '0');
const mm = m[5].padStart(2, '0');
cbuildHtmlCertificado function · javascript · L10-L87 (78 LOC)certificado.js
export function buildHtmlCertificado({ nombreCompleto, dni, documentoTipo, cargo, cursotitulo, duracion, notaTexto, fechaHoy, codigo }) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Certificado - ${nombreCompleto}</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: 297mm; height: 210mm; overflow: hidden; background: white; }
.certificado { width: 297mm; height: 210mm; position: relative; font-family: 'Crimson Text', Georgia, serif; overflow: hidden; }
.fondo { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.borde-ext { position: absolute; inset: 6mm; border: 2.5px solid #c9a84c; z-index: 1; pointer-events: none; }
.borde-int { position: absolute; inset: 9mm; border: 0.8px solid #c9a84c; opacity:descargarCertificadoPDF function · javascript · L90-L127 (38 LOC)certificado.js
export async function descargarCertificadoPDF(htmlContent, nombreArchivo) {
// Overlay de carga mientras se genera el PDF
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:99998;display:flex;align-items:center;justify-content:center;color:white;font-size:1.1rem;font-family:sans-serif;';
overlay.textContent = '⏳ Generando certificado PDF...';
document.body.appendChild(overlay);
// Contenedor del certificado visible para que html2canvas lo capture correctamente
const contenedor = document.createElement('div');
contenedor.style.cssText = 'position:fixed;top:0;left:0;width:1122px;height:794px;overflow:hidden;background:white;z-index:99999;';
contenedor.innerHTML = htmlContent;
document.body.appendChild(contenedor);
// Esperar imágenes
await Promise.all(
Array.from(contenedor.querySelectorAll('img')).map(img =>
new Promise(resolve => {
if (img.complete) return resolve(generarCertificadoPDFBlob function · javascript · L130-L160 (31 LOC)certificado.js
export async function generarCertificadoPDFBlob(htmlContent) {
const contenedor = document.createElement('div');
contenedor.style.cssText = 'position:fixed;top:0;left:0;width:1122px;height:794px;overflow:hidden;background:white;z-index:99999;opacity:0.01;pointer-events:none;';
contenedor.innerHTML = htmlContent;
document.body.appendChild(contenedor);
try {
await Promise.all(
Array.from(contenedor.querySelectorAll('img')).map(img =>
new Promise(resolve => {
if (img.complete) return resolve(null);
img.onload = img.onerror = resolve;
})
)
);
await new Promise(r => setTimeout(r, 1500));
const el = contenedor.querySelector('.certificado') || contenedor;
const pdfBlob = await window.html2pdf().set({
margin: 0,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, allowTaint: true, width: 1122, height: 794, windowWidth: 1122, windowHeight: 794, scrollX: 0, scgenerarCertificadoPDF function · javascript · L162-L234 (73 LOC)certificado.js
export async function generarCertificadoPDF(curso, nota) {
const { data: { user } } = await supabase.auth.getUser();
const { data: perfil } = await supabase
.from('profiles')
.select('nombres, apellidos, documento_numero, documento_tipo, cargo_id, empresa_id, cargos(nombre), empresas(nombre)')
.eq('id', user.id)
.single();
const nombreCompleto = `${perfil?.apellidos || ''} ${perfil?.nombres || ''}`.trim().toUpperCase();
const dni = perfil?.documento_numero || '';
const cargo = perfil?.cargos?.nombre || '';
const duracion = curso.duracion ? `${curso.duracion} hora${curso.duracion > 1 ? 's' : ''}` : '';
const notaTexto = nota?.toFixed ? nota.toFixed(1) : String(nota);
const fechaHoy = new Date().toLocaleDateString('es-PE', { day: '2-digit', month: 'long', year: 'numeric' });
// Llamar Edge Function: guarda en BD + envía email → devuelve código
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBSame scanner, your repo: https://repobility.com — Repobility
login function · javascript · L50-L82 (33 LOC)main.js
async function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const btnLogin = document.querySelector('#login-section .btn-primary');
if (btnLogin) { btnLogin.disabled = true; btnLogin.textContent = 'Ingresando...'; }
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (btnLogin) { btnLogin.disabled = false; btnLogin.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg> Ingresar'; }
if (error) {
alert("❌ Correo o contraseña incorrectos.");
return;
}
const { data: perfil } = await supabase
.from('profiles')
.select('debe_cambiar_password')
.eq('id', data.user.id)
.single();
if (perfil?.debe_cambiar_password) {
window.location.hreflogout function · javascript · L88-L91 (4 LOC)main.js
async function logout() {
await supabase.auth.signOut();
location.reload();
}verificarAdmin function · javascript · L97-L145 (49 LOC)main.js
async function verificarAdmin(userId) {
const adminPanel = document.getElementById('admin-panel');
adminPanel.style.display = 'none';
const { data: perfil, error } = await supabase
.from('profiles')
.select('rol')
.eq('id', userId)
.single();
if (error) return;
if (perfil?.rol === 'admin' || perfil?.rol === 'superadmin') {
adminPanel.style.display = 'block';
if (perfil?.rol === 'superadmin') {
adminPanel.innerHTML = `
<div class="admin-panel-card">
<div class="admin-panel-info">
<h3>⚙️ Panel de Administración</h3>
<p>Gestión de usuarios, cursos y reportes</p>
</div>
<div class="admin-panel-actions">
<button onclick="window.location.href='admin.html'" class="btn-admin">
🛠️ Admin
</button>
<button onclick="window.location.href='superadmin.html'" class="btn-admin btn-superadmin">
⚙️ Superadmin
</button>
cargarCursos function · javascript · L150-L247 (98 LOC)main.js
async function cargarCursos() {
const { data: cursos, error } = await supabase
.from('cursos')
.select('*')
.eq('activo', true)
.order('titulo');
if (error) { alert("❌ Error al cargar cursos: " + error.message); return; }
// Perfil + envíos en paralelo (sin columnas opcionales que pueden no existir)
const [{ data: perfil }, { data: envios }] = await Promise.all([
supabase
.from('profiles')
.select('nombres, apellidos, empresa_id, empresas(nombre)')
.eq('id', usuarioActual.id)
.single(),
supabase
.from('envios_formulario')
.select('id_curso, aprobado, created_at, formularios(tipo)')
.eq('usuario_email', usuarioActual.email)
.eq('estado', 'completado'),
]);
const empresa = perfil?.empresas;
if (empresa) {
// Título
const headerTitle = document.querySelector('.header-title');
if (headerTitle) headerTitle.textContent = empresa.nombre || 'CV Global S.A.C.';
}
// Nombre del trabajador construirPasos function · javascript · L252-L299 (48 LOC)main.js
async function construirPasos(curso) {
pasosCurso = [];
formularios = {};
materialVisto = false;
window.videosVistos = {};
if (curso.url_material) pasosCurso.push('material');
const { data: videos } = await supabase
.from('videos_curso')
.select('*')
.eq('id_curso', curso.id)
.eq('activo', true)
.order('orden');
if (videos && videos.length > 0) {
videos.forEach(v => pasosCurso.push({ tipo: 'video', ...v }));
}
pasosCurso.push('asistencia');
const { data: encuesta } = await supabase
.from('formularios')
.select('*')
.eq('tipo', 'encuesta')
.eq('activo', true)
.is('id_curso', null)
.single();
if (encuesta) {
formularios['encuesta'] = encuesta;
pasosCurso.push('encuesta');
}
const { data: formsCurso } = await supabase
.from('formularios')
.select('*')
.eq('activo', true)
.eq('id_curso', curso.id);
['examen', 'eficacia'].forEach(tipo => {
const form = formsCurso?.find(f => f.tipomostrarCurso function · javascript · L304-L316 (13 LOC)main.js
async function mostrarCurso(curso) {
cursoSeleccionado = curso;
pasoActual = 0;
tituloCurso.textContent = curso.titulo;
cursoSection.style.display = 'block';
cursosDisponiblesSection.style.display = 'none';
// Mostrar certificado si ya aprobó este curso anteriormente
certificadoSection.style.display = cursosAprobados[curso.id]?.aprobado ? 'block' : 'none';
await construirPasos(curso);
await mostrarPasoActual();
}mostrarPasoActual function · javascript · L321-L618 (298 LOC)main.js
async function mostrarPasoActual() {
const paso = pasosCurso[pasoActual];
const tipoPaso = typeof paso === 'object' ? paso.tipo : paso;
let contenidoHTML = '';
let tituloPaso = typeof paso === 'object' ? paso.titulo : obtenerTituloPaso(paso);
let siguienteHabilitado = true;
switch (tipoPaso) {
// ── MATERIAL ──────────────────
case 'material': {
siguienteHabilitado = materialVisto;
const url = cursoSeleccionado.url_material;
const esOneDrive = url.includes('1drv.ms') || url.includes('onedrive.live.com');
const esSupabase = url.includes('supabase.co');
// PDFs y archivos de Supabase se embeben directo; Office/OneDrive usan el visor de Microsoft
const srcVisor = (esSupabase || url.toLowerCase().endsWith('.pdf'))
? url
: esOneDrive
? url
: "https://view.officeapps.live.com/op/embed.aspx?src=" + encodeURIComponent(url);
contenidoHTML = `
<div class="material-cta">
pasoAnterior function · javascript · L794-L796 (3 LOC)main.js
function pasoAnterior() {
if (pasoActual > 0) { pasoActual--; mostrarPasoActual(); }
}Repobility · MCP-ready · https://repobility.com
siguientePaso function · javascript · L798-L800 (3 LOC)main.js
function siguientePaso() {
if (pasoActual < pasosCurso.length - 1) { pasoActual++; mostrarPasoActual(); }
}volverACursos function · javascript · L808-L814 (7 LOC)main.js
function volverACursos() {
cursoSection.style.display = 'none';
cursosDisponiblesSection.style.display = 'block';
pasoActual = 0;
cursoSeleccionado = null;
window.scrollTo(0, 0);
}generarCertificado function · javascript · L820-L841 (22 LOC)main.js
async function generarCertificado() {
const { data: userData } = await supabase.auth.getUser();
const user = userData?.user;
if (!user || !cursoSeleccionado) {
alert("❌ Usuario o curso no válido");
return;
}
const { data: envioExamen } = await supabase
.from('envios_formulario')
.select('puntaje')
.eq('usuario_id', user.id)
.eq('id_curso', cursoSeleccionado.id)
.eq('estado', 'completado')
.order('created_at', { ascending: false })
.limit(1)
.single();
const nota = envioExamen?.puntaje || 0;
await generarCertificadoPDF(cursoSeleccionado, nota);
}calcularNivel function · javascript · L862-L874 (13 LOC)main.js
function calcularNivel(xp) {
let actual = NIVELES[0];
for (const n of NIVELES) {
if (xp >= n.min) actual = n;
else break;
}
const idx = NIVELES.indexOf(actual);
const siguiente = NIVELES[idx + 1];
const progreso = siguiente
? Math.round(((xp - actual.min) / (siguiente.min - actual.min)) * 100)
: 100;
return { ...actual, siguiente, progreso, xp };
}otorgarXP function · javascript · L876-L882 (7 LOC)main.js
async function otorgarXP(userId, cantidad) {
const { data: perfil } = await supabase.from('profiles').select('xp').eq('id', userId).single();
const xpNuevo = (perfil?.xp || 0) + cantidad;
const nivelNuevo = calcularNivel(xpNuevo).nivel;
await supabase.from('profiles').update({ xp: xpNuevo, nivel: nivelNuevo }).eq('id', userId);
return xpNuevo;
}verificarBadges function · javascript · L884-L910 (27 LOC)main.js
async function verificarBadges(userId) {
const { data: yaGanados } = await supabase
.from('badges_usuario').select('badge_code').eq('usuario_id', userId);
const ganados = new Set((yaGanados || []).map(b => b.badge_code));
const { count: examenes } = await supabase
.from('envios_formulario')
.select('id', { count: 'exact', head: true })
.eq('usuario_id', userId)
.eq('estado', 'completado')
.eq('aprobado', true);
const { count: totalCursos } = await supabase
.from('cursos').select('id', { count: 'exact', head: true }).eq('activo', true);
const nuevos = [];
if (!ganados.has('primer_curso') && examenes >= 1) nuevos.push('primer_curso');
if (!ganados.has('velocista') && examenes >= 3) nuevos.push('velocista');
if (!ganados.has('completista') && totalCursos > 0 && examenes >= totalCursos) nuevos.push('completista');
if (nuevos.length > 0) {
await supabase.from('badges_usuario').insert(
nuevos.map(code => ({ usuario_id: userId,otorgarBadgePrimerIntento function · javascript · L912-L919 (8 LOC)main.js
async function otorgarBadgePrimerIntento(userId) {
const { data: ya } = await supabase.from('badges_usuario')
.select('id').eq('usuario_id', userId).eq('badge_code', 'primer_intento').maybeSingle();
if (!ya) {
await supabase.from('badges_usuario').insert({ usuario_id: userId, badge_code: 'primer_intento' });
mostrarNotifBadge(BADGES_DEF['primer_intento']);
}
}mostrarNotifBadge function · javascript · L921-L935 (15 LOC)main.js
function mostrarNotifBadge(badge) {
if (!badge) return;
const notif = document.createElement('div');
notif.style.cssText = `
position:fixed; bottom:80px; left:50%; transform:translateX(-50%);
background:#1e3a5f; color:white; padding:14px 24px; border-radius:50px;
box-shadow:0 8px 32px rgba(0,0,0,0.25); font-size:0.88rem; font-weight:600;
display:flex; align-items:center; gap:10px; z-index:9999;
animation:slideUp 0.4s ease; white-space:nowrap;
`;
notif.innerHTML = `<span style="font-size:1.4rem">${badge.emoji}</span> ¡Insignia desbloqueada! <strong>${badge.nombre}</strong>`;
document.body.appendChild(notif);
setTimeout(() => { notif.style.opacity = '0'; notif.style.transition = 'opacity 0.5s'; }, 3500);
setTimeout(() => notif.remove(), 4100);
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
cargarGamificacion function · javascript · L937-L988 (52 LOC)main.js
async function cargarGamificacion(perfilBase) {
const widget = document.getElementById('gamificacion-widget');
if (!widget) return;
// Reusar perfil ya cargado; si no trae xp, hacer fetch mínimo
let xp = perfilBase?.xp;
let empresa_id = perfilBase?.empresa_id;
if (xp == null) {
const { data: p } = await supabase
.from('profiles').select('xp, empresa_id').eq('id', usuarioActual.id).single();
xp = p?.xp || 0;
empresa_id = p?.empresa_id;
}
const nivelInfo = calcularNivel(xp);
const { data: badges } = await supabase
.from('badges_usuario').select('badge_code').eq('usuario_id', usuarioActual.id);
let rankingHTML = '';
if (empresa_id) {
const { data: ranking } = await supabase
.from('profiles').select('id, xp').eq('empresa_id', empresa_id).order('xp', { ascending: false });
if (ranking) {
const pos = ranking.findIndex(r => r.id === usuarioActual.id) + 1;
rankingHTML = `<div class="gami-ranking">🏅 Posición <strong>#${poobtenerTituloPaso function · javascript · L993-L1003 (11 LOC)main.js
function obtenerTituloPaso(paso) {
const titulos = {
'material': '📚 Material',
'video': '🎥 Video',
'asistencia': '✅ Asistencia',
'encuesta': '📋 Encuesta',
'examen': '📝 Evaluación',
'eficacia': '🎯 Eficacia'
};
return titulos[paso] || paso;
}App function · javascript · L4-L35 (32 LOC)src/App.jsx
function App() {
const [cursos, setCursos] = useState([]);
useEffect(() => {
async function cargarCursos() {
const { data, error } = await supabase
.from("cursos")
.select("*");
if (error) {
console.error(error);
} else {
setCursos(data);
}
}
cargarCursos();
}, []);
return (
<div>
<h1>Cursos disponibles</h1>
{cursos.map((curso) => (
<div key={curso.id}>
<h2>{curso.titulo}</h2>
<p>{curso.descripcion}</p>
</div>
))}
</div>
);
}cargarCursos function · javascript · L8-L18 (11 LOC)src/App.jsx
async function cargarCursos() {
const { data, error } = await supabase
.from("cursos")
.select("*");
if (error) {
console.error(error);
} else {
setCursos(data);
}
}json function · typescript · L170-L175 (6 LOC)supabase/functions/enviar-notificaciones/index.ts
function json(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}configurarRENIEC function · javascript · L33-L93 (61 LOC)superadmin.js
function configurarRENIEC(idDni, idTipo, idNombres, idApellidos) {
const inputDni = document.getElementById(idDni);
if (!inputDni) return;
// Desbloquear al cambiar a CE o Pasaporte
document.getElementById(idTipo)?.addEventListener('change', () => {
const tipo = document.getElementById(idTipo).value;
const nombresEl = document.getElementById(idNombres);
const apellidosEl = document.getElementById(idApellidos);
const msgEl = document.getElementById(idDni + '-reniec-msg');
if (tipo !== 'DNI') {
nombresEl.disabled = false;
apellidosEl.disabled = false;
if (msgEl) msgEl.textContent = '';
} else {
nombresEl.disabled = true;
apellidosEl.disabled = true;
nombresEl.value = '';
apellidosEl.value = '';
}
});
inputDni.addEventListener('input', async () => {
const dni = inputDni.value.trim();
const tipo = document.getElementById(idTipo)?.value;
if (tipo !== 'DNI' || dni.length !== 8) return;
const cargarEmpresas function · javascript · L98-L132 (35 LOC)superadmin.js
async function cargarEmpresas() {
const { data: empresas } = await supabase
.from('empresas')
.select('*')
.order('nombre');
// Llenar tabla
const tbody = document.querySelector('#tabla-empresas tbody');
tbody.innerHTML = '';
empresas?.forEach(e => {
tbody.innerHTML += `
<tr>
<td>${e.nombre}</td>
<td>${e.ruc}</td>
<td>${e.activo ? '✅ Activa' : '❌ Inactiva'}</td>
<td>
<button onclick="toggleEmpresa('${e.id}', ${e.activo})"
style="padding:5px 10px; background:${e.activo ? '#dc3545' : '#28a745'};
color:white; border:none; border-radius:4px; cursor:pointer;">
${e.activo ? 'Desactivar' : 'Activar'}
</button>
</td>
</tr>`;
});
// Llenar selectores
const selAdmin = document.getElementById('admin-empresa');
const selFiltro = document.getElementById('filtro-empresa');
selAdmin.innerHTML = '<option value="">-- Selecciona empresa --</option>';
cargarCargos function · javascript · L160-L189 (30 LOC)superadmin.js
async function cargarCargos() {
const { data: cargos } = await supabase
.from('cargos')
.select('*')
.order('nombre');
const tbody = document.querySelector('#tabla-cargos tbody');
tbody.innerHTML = '';
cargos?.forEach(c => {
tbody.innerHTML += `
<tr>
<td>${c.nombre}</td>
<td>${c.activo ? '✅ Activo' : '❌ Inactivo'}</td>
<td>
<button onclick="toggleCargo('${c.id}', ${c.activo})"
style="padding:5px 10px; background:${c.activo ? '#dc3545' : '#28a745'};
color:white; border:none; border-radius:4px; cursor:pointer;">
${c.activo ? 'Desactivar' : 'Activar'}
</button>
</td>
</tr>`;
});
// Llenar selector de cargos en form admin
const selCargo = document.getElementById('admin-cargo');
selCargo.innerHTML = '<option value="">-- Selecciona cargo --</option>';
cargos?.filter(c => c.activo).forEach(c => {
selCargo.innerHTML += `<option value="${c.id}">${c.nRepobility analyzer · published findings · https://repobility.com
cargarAdmins function · javascript · L213-L243 (31 LOC)superadmin.js
async function cargarAdmins() {
const { data } = await supabase
.from('profiles')
.select('*, empresas(nombre)')
.eq('rol', 'admin')
.order('apellidos');
const tbody = document.querySelector('#tabla-admins tbody');
tbody.innerHTML = '';
data?.forEach(u => {
tbody.innerHTML += `
<tr>
<td>${u.apellidos || ''} ${u.nombres || ''}</td>
<td>${u.email}</td>
<td>${u.empresas?.nombre || '—'}</td>
<td>${u.documento_tipo}: ${u.documento_numero || '—'}</td>
<td>${u.activo ? '✅ Activo' : '❌ Inactivo'}</td>
<td style="display:flex; gap:6px;">
<button onclick="toggleUsuario('${u.id}', ${u.activo})"
style="padding:5px 10px; background:${u.activo ? '#dc3545' : '#28a745'};
color:white; border:none; border-radius:4px; cursor:pointer;">
${u.activo ? 'Desactivar' : 'Activar'}
</button>
<button onclick="eliminarAdmin('${u.id}', '${u.apellidos} ${u.nombrecargarTodosUsuarios function · javascript · L320-L328 (9 LOC)superadmin.js
async function cargarTodosUsuarios() {
const { data } = await supabase
.from('profiles')
.select('*, empresas(nombre), cargos(nombre)')
.order('apellidos');
todosUsuarios = data || [];
renderizarUsuarios(todosUsuarios);
}renderizarUsuarios function · javascript · L330-L364 (35 LOC)superadmin.js
function renderizarUsuarios(usuarios) {
const tbody = document.querySelector('#tabla-usuarios tbody');
tbody.innerHTML = '';
if (!usuarios.length) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:24px;color:#888;">
No se encontraron usuarios.</td></tr>`;
return;
}
usuarios.forEach(u => {
const rolBadge = u.rol === 'superadmin'
? `<span class="badge-superadmin">Superadmin</span>`
: u.rol === 'admin'
? `<span class="badge-admin">Admin</span>`
: `<span class="badge-trabajador">Trabajador</span>`;
tbody.innerHTML += `
<tr>
<td>${u.apellidos || ''} ${u.nombres || ''}</td>
<td>${u.email}</td>
<td>${u.empresas?.nombre || '—'}</td>
<td>${u.cargos?.nombre || '—'}</td>
<td>${rolBadge}</td>
<td>${u.activo ? '✅' : '❌'}</td>
<td>
<button onclick="toggleUsuario('${u.id}', ${u.activo})"
style="padding:5px 10px; background:${u.activo ? initBranding function · javascript · L412-L445 (34 LOC)superadmin.js
async function initBranding() {
const { data: empresas } = await supabase.from('empresas').select('id, nombre').eq('activo', true).order('nombre');
const sel = document.getElementById('branding-empresa');
if (!sel) return;
sel.innerHTML = '<option value="">-- Selecciona una empresa --</option>';
(empresas || []).forEach(e => sel.insertAdjacentHTML('beforeend', `<option value="${e.id}">${e.nombre}</option>`));
sel.addEventListener('change', async () => {
const id = sel.value;
if (!id) return;
const { data } = await supabase.from('empresas').select('logo_url, color_primario, color_secundario').eq('id', id).single();
const logo = data?.logo_url || '';
const color1 = data?.color_primario || '#1e3a5f';
const color2 = data?.color_secundario|| '#c9a84c';
document.getElementById('branding-logo').value = logo;
document.getElementById('branding-color-primario').value = color1;
document.getElementById('branding-color-primactualizarPreviewLogo function · javascript · L447-L459 (13 LOC)superadmin.js
function actualizarPreviewLogo(url) {
const img = document.getElementById('branding-logo-preview');
const placeholder = document.getElementById('branding-logo-placeholder');
if (url) {
img.src = url;
img.style.display = 'block';
placeholder.style.display = 'none';
img.onerror = () => { img.style.display = 'none'; placeholder.style.display = 'block'; };
} else {
img.style.display = 'none';
placeholder.style.display = 'block';
}
}networkFirst function · javascript · L70-L85 (16 LOC)sw.js
async function networkFirst(request) {
if (request.method !== 'GET') return fetch(request);
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
const offline = await caches.match('/offline.html');
return offline || new Response('Sin conexión', { status: 503 });
}
}cacheFirst function · javascript · L88-L106 (19 LOC)sw.js
async function cacheFirst(request) {
// Solo cachear GET
if (request.method !== 'GET') return fetch(request);
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
const offline = await caches.match('/offline.html');
return offline || new Response('Sin conexión', { status: 503 });
}
}getContainer function · javascript · L7-L20 (14 LOC)toast.js
function getContainer() {
let c = document.getElementById('toast-container');
if (!c) {
c = document.createElement('div');
c.id = 'toast-container';
c.style.cssText = [
'position:fixed', 'top:16px', 'left:50%', 'transform:translateX(-50%)',
'z-index:99999', 'display:flex', 'flex-direction:column', 'align-items:center',
'gap:8px', 'width:min(420px,calc(100vw - 32px))', 'pointer-events:none',
].join(';');
document.body.appendChild(c);
}
return c;
}Same scanner, your repo: https://repobility.com — Repobility
toast function · javascript · L22-L44 (23 LOC)toast.js
export function toast(message, type = 'info', duration) {
const auto = duration ?? (message.length > 80 ? 5500 : 3500);
const container = getContainer();
const el = document.createElement('div');
el.style.cssText = [
`background:${COLORS[type]}`, 'color:white', 'padding:13px 18px',
'border-radius:12px', 'box-shadow:0 4px 24px rgba(0,0,0,0.22)',
'font-size:0.88rem', 'font-weight:500', 'display:flex', 'align-items:flex-start',
'gap:10px', 'animation:toastIn 0.3s ease', 'line-height:1.5',
'cursor:pointer', 'word-break:break-word', 'width:100%', 'pointer-events:all',
].join(';');
el.innerHTML = `<span style="flex-shrink:0;font-size:1.05rem">${ICONS[type]}</span><span>${message.replace(/\n/g, '<br>')}</span>`;
function dismiss() {
el.style.cssText += ';opacity:0;transform:translateY(-8px);transition:opacity 0.3s,transform 0.3s';
setTimeout(() => el.remove(), 320);
}
el.onclick = dismiss;
container.appendChild(el);
setTimeout(dismiss, autodismiss function · javascript · L36-L39 (4 LOC)toast.js
function dismiss() {
el.style.cssText += ';opacity:0;transform:translateY(-8px);transition:opacity 0.3s,transform 0.3s';
setTimeout(() => el.remove(), 320);
}alertToToast function · javascript · L47-L56 (10 LOC)toast.js
export function alertToToast(msg) {
if (!msg) return;
const s = String(msg);
const type = s.startsWith('✅') ? 'success'
: s.startsWith('❌') ? 'error'
: s.startsWith('⚠️') ? 'warning'
: 'info';
const clean = translateError(s.replace(/^[✅❌⚠️ℹ️]\s*/, ''));
toast(clean, type);
}page 1 / 2next ›