Tul xxx Tul
User / IP
:
216.73.216.217
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
propuesta
/
Viewing: index.html
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Sistema de Gestión de Empleados y Nómina</title> <link rel="icon" type="image/png" href="PNG.png" /> <link rel="apple-touch-icon" href="PNG.png" /> <script>document.documentElement.classList.add('js')</script> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet"> <style> *{margin:0;padding:0;box-sizing:border-box;font-family:'Poppins',sans-serif} body{background:#0f172a;color:#fff} section{padding:70px 10%} h1,h2,h3{font-weight:700} p{color:#cbd5e1} img{display:block;max-width:100%} /* HERO */ .hero{ background: linear-gradient(120deg, rgba(2,6,23,.88), rgba(15,23,42,.62), rgba(2,6,23,.88)), radial-gradient(1200px 380px at 50% 0%, rgba(56,189,248,.20), rgba(0,0,0,0)), url('portada.png'); background-size:cover; background-position:center; background-repeat:no-repeat; text-align:center; padding-top:100px; position:relative; overflow:hidden } .hero::before{content:"";position:absolute;inset:-35%;background:conic-gradient(from 180deg at 50% 50%, rgba(56,189,248,.0), rgba(56,189,248,.22), rgba(167,139,250,.22), rgba(56,189,248,.18), rgba(56,189,248,.0));filter:blur(90px);opacity:.55;animation:aurora 16s ease-in-out infinite;transform:translate3d(0,0,0)} .hero>*{position:relative;z-index:1} .brand{display:flex;justify-content:center;align-items:center;margin-bottom:18px} .brand img{height:58px;width:auto;filter:drop-shadow(0 14px 28px rgba(56,189,248,.18))} .hero h1{font-size:42px;margin-bottom:15px;text-shadow:0 10px 30px rgba(0,0,0,.55)} .hero span{color:#38bdf8} .hero p{max-width:700px;margin:auto;margin-bottom:30px;text-shadow:0 10px 30px rgba(0,0,0,.55)} .btn{ display:inline-block;background:#38bdf8;color:#000; padding:14px 28px;border-radius:10px;text-decoration:none; font-weight:600;transition:.3s } .btn:hover{transform:scale(1.05)} /* FEATURES */ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:25px;margin-top:40px} .card{ background:#1e293b;border-radius:16px;padding:25px; box-shadow:0 0 20px rgba(0,0,0,.3);transition:.3s } .card:hover{transform:translateY(-6px);box-shadow:0 0 25px #38bdf8} .card h3{color:#38bdf8;margin-bottom:10px} /* PRICING */ .pricing{background:#111827;text-align:center} .price-cards{display:flex;flex-wrap:wrap;justify-content:center;gap:30px;margin-top:40px} .price-card{ background:#1e293b;border-radius:18px;padding:35px;width:300px; box-shadow:0 0 20px rgba(0,0,0,.4);transition:.3s } .price-card:hover{transform:scale(1.05);box-shadow:0 0 30px #38bdf8} .price{font-size:40px;color:#22c55e;margin:15px 0;text-shadow:0 0 18px rgba(34,197,94,.22)} .price-desc{margin-top:12px;color:#e2e8f0;line-height:1.6} /* PAYMENT */ .payment{ text-align:center;background:#0f172a } .payment .card{background:#1e293b} /* CONTRACT */ .contract{background:transparent;color:#0b1224} .contract .contract-wrap{max-width:1100px;margin:0 auto;background:#fff;border:1px solid rgba(15,23,42,.12);border-radius:22px;padding:34px 28px;box-shadow:0 26px 70px rgba(2,6,23,.10)} .contract .contract-top{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap} .pdf-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;appearance:none;border:none;background:linear-gradient(135deg,#0ea5e9 0%,#3b82f6 100%);color:#fff;font-weight:600;border-radius:12px;padding:12px 24px;font-size:14px;letter-spacing:.3px;cursor:pointer;box-shadow:0 10px 20px -5px rgba(14,165,233,.4);transition:all .3s cubic-bezier(.4,0,.2,1);position:relative;overflow:hidden;z-index:1} .pdf-btn::before{content:"";position:absolute;inset:0;background:linear-gradient(135deg,#3b82f6 0%,#0ea5e9 100%);opacity:0;z-index:-1;transition:opacity .3s ease} .pdf-btn:hover{transform:translateY(-2px);box-shadow:0 15px 30px -5px rgba(14,165,233,.5)} .pdf-btn:hover::before{opacity:1} .pdf-btn:active{transform:translateY(0);box-shadow:0 5px 10px -5px rgba(14,165,233,.4)} .pdf-btn svg{width:18px;height:18px;stroke-width:2.5} .pdf-btn:focus-visible{outline:none;box-shadow:0 0 0 4px rgba(56,189,248,.3)} .contract h2{margin-bottom:10px;color:#0b1224} .contract h3{margin-top:18px;margin-bottom:10px;color:#0b1224} .contract .lead{color:#0b1224;max-width:980px;margin:0 auto 18px;line-height:1.65} .contract .contract-body{margin-top:18px;text-align:left;line-height:1.7;color:#0b1224} .contract .contract-body p{color:#0b1224} .contract .contract-body ul{margin:10px 0 10px 22px} .contract .contract-body li{margin:6px 0;color:#0b1224} .signatures{margin-top:26px;display:grid;grid-template-columns:repeat(3,minmax(220px,1fr));gap:18px} .sig-card{background:#fff;border:1px solid rgba(15,23,42,.12);border-radius:18px;padding:18px} .sig-title{font-weight:800;color:#0b1224;font-size:14px;letter-spacing:.2px;margin:2px 0 10px;text-transform:uppercase} .sig-sub{color:rgba(15,23,42,.72);font-size:12px;margin:-6px 0 14px} .sig-field{width:100%;background:#fff;border:1px solid rgba(15,23,42,.16);color:#0b1224;border-radius:12px;padding:12px 12px;font-size:14px;outline:none} .sig-field::placeholder{color:rgba(15,23,42,.55)} .sig-field:focus{border-color:rgba(34,197,94,.65);box-shadow:0 0 0 4px rgba(34,197,94,.14)} .sig-pad{margin-top:14px;border:1px dashed rgba(15,23,42,.22);border-radius:14px;background:linear-gradient(180deg, rgba(2,6,23,.02), rgba(2,6,23,.00));padding:12px;position:relative} .sig-canvas{display:block;width:100%;height:140px;border-radius:10px;background:transparent;touch-action:none;cursor:crosshair} .sig-line{height:1px;background:linear-gradient(90deg, rgba(15,23,42,.0), rgba(15,23,42,.55), rgba(15,23,42,.0));margin:8px 0 0} .sig-under{margin-top:12px;display:flex;flex-direction:column;gap:10px} .sig-role{font-weight:900;color:#0b1224;font-size:13px;letter-spacing:.2px} .sig-actions{display:flex;justify-content:flex-end;margin-top:10px} .sig-clear{appearance:none;border:1px solid rgba(15,23,42,.16);background:#fff;color:#0b1224;border-radius:999px;padding:8px 12px;font-weight:700;font-size:12px;cursor:pointer;transition:transform .2s, box-shadow .2s} .sig-clear:hover{transform:translateY(-1px);box-shadow:0 10px 25px rgba(2,6,23,.10)} .sig-clear:active{transform:translateY(0)} .sig-clear:focus-visible{outline:none;box-shadow:0 0 0 4px rgba(34,197,94,.14)} .saved-glow{border-color:rgba(34,197,94,.75) !important;box-shadow:0 0 0 4px rgba(34,197,94,.16), 0 18px 45px rgba(34,197,94,.12) !important;transition:box-shadow .25s, border-color .25s} @media (max-width: 900px){ .signatures{grid-template-columns:1fr} } /* FOOTER */ footer{text-align:center;padding:40px;background:#020617;color:#64748b;font-size:14px;display:flex;flex-direction:column;gap:14px;align-items:center;justify-content:center} .dev-line{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap} .aircan-btn{display:inline-flex;align-items:center;justify-content:center;padding:12px 20px;border-radius:999px;background:linear-gradient(135deg,#38bdf8,#a78bfa);color:#020617;font-weight:800;text-decoration:none;font-size:16px;letter-spacing:.2px;box-shadow:0 12px 34px rgba(56,189,248,.20), inset 0 1px 0 rgba(255,255,255,.25);transition:transform .25s, box-shadow .25s} .aircan-btn:hover{transform:translateY(-2px) scale(1.03);box-shadow:0 18px 44px rgba(167,139,250,.20), 0 0 0 4px rgba(56,189,248,.14)} .aircan-btn:active{transform:translateY(0) scale(.99)} .aircan-btn:focus-visible{outline:none;box-shadow:0 0 0 4px rgba(56,189,248,.30), 0 18px 44px rgba(56,189,248,.20)} .js.reveal-ready .reveal{opacity:0;transform:translate3d(0,18px,0);filter:blur(10px);transition:opacity .85s cubic-bezier(.2,.8,.2,1), transform .85s cubic-bezier(.2,.8,.2,1), filter .85s cubic-bezier(.2,.8,.2,1);transition-delay:var(--d,0ms);will-change:opacity, transform, filter} .js.reveal-ready .reveal.is-visible{opacity:1;transform:none;filter:none} @keyframes aurora{0%{transform:translate3d(-6%, -3%, 0) rotate(0deg) scale(1.05)}50%{transform:translate3d(6%, 3%, 0) rotate(180deg) scale(1.15)}100%{transform:translate3d(-6%, -3%, 0) rotate(360deg) scale(1.05)}} @media (prefers-reduced-motion: reduce){ .hero::before{animation:none} .js.reveal-ready .reveal{opacity:1;transform:none;filter:none;transition:none} } @media (max-width: 520px){ .hero{padding-top:80px} .hero h1{font-size:32px} .brand img{height:46px} .contract .contract-wrap{padding:24px 18px} .contract .contract-body p, .contract .contract-body li{font-size:16px;line-height:1.6} .sig-canvas{height:220px !important;min-height:220px} } </style> </head> <body> <section class="hero"> <div class="brand reveal"> <img src="PNG.png" alt="Aircan" loading="eager" /> </div> <h1 class="reveal">Sistema Profesional de <span>Gestión de Empleados y Nómina</span></h1> <p class="reveal">Digitaliza tu empresa, controla asistencias con ubicación, automatiza pagos y cumple con obligaciones laborales. Plataforma web instalable tipo app.</p> </section> <section> <h2 class="reveal">🚀 Módulos Principales</h2> <div class="grid"> <div class="card"><h3>👥 Gestión de Empleados</h3><p>Registro, contratos, documentos laborales, estados y perfiles completos.</p></div> <div class="card"><h3>📍 Asistencia con GPS</h3><p>Marcación de entrada y salida con ubicación real mediante Google Maps.</p></div> <div class="card"><h3>💵 Nómina Automática</h3><p>Salarios, horas extras, deducciones, bonos, neto a pagar e historial.</p></div> <div class="card"><h3>🏖 Vacaciones</h3><p>Solicitudes, aprobaciones, control de días acumulados y calendario general.</p></div> <div class="card"><h3>📑 Liquidaciones</h3><p>Cálculo automático de aguinaldo, prestaciones e indemnizaciones.</p></div> <div class="card"><h3>📊 Reportes</h3><p>Exportación en Excel y PDF para pagos, asistencias e impuestos.</p></div> </div> </section> <section> <h2 class="reveal">🔐 Seguridad y Accesos</h2> <div class="grid"> <div class="card"><h3>Administrador</h3><p>Control total de empleados, nómina, vacaciones, documentos y reportes.</p></div> <div class="card"><h3>Empleado</h3><p>Acceso a su perfil, pagos, asistencias, vacaciones y documentos descargables.</p></div> <div class="card"><h3>PWA Instalable</h3><p>Funciona como aplicación móvil y de escritorio sin instalar desde tiendas.</p></div> </div> </section> <section class="pricing"> <h2 class="reveal">💼 Planes de Inversión</h2> <div class="price-cards"> <div class="price-card" id="investmentPlanCard"> <h3 id="investmentPlanTitle">Plan Profesional</h3> <div class="price" id="investmentPlanPrice">$750</div> <p class="price-desc" id="investmentPlanDescription" hidden></p> </div> </div> </section> <section class="payment"> <h2 class="reveal">💳 Opciones de Pago</h2> <div class="grid"> <div class="card"><h3>Opción 1</h3><p>50% inicio — 50% entrega</p></div> <div class="card"><h3>Opción 2</h3><p>40% inicio — 30% avance — 30% entrega</p></div> </div> </section> <section> <h2 class="reveal">📅 Entrega del Proyecto</h2> <div class="grid"> <div class="card"><h3>⏳ Tiempo de Desarrollo</h3><p>15 a 30 días hábiles</p></div> <div class="card"><h3>⚙️ Incluye</h3><p>Instalación, configuración inicial y capacitación.</p></div> <div class="card"><h3>🏢 Propiedad</h3><p>El sistema será propiedad total de la empresa con entrega de código fuente.</p></div> </div> </section> <section class="contract"> <div class="contract-wrap"> <div class="contract-top"> <h2>CONTRATO DE PRESTACIÓN DE SERVICIOS DE DESARROLLO DE SOFTWARE NÓMINA</h2> <button class="pdf-btn" type="button" id="downloadContractPdf"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> <span>Descargar contrato en PDF</span> </button> </div> <div class="contract-body"> <p>El presente documento tiene como finalidad presentar el contrato de desarrollo de un Sistema de Gestión de Empleados y Nómina, incluyendo las características, módulos y condiciones generales del proyecto.</p> <h3>Objeto del contrato</h3> <p>EL DESARROLLADOR se compromete a desarrollar un Sistema de Gestión de Empleados y Nómina, el cual incluirá los siguientes módulos:</p> <ul> <li>Registro y control de empleados</li> <li>Marcación de asistencia con ubicación (Google Maps)</li> <li>Módulo de vacaciones (solicitud y aprobación)</li> <li>Nómina y pagos</li> <li>Historial de pagos y reportes</li> <li>Cálculo de prestaciones, liquidaciones y deducciones legales</li> <li>Módulo de documentos laborales (contratos, constancias, cartas)</li> <li>Sistema instalable tipo aplicación web (PWA)</li> </ul> <h3>Roles del sistema</h3> <p>El sistema contará con los siguientes roles:</p> <ul> <li>Administrador</li> <li>Empleado</li> </ul> <h3>Funcionalidades principales</h3> <p>El sistema permitirá, entre otras funciones:</p> <ul> <li>Autenticación con usuarios y permisos.</li> <li>Perfil del empleado con acceso a datos personales, pagos, asistencias, vacaciones y documentos.</li> <li>Panel administrativo con gestión de empleados, nómina, pagos, vacaciones, documentos y usuarios.</li> <li>Registro automático de asistencia con fecha, hora y ubicación sin posibilidad de edición por parte del empleado.</li> <li>Control de nómina (semanal, quincenal o mensual).</li> <li>Generación de reportes en PDF y Excel.</li> <li>Módulo de liquidaciones, aguinaldos y prestaciones.</li> <li>Configuración de seguridad social e impuestos (INATEC, deducciones VIR).</li> <li>Gestor de documentos laborales.</li> </ul> <h3>Tiempo de entrega</h3> <p>El tiempo estimado de desarrollo es de 15 a 30 días hábiles, contados a partir de la entrega del anticipo y la información necesaria por parte del cliente.</p> <h3>Propiedad del sistema</h3> <p>Una vez realizado el pago total, EL CLIENTE será propietario del sistema y del código fuente, sin pagos mensuales obligatorios.</p> <h3>Soporte</h3> <p>El desarrollo incluye instalación, configuración inicial y capacitación básica para el uso del sistema.</p> <h3>Firmado en conformidad</h3> <div class="signatures"> <div class="sig-card"> <div class="sig-role">Empresa vendedora (proveedora)</div> <input class="sig-field" type="text" placeholder="Nombre y Apellido" autocomplete="name" data-sig-name="seller" /> <div class="sig-pad" data-sig-pad="seller"> <canvas class="sig-canvas" width="900" height="300" aria-label="Firma de empresa proveedora"></canvas> <div class="sig-line"></div> <div class="sig-actions"><button class="sig-clear" type="button" data-sig-clear="seller">Limpiar firma</button></div> </div> </div> <div class="sig-card"> <div class="sig-role">Empresa compradora (cliente)</div> <input class="sig-field" type="text" placeholder="Nombre y Apellido" autocomplete="name" data-sig-name="buyer" /> <div class="sig-pad" data-sig-pad="buyer"> <canvas class="sig-canvas" width="900" height="300" aria-label="Firma de empresa cliente"></canvas> <div class="sig-line"></div> <div class="sig-actions"><button class="sig-clear" type="button" data-sig-clear="buyer">Limpiar firma</button></div> </div> </div> <div class="sig-card"> <div class="sig-role">Desarrollador (proveedor del servicio)</div> <input class="sig-field" type="text" placeholder="Nombre y Apellido" autocomplete="name" data-sig-name="dev" /> <div class="sig-pad" data-sig-pad="dev"> <canvas class="sig-canvas" width="900" height="300" aria-label="Firma del desarrollador"></canvas> <div class="sig-line"></div> <div class="sig-actions"><button class="sig-clear" type="button" data-sig-clear="dev">Limpiar firma</button></div> </div> </div> </div> </div> </div> </section> <footer> <div>Propuesta de Sistema de Gestión de Empleados y Nómina — 2026</div> <div class="dev-line"> <span>Desarrollado por</span> <a class="aircan-btn" href="https://aircan.me/" target="_blank" rel="noopener noreferrer">Aircan</a> </div> </footer> <script> (() => { const prefersReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReduced) return; const root = document.documentElement; const revealSet = new Set(); const addReveal = (el, delayMs = 0) => { if (!el) return; el.classList.add('reveal'); el.style.setProperty('--d', `${delayMs}ms`); revealSet.add(el); }; document.querySelectorAll('.hero .brand, .hero h1, .hero p').forEach((el, i) => addReveal(el, i * 140)); document.querySelectorAll('section:not(.hero) > h2').forEach((el) => addReveal(el, 0)); document.querySelectorAll('.grid, .price-cards').forEach((container) => { Array.from(container.children).forEach((child, idx) => addReveal(child, Math.min(idx * 90, 720))); }); root.classList.add('reveal-ready'); document.querySelectorAll('.hero .reveal').forEach((el) => el.classList.add('is-visible')); const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add('is-visible'); io.unobserve(entry.target); }); }, { threshold: 0.14, rootMargin: '0px 0px -12% 0px' } ); revealSet.forEach((el) => io.observe(el)); })(); </script> <script> (() => { const titleEl = document.getElementById('investmentPlanTitle'); const priceEl = document.getElementById('investmentPlanPrice'); const descEl = document.getElementById('investmentPlanDescription'); if (!titleEl || !priceEl || !descEl) return; const apply = (data) => { const title = (data && data.title ? String(data.title) : '').trim(); const price = (data && data.price_label ? String(data.price_label) : '').trim(); const desc = data && data.description !== undefined && data.description !== null ? String(data.description).trim() : ''; if (title) titleEl.textContent = title; if (price) priceEl.textContent = price; if (desc) { descEl.textContent = desc; descEl.hidden = false; } else { descEl.textContent = ''; descEl.hidden = true; } }; const safeFetchJson = async (url) => { try { const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) return null; return await res.json(); } catch (_) { return null; } }; (async () => { const json = await safeFetchJson('get_plan.php'); if (!json || !json.ok || !json.data) return; apply(json.data); })(); })(); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" integrity="sha512-GsLlZN/3F2ErC5ifS5QtgpiJtWd43JWSuIgh7mbzZ8zBps+dvLusV+eNQATqgA/HdeKFVgA5v3S/cIrLF7QnIg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script> (() => { const DOC_KEY = 'propuesta_nomina_2026'; const keyFor = (id) => `aircan_propuesta_sig_${id}`; const nameKeyFor = (id) => `aircan_propuesta_sig_name_${id}`; const apiGet = (role) => `get_signature.php?doc_key=${encodeURIComponent(DOC_KEY)}&role_key=${encodeURIComponent(role)}`; const apiSave = () => 'save_signature.php'; const safeFetchJson = async (url, options) => { try { const res = await fetch(url, options); if (!res.ok) return null; return await res.json(); } catch (_) { return null; } }; const drawImageCover = (ctx, img, w, h) => { const iw = img.naturalWidth || img.width; const ih = img.naturalHeight || img.height; if (!iw || !ih) return; const s = Math.min(w / iw, h / ih); const nw = iw * s; const nh = ih * s; const x = (w - nw) / 2; const y = (h - nh) / 2; ctx.drawImage(img, x, y, nw, nh); }; const setupPad = (id) => { const pad = document.querySelector(`[data-sig-pad="${id}"]`); const canvas = pad ? pad.querySelector('canvas') : null; const btn = document.querySelector(`[data-sig-clear="${id}"]`); const nameInput = document.querySelector(`[data-sig-name="${id}"]`); if (!pad || !canvas || !btn || !nameInput) return; const ctx = canvas.getContext('2d'); const getDpr = () => Math.max(1, Math.min(3, window.devicePixelRatio || 1)); const resize = () => { const dpr = getDpr(); const rect = canvas.getBoundingClientRect(); const w = Math.max(1, Math.floor(rect.width)); const h = Math.max(1, Math.floor(rect.height)); canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); // canvas.style.width = `${w}px`; // Dejar que CSS controle el tamaño // canvas.style.height = `${h}px`; // Dejar que CSS controle el tamaño ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#0b1224'; ctx.lineWidth = 2.3; restoreFromLocal(); }; const restoreFromLocal = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); const saved = localStorage.getItem(keyFor(id)); if (saved) { const img = new Image(); img.onload = () => drawImageCover(ctx, img, canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height); img.src = saved; } }; const drawDataUrl = (dataUrl) => { if (!dataUrl) return; const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); drawImageCover(ctx, img, canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height); }; img.src = dataUrl; }; const restoreFromDb = async () => { const json = await safeFetchJson(apiGet(id)); if (!json || !json.ok) return false; if (!json.data) return true; if (json.data.signer_name !== null && json.data.signer_name !== undefined) { nameInput.value = json.data.signer_name; try { localStorage.setItem(nameKeyFor(id), nameInput.value || ''); } catch (_) {} } if (json.data.signature_data) { drawDataUrl(json.data.signature_data); try { localStorage.setItem(keyFor(id), json.data.signature_data); } catch (_) {} } return true; }; const saveToDb = async ({ name, signature, nameSet = false, sigSet = false }) => { const payload = { doc_key: DOC_KEY, role_key: id, }; if (nameSet) payload.signer_name = name; if (sigSet) payload.signature_data = signature; const json = await safeFetchJson(apiSave(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return !!(json && json.ok); }; const glow = (el) => { if (!el) return; el.classList.add('saved-glow'); window.setTimeout(() => el.classList.remove('saved-glow'), 900); }; let drawing = false; let last = null; const pointFromEvent = (e) => { const rect = canvas.getBoundingClientRect(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }; const start = (e) => { e.preventDefault(); drawing = true; last = pointFromEvent(e); }; const move = (e) => { if (!drawing) return; e.preventDefault(); const p = pointFromEvent(e); if (!last) last = p; ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke(); last = p; }; const end = async (e) => { if (!drawing) return; e.preventDefault(); drawing = false; last = null; const dataUrl = canvas.toDataURL('image/png'); try { localStorage.setItem(keyFor(id), dataUrl); } catch (_) {} const ok = await saveToDb({ name: nameInput.value || null, signature: dataUrl, nameSet: true, sigSet: true }); if (ok) glow(pad); }; const clear = async () => { const yes = window.confirm('¿Deseas eliminar la firma de este bloque?'); if (!yes) return; ctx.clearRect(0, 0, canvas.width, canvas.height); try { localStorage.removeItem(keyFor(id)); } catch (_) {} await saveToDb({ signature: null, sigSet: true }); glow(pad); }; nameInput.value = localStorage.getItem(nameKeyFor(id)) || ''; let t = null; const saveName = async () => { const ok = await saveToDb({ name: nameInput.value || null, nameSet: true }); if (ok) glow(nameInput); }; nameInput.addEventListener('input', () => { try { localStorage.setItem(nameKeyFor(id), nameInput.value || ''); } catch (_) {} if (t) window.clearTimeout(t); t = window.setTimeout(() => saveName(), 3000); }); nameInput.addEventListener('blur', () => { if (t) window.clearTimeout(t); saveName(); }); btn.addEventListener('click', clear); canvas.addEventListener('mousedown', start); window.addEventListener('mousemove', move); window.addEventListener('mouseup', end); canvas.addEventListener('touchstart', start, { passive: false }); canvas.addEventListener('touchmove', move, { passive: false }); canvas.addEventListener('touchend', end, { passive: false }); canvas.addEventListener('touchcancel', end, { passive: false }); const ro = new ResizeObserver(() => resize()); ro.observe(canvas); resize(); (async () => { const ok = await restoreFromDb(); if (!ok) restoreFromLocal(); })(); }; ['seller', 'buyer', 'dev'].forEach(setupPad); const buildContractPrintPayload = (logoDataUrl = '') => { const titleEl = document.querySelector('.contract h2'); const bodyEl = document.querySelector('.contract .contract-body'); if (!titleEl || !bodyEl) return null; const titleText = (titleEl.textContent || '').trim(); const bodyClone = bodyEl.cloneNode(true); const sigBlock = bodyClone.querySelector('.signatures'); if (sigBlock) { const heading = sigBlock.previousElementSibling; if (heading && heading.tagName === 'H3' && /Firmado/i.test(heading.textContent || '')) { heading.remove(); } sigBlock.remove(); } const bodyHtml = bodyClone.innerHTML; const today = new Date(); const fecha = today.toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric', }); const roles = [ { id: 'seller', label: 'Empresa vendedora (proveedora)' }, { id: 'buyer', label: 'Empresa compradora (cliente)' }, { id: 'dev', label: 'Desarrollador (proveedor del servicio)' }, ]; const signatureCards = roles.map((role) => { const nameInput = document.querySelector(`[data-sig-name="${role.id}"]`); const name = (nameInput ? nameInput.value : '').trim() || 'Nombre y Apellido'; const pad = document.querySelector(`[data-sig-pad="${role.id}"]`); const canvas = pad ? pad.querySelector('canvas') : null; const sigUrl = canvas ? canvas.toDataURL('image/png') : ''; return ` <div class="sig-card"> <div class="sig-box"> ${sigUrl ? `<img alt="Firma" src="${sigUrl}" />` : '<div class="sig-empty">Firma pendiente</div>'} </div> <div class="sig-line"></div> <div class="sig-name">${name}</div> <div class="sig-role">${role.label}</div> </div>`; }).join(''); const styleText = ` *{box-sizing:border-box;font-family:Arial, Helvetica, sans-serif} body{margin:0;background:#fff;color:#000} .page, .page *{color:#000} .header, .header *{color:#fff} .page{width:210mm;min-height:297mm;margin:0;padding:25mm;background:#fff;color:#000} .header{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:10px;margin-bottom:30px;background:transparent;color:#0f172a} .brand{display:flex;align-items:center;gap:15px} .brand img{height:35px;width:auto;display:block} .brand .brand-title{font-weight:900;font-size:24px;letter-spacing:-0.5px;color:#0f172a} .header h1{margin:0;font-size:14px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#64748b;text-align:right} .header .meta-group{text-align:right} .header .meta{font-size:12px;color:#64748b;margin-top:0} .section-title{margin:30px 0 15px;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#22c55e;border-bottom:1px solid #e2e8f0;padding-bottom:8px} .rule{display:none} h2{font-size:22px;font-weight:800;margin:0 0 20px;color:#0f172a;text-align:left;letter-spacing:-0.5px} h3{font-size:15px;font-weight:700;margin:25px 0 10px;color:#0f172a;page-break-after:avoid} p{font-size:13px;line-height:1.8;color:#334155;text-align:justify;margin-bottom:12px;page-break-inside:avoid} li{font-size:13px;line-height:1.8;color:#334155;margin-bottom:6px;page-break-inside:avoid} ul{margin:10px 0 15px 20px;page-break-inside:avoid} .block{padding:0;border:none;background:transparent} .signatures{margin-top:60px;display:flex;justify-content:space-between;gap:40px;page-break-inside:avoid} .sig-card{border:none;background:transparent;text-align:center;flex:1} .sig-box{height:100px;display:flex;align-items:flex-end;justify-content:center;margin-bottom:10px} .sig-box img{max-height:120px;max-width:100%;object-fit:contain} .sig-line{height:1px;background:#0f172a;width:100%;margin:0 auto 10px} .sig-name{font-weight:700;font-size:14px;color:#0f172a;margin-bottom:4px;text-transform:capitalize} .sig-role{font-weight:400;font-size:12px;color:#64748b} .sig-empty{font-size:11px;color:#94a3b8;font-style:italic} @page{size:A4;margin:0} @media print{ .page{padding:0} .header{break-inside:avoid} .signatures{break-inside:avoid} } `; const pageHtml = ` <div class="page"> <header class="header"> <div class="brand"> ${logoDataUrl ? `<img src="${logoDataUrl}" alt="Aircan" />` : '<div class="brand-title">Aircan</div>'} </div> <div class="meta-group"> <div class="meta">Fecha: ${fecha}</div> </div> </header> <div class="rule"></div> <div class="block"> <h2>${titleText}</h2> ${bodyHtml} </div> <div class="signatures">${signatureCards}</div> </div>`; return { pageHtml, styleText }; }; const getLogoDataUrl = async () => { const img = document.querySelector('.brand img') || document.querySelector('img[alt="Aircan"]'); if (!img) return ''; if (!img.complete) { await new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; }); } try { const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return canvas.toDataURL('image/png'); } catch (_) { return ''; } }; const waitForImages = (root) => { const imgs = Array.from(root.querySelectorAll('img')); const pending = imgs.filter((img) => !img.complete); if (!pending.length) return Promise.resolve(); return Promise.all( pending.map((img) => new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; })) ); }; const downloadPdf = async () => { const logoDataUrl = await getLogoDataUrl(); const payload = buildContractPrintPayload(logoDataUrl); if (!payload) return; if (!window.html2pdf) { alert('No se pudo cargar el generador de PDF. Revisa tu conexión.'); return; } const pdfBtn = document.getElementById('downloadContractPdf'); const prevLabel = pdfBtn ? pdfBtn.textContent : ''; if (pdfBtn) { pdfBtn.disabled = true; pdfBtn.textContent = 'Generando PDF...'; } const tempStyle = document.createElement('style'); tempStyle.textContent = payload.styleText; document.head.appendChild(tempStyle); const layer = document.createElement('div'); layer.style.position = 'absolute'; layer.style.top = '0'; layer.style.left = '0'; layer.style.width = '210mm'; layer.style.background = '#fff'; layer.style.color = '#000'; layer.style.zIndex = '9999'; layer.style.display = 'flex'; layer.style.justifyContent = 'center'; layer.style.alignItems = 'flex-start'; layer.style.padding = '24px 0'; layer.innerHTML = payload.pageHtml; document.body.appendChild(layer); const page = layer.querySelector('.page'); if (!page) { if (layer.parentNode) layer.parentNode.removeChild(layer); if (tempStyle.parentNode) tempStyle.parentNode.removeChild(tempStyle); if (pdfBtn) { pdfBtn.disabled = false; pdfBtn.textContent = prevLabel; } return; } const cleanup = () => { if (layer && layer.parentNode) layer.parentNode.removeChild(layer); if (tempStyle && tempStyle.parentNode) tempStyle.parentNode.removeChild(tempStyle); if (pdfBtn) { pdfBtn.disabled = false; pdfBtn.textContent = prevLabel; } }; await waitForImages(layer); if (document.fonts && document.fonts.ready) { try { await document.fonts.ready; } catch (_) {} } await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); const opts = { margin: 0, filename: 'Contrato_Nomina_Informe.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, allowTaint: true, backgroundColor: '#ffffff', logging: false, scrollY: 0, scrollX: 0, windowWidth: 794, x: 0, y: 0, }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'], avoid: ['.header', '.signatures', 'h3', 'p', 'li', 'ul'] }, }; window.html2pdf() .set(opts) .from(page) .save() .then(cleanup) .catch(() => cleanup()); }; const pdfBtn = document.getElementById('downloadContractPdf'); if (pdfBtn) pdfBtn.addEventListener('click', downloadPdf); })(); </script> </body> </html>
Coded With 💗 by
0x6ick