Files
Portal/crear-cita.html
2026-03-16 22:32:40 +00:00

425 lines
25 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Portal del Cliente - IntegraRepara</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Fondo Marsalva Style: Círculos perfectos */
body {
background-color: #f8fafc;
min-height: 100vh;
background-attachment: fixed;
}
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* Ocultar scrollbar */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Efecto Cristal más compacto */
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 2.5rem;
box-shadow: 0 20px 40px -12px rgba(203, 213, 225, 0.8);
border: 1px solid white;
}
.blob { position: absolute; filter: blur(60px); z-index: -1; opacity: 0.5; }
</style>
</head>
<body class="text-slate-800 font-sans antialiased py-4 px-4 relative flex flex-col items-center min-h-screen justify-center">
<div class="blob bg-blue-300 w-80 h-80 rounded-full top-[-100px] left-[-100px]"></div>
<div class="blob bg-emerald-200 w-80 h-80 rounded-full top-[20%] right-[-150px]"></div>
<div class="flex flex-col items-center justify-center w-full mb-6 z-10 relative fade-in">
<div id="logoContainer" class="w-32 h-32 shrink-0 overflow-hidden bg-white rounded-[2rem] shadow-xl shadow-slate-200 flex items-center justify-center p-4 mb-4 border border-white mx-auto">
<i data-lucide="image" class="text-slate-200 w-10 h-10 absolute" id="defaultLogo"></i>
<img id="companyLogoImg" src="" class="hidden max-w-full max-h-full object-contain relative z-10">
</div>
<p class="text-[9px] font-black text-blue-500 uppercase tracking-[0.2em] mb-1">Portal del Cliente</p>
<h1 id="companyNameDisplay" class="text-xl font-black text-slate-800 tracking-tight text-center uppercase">
Cargando...
</h1>
</div>
<div id="welcomeBackBox" class="w-full max-w-md hidden mb-3 fade-in text-center px-2 z-10">
<h2 class="text-3xl font-black text-slate-800 tracking-tight italic">Hola, <span id="clientFirstName" class="text-blue-600 not-italic"></span></h2>
</div>
<div class="w-full max-w-md glass-card relative overflow-hidden transition-all duration-500 z-10">
<div class="p-5 md:p-6">
<div id="step1" class="fade-in space-y-4">
<div class="text-center">
<h3 class="text-lg font-extrabold text-slate-800 uppercase tracking-tight">Bienvenido</h3>
<p class="text-[10px] text-slate-500 font-bold uppercase tracking-widest mt-0.5">Introduce tu móvil para empezar</p>
</div>
<div>
<label class="text-[9px] font-bold text-slate-400 uppercase tracking-widest ml-1">Teléfono Móvil</label>
<input type="tel" id="cliPhone" placeholder="600 000 000" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-3 rounded-2xl text-xl text-center tracking-[0.1em] font-black focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/10 outline-none transition-all">
</div>
<button type="button" onclick="requestOTP()" id="btnStep1" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-black py-3.5 rounded-2xl shadow-xl shadow-blue-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-widest text-[11px]">
<span>Recibir Código</span> <i data-lucide="message-circle" class="w-4 h-4"></i>
</button>
</div>
<div id="step2" class="fade-in space-y-4 hidden">
<div class="text-center">
<div class="w-12 h-12 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center mx-auto mb-2">
<i data-lucide="lock" class="w-6 h-6"></i>
</div>
<h3 class="text-lg font-extrabold text-slate-800 mb-1 uppercase tracking-tight">Verificación</h3>
<p class="text-[10px] text-slate-500 font-bold px-4 uppercase tracking-widest">Código enviado al <strong id="displayPhone" class="text-blue-600"></strong></p>
</div>
<div>
<input type="number" id="cliCode" placeholder="0 0 0 0" maxlength="4" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-3.5 rounded-2xl text-3xl text-center tracking-[0.4em] font-black focus:border-blue-500 focus:bg-white outline-none transition-all">
</div>
<button type="button" onclick="verifyOTP()" id="btnStep2" class="w-full bg-slate-800 hover:bg-slate-900 text-white font-black py-3.5 rounded-2xl shadow-xl shadow-slate-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-widest text-[11px]">
<span>Verificar Acceso</span> <i data-lucide="shield-check" class="w-4 h-4"></i>
</button>
<button type="button" onclick="goToStep(1)" class="w-full text-slate-400 text-[9px] font-bold uppercase tracking-[0.2em] mt-3 hover:text-slate-600 transition-colors">Cambiar número</button>
</div>
<form id="step3" class="fade-in space-y-3.5 hidden" onsubmit="submitFinalRequest(event)">
<div id="boxName">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Tu Nombre Completo *</label>
<input type="text" id="cliName" placeholder="Ej: Juan García" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-2.5 rounded-xl text-sm font-bold focus:border-blue-500 focus:bg-white outline-none transition-all">
</div>
<div id="boxAddress">
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Dirección de la avería *</label>
<select id="cliAddressSelect" onchange="toggleNewAddressInput()" class="hidden w-full mt-1 mb-2 bg-blue-50 border-2 border-blue-100 px-4 py-2.5 rounded-xl text-sm font-black outline-none cursor-pointer text-blue-700"></select>
<input type="text" id="cliAddressInput" placeholder="Calle, Número, Piso..." class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-2.5 rounded-xl text-sm font-bold focus:border-blue-500 focus:bg-white outline-none transition-all">
</div>
<div>
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Especialidad necesaria *</label>
<div class="relative mt-1">
<select id="cliGuild" required class="w-full bg-slate-50 border-2 border-slate-100 px-4 py-2.5 rounded-xl text-sm font-bold text-slate-700 focus:border-blue-500 focus:bg-white outline-none appearance-none cursor-pointer">
<option value="">Cargando especialistas...</option>
</select>
<div class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400">
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</div>
</div>
</div>
<div>
<label class="text-[9px] font-black text-slate-500 uppercase tracking-widest ml-1">Describe el problema *</label>
<textarea id="cliDesc" required rows="2" placeholder="Explícanos brevemente qué ocurre..." class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-2.5 rounded-xl text-sm font-medium focus:border-blue-500 focus:bg-white outline-none resize-none transition-all"></textarea>
</div>
<div class="bg-red-50/50 border-2 border-red-100 rounded-2xl p-3.5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-black text-red-700 text-[13px] flex items-center gap-1.5 uppercase tracking-tight">
<i data-lucide="flame" class="w-4 h-4"></i> ¿Es una Urgencia?
</span>
<span class="text-[8px] font-bold text-red-400 uppercase mt-0.5 tracking-widest">Asistencia Inmediata</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="cliUrgent" onchange="handleUrgency(this)" class="sr-only peer">
<div class="w-12 h-7 bg-slate-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-red-100 peer-checked:bg-red-600 transition-all duration-300"></div>
<div class="absolute left-1 top-1 w-5 h-5 bg-white rounded-full transition-all duration-300 peer-checked:translate-x-5 shadow-sm border border-slate-200"></div>
</label>
</div>
</div>
<button type="submit" id="btnSubmitFinal" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-black py-3.5 rounded-2xl shadow-xl shadow-blue-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-widest text-[11px] mt-2">
<span>Solicitar Técnico Ahora</span> <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</button>
<button type="button" onclick="logoutClient()" class="w-full text-slate-300 text-[9px] font-black uppercase tracking-[0.2em] mt-3 hover:text-slate-500 transition-colors">
Cerrar sesión / Cambiar móvil
</button>
</form>
</div>
</div>
<div id="urgentModal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-md z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-3xl p-6 md:p-8 max-w-sm w-full shadow-2xl text-center fade-in border border-white">
<div class="w-16 h-16 bg-red-100 text-red-600 rounded-full flex items-center justify-center mx-auto mb-4 animate-pulse">
<i data-lucide="alert-circle" class="w-8 h-8"></i>
</div>
<h3 class="text-xl font-black tracking-tight text-slate-800 mb-2 uppercase">Servicio Urgente</h3>
<p class="text-[11px] font-bold tracking-widest text-slate-500 mb-6 leading-relaxed uppercase">
No requieren cita previa. <br><br>
<span class="text-red-500">Conlleva un coste adicional de tarifa de urgencia.</span>
</p>
<div class="flex flex-col gap-2.5">
<button type="button" onclick="acceptUrgency()" class="w-full bg-red-600 hover:bg-red-700 text-white font-black py-3.5 rounded-xl shadow-lg shadow-red-200 transition-all text-[11px] uppercase tracking-widest">Acepto el cargo</button>
<button type="button" onclick="cancelUrgency()" class="w-full bg-slate-100 hover:bg-slate-200 text-slate-500 font-bold py-3.5 rounded-xl transition-all text-[11px] uppercase tracking-widest">Cancelar</button>
</div>
</div>
</div>
<div id="successScreen" class="fixed inset-0 bg-white z-[100] hidden flex-col items-center justify-center p-8 text-center fade-in">
<div class="w-24 h-24 bg-emerald-50 text-emerald-500 rounded-3xl flex items-center justify-center mb-6 shadow-2xl shadow-emerald-100">
<i data-lucide="check-circle" class="w-12 h-12"></i>
</div>
<h2 class="text-3xl font-black text-slate-800 mb-3 tracking-tight uppercase">¡Enviada!</h2>
<p class="text-slate-400 font-bold uppercase tracking-widest text-[10px] max-w-xs leading-relaxed">Hemos recibido tu aviso. El técnico te escribirá de inmediato por WhatsApp.</p>
<button onclick="location.reload()" class="mt-8 text-blue-600 font-black uppercase tracking-widest text-[10px]">Nueva solicitud</button>
</div>
<div id="loader" class="fixed inset-0 bg-slate-50/90 backdrop-blur-md z-50 flex flex-col items-center justify-center transition-opacity duration-300">
<div class="relative w-12 h-12 flex items-center justify-center mb-3">
<div class="absolute inset-0 border-4 border-blue-100 border-t-blue-600 rounded-full animate-spin"></div>
<i data-lucide="wrench" class="w-5 h-5 text-blue-600 animate-pulse"></i>
</div>
<p class="text-[9px] font-black tracking-widest uppercase text-slate-500 animate-pulse">Conectando...</p>
</div>
<script>
lucide.createIcons();
// 🔧 CONFIGURACIÓN SAAS ESTRICTA
const API_URL = 'https://integrarepara-api.integrarepara.es';
const urlParams = new URLSearchParams(window.location.search);
// 🚨 CAMBIO 1: Quitamos el "|| 1". Declaramos con 'let' para rescatarlo de la sesión si hace falta.
let OWNER_ID = parseInt(urlParams.get('c'));
let currentClient = null;
// 🔄 INICIO: CARGAR DATOS Y SESIÓN
document.addEventListener("DOMContentLoaded", () => {
const sesionGuardada = localStorage.getItem('clienteSesion');
let datosSesion = null;
// 1. Revisamos si hay sesión y si está caducada
if (sesionGuardada) {
try {
datosSesion = JSON.parse(sesionGuardada);
if (Date.now() < datosSesion.expires) {
// 🛡️ ESCUDO 1: Si el cliente borró el '?c=ID' de la URL pero tiene sesión guardada, lo rescatamos
if (!OWNER_ID && datosSesion.owner_id) {
OWNER_ID = datosSesion.owner_id;
}
} else {
localStorage.removeItem('clienteSesion');
datosSesion = null;
}
} catch (e) {
localStorage.removeItem('clienteSesion');
datosSesion = null;
}
}
// 🛑 ESCUDO 2: BLOQUEO TOTAL SI NO HAY EMPRESA (Ni en URL ni en sesión)
if (!OWNER_ID || isNaN(OWNER_ID)) {
// Borramos todo y mostramos el mensaje de error hermético
document.body.innerHTML = `
<div class="min-h-screen w-full flex flex-col items-center justify-center p-8 text-center bg-slate-50">
<div class="w-24 h-24 bg-red-100 text-red-600 rounded-[2rem] flex items-center justify-center mb-6 shadow-xl shadow-red-100">
<i data-lucide="shield-alert" class="w-12 h-12"></i>
</div>
<h2 class="text-3xl font-black text-slate-800 mb-3 uppercase tracking-tighter">Acceso Denegado</h2>
<p class="text-slate-500 font-bold text-[10px] uppercase tracking-widest leading-relaxed">Falta la clave de la empresa.<br>Utiliza el enlace exacto que te proporcionó el servicio técnico.</p>
</div>`;
lucide.createIcons();
return; // ⛔ Cortamos la ejecución aquí. No hace NADA más.
}
// 2. Si pasamos el escudo, cargamos los datos con total seguridad
loadCompanyData();
// 3. Si teníamos sesión activa, pre-cargamos el paso 3 (Como lo tenías tú)
if (datosSesion) {
document.getElementById('cliPhone').value = datosSesion.phone;
prepareStep3(datosSesion.client);
goToStep(3);
}
});
// 🏢 DATOS DE EMPRESA Y GREMIOS
async function loadCompanyData() {
try {
const res = await fetch(`${API_URL}/public/company/${OWNER_ID}/guilds`);
const data = await res.json();
if (data.ok) {
document.getElementById('companyNameDisplay').innerText = data.name || "IntegraRepara";
if (data.logo) {
document.getElementById('defaultLogo').classList.add('hidden');
const img = document.getElementById('companyLogoImg');
img.src = data.logo;
img.classList.remove('hidden');
}
const selectGremio = document.getElementById('cliGuild');
selectGremio.innerHTML = '<option value="">Selecciona una especialidad...</option>';
if (data.guilds && data.guilds.length > 0) {
data.guilds.forEach(g => {
selectGremio.innerHTML += `<option value="${g.id}">${g.name}</option>`;
});
} else {
selectGremio.innerHTML += `<option value="0">Servicio General</option>`;
}
}
} catch (e) {
console.error("Error cargando empresa", e);
document.getElementById('companyNameDisplay').innerText = "Servicio Técnico";
}
}
// 🚪 SALIR
function logoutClient() {
localStorage.removeItem('clienteSesion');
document.getElementById('cliPhone').value = "";
document.getElementById('cliCode').value = "";
document.getElementById('welcomeBackBox').classList.add('hidden');
goToStep(1);
}
// 🚀 NAVEGACIÓN
function goToStep(step) {
document.getElementById('step1').classList.add('hidden');
document.getElementById('step2').classList.add('hidden');
document.getElementById('step3').classList.add('hidden');
if (step === 1) document.getElementById('step1').classList.remove('hidden');
else if (step === 2) document.getElementById('step2').classList.remove('hidden');
else if (step === 3) document.getElementById('step3').classList.remove('hidden');
lucide.createIcons();
}
// 📲 PEDIR OTP
async function requestOTP() {
const phone = document.getElementById('cliPhone').value;
if (phone.length < 9) return alert("Móvil no válido");
const btn = document.getElementById('btnStep1');
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>';
lucide.createIcons();
try {
const res = await fetch(`${API_URL}/public/auth/request-otp`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, owner_id: OWNER_ID })
});
if (res.ok) {
document.getElementById('displayPhone').innerText = phone;
goToStep(2);
}
} catch (e) { alert("Error"); }
finally { btn.disabled = false; btn.innerHTML = '<span>Recibir Código</span>'; lucide.createIcons(); }
}
// 🔑 VERIFICAR OTP
async function verifyOTP() {
const phone = document.getElementById('cliPhone').value;
const code = document.getElementById('cliCode').value;
const btn = document.getElementById('btnStep2');
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>';
lucide.createIcons();
try {
const res = await fetch(`${API_URL}/public/auth/verify-otp`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code, owner_id: OWNER_ID })
});
const data = await res.json();
if (data.ok) {
const caducidad = Date.now() + (10 * 24 * 60 * 60 * 1000);
// 🚨 CAMBIO 2: Guardamos 'owner_id' en la sesión para que funcione el Escudo 1
localStorage.setItem('clienteSesion', JSON.stringify({
phone: phone, client: data.exists ? data.client : null, expires: caducidad, owner_id: OWNER_ID
}));
prepareStep3(data.exists ? data.client : null);
goToStep(3);
} else { alert("Código incorrecto"); }
} catch (e) { alert("Error"); }
finally { btn.disabled = false; btn.innerHTML = '<span>Verificar</span>'; lucide.createIcons(); }
}
// 📝 PASO FINAL
function prepareStep3(clientData) {
currentClient = clientData;
const welcomeBox = document.getElementById('welcomeBackBox');
if (clientData && clientData.full_name) {
welcomeBox.classList.remove('hidden');
document.getElementById('clientFirstName').innerText = clientData.full_name.split(' ')[0].toUpperCase();
document.getElementById('boxName').classList.add('hidden');
document.getElementById('cliName').value = clientData.full_name;
let addrs = [];
try { addrs = typeof clientData.addresses === 'string' ? JSON.parse(clientData.addresses) : clientData.addresses; } catch(e) {}
if (addrs && addrs.length > 0) {
const selectAddr = document.getElementById('cliAddressSelect');
selectAddr.classList.remove('hidden');
document.getElementById('cliAddressInput').classList.add('hidden');
selectAddr.innerHTML = addrs.map(a => `<option value="${a}">${a}</option>`).join('') + `<option value="NEW"> Nueva dirección...</option>`;
document.getElementById('cliAddressInput').value = addrs[0];
}
} else {
welcomeBox.classList.add('hidden');
document.getElementById('boxName').classList.remove('hidden');
}
}
function toggleNewAddressInput() {
const sel = document.getElementById('cliAddressSelect');
const inp = document.getElementById('cliAddressInput');
if (sel.value === "NEW") {
inp.classList.remove('hidden'); inp.value = ""; inp.focus();
} else {
inp.classList.add('hidden'); inp.value = sel.value;
}
}
// 🚨 URGENCIA
function handleUrgency(cb) { if (cb.checked) document.getElementById('urgentModal').classList.remove('hidden'); }
function cancelUrgency() { document.getElementById('cliUrgent').checked = false; document.getElementById('urgentModal').classList.add('hidden'); }
function acceptUrgency() { document.getElementById('urgentModal').classList.add('hidden'); }
// 📤 ENVIAR
async function submitFinalRequest(e) {
e.preventDefault();
const btn = document.getElementById('btnSubmitFinal');
btn.disabled = true; btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>'; lucide.createIcons();
let finalAddress = document.getElementById('cliAddressInput').value;
const payload = {
phone: document.getElementById('cliPhone').value,
name: document.getElementById('cliName').value,
address: finalAddress,
guild_id: document.getElementById('cliGuild').value,
description: document.getElementById('cliDesc').value,
is_urgent: document.getElementById('cliUrgent').checked,
owner_id: OWNER_ID
};
try {
const res = await fetch(`${API_URL}/public/new-request`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.ok) {
if (data.action === 'queued') {
document.getElementById('successScreen').classList.remove('hidden');
document.getElementById('successScreen').classList.add('flex');
} else if (data.action === 'calendar') {
window.location.href = data.redirectUrl;
}
} else { alert("Error al crear la solicitud"); }
} catch (err) { alert('Error de conexión'); }
finally { btn.disabled = false; btn.innerHTML = '<span>Solicitar Técnico Ahora</span>'; lucide.createIcons(); }
}
// Quitar loader inicial rápido si no hay token que cargar
setTimeout(() => {
document.getElementById('loader').classList.add('opacity-0', 'pointer-events-none');
setTimeout(() => document.getElementById('loader').classList.add('hidden'), 300);
}, 800);
</script>
</body>
</html>