167 lines
19 KiB
JavaScript
167 lines
19 KiB
JavaScript
(function(){
|
|
function esc(s){return String(s||'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));}
|
|
function moneyText(s){return s || '$ 0,00';}
|
|
function buildUrl(base, params){ const u = new URL(base, window.location.origin); Object.entries(params||{}).forEach(([k,v])=>{ if(v!==undefined && v!==null && String(v)!=='') u.searchParams.set(k,v); }); return u.toString(); }
|
|
function tomorrowISO(){ const d = new Date(Date.now()+86400000); return d.toISOString().slice(0,10); }
|
|
function byId(arr,id){ return (arr||[]).find(x=>String(x.id)===String(id)); }
|
|
|
|
const DEFAULT_STEPS = [
|
|
['mode','Inicio'], ['location','Ubicación'], ['main','Institución / especialidad'], ['place','Sede / profesional'], ['service','Servicio'], ['schedule','Fecha y hora'], ['patient','Tus datos'], ['confirm','Confirmación']
|
|
];
|
|
|
|
function createBookingWizard(root, cfg, opts){
|
|
opts = opts || {};
|
|
const content = root.querySelector('#bwContent') || root;
|
|
const title = root.querySelector('#bwTitle');
|
|
const subtitle = root.querySelector('#bwSubtitle');
|
|
const badge = root.querySelector('#bwStepBadge');
|
|
const prev = root.querySelector('#bwPrev');
|
|
const next = root.querySelector('#bwNext');
|
|
const fill = root.querySelector('#bwProgressFill');
|
|
const label = root.querySelector('#bwProgressLabel');
|
|
const stepList = root.querySelector('#bwStepList');
|
|
const summary = root.querySelector('#bwSummary');
|
|
const state = {
|
|
step: 0, mode: opts.defaultMode || '', province:'', city:'', institution_id: cfg.initialInstitutionId || '', branch_id:'', service_id: cfg.initialServiceId || '', professional_id:'', booking_type:'first_available', date: tomorrowISO(), time:'',
|
|
client_name: cfg.linkedPatient?.name || '', client_email: cfg.linkedPatient?.email || '', client_phone: cfg.linkedPatient?.phone || '', notes:'', options:{}, slots:[], source: opts.source || 'website'
|
|
};
|
|
if(opts.prefill) Object.assign(state, opts.prefill);
|
|
|
|
async function loadOptions(extra){
|
|
const params = Object.assign({mode: state.mode, province: state.province, city: state.city, institution_id: state.institution_id, branch_id: state.branch_id, service_id: state.service_id}, extra||{});
|
|
const resp = await fetch(buildUrl(cfg.optionsUrl, params));
|
|
const data = await resp.json();
|
|
if(data.ok) state.options = data;
|
|
return data;
|
|
}
|
|
async function loadSlots(){
|
|
if(!state.service_id || !state.date) return {ok:false, items:[]};
|
|
const params = {service_id:state.service_id, date:state.date, institution_id:state.institution_id, branch_id:state.branch_id, professional_id:state.professional_id, province:state.province, city:state.city, booking_type:state.booking_type};
|
|
const resp = await fetch(buildUrl(cfg.slotsUrl, params));
|
|
const data = await resp.json();
|
|
state.slots = data.items || [];
|
|
return data;
|
|
}
|
|
function setChoice(key,val){ state[key]=val; state.time=''; if(key==='institution_id'){ state.branch_id=''; state.service_id=''; state.professional_id=''; } if(key==='branch_id'){ state.service_id=''; state.professional_id=''; } if(key==='service_id'){ state.professional_id=''; } render(); }
|
|
function stepMeta(){
|
|
const m = {
|
|
mode:['¿Cómo querés buscar tu turno?','Elegí si ya conocés la institución o si preferís que te mostremos opciones disponibles.'],
|
|
location:['Elegí tu ubicación','Esto permite filtrar sedes, instituciones y profesionales por provincia y ciudad.'],
|
|
main:[state.mode==='known'?'Elegí la institución':'Elegí especialidad o servicio', state.mode==='known'?'Buscá la clínica, sanatorio, consultorio o institución donde querés atenderte.':'Te mostramos servicios y especialidades disponibles en tu zona.'],
|
|
place:[state.mode==='known'?'Elegí la sede':'Elegí profesional o institución sugerida', state.mode==='known'?'Si la institución tiene varias sedes, seleccioná la más conveniente.':'Podés elegir un profesional concreto o dejar que el sistema asigne el primero disponible.'],
|
|
service:['Elegí el tipo de atención','Seleccioná el servicio, modalidad o tipo de consulta.'],
|
|
schedule:['Elegí fecha y horario','Seleccioná un día y luego uno de los horarios disponibles.'],
|
|
patient:['Completá tus datos','Usaremos esta información para confirmar y administrar tu turno.'],
|
|
confirm:['Revisá y confirmá','Verificá los datos antes de finalizar la solicitud.']
|
|
};
|
|
return m[DEFAULT_STEPS[state.step][0]];
|
|
}
|
|
function updateChrome(){
|
|
const pct = Math.round(((state.step+1)/DEFAULT_STEPS.length)*100);
|
|
if(fill) fill.style.width = pct+'%';
|
|
if(label) label.textContent = `Paso ${state.step+1} de ${DEFAULT_STEPS.length}`;
|
|
if(badge) badge.textContent = `Paso ${state.step+1}`;
|
|
const [t,st] = stepMeta(); if(title) title.textContent=t; if(subtitle) subtitle.textContent=st;
|
|
if(stepList){ stepList.innerHTML = DEFAULT_STEPS.map((s,i)=>`<li class="${i<state.step?'done':i===state.step?'active':''}"><span>${i+1}</span><b>${esc(s[1])}</b></li>`).join(''); }
|
|
if(prev) prev.style.visibility = state.step===0?'hidden':'visible';
|
|
if(next) next.innerHTML = state.step===DEFAULT_STEPS.length-1 ? 'Confirmar turno <i class="bi bi-check2-circle"></i>' : 'Siguiente <i class="bi bi-arrow-right"></i>';
|
|
renderSummary();
|
|
}
|
|
function renderSummary(){
|
|
if(!summary) return;
|
|
const o=state.options||{};
|
|
const inst=byId(o.institutions,state.institution_id); const br=byId(o.branches,state.branch_id); const svc=byId(o.services,state.service_id); const prof=byId(o.professionals,state.professional_id);
|
|
const rows = [
|
|
['Búsqueda', state.mode==='known'?'Conozco la institución': state.mode==='unknown'?'No conozco la institución':'Pendiente'], ['Provincia', state.province], ['Ciudad', state.city], ['Institución', inst?.name || ''], ['Sede', br?.label || ''], ['Servicio', svc? `${svc.name} · ${moneyText(svc.price)}`:''], ['Profesional', prof? `${prof.name} · ${prof.specialty}` : (state.booking_type==='first_available'?'Primero disponible':'')], ['Fecha', state.date], ['Horario', state.time]
|
|
];
|
|
summary.innerHTML = rows.map(([k,v])=>`<div class="summary-row"><span>${esc(k)}</span><strong>${esc(v||'—')}</strong></div>`).join('');
|
|
}
|
|
function cards(items, selected, click, empty){
|
|
if(!items || !items.length) return `<div class="alert alert-warning">${esc(empty||'No hay opciones para la selección actual.')}</div>`;
|
|
return `<div class="booking-card-grid">${items.map(it=>`<button type="button" class="booking-choice-card ${String(selected)===String(it.id)?'active':''}" data-id="${it.id}"><strong>${esc(it.name||it.label)}</strong><small>${esc(it.description || it.label || [it.city,it.province].filter(Boolean).join(' · ') || it.specialty || '')}</small>${it.price?`<span>${esc(it.price)} · ${esc(it.mode||'')}</span>`:''}${it.branches_count!==undefined?`<span>${it.branches_count} sede(s)</span>`:''}</button>`).join('')}</div>`;
|
|
}
|
|
async function render(){
|
|
updateChrome();
|
|
const key = DEFAULT_STEPS[state.step][0];
|
|
if(key!=='mode') await loadOptions();
|
|
if(key==='mode'){
|
|
content.innerHTML = `<div class="booking-card-grid two"><button type="button" class="booking-choice-card big ${state.mode==='known'?'active':''}" data-mode="known"><i class="bi bi-building-check"></i><strong>Sí, conozco la institución</strong><small>Quiero elegir clínica, sanatorio, consultorio o institución.</small></button><button type="button" class="booking-choice-card big ${state.mode==='unknown'?'active':''}" data-mode="unknown"><i class="bi bi-search-heart"></i><strong>No, quiero ver opciones</strong><small>Mostrame especialidades, profesionales o instituciones disponibles en mi zona.</small></button></div>`;
|
|
content.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{state.mode=b.dataset.mode; render();});
|
|
} else if(key==='location'){
|
|
const provinces = state.options.provinces || [];
|
|
const cities = (state.options.cities_by_province||{})[state.province] || [];
|
|
content.innerHTML = `<div class="row g-3"><div class="col-md-6"><label class="form-label">Provincia</label><select class="form-select" id="bwProvince"><option value="">Todas las provincias</option>${provinces.map(p=>`<option ${p===state.province?'selected':''}>${esc(p)}</option>`).join('')}</select></div><div class="col-md-6"><label class="form-label">Ciudad / localidad</label><select class="form-select" id="bwCity"><option value="">Todas las ciudades</option>${cities.map(c=>`<option ${c===state.city?'selected':''}>${esc(c)}</option>`).join('')}</select></div></div><div class="booking-help mt-3"><i class="bi bi-info-circle"></i> Si no elegís ciudad, el sistema mostrará todas las opciones disponibles de la provincia.</div>`;
|
|
content.querySelector('#bwProvince').onchange=e=>{state.province=e.target.value; state.city=''; render();}; content.querySelector('#bwCity').onchange=e=>{state.city=e.target.value; renderSummary();};
|
|
} else if(key==='main'){
|
|
if(state.mode==='known'){
|
|
content.innerHTML = cards(state.options.institutions, state.institution_id, null, 'No encontramos instituciones activas para esa ubicación.');
|
|
content.querySelectorAll('[data-id]').forEach(b=>b.onclick=()=>setChoice('institution_id', b.dataset.id));
|
|
} else {
|
|
content.innerHTML = cards(state.options.services, state.service_id, null, 'No hay servicios cargados para esa ubicación.');
|
|
content.querySelectorAll('[data-id]').forEach(b=>b.onclick=()=>setChoice('service_id', b.dataset.id));
|
|
}
|
|
} else if(key==='place'){
|
|
if(state.mode==='known'){
|
|
content.innerHTML = cards(state.options.branches, state.branch_id, null, 'La institución no tiene sedes activas cargadas para esa ubicación. Podés continuar con sede pendiente.');
|
|
content.querySelectorAll('[data-id]').forEach(b=>b.onclick=()=>setChoice('branch_id', b.dataset.id));
|
|
} else {
|
|
const profs = state.options.professionals || [];
|
|
content.innerHTML = `<div class="booking-help mb-3"><i class="bi bi-lightbulb"></i> Podés elegir profesional o dejar que el sistema use el primero disponible.</div><button type="button" class="booking-choice-card w-100 mb-3 ${!state.professional_id?'active':''}" id="bwFirstAvailable"><strong>Asignar primero disponible</strong><small>Recomendado si querés obtener un horario más rápido.</small></button>${cards(profs.map(p=>({id:p.id,name:p.name,description:[p.specialty,p.institution_name,p.location].filter(Boolean).join(' · ')})), state.professional_id, null, 'No hay profesionales cargados para ese servicio/ubicación.')}`;
|
|
content.querySelector('#bwFirstAvailable').onclick=()=>{state.professional_id=''; state.booking_type='first_available'; render();}; content.querySelectorAll('[data-id]').forEach(b=>b.onclick=()=>{state.professional_id=b.dataset.id; state.booking_type='specific'; render();});
|
|
}
|
|
} else if(key==='service'){
|
|
if(state.mode==='known'){
|
|
content.innerHTML = cards(state.options.services, state.service_id, null, 'No hay servicios activos para esta institución/sede.');
|
|
content.querySelectorAll('[data-id]').forEach(b=>b.onclick=()=>setChoice('service_id', b.dataset.id));
|
|
} else {
|
|
const svc=byId(state.options.services,state.service_id);
|
|
content.innerHTML = `<div class="booking-confirm-box"><h3 class="h6">Servicio seleccionado</h3><p class="mb-1"><strong>${esc(svc?.name||'Pendiente')}</strong></p><p class="text-muted mb-0">${esc((svc?.price||'') + (svc?.mode ? ' · '+svc.mode : ''))}</p></div>`;
|
|
}
|
|
} else if(key==='schedule'){
|
|
content.innerHTML = `<div class="row g-3 align-items-end"><div class="col-md-5"><label class="form-label">Fecha</label><input type="date" class="form-control" id="bwDate" min="${tomorrowISO()}" value="${esc(state.date)}"></div><div class="col-md-4"><label class="form-label">Asignación</label><select class="form-select" id="bwBookingType"><option value="specific" ${state.booking_type==='specific'?'selected':''}>Profesional elegido</option><option value="first_available" ${state.booking_type==='first_available'?'selected':''}>Primero disponible</option><option value="round_robin" ${state.booking_type==='round_robin'?'selected':''}>Distribución automática</option></select></div><div class="col-md-3 d-grid"><button type="button" class="btn btn-outline-primary" id="bwLoadSlots">Buscar horarios</button></div></div><div id="bwSlots" class="booking-slots mt-3">Elegí fecha y presioná “Buscar horarios”.</div>`;
|
|
content.querySelector('#bwDate').onchange=e=>{state.date=e.target.value; state.time='';}; content.querySelector('#bwBookingType').onchange=e=>{state.booking_type=e.target.value; if(e.target.value!=='specific') state.professional_id='';}; content.querySelector('#bwLoadSlots').onclick=async()=>{ const box=content.querySelector('#bwSlots'); box.innerHTML='Buscando horarios disponibles...'; const data=await loadSlots(); if(!data.ok||!state.slots.length){box.innerHTML='<div class="alert alert-warning">No hay horarios disponibles para esa fecha.</div>'; return;} box.innerHTML=state.slots.map(item=>`<div class="slot-group"><div class="slot-group-title">${esc(item.professional_name)} <span>${esc(item.specialty||'')}</span></div><div class="slot-grid">${(item.slots||[]).map(slot=>`<button type="button" class="slot-btn ${state.time===slot.time&&String(state.professional_id)===String(item.professional_id)?'active':''}" data-prof="${item.professional_id}" data-time="${esc(slot.time)}">${esc(slot.time)}</button>`).join('')}</div></div>`).join(''); box.querySelectorAll('.slot-btn').forEach(btn=>btn.onclick=()=>{box.querySelectorAll('.slot-btn').forEach(x=>x.classList.remove('active')); btn.classList.add('active'); state.professional_id=btn.dataset.prof; state.time=btn.dataset.time; state.booking_type='specific'; renderSummary();}); };
|
|
if(state.service_id) content.querySelector('#bwLoadSlots').click();
|
|
} else if(key==='patient'){
|
|
content.innerHTML = `<div class="row g-3"><div class="col-md-6"><label class="form-label">Nombre y apellido</label><input class="form-control" id="bwName" value="${esc(state.client_name)}" ${cfg.linkedPatient?'readonly':''}></div><div class="col-md-6"><label class="form-label">Email</label><input type="email" class="form-control" id="bwEmail" value="${esc(state.client_email)}" ${cfg.linkedPatient?'readonly':''}></div><div class="col-md-6"><label class="form-label">Teléfono</label><input class="form-control" id="bwPhone" value="${esc(state.client_phone)}" ${cfg.linkedPatient?'readonly':''}></div><div class="col-md-6"><label class="form-label">Observaciones</label><input class="form-control" id="bwNotes" value="${esc(state.notes)}" placeholder="Motivo de consulta, referencia, modalidad..."></div></div>`;
|
|
['Name','Email','Phone','Notes'].forEach(k=>{const el=content.querySelector('#bw'+k); if(el) el.oninput=()=>{state['client_'+k.toLowerCase().replace('name','name').replace('phone','phone').replace('email','email').replace('notes','notes')] = el.value; if(k==='Notes') state.notes=el.value;};});
|
|
content.querySelector('#bwName').oninput=e=>state.client_name=e.target.value; content.querySelector('#bwEmail').oninput=e=>state.client_email=e.target.value; content.querySelector('#bwPhone').oninput=e=>state.client_phone=e.target.value; content.querySelector('#bwNotes').oninput=e=>state.notes=e.target.value;
|
|
} else if(key==='confirm'){
|
|
const o=state.options||{}; const svc=byId(o.services,state.service_id); const inst=byId(o.institutions,state.institution_id); const br=byId(o.branches,state.branch_id); const prof=byId(o.professionals,state.professional_id);
|
|
content.innerHTML = `<div class="booking-confirm-box"><h3 class="h5 mb-3">Revisá tu solicitud</h3><div class="booking-review-grid"><div><span>Servicio</span><strong>${esc(svc?.name||'—')}</strong></div><div><span>Institución</span><strong>${esc(inst?.name || prof?.institution_name || '—')}</strong></div><div><span>Sede</span><strong>${esc(br?.label||'—')}</strong></div><div><span>Profesional</span><strong>${esc(prof?.name || 'Primero disponible')}</strong></div><div><span>Fecha</span><strong>${esc(state.date||'—')}</strong></div><div><span>Horario</span><strong>${esc(state.time||'—')}</strong></div><div><span>Paciente</span><strong>${esc(state.client_name||'—')}</strong></div><div><span>Email</span><strong>${esc(state.client_email||'—')}</strong></div></div></div>`;
|
|
}
|
|
updateChrome();
|
|
}
|
|
function canGoNext(){
|
|
const key=DEFAULT_STEPS[state.step][0];
|
|
if(key==='mode' && !state.mode) return 'Elegí cómo querés buscar el turno.';
|
|
if(key==='main' && state.mode==='known' && !state.institution_id) return 'Elegí la institución.';
|
|
if(key==='main' && state.mode==='unknown' && !state.service_id) return 'Elegí una especialidad o servicio.';
|
|
if(key==='service' && !state.service_id) return 'Elegí el servicio.';
|
|
if(key==='schedule' && (!state.date || !state.time)) return 'Elegí fecha y horario.';
|
|
if(key==='patient' && (!state.client_name || !state.client_email)) return 'Completá nombre y email.';
|
|
return '';
|
|
}
|
|
async function submit(){
|
|
const payload = {source:state.source, mode:state.mode, province:state.province, city:state.city, institution_id:state.institution_id, branch_id:state.branch_id, service_id:state.service_id, professional_id:state.professional_id, booking_type:state.booking_type, date:state.date, time:state.time, client_name:state.client_name, client_email:state.client_email, client_phone:state.client_phone, notes:state.notes};
|
|
if(next) {next.disabled=true; next.innerHTML='Confirmando...';}
|
|
try{
|
|
const resp=await fetch(cfg.createUrl,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':cfg.csrfToken||''},body:JSON.stringify(payload)}); const data=await resp.json();
|
|
if(!data.ok) throw new Error(data.error||'No se pudo confirmar el turno.');
|
|
if(opts.onSuccess){ opts.onSuccess(data); }
|
|
else { window.location.href = data.redirect_url || data.success_url; }
|
|
}catch(err){ alert(err.message||'No se pudo confirmar el turno.'); }
|
|
finally{ if(next){next.disabled=false; next.innerHTML='Confirmar turno <i class="bi bi-check2-circle"></i>'; } }
|
|
}
|
|
prev?.addEventListener('click',()=>{ if(state.step>0){state.step--; render();} });
|
|
next?.addEventListener('click',async()=>{ const msg=canGoNext(); if(msg){alert(msg); return;} if(state.step===DEFAULT_STEPS.length-1){await submit(); return;} state.step++; render(); });
|
|
loadOptions().then(render);
|
|
return {state, render, submit};
|
|
}
|
|
|
|
window.BookingWizard = { create: createBookingWizard };
|
|
document.addEventListener('DOMContentLoaded', function(){
|
|
const root = document.getElementById('bookingWizardRoot');
|
|
if(root && window.BOOKING_WIZARD_CONFIG){ createBookingWizard(root, window.BOOKING_WIZARD_CONFIG, {source:'website'}); }
|
|
});
|
|
})();
|