281 lines
14 KiB
HTML
281 lines
14 KiB
HTML
<!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>Agendar Cita - IntegraRepara</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
body { background-color: #f8fafc; }
|
|
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
|
|
.slot-btn { transition: all 0.2s; }
|
|
.slot-btn.selected { background-color: #2563eb; color: white; border-color: #2563eb; transform: scale(1.02); box-shadow: 0 4px 12px rgba(37,99,235,0.2); }
|
|
|
|
/* Estilos para los días del carrusel */
|
|
.day-chip { transition: all 0.2s; border: 2px solid transparent; }
|
|
.day-chip.active { background-color: #2563eb !important; color: white !important; border-color: #2563eb !important; transform: scale(1.05); box-shadow: 0 4px 10px rgba(37,99,235,0.2); }
|
|
.day-chip.inactive { background-color: white; color: #475569; border-color: #e2e8f0; }
|
|
</style>
|
|
</head>
|
|
<body class="text-slate-800 font-sans antialiased min-h-screen flex flex-col items-center">
|
|
|
|
<div id="loader" class="fixed inset-0 bg-slate-50 z-50 flex flex-col items-center justify-center">
|
|
<div class="w-12 h-12 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin mb-4"></div>
|
|
<p class="text-sm font-bold text-slate-500 animate-pulse">Buscando rutas de tu técnico...</p>
|
|
</div>
|
|
|
|
<main id="mainContent" class="hidden w-full max-w-md mx-auto min-h-screen flex flex-col relative pb-10">
|
|
|
|
<header class="bg-white p-6 pt-10 border-b border-slate-200 sticky top-0 z-10 shrink-0 shadow-sm">
|
|
<button onclick="window.history.back()" class="w-10 h-10 bg-slate-50 rounded-full flex items-center justify-center text-slate-500 hover:text-slate-800 mb-4 transition-colors">
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
</button>
|
|
<h1 class="text-2xl font-black tracking-tight leading-none text-slate-800">¿Cuándo te viene mejor?</h1>
|
|
<p class="text-sm text-slate-500 font-medium mt-2">Te mostraremos franjas de <strong class="text-slate-700">1 hora de margen de llegada</strong> en las que el técnico estará por tu zona.</p>
|
|
</header>
|
|
|
|
<div class="p-6 flex-1 flex flex-col gap-6 fade-in">
|
|
|
|
<div id="step1">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<button onclick="selectPreference('morning')" id="btnMorning" class="bg-white border-2 border-slate-200 p-6 rounded-3xl flex flex-col items-center gap-3 hover:border-blue-400 hover:bg-blue-50 transition-all text-slate-600">
|
|
<i data-lucide="sun" class="w-10 h-10 text-amber-500"></i>
|
|
<span class="font-black uppercase text-sm tracking-widest">Mañanas</span>
|
|
</button>
|
|
<button onclick="selectPreference('afternoon')" id="btnAfternoon" class="bg-white border-2 border-slate-200 p-6 rounded-3xl flex flex-col items-center gap-3 hover:border-blue-400 hover:bg-blue-50 transition-all text-slate-600">
|
|
<i data-lucide="sunset" class="w-10 h-10 text-orange-500"></i>
|
|
<span class="font-black uppercase text-sm tracking-widest">Tardes</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="step2" class="hidden space-y-6 fade-in">
|
|
<div>
|
|
<h3 class="font-black text-slate-800 uppercase tracking-widest text-xs mb-3">1. Elige un día disponible</h3>
|
|
<div id="dayCarousel" class="flex overflow-x-auto gap-3 pb-2 no-scrollbar">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="hoursContainer" class="hidden space-y-4 fade-in">
|
|
<h3 class="font-black text-slate-800 uppercase tracking-widest text-xs mb-1">2. Ventana de llegada aproximada</h3>
|
|
<div id="slotsGrid" class="grid grid-cols-2 gap-3">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-auto pt-6">
|
|
<button id="btnConfirm" onclick="confirmBooking()" class="w-full bg-blue-600 text-white font-black py-4 rounded-2xl shadow-lg shadow-blue-500/30 opacity-50 pointer-events-none transition-all flex justify-center items-center gap-2 uppercase tracking-widest text-xs">
|
|
Confirmar Cita <i data-lucide="check-circle" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<div id="successScreen" class="hidden w-full max-w-md mx-auto p-6 flex flex-col items-center justify-center min-h-screen text-center fade-in bg-blue-600 text-white relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMiIgY3k9IjIiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4xKSIvPjwvc3ZnPg==')] opacity-50"></div>
|
|
<div class="w-24 h-24 bg-white text-blue-600 rounded-full flex items-center justify-center mb-6 shadow-2xl relative z-10">
|
|
<i data-lucide="calendar-check" class="w-12 h-12"></i>
|
|
</div>
|
|
<h2 class="text-3xl font-black mb-2 relative z-10">Solicitud Enviada</h2>
|
|
<p class="font-medium opacity-90 mb-8 relative z-10">El técnico ha sido avisado de tu preferencia. Recibirás un WhatsApp de confirmación en breve.</p>
|
|
<button onclick="window.location.href = window.location.href.replace(/cit(a|ar)\.html/, 'index.html')" class="bg-slate-900 text-white font-black py-4 px-8 rounded-2xl shadow-lg uppercase text-xs tracking-widest relative z-10 active:scale-95 transition-transform">
|
|
Volver a mi Ficha
|
|
</button>
|
|
</div>
|
|
|
|
<script>
|
|
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
|
? 'http://localhost:3000' : 'https://integrarepara-api.integrarepara.es';
|
|
|
|
let agendaData = [];
|
|
let currentPreference = null;
|
|
let selectedDate = null;
|
|
let selectedTime = null;
|
|
let filteredDays = [];
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
lucide.createIcons();
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const token = urlParams.get('token');
|
|
const serviceId = urlParams.get('service');
|
|
|
|
if (!token || !serviceId) {
|
|
alert("Enlace inválido"); return;
|
|
}
|
|
|
|
try {
|
|
// Pedir la disponibilidad inteligente al servidor
|
|
const res = await fetch(`${API_URL}/public/portal/${token}/slots?serviceId=${serviceId}`);
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
agendaData = data.days;
|
|
document.getElementById('loader').classList.add('hidden');
|
|
document.getElementById('mainContent').classList.remove('hidden');
|
|
} else {
|
|
alert(data.error || "No se han podido cargar los horarios");
|
|
}
|
|
} catch (e) { alert("Error de conexión"); }
|
|
});
|
|
|
|
function selectPreference(pref) {
|
|
currentPreference = pref;
|
|
selectedDate = null;
|
|
selectedTime = null;
|
|
updateConfirmButton();
|
|
|
|
// UI feedback botones principales
|
|
document.getElementById('btnMorning').classList.remove('border-blue-500', 'bg-blue-50');
|
|
document.getElementById('btnAfternoon').classList.remove('border-blue-500', 'bg-blue-50');
|
|
|
|
if (pref === 'morning') document.getElementById('btnMorning').classList.add('border-blue-500', 'bg-blue-50');
|
|
if (pref === 'afternoon') document.getElementById('btnAfternoon').classList.add('border-blue-500', 'bg-blue-50');
|
|
|
|
document.getElementById('step2').classList.remove('hidden');
|
|
|
|
// Filtrar los días que sí tienen huecos para esta preferencia (mañana o tarde)
|
|
filteredDays = agendaData.filter(day => {
|
|
return pref === 'morning' ? day.morning.length > 0 : day.afternoon.length > 0;
|
|
});
|
|
|
|
renderDayCarousel();
|
|
document.getElementById('hoursContainer').classList.add('hidden');
|
|
}
|
|
|
|
// --- RENDERIZAR CARRUSEL DE DÍAS ---
|
|
function renderDayCarousel() {
|
|
const carousel = document.getElementById('dayCarousel');
|
|
carousel.innerHTML = "";
|
|
|
|
if (filteredDays.length === 0) {
|
|
carousel.innerHTML = '<p class="text-sm text-slate-500 italic p-2">No hay rutas disponibles en este turno para los próximos días.</p>';
|
|
return;
|
|
}
|
|
|
|
filteredDays.forEach(day => {
|
|
// Extraer un formato corto para el chip (ej: "Lun 15")
|
|
const dObj = new Date(day.date);
|
|
const dayNameShort = dObj.toLocaleDateString('es-ES', { weekday: 'short' }).replace('.', '');
|
|
const dayNum = dObj.getDate();
|
|
|
|
const isSelected = day.date === selectedDate;
|
|
|
|
carousel.innerHTML += `
|
|
<button onclick="selectDay('${day.date}')" class="day-chip flex flex-col items-center justify-center min-w-[4.5rem] p-3 rounded-2xl shrink-0 ${isSelected ? 'active' : 'inactive'}">
|
|
<span class="text-[10px] font-black uppercase opacity-80">${dayNameShort}</span>
|
|
<span class="text-xl font-black leading-none mt-1">${dayNum}</span>
|
|
</button>
|
|
`;
|
|
});
|
|
}
|
|
|
|
function selectDay(dateStr) {
|
|
selectedDate = dateStr;
|
|
selectedTime = null;
|
|
renderDayCarousel(); // Repintar para marcar como activo
|
|
renderSlots();
|
|
}
|
|
|
|
// --- FUNCIÓN HELPER PARA SUMAR 1 HORA ---
|
|
function addOneHour(timeStr) {
|
|
let [h, m] = timeStr.split(':').map(Number);
|
|
let totalMins = h * 60 + m + 60; // Sumamos 60 mins
|
|
let newH = Math.floor(totalMins / 60);
|
|
let newM = totalMins % 60;
|
|
return `${String(newH).padStart(2,'0')}:${String(newM).padStart(2,'0')}`;
|
|
}
|
|
|
|
function renderSlots() {
|
|
updateConfirmButton();
|
|
|
|
if (!selectedDate) {
|
|
document.getElementById('hoursContainer').classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
const dayObj = agendaData.find(d => d.date === selectedDate);
|
|
const slots = currentPreference === 'morning' ? dayObj.morning : dayObj.afternoon;
|
|
|
|
const grid = document.getElementById('slotsGrid');
|
|
|
|
// Aquí calculamos el tramo y lo pintamos
|
|
grid.innerHTML = slots.map(time => {
|
|
const endTime = addOneHour(time);
|
|
return `
|
|
<button onclick="selectTime('${time}', this)" class="slot-btn bg-white border-2 border-slate-200 py-4 px-2 rounded-2xl font-black text-slate-600 shadow-sm hover:border-blue-300 hover:bg-blue-50 flex flex-col items-center justify-center gap-1">
|
|
<span class="text-[10px] uppercase tracking-widest opacity-60 font-bold leading-none">Llegada entre</span>
|
|
<span class="text-sm">${time} - ${endTime}</span>
|
|
</button>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('hoursContainer').classList.remove('hidden');
|
|
}
|
|
|
|
function selectTime(time, btnEl) {
|
|
selectedTime = time;
|
|
document.querySelectorAll('.slot-btn').forEach(b => {
|
|
b.classList.remove('selected', 'border-blue-500');
|
|
b.classList.add('border-slate-200', 'text-slate-600');
|
|
});
|
|
|
|
btnEl.classList.remove('border-slate-200', 'text-slate-600', 'hover:border-blue-300', 'hover:bg-blue-50');
|
|
btnEl.classList.add('selected', 'border-blue-500');
|
|
|
|
updateConfirmButton();
|
|
}
|
|
|
|
function updateConfirmButton() {
|
|
const btn = document.getElementById('btnConfirm');
|
|
if (selectedDate && selectedTime) {
|
|
btn.classList.remove('opacity-50', 'pointer-events-none');
|
|
btn.classList.add('active:scale-95');
|
|
} else {
|
|
btn.classList.add('opacity-50', 'pointer-events-none');
|
|
btn.classList.remove('active:scale-95');
|
|
}
|
|
}
|
|
|
|
async function confirmBooking() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const token = urlParams.get('token');
|
|
const serviceId = urlParams.get('service');
|
|
|
|
const btn = document.getElementById('btnConfirm');
|
|
const originalHtml = btn.innerHTML;
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Solicitando...';
|
|
lucide.createIcons();
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/public/portal/${token}/book`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ serviceId, date: selectedDate, time: selectedTime })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
document.getElementById('mainContent').classList.add('hidden');
|
|
document.getElementById('successScreen').classList.remove('hidden');
|
|
document.getElementById('successScreen').classList.add('flex');
|
|
} else {
|
|
alert("Error al confirmar la cita. Es posible que el hueco ya se haya ocupado.");
|
|
btn.innerHTML = originalHtml;
|
|
lucide.createIcons();
|
|
}
|
|
} catch(e) {
|
|
alert("Error de conexión");
|
|
btn.innerHTML = originalHtml;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |