{"id":9,"date":"2026-03-04T19:24:48","date_gmt":"2026-03-04T19:24:48","guid":{"rendered":"https:\/\/fichero.cavarouniformandwork.com\/?page_id=9"},"modified":"2026-03-08T19:00:49","modified_gmt":"2026-03-08T19:00:49","slug":"home","status":"publish","type":"page","link":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/home\/","title":{"rendered":"Fichero"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Fichaje \u00b7 Cavaro Uniform &#038; Work<\/title>\n  <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Bebas+Neue&#038;family=DM+Sans:wght@300;400;500;600&#038;display=swap\" rel=\"stylesheet\">\n  <style>\n    :root {\n      --navy: #03045E;\n      --navy-mid: #0a0f7a;\n      --navy-light: #1a2490;\n      --gold: #C9A84C;\n      --gold-light: #e8c96a;\n      --white: #f8f9ff;\n      --gray: #8b92b8;\n      --success: #22c55e;\n      --error: #ef4444;\n      --warning: #f59e0b;\n    }\n    * { margin: 0; padding: 0; box-sizing: border-box; }\n    body { font-family: 'DM Sans', sans-serif; background: var(--navy); color: var(--white); min-height: 100vh; overflow-x: hidden; }\n    .bg-grid { position: fixed; inset: 0; background-image: linear-gradient(rgba(201,168,76,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(201,168,76,0.04) 1px, transparent 1px); background-size: 40px 40px; z-index: 0; }\n    .bg-glow { position: fixed; width: 600px; height: 600px; border-radius: 50%; background: radial-gradient(circle, rgba(201,168,76,0.08) 0%, transparent 70%); top: -200px; right: -200px; z-index: 0; animation: pulse 6s ease-in-out infinite; }\n    .bg-glow-2 { position: fixed; width: 400px; height: 400px; border-radius: 50%; background: radial-gradient(circle, rgba(26,36,144,0.4) 0%, transparent 70%); bottom: -100px; left: -100px; z-index: 0; animation: pulse 8s ease-in-out infinite reverse; }\n    @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.7; } }\n    .container { position: relative; z-index: 1; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px 16px; }\n    .header { text-align: center; margin-bottom: 40px; animation: slideDown 0.6s ease forwards; }\n    @keyframes slideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }\n    .logo-wrap { display: inline-block; background: rgba(255,255,255,0.06); border: 1px solid rgba(201,168,76,0.2); border-radius: 16px; padding: 16px 32px; margin-bottom: 24px; backdrop-filter: blur(10px); }\n    .logo-wrap img { height: 48px; width: auto; display: block; }\n    .page-title { font-family: 'Bebas Neue', sans-serif; font-size: clamp(28px, 6vw, 42px); letter-spacing: 4px; color: var(--white); line-height: 1; }\n    .page-subtitle { font-size: 13px; color: var(--gray); letter-spacing: 2px; text-transform: uppercase; margin-top: 6px; }\n    .card { width: 100%; max-width: 420px; background: rgba(255,255,255,0.04); border: 1px solid rgba(201,168,76,0.15); border-radius: 24px; padding: 32px; backdrop-filter: blur(20px); animation: slideUp 0.6s ease 0.2s forwards; opacity: 0; }\n    @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }\n    .employee-banner { display: flex; align-items: center; gap: 14px; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.2); border-radius: 14px; padding: 14px 18px; margin-bottom: 28px; }\n    .employee-avatar { width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, var(--gold), var(--navy-light)); display: flex; align-items: center; justify-content: center; font-family: 'Bebas Neue', sans-serif; font-size: 20px; color: var(--white); flex-shrink: 0; }\n    .employee-info .label { font-size: 11px; color: var(--gray); text-transform: uppercase; letter-spacing: 1px; }\n    .employee-info .name { font-size: 16px; font-weight: 600; color: var(--white); }\n    .clock-section { text-align: center; margin-bottom: 28px; }\n    .clock { font-family: 'Bebas Neue', sans-serif; font-size: clamp(56px, 15vw, 80px); letter-spacing: 4px; color: var(--white); line-height: 1; text-shadow: 0 0 40px rgba(201,168,76,0.3); }\n    .clock span { color: var(--gold); animation: blink 1s step-end infinite; }\n    @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n    .date-display { font-size: 13px; color: var(--gray); letter-spacing: 1px; text-transform: uppercase; margin-top: 4px; }\n    .location-status { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-radius: 12px; margin-bottom: 28px; font-size: 13px; font-weight: 500; transition: all 0.3s ease; }\n    .location-status.checking { background: rgba(139,146,184,0.1); border: 1px solid rgba(139,146,184,0.2); color: var(--gray); }\n    .location-status.ok { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.2); color: var(--success); }\n    .location-status.warning { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.2); color: var(--warning); }\n    .location-status.error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); color: var(--error); }\n    .location-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; }\n    .location-dot.pulse { animation: dotPulse 1.5s ease-in-out infinite; }\n    @keyframes dotPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } }\n\n    \/* Alerta fuera del taller *\/\n    .fuera-taller-banner { display: none; align-items: center; gap: 12px; background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); border-radius: 14px; padding: 14px 18px; margin-bottom: 20px; }\n    .fuera-taller-banner.show { display: flex; animation: fadeIn 0.3s ease; }\n    .fuera-taller-banner .ft-icon { font-size: 24px; flex-shrink: 0; }\n    .fuera-taller-banner .ft-text { font-size: 12px; color: var(--warning); line-height: 1.5; }\n    .fuera-taller-banner .ft-dist { font-family: 'Bebas Neue', sans-serif; font-size: 22px; color: var(--warning); letter-spacing: 2px; }\n\n    .fichajes-section { margin-bottom: 28px; }\n    .section-label { font-size: 11px; color: var(--gray); text-transform: uppercase; letter-spacing: 2px; margin-bottom: 12px; }\n    .fichajes-list { display: flex; flex-direction: column; gap: 8px; }\n    .fichaje-item { display: flex; align-items: center; justify-content: space-between; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; padding: 10px 14px; font-size: 13px; animation: fadeIn 0.3s ease forwards; }\n    @keyframes fadeIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }\n    .fichaje-type { display: flex; align-items: center; gap: 8px; font-weight: 500; }\n    .fichaje-dot { width: 6px; height: 6px; border-radius: 50%; }\n    .fichaje-dot.entrada { background: var(--success); }\n    .fichaje-dot.salida { background: var(--gold); }\n    .fichaje-dot.alerta { background: var(--warning); }\n    .fichaje-hora { font-family: 'Bebas Neue', sans-serif; font-size: 16px; letter-spacing: 1px; color: var(--white); }\n    .fichaje-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; letter-spacing: 0.5px; }\n    .fichaje-badge.alerta { background: rgba(245,158,11,0.15); color: var(--warning); border: 1px solid rgba(245,158,11,0.3); }\n    .fichaje-badge.normal { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }\n    .no-fichajes { text-align: center; color: var(--gray); font-size: 13px; padding: 16px; background: rgba(255,255,255,0.02); border-radius: 10px; border: 1px dashed rgba(255,255,255,0.08); }\n    .divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(201,168,76,0.2), transparent); margin: 24px 0; }\n    .btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n    .btn { position: relative; padding: 18px 16px; border-radius: 14px; border: none; cursor: pointer; font-family: 'Bebas Neue', sans-serif; font-size: 18px; letter-spacing: 2px; transition: all 0.2s ease; overflow: hidden; display: flex; flex-direction: column; align-items: center; gap: 4px; }\n    .btn::before { content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.2s; }\n    .btn:hover::before { opacity: 1; }\n    .btn:active { transform: scale(0.97); }\n    .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }\n    .btn-entrada { background: linear-gradient(135deg, rgba(34,197,94,0.15), rgba(34,197,94,0.05)); border: 1px solid rgba(34,197,94,0.3); color: var(--success); }\n    .btn-entrada::before { background: rgba(34,197,94,0.1); }\n    .btn-salida { background: linear-gradient(135deg, rgba(201,168,76,0.15), rgba(201,168,76,0.05)); border: 1px solid rgba(201,168,76,0.3); color: var(--gold); }\n    .btn-salida::before { background: rgba(201,168,76,0.1); }\n    .btn-icon { font-size: 24px; line-height: 1; }\n    .btn-sub { font-family: 'DM Sans', sans-serif; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; opacity: 0.7; }\n\n    \/* Spinner en bot\u00f3n *\/\n    .btn-loading { opacity: 0.7; pointer-events: none; }\n    .spinner { display: inline-block; width: 18px; height: 18px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 0.7s linear infinite; }\n    @keyframes spin { to { transform: rotate(360deg); } }\n\n    .toast { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%) translateY(100px); background: var(--navy-light); border: 1px solid rgba(201,168,76,0.3); border-radius: 14px; padding: 16px 24px; display: flex; align-items: center; gap: 12px; font-size: 14px; font-weight: 500; z-index: 100; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); white-space: nowrap; backdrop-filter: blur(20px); box-shadow: 0 20px 60px rgba(0,0,0,0.4); }\n    .toast.show { transform: translateX(-50%) translateY(0); }\n    .toast.success { border-color: rgba(34,197,94,0.4); }\n    .toast.error { border-color: rgba(239,68,68,0.4); }\n    .toast.warning { border-color: rgba(245,158,11,0.4); }\n\n    \/* Overlay \u00e9xito \u2014 dentro del taller: permanente *\/\n    .success-overlay { display: none; position: fixed; inset: 0; background: rgba(3,4,94,0.97); z-index: 200; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 32px; backdrop-filter: blur(20px); }\n    .success-overlay.show { display: flex; animation: fadeInOverlay 0.4s ease; }\n    @keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }\n    .success-icon { font-size: 80px; margin-bottom: 24px; animation: bounceIn 0.6s ease 0.2s both; }\n    @keyframes bounceIn { 0% { transform: scale(0); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } }\n    .success-title { font-family: 'Bebas Neue', sans-serif; font-size: 48px; letter-spacing: 4px; color: var(--success); margin-bottom: 8px; }\n    .success-details { font-size: 15px; color: var(--gray); line-height: 1.6; }\n    .success-time { font-family: 'Bebas Neue', sans-serif; font-size: 64px; color: var(--white); letter-spacing: 4px; margin: 16px 0; }\n    .success-note { margin-top: 24px; font-size: 12px; color: rgba(139,146,184,0.6); letter-spacing: 1px; text-transform: uppercase; }\n\n    \/* Overlay alerta \u2014 fuera del taller: temporal *\/\n    .alert-overlay { display: none; position: fixed; inset: 0; background: rgba(3,4,94,0.95); z-index: 200; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 32px; backdrop-filter: blur(20px); }\n    .alert-overlay.show { display: flex; animation: fadeInOverlay 0.4s ease; }\n    .alert-icon { font-size: 80px; margin-bottom: 24px; animation: bounceIn 0.6s ease 0.2s both; }\n    .alert-title { font-family: 'Bebas Neue', sans-serif; font-size: 40px; letter-spacing: 4px; color: var(--warning); margin-bottom: 8px; }\n    .alert-details { font-size: 15px; color: var(--gray); line-height: 1.8; }\n    .alert-time { font-family: 'Bebas Neue', sans-serif; font-size: 52px; color: var(--white); letter-spacing: 4px; margin: 12px 0; }\n    .alert-note { margin-top: 20px; font-size: 12px; color: rgba(245,158,11,0.6); letter-spacing: 1px; text-transform: uppercase; border: 1px solid rgba(245,158,11,0.2); border-radius: 8px; padding: 10px 16px; }\n\n    \/* Overlay token inv\u00e1lido *\/\n    .expired-overlay { display: none; position: fixed; inset: 0; background: rgba(3,4,94,0.97); z-index: 200; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 32px; }\n    .expired-overlay.show { display: flex; }\n\n    .footer { margin-top: 32px; text-align: center; font-size: 11px; color: rgba(139,146,184,0.5); letter-spacing: 1px; animation: slideUp 0.6s ease 0.4s forwards; opacity: 0; }\n    @media (max-width: 380px) { .card { padding: 24px 20px; } .btn-group { grid-template-columns: 1fr; } }\n  <\/style>\n<\/head>\n<body>\n<div class=\"bg-grid\"><\/div>\n<div class=\"bg-glow\"><\/div>\n<div class=\"bg-glow-2\"><\/div>\n\n<!-- Token inv\u00e1lido o expirado -->\n<div class=\"expired-overlay\" id=\"expiredOverlay\">\n  <div style=\"font-size:72px; margin-bottom:24px;\">\u26d4<\/div>\n  <div style=\"font-family:'Bebas Neue',sans-serif; font-size:40px; letter-spacing:4px; color:var(--error); margin-bottom:12px;\">ENLACE NO V\u00c1LIDO<\/div>\n  <div style=\"color:var(--gray); font-size:15px; line-height:1.7;\">Este enlace ya ha sido utilizado<br>o ha expirado.<br><br>Contacta con tu responsable<br>si necesitas fichar.<\/div>\n<\/div>\n\n<!-- Fichaje OK dentro del taller \u2014 PERMANENTE -->\n<div class=\"success-overlay\" id=\"successOverlay\">\n  <div class=\"success-icon\" id=\"successIcon\">\u2705<\/div>\n  <div class=\"success-title\" id=\"successTitle\">FICHAJE REGISTRADO<\/div>\n  <div class=\"success-time\" id=\"successTime\"><\/div>\n  <div class=\"success-details\" id=\"successDetails\"><\/div>\n  <div class=\"success-note\">Enlace utilizado \u00b7 Puedes cerrar esta p\u00e1gina<\/div>\n<\/div>\n\n<!-- Fichaje registrado fuera del taller \u2014 TEMPORAL -->\n<div class=\"alert-overlay\" id=\"alertOverlay\">\n  <div class=\"alert-icon\">\u26a0\ufe0f<\/div>\n  <div class=\"alert-title\">FICHAJE FUERA DEL TALLER<\/div>\n  <div class=\"alert-time\" id=\"alertTime\"><\/div>\n  <div class=\"alert-details\" id=\"alertDetails\"><\/div>\n  <div class=\"alert-note\">\ud83d\udccd Ac\u00e9rcate al taller para fichar correctamente<\/div>\n<\/div>\n\n<div class=\"toast\" id=\"toast\">\n  <span id=\"toastIcon\">\u2713<\/span>\n  <span id=\"toastMsg\">Mensaje<\/span>\n<\/div>\n\n<div class=\"container\">\n  <div class=\"header\">\n    <div class=\"logo-wrap\">\n      <img decoding=\"async\" src=\"https:\/\/cavarouniformandwork.com\/wp-content\/uploads\/2024\/06\/aae09db4-4051-49d6-9dac-457cc13ed52e-1536x504.png\" alt=\"Cavaro Uniform &#038; Work\">\n    <\/div>\n    <div class=\"page-title\">Control de Presencia<\/div>\n    <div class=\"page-subtitle\">Sistema de Fichaje Digital<\/div>\n  <\/div>\n\n  <div class=\"card\">\n    <div class=\"employee-banner\">\n      <div class=\"employee-avatar\" id=\"employeeAvatar\">?<\/div>\n      <div class=\"employee-info\">\n        <div class=\"label\">Empleado identificado<\/div>\n        <div class=\"name\" id=\"employeeName\">Cargando&#8230;<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"clock-section\">\n      <div class=\"clock\" id=\"clock\">00<span>:<\/span>00<span>:<\/span>00<\/div>\n      <div class=\"date-display\" id=\"dateDisplay\"><\/div>\n    <\/div>\n\n    <div class=\"location-status checking\" id=\"locationStatus\">\n      <div class=\"location-dot pulse\" id=\"locationDot\"><\/div>\n      <span id=\"locationText\">Obteniendo ubicaci\u00f3n&#8230;<\/span>\n    <\/div>\n\n    <!-- Banner fuera del taller -->\n    <div class=\"fuera-taller-banner\" id=\"fueraTallerBanner\">\n      <div class=\"ft-icon\">\ud83d\udccd<\/div>\n      <div class=\"ft-text\">\n        Est\u00e1s a <span class=\"ft-dist\" id=\"fueraDist\">&#8212;<\/span> del taller.<br>\n        El fichaje quedar\u00e1 registrado como <strong style=\"color:var(--warning)\">ALERTA<\/strong>.<br>\n        <span style=\"font-size:11px; opacity:0.7;\">El enlace seguir\u00e1 activo hasta que est\u00e9s a menos de 5m.<\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"fichajes-section\">\n      <div class=\"section-label\">\ud83d\udccb Fichajes de hoy<\/div>\n      <div class=\"fichajes-list\" id=\"fichajesList\">\n        <div class=\"no-fichajes\">Sin fichajes registrados hoy<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"divider\"><\/div>\n\n    <div class=\"btn-group\">\n      <button class=\"btn btn-entrada\" id=\"btnEntrada\" onclick=\"fichar('entrada')\">\n        <span class=\"btn-icon\">\ud83d\udfe2<\/span>\n        ENTRADA\n        <span class=\"btn-sub\">Inicio jornada<\/span>\n      <\/button>\n      <button class=\"btn btn-salida\" id=\"btnSalida\" onclick=\"fichar('salida')\">\n        <span class=\"btn-icon\">\ud83d\udfe1<\/span>\n        SALIDA\n        <span class=\"btn-sub\">Fin jornada<\/span>\n      <\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"footer\">\n    CAVARO UNIFORM &#038; WORK \u00b7 SISTEMA DE FICHAJE SEGURO<br>\n    Ubicaci\u00f3n requerida para validar el fichaje\n  <\/div>\n<\/div>\n\n<script>\n  const WEBHOOK_URL = 'https:\/\/icem.app.n8n.cloud\/webhook\/fichaje-recibido';\n  const TALLER_LAT = 40.32157049860786;\n  const TALLER_LNG = -3.6844585459536074;\n  const MAX_DISTANCIA_METROS = 200;   \/\/ radio para estado \"Normal\"\n  const MIN_DISTANCIA_EXPIRA = 5;     \/\/ radio para expirar el token (fichaje definitivo)\n\n  let locationData = null;\n  let distanciaMetros = null;\n  let fichajesHoy = [];\n  let gpsInterval = null;\n  let tokenExpirado = false;\n\n  const params = new URLSearchParams(window.location.search);\n  const token = params.get('token');\n  const empleadoNombre = params.get('nombre') || 'Empleado';\n\n  if (!token) {\n    document.getElementById('expiredOverlay').classList.add('show');\n  }\n\n  const nombre = decodeURIComponent(empleadoNombre);\n  document.getElementById('employeeName').textContent = nombre;\n  document.getElementById('employeeAvatar').textContent = nombre.charAt(0).toUpperCase();\n\n  \/\/ Reloj\n  function updateClock() {\n    const now = new Date();\n    const h = String(now.getHours()).padStart(2, '0');\n    const m = String(now.getMinutes()).padStart(2, '0');\n    const s = String(now.getSeconds()).padStart(2, '0');\n    document.getElementById('clock').innerHTML = `${h}<span>:<\/span>${m}<span>:<\/span>${s}`;\n    const dias = ['Domingo','Lunes','Martes','Mi\u00e9rcoles','Jueves','Viernes','S\u00e1bado'];\n    const meses = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];\n    document.getElementById('dateDisplay').textContent = `${dias[now.getDay()]}, ${now.getDate()} de ${meses[now.getMonth()]} de ${now.getFullYear()}`;\n  }\n  updateClock();\n  setInterval(updateClock, 1000);\n\n  \/\/ Distancia\n  function calcularDistancia(lat1, lng1, lat2, lng2) {\n    const R = 6371000;\n    const dLat = (lat2 - lat1) * Math.PI \/ 180;\n    const dLng = (lng2 - lng1) * Math.PI \/ 180;\n    const a = Math.sin(dLat\/2)**2 + Math.cos(lat1*Math.PI\/180) * Math.cos(lat2*Math.PI\/180) * Math.sin(dLng\/2)**2;\n    return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n  }\n\n  function setLocationStatus(type, text) {\n    const el = document.getElementById('locationStatus');\n    const dot = document.getElementById('locationDot');\n    el.className = `location-status ${type}`;\n    dot.className = `location-dot${type === 'checking' ? ' pulse' : ''}`;\n    document.getElementById('locationText').textContent = text;\n  }\n\n  function actualizarUbicacion() {\n    if (!navigator.geolocation) {\n      setLocationStatus('error', '\ud83d\udccd Geolocalizaci\u00f3n no disponible en este dispositivo');\n      return;\n    }\n    navigator.geolocation.getCurrentPosition(\n      (pos) => {\n        locationData = { lat: pos.coords.latitude, lng: pos.coords.longitude, precision: pos.coords.accuracy };\n        distanciaMetros = calcularDistancia(locationData.lat, locationData.lng, TALLER_LAT, TALLER_LNG);\n\n        const banner = document.getElementById('fueraTallerBanner');\n\n        if (distanciaMetros <= MAX_DISTANCIA_METROS) {\n          setLocationStatus('ok', `\ud83d\udccd Ubicaci\u00f3n verificada \u00b7 ${Math.round(distanciaMetros)}m del taller`);\n          banner.classList.remove('show');\n        } else {\n          setLocationStatus('warning', `\u26a0\ufe0f Fuera del taller \u00b7 ${Math.round(distanciaMetros)}m de distancia`);\n          document.getElementById('fueraDist').textContent = `${Math.round(distanciaMetros)}m`;\n          banner.classList.add('show');\n        }\n      },\n      (err) => {\n        setLocationStatus('error', '\u274c No se pudo obtener la ubicaci\u00f3n. Activa el GPS.');\n      },\n      { enableHighAccuracy: true, timeout: 10000 }\n    );\n  }\n\n  \/\/ GPS inicial + refresco cada 10 segundos\n  actualizarUbicacion();\n  gpsInterval = setInterval(actualizarUbicacion, 10000);\n\n  function renderFichajes() {\n    const lista = document.getElementById('fichajesList');\n    if (fichajesHoy.length === 0) {\n      lista.innerHTML = '<div class=\"no-fichajes\">Sin fichajes registrados hoy<\/div>';\n      return;\n    }\n    lista.innerHTML = fichajesHoy.map(f => `\n      <div class=\"fichaje-item\">\n        <div class=\"fichaje-type\">\n          <div class=\"fichaje-dot ${f.alerta ? 'alerta' : f.tipo}\"><\/div>\n          ${f.tipo === 'entrada' ? '\ud83d\udfe2 Entrada' : '\ud83d\udfe1 Salida'}\n        <\/div>\n        <div style=\"display:flex; align-items:center; gap:8px;\">\n          <span class=\"fichaje-badge ${f.alerta ? 'alerta' : 'normal'}\">${f.alerta ? 'ALERTA' : 'OK'}<\/span>\n          <div class=\"fichaje-hora\">${f.hora}<\/div>\n        <\/div>\n      <\/div>\n    `).join('');\n  }\n\n  function setBtnLoading(loading) {\n    const btnE = document.getElementById('btnEntrada');\n    const btnS = document.getElementById('btnSalida');\n    if (loading) {\n      btnE.disabled = true;\n      btnS.disabled = true;\n      btnE.innerHTML = `<span class=\"spinner\"><\/span>`;\n      btnS.innerHTML = `<span class=\"spinner\"><\/span>`;\n    } else {\n      btnE.disabled = false;\n      btnS.disabled = false;\n      btnE.innerHTML = `<span class=\"btn-icon\">\ud83d\udfe2<\/span>ENTRADA<span class=\"btn-sub\">Inicio jornada<\/span>`;\n      btnS.innerHTML = `<span class=\"btn-icon\">\ud83d\udfe1<\/span>SALIDA<span class=\"btn-sub\">Fin jornada<\/span>`;\n    }\n  }\n\n  async function fichar(tipo) {\n    if (tokenExpirado) return;\n\n    if (!locationData) {\n      showToast('error', '\u274c Esperando ubicaci\u00f3n GPS...');\n      return;\n    }\n\n    setBtnLoading(true);\n\n    const ahora = new Date();\n    const horaActual = `${String(ahora.getHours()).padStart(2,'0')}:${String(ahora.getMinutes()).padStart(2,'0')}`;\n    const fechaActual = ahora.toLocaleDateString('es-ES');\n    const dentroTaller = distanciaMetros <= MAX_DISTANCIA_METROS;\n    const muycerca = distanciaMetros <= MIN_DISTANCIA_EXPIRA;\n    const estado = dentroTaller ? 'Normal' : 'Alerta';\n\n    try {\n      const response = await fetch(WEBHOOK_URL, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        body: JSON.stringify({\n          token: token,\n          empleado: nombre,\n          tipo: tipo,\n          hora: horaActual,\n          fecha: fechaActual,\n          timestamp: ahora.toISOString(),\n          lat: locationData.lat,\n          lng: locationData.lng,\n          precision_gps: locationData.precision,\n          distancia_taller: Math.round(distanciaMetros),\n          dentro_taller: dentroTaller,\n          estado: estado\n        })\n      });\n\n      fichajesHoy.push({ tipo, hora: horaActual, alerta: !dentroTaller });\n      renderFichajes();\n\n      if (dentroTaller) {\n        \/\/ \u2705 DENTRO DEL TALLER \u2014 overlay permanente, token expira\n        clearInterval(gpsInterval); \/\/ para el GPS\n        tokenExpirado = true;\n\n        const overlay = document.getElementById('successOverlay');\n        document.getElementById('successIcon').textContent = tipo === 'entrada' ? '\u2705' : '\ud83d\udc4b';\n        document.getElementById('successTitle').textContent = tipo === 'entrada' ? 'ENTRADA REGISTRADA' : 'SALIDA REGISTRADA';\n        document.getElementById('successTime').textContent = horaActual;\n        document.getElementById('successDetails').innerHTML = `\n          <strong>${nombre}<\/strong><br>\n          ${fechaActual}<br><br>\n          <span style=\"color:var(--success)\">\ud83d\udccd Ubicaci\u00f3n verificada \u00b7 ${Math.round(distanciaMetros)}m del taller<\/span>\n        `;\n        overlay.classList.add('show');\n        \/\/ No se cierra \u2014 permanente\n\n      } else {\n        \/\/ \u26a0\ufe0f FUERA DEL TALLER \u2014 overlay temporal 4s, enlace sigue activo\n        setBtnLoading(false);\n\n        const overlay = document.getElementById('alertOverlay');\n        document.getElementById('alertTime').textContent = horaActual;\n        document.getElementById('alertDetails').innerHTML = `\n          <strong>${nombre}<\/strong><br>\n          ${fechaActual}<br><br>\n          <span style=\"color:var(--warning)\">\u26a0\ufe0f Registrado a ${Math.round(distanciaMetros)}m del taller<\/span><br>\n          <span style=\"font-size:13px; color:var(--gray)\">Estado: ALERTA \u00b7 Fichaje guardado<\/span>\n        `;\n        overlay.classList.add('show');\n\n        setTimeout(() => {\n          overlay.classList.remove('show');\n        }, 4000);\n      }\n\n    } catch (err) {\n      setBtnLoading(false);\n      showToast('error', '\u274c Error de conexi\u00f3n. Intenta de nuevo.');\n    }\n  }\n\n  function showToast(type, msg) {\n    const toast = document.getElementById('toast');\n    document.getElementById('toastMsg').textContent = msg;\n    toast.className = `toast ${type} show`;\n    setTimeout(() => toast.classList.remove('show'), 3500);\n  }\n<\/script>\n<\/body>\n<\/html>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Fichaje \u00b7 Cavaro Uniform &#038; Work \u26d4 ENLACE NO V\u00c1LIDO Este enlace ya ha sido utilizadoo ha expirado. Contacta con tu responsablesi necesitas fichar. \u2705 FICHAJE REGISTRADO Enlace utilizado \u00b7 Puedes cerrar esta p\u00e1gina \u26a0\ufe0f FICHAJE FUERA DEL TALLER \ud83d\udccd Ac\u00e9rcate al taller para fichar correctamente \u2713 Mensaje Control de Presencia Sistema de Fichaje Digital [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-9","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/pages\/9","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/comments?post=9"}],"version-history":[{"count":11,"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/pages\/9\/revisions"}],"predecessor-version":[{"id":26,"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/pages\/9\/revisions\/26"}],"wp:attachment":[{"href":"https:\/\/fichero.cavarouniformandwork.com\/index.php\/wp-json\/wp\/v2\/media?parent=9"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}