← back to danijustiniani31415__plataforma-cursos

Function bodies 54 total

All specs Real LLM only Function bodies
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.e
cargarResumenCumplimiento 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.formul
Repobility 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');
    c
buildHtmlCertificado 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, sc
generarCertificadoPDF 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.eyJpc3MiOiJzdXB
Same 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.href
logout 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.tipo
mostrarCurso 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>#${po
obtenerTituloPaso 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.n
Repobility 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.nombre
cargarTodosUsuarios 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-prim
actualizarPreviewLogo 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, auto
dismiss 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 ›