Files
Portal/crear-cita.html
2026-03-14 21:40:32 +00:00

421 lines
23 KiB
HTML
Raw 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 de puntitos como en la imagen */
body {
background-color: #f8fafc;
background-image: radial-gradient(#cbd5e1 1.5px, transparent 1.5px);
background-size: 24px 24px;
}
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* Toggle Switch Custom */
.toggle-checkbox:checked { right: 0; border-color: #ef4444; }
.toggle-checkbox:checked + .toggle-label { background-color: #ef4444; }
.toggle-checkbox { right: 0; z-index: 1; border-color: #e2e8f0; transition: all 0.3s; }
.toggle-label { background-color: #e2e8f0; transition: all 0.3s; }
</style>
</head>
<body class="text-slate-800 font-sans antialiased min-h-screen flex flex-col items-center py-10 px-4 relative">
<div class="flex flex-col items-center justify-center w-full mb-8 z-10 relative fade-in">
<div id="logoContainer" class="w-28 h-28 bg-white rounded-3xl shadow-xl shadow-slate-200/50 flex items-center justify-center p-3 mb-6 border border-slate-50">
<i data-lucide="image" class="text-slate-300 w-10 h-10" id="defaultLogo"></i>
<img id="companyLogoImg" src="" class="hidden w-full h-full object-contain rounded-2xl">
</div>
<p class="text-[10px] font-black text-blue-500 uppercase tracking-widest mb-1">Portal del Cliente</p>
<h1 id="companyNameDisplay" class="text-2xl font-black text-slate-800 tracking-tight text-center uppercase">
Cargando...
</h1>
</div>
<div id="welcomeBackBox" class="w-full max-w-md hidden mb-6 fade-in text-center md:text-left">
<h2 class="text-4xl font-black text-slate-800 tracking-tight">Hola, <span id="clientFirstName" class="text-blue-600"></span></h2>
</div>
<div class="w-full max-w-md bg-white rounded-[2rem] shadow-xl shadow-slate-200/50 border border-slate-100 relative p-6 md:p-8">
<div id="step1" class="fade-in space-y-6">
<div class="text-center mb-6">
<h3 class="text-lg font-bold text-slate-800">Identificación</h3>
<p class="text-sm text-slate-500">Introduce tu móvil para empezar</p>
</div>
<div>
<label class="text-[10px] font-bold text-slate-500 uppercase ml-1">Teléfono Móvil</label>
<input type="tel" id="cliPhone" placeholder="Ej: 600123456" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-4 rounded-xl text-lg text-center tracking-widest font-black focus:border-blue-500 focus:bg-white 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-4 rounded-xl shadow-xl shadow-blue-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-wider text-sm">
<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-6 hidden">
<div class="text-center mb-4">
<h3 class="text-lg font-bold text-slate-800 mb-2">Verificación</h3>
<p class="text-sm text-slate-500 font-medium">Hemos enviado un WhatsApp al <strong id="displayPhone" class="text-slate-800"></strong> con un código de 4 cifras.</p>
</div>
<div>
<label class="text-[10px] font-bold text-slate-500 uppercase ml-1">Código de Seguridad</label>
<input type="number" id="cliCode" placeholder="· · · ·" maxlength="4" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-4 rounded-xl text-3xl text-center tracking-[1em] 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-4 rounded-xl shadow-xl shadow-slate-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-wider text-sm">
<span>Verificar Acceso</span> <i data-lucide="check-circle" class="w-4 h-4"></i>
</button>
<button type="button" onclick="goToStep(1)" class="w-full text-slate-400 text-xs font-bold uppercase tracking-wider mt-2 hover:text-slate-600">Cambiar número</button>
</div>
<form id="step3" class="fade-in space-y-6 hidden" onsubmit="submitFinalRequest(event)">
<div id="boxName">
<label class="text-[10px] font-bold text-slate-500 uppercase ml-1">Tu Nombre Completo *</label>
<input type="text" id="cliName" placeholder="Ej: Juan Pérez" class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-3 rounded-xl text-sm font-bold focus:border-blue-500 outline-none">
</div>
<div id="boxAddress">
<label class="text-[10px] font-bold text-slate-500 uppercase ml-1">Dirección del Servicio *</label>
<select id="cliAddressSelect" onchange="toggleNewAddressInput()" class="hidden w-full mt-1 mb-2 bg-slate-50 border-2 border-slate-100 px-4 py-3 rounded-xl text-sm font-bold outline-none cursor-pointer text-blue-700"></select>
<input type="text" id="cliAddressInput" placeholder="Calle, Número, Piso, Código Postal..." class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-3 rounded-xl text-sm font-bold focus:border-blue-500 outline-none">
</div>
<div>
<label class="text-[10px] font-bold text-slate-500 uppercase ml-1">¿Qué profesional necesitas? *</label>
<select id="cliGuild" required class="w-full mt-1 bg-slate-50 border-2 border-slate-100 px-4 py-3 rounded-xl text-sm font-bold text-slate-700 focus:border-blue-500 outline-none cursor-pointer">
<option value="">Cargando gremios...</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold text-slate-500 uppercase 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-3 rounded-xl text-sm font-medium focus:border-blue-500 outline-none resize-none"></textarea>
</div>
<div class="bg-red-50 border-2 border-red-100 rounded-2xl p-4">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-black text-red-700 text-sm flex items-center gap-1.5"><i data-lucide="flame" class="w-4 h-4"></i> ¿Es una Urgencia?</span>
<span class="text-[10px] font-bold text-red-500 uppercase mt-0.5">Asistencia Inmediata</span>
</div>
<div class="relative inline-block w-12 mr-2 align-middle select-none">
<input type="checkbox" id="cliUrgent" onchange="handleUrgency(this)" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="cliUrgent" class="toggle-label block overflow-hidden h-6 rounded-full bg-red-200 cursor-pointer"></label>
</div>
</div>
</div>
<button type="submit" id="btnSubmitFinal" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-black py-4 rounded-xl shadow-xl shadow-blue-200 transition-all flex items-center justify-center gap-2 active:scale-95 uppercase tracking-wider text-sm mt-4">
<span>Buscar Técnico</span> <i data-lucide="search" class="w-4 h-4"></i>
</button>
<button type="button" onclick="logoutClient()" class="w-full text-slate-400 text-[10px] font-bold uppercase tracking-widest mt-4 hover:text-slate-600 transition-colors">
Cambiar de número de teléfono
</button>
</form>
</div>
<div id="urgentModal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm 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">
<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-triangle" class="w-8 h-8"></i>
</div>
<h3 class="text-xl font-black text-slate-800 mb-2">Servicio de Urgencia</h3>
<p class="text-sm text-slate-600 mb-6 font-medium">
Los servicios de urgencia <strong>no requieren cita previa</strong>. Un técnico acudirá a tu domicilio a la mayor brevedad posible.<br><br>
Ten en cuenta que esta modalidad conlleva un <strong>coste adicional de tarifa de urgencia</strong>.
</p>
<div class="flex gap-3">
<button type="button" onclick="cancelUrgency()" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold py-3 rounded-xl transition-all">Cancelar</button>
<button type="button" onclick="acceptUrgency()" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-3 rounded-xl shadow-lg shadow-red-200 transition-all">Acepto</button>
</div>
</div>
</div>
<div id="successScreen" class="fixed inset-0 bg-slate-50 z-[100] hidden flex-col items-center justify-center p-6 text-center">
<div class="w-24 h-24 bg-emerald-100 text-emerald-500 rounded-[2rem] flex items-center justify-center mb-6 shadow-lg shadow-emerald-200">
<i data-lucide="zap" class="w-12 h-12"></i>
</div>
<h2 class="text-3xl font-black text-slate-800 mb-2">¡Técnico Avisado!</h2>
<p class="text-slate-500 font-medium max-w-sm">Tu solicitud de urgencia ha entrado en nuestra bolsa prioritaria. El primer operario disponible aceptará tu aviso y se pondrá en camino de inmediato.</p>
</div>
<script>
lucide.createIcons();
// 🔧 CONFIGURACIÓN
const API_URL = 'https://integrarepara-api.integrarepara.es';
// Capturamos el ID de la empresa desde el enlace (o usamos 1 por defecto)
const urlParams = new URLSearchParams(window.location.search);
const OWNER_ID = parseInt(urlParams.get('c')) || 1;
let currentClient = null;
// 🔄 AUTO-LOGIN Y CARGA DE DATOS DE EMPRESA AL ABRIR LA WEB
document.addEventListener("DOMContentLoaded", () => {
loadCompanyData(); // Cargar logo, nombre y gremios
const sesionGuardada = localStorage.getItem('clienteSesion');
if (sesionGuardada) {
try {
const datos = JSON.parse(sesionGuardada);
if (Date.now() < datos.expires) {
console.log("Sesión recuperada. Saltando validación...");
document.getElementById('cliPhone').value = datos.phone;
prepareStep3(datos.client);
goToStep(3);
} else {
localStorage.removeItem('clienteSesion');
}
} catch (e) {
localStorage.removeItem('clienteSesion');
}
}
});
// --- CARGAR DATOS PÚBLICOS DE LA 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) {
// Actualizar Nombre
document.getElementById('companyNameDisplay').innerText = data.name || "IntegraRepara";
// Actualizar Logo
if (data.logo) {
document.getElementById('defaultLogo').classList.add('hidden');
const img = document.getElementById('companyLogoImg');
img.src = data.logo;
img.classList.remove('hidden');
}
// Llenar Select de Gremios
const selectGremio = document.getElementById('cliGuild');
selectGremio.innerHTML = '<option value="">Selecciona un profesional...</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 al cargar la empresa", e);
document.getElementById('companyNameDisplay').innerText = "Servicio Técnico";
document.getElementById('cliGuild').innerHTML = '<option value="0">Servicio General</option>';
}
}
// 🚪 FUNCIÓN PARA CERRAR SESIÓN
function logoutClient() {
localStorage.removeItem('clienteSesion');
document.getElementById('cliPhone').value = "";
document.getElementById('cliCode').value = "";
document.getElementById('welcomeBackBox').classList.add('hidden');
goToStep(1);
}
// --- NAVEGACIÓN ENTRE PASOS ---
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');
}
}
// --- PASO 1: PEDIR OTP ---
async function requestOTP() {
const phone = document.getElementById('cliPhone').value;
if (phone.length < 9) return alert("Introduce un teléfono válido.");
const btn = document.getElementById('btnStep1');
btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Enviando...';
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 })
});
const data = await res.json();
if (data.ok) {
document.getElementById('displayPhone').innerText = phone;
goToStep(2);
} else {
alert("Error al enviar el código.");
}
} catch (e) {
alert("Error de conexión.");
} finally {
btn.innerHTML = '<span>Recibir Código</span> <i data-lucide="message-circle" class="w-4 h-4"></i>';
lucide.createIcons();
}
}
// --- PASO 2: VERIFICAR OTP ---
async function verifyOTP() {
const phone = document.getElementById('cliPhone').value;
const code = document.getElementById('cliCode').value;
if (code.length !== 4) return alert("El código debe tener 4 números.");
const btn = document.getElementById('btnStep2');
btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Comprobando...';
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) {
// 💾 MAGIA: Guardamos la sesión por 10 días
const caducidad = Date.now() + (10 * 24 * 60 * 60 * 1000);
localStorage.setItem('clienteSesion', JSON.stringify({
phone: phone,
client: data.exists ? data.client : null,
expires: caducidad
}));
prepareStep3(data.exists ? data.client : null);
goToStep(3);
} else {
alert("❌ " + (data.error || "Código incorrecto."));
}
} catch (e) {
alert("Error de conexión.");
} finally {
btn.innerHTML = '<span>Verificar Acceso</span> <i data-lucide="check-circle" class="w-4 h-4"></i>';
lucide.createIcons();
}
}
// --- ADAPTAR UI DEPENDIENDO DE SI EXISTE ---
function prepareStep3(clientData) {
currentClient = clientData;
const nameBox = document.getElementById('boxName');
const welcomeBox = document.getElementById('welcomeBackBox');
const firstNameSpan = document.getElementById('clientFirstName');
const selectAddr = document.getElementById('cliAddressSelect');
const inputAddr = document.getElementById('cliAddressInput');
if (clientData) {
// Ya lo conocemos
welcomeBox.classList.remove('hidden');
// Extraemos solo el primer nombre (hasta el primer espacio)
const firstName = clientData.full_name.split(' ')[0];
firstNameSpan.innerText = firstName.toUpperCase();
nameBox.classList.add('hidden'); // Ocultamos pedir nombre
document.getElementById('cliName').value = clientData.full_name; // Lo rellenamos oculto
// Direcciones
let addrs = [];
try { addrs = typeof clientData.addresses === 'string' ? JSON.parse(clientData.addresses) : clientData.addresses; } catch(e) {}
if (addrs && addrs.length > 0) {
selectAddr.classList.remove('hidden');
inputAddr.classList.add('hidden');
selectAddr.innerHTML = '';
addrs.forEach(a => selectAddr.innerHTML += `<option value="${a}">${a}</option>`);
selectAddr.innerHTML += `<option value="NEW"> Añadir nueva dirección...</option>`;
inputAddr.value = addrs[0]; // Por defecto la primera
}
} else {
// Cliente Nuevo
welcomeBox.classList.add('hidden');
nameBox.classList.remove('hidden');
selectAddr.classList.add('hidden');
inputAddr.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;
}
}
// --- LÓGICA URGENCIA ---
const urgentToggle = document.getElementById('cliUrgent');
const urgentModal = document.getElementById('urgentModal');
function handleUrgency(checkbox) { if (checkbox.checked) urgentModal.classList.remove('hidden'); }
function cancelUrgency() { urgentToggle.checked = false; urgentModal.classList.add('hidden'); }
function acceptUrgency() { urgentModal.classList.add('hidden'); }
// --- ENVÍO FINAL ---
async function submitFinalRequest(e) {
e.preventDefault();
const btn = document.getElementById('btnSubmitFinal');
btn.disabled = true; btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Procesando...'; lucide.createIcons();
let finalAddress = document.getElementById('cliAddressInput').value;
if (document.getElementById('cliAddressSelect').value !== "NEW" && !document.getElementById('cliAddressSelect').classList.contains('hidden')) {
finalAddress = document.getElementById('cliAddressSelect').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: urgentToggle.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: ' + (data.error || 'No se pudo crear la solicitud.'));
btn.disabled = false; btn.innerHTML = '<span>Buscar Técnico</span> <i data-lucide="search" class="w-4 h-4"></i>'; lucide.createIcons();
}
} catch (err) {
alert('❌ Error de conexión.');
btn.disabled = false; btn.innerHTML = '<span>Buscar Técnico</span> <i data-lucide="search" class="w-4 h-4"></i>'; lucide.createIcons();
}
}
</script>
</body>
</html>