`; try { mount(root); } catch (err) { console.error(err); root.innerHTML = `
No se pudo cargar el módulo. Revisa consola del navegador (F12). Detalle: ${String(err && err.message ? err.message : err)}
`; } }); }; // 1) DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.PZ_PYP_INIT_ALL()); } else { window.PZ_PYP_INIT_ALL(); } // 2) Elementor frontend hook (cuando el widget se inserta después) window.addEventListener('elementor/frontend/init', () => { // Espera un tick a que Elementor meta el HTML al DOM setTimeout(() => window.PZ_PYP_INIT_ALL(), 50); }); // 3) Reintentos (por si Elementor tarda o hay lazy render) let tries = 0; const t = setInterval(() => { tries++; window.PZ_PYP_INIT_ALL(); if (tries >= 10) clearInterval(t); }, 300); function getTZ(root){ return (root.getAttribute('data-tz') || 'America/Bogota').trim() || 'America/Bogota'; } function nowPartsInTZ(tz){ const dtf = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false }); const parts = dtf.formatToParts(new Date()); const m = {}; for (const p of parts) if (p.type !== 'literal') m[p.type] = p.value; return { year:+m.year, month:+m.month, day:+m.day, hour:+m.hour, minute:+m.minute, second:+m.second }; } function ymdFromParts(p){ return `${p.year}-${pad2(p.month)}-${pad2(p.day)}`; } function minutesOfDayFromParts(p){ return p.hour*60 + p.minute; } function dateUTCNoonFromYMD(ymd){ const [y,m,d] = ymd.split('-').map(Number); return new Date(Date.UTC(y, m-1, d, 12, 0, 0)); } function addDaysYMD(ymd, delta){ const dt = dateUTCNoonFromYMD(ymd); dt.setUTCDate(dt.getUTCDate() + delta); return `${dt.getUTCFullYear()}-${pad2(dt.getUTCMonth()+1)}-${pad2(dt.getUTCDate())}`; } function fmtTimeNoSeconds(tz){ return new Intl.DateTimeFormat('es-CO', { timeZone: tz, hour:'numeric', minute:'2-digit', hour12:true }).format(new Date()); } function weekdayUpperES(tz, ymd){ const dt = dateUTCNoonFromYMD(ymd); const w = new Intl.DateTimeFormat('es-CO', { timeZone: tz, weekday:'long' }).format(dt); return String(w).toUpperCase(); } function prettyDateES(ymd){ const [y,m,d] = ymd.split('-').map(Number); const meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; return `${d} de ${meses[m-1]} ${y}`; } function isHHMM(s){ return /^([01]\d|2[0-3]):([0-5]\d)$/.test(String(s||'').trim()); } function time24to12ES(hhmm){ if (!isHHMM(hhmm)) return String(hhmm||''); const [h,m] = hhmm.split(':').map(Number); const h12 = ((h + 11) % 12) + 1; const suf = h >= 12 ? 'p. m.' : 'a. m.'; return `${h12}:${pad2(m)} ${suf}`; } function rangeLabel(slot){ return `${time24to12ES(String(slot.start))} – ${time24to12ES(String(slot.end))}`; } function scheduleHtmlMultiline(schedule){ const slots = Array.isArray(schedule) ? schedule : []; if (slots.length === 1 && String(slots[0].start)==="00:00" && String(slots[0].end)==="23:59") return `
Todo el día
`; if (!slots.length) return `
Sin restricción
`; if (slots.length === 1) return `
${rangeLabel(slots[0])}
`; return `
${slots.map(s => `${rangeLabel(s)} `).join("")} `; } function toMinutes(hhmm){ if (!isHHMM(hhmm)) return null; const [h,m] = hhmm.split(':').map(Number); return h*60 + m; } function getHorarioEstado(schedule, nowMin){ const slots = (Array.isArray(schedule) ? schedule : []) .map(s => ({ s: toMinutes(String(s.start||'')), e: toMinutes(String(s.end||'')) })) .filter(x => x.s !== null && x.e !== null) .sort((a,b) => a.s - b.s); if (!slots.length) return { kind:'none', endMin:null }; for (const slot of slots){ if (nowMin >= slot.s && nowMin < slot.e) return { kind:'active', endMin: slot.e }; } return { kind:'inactive', endMin:null }; } function countdownHM(endMin, nowMin){ if (endMin === null) return null; const rem = Math.max(0, endMin - nowMin); return { h: Math.floor(rem/60), m: rem % 60 }; } function safeUrl(u){ const s = String(u ?? '').trim(); return /^https?:\/\//i.test(s) ? s : ''; } function dayKeyFromYMD(ymd){ const dt = dateUTCNoonFromYMD(ymd); return ['sun','mon','tue','wed','thu','fri','sat'][dt.getUTCDay()]; } function iconSvg(key){ const common = `width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"`; const car = `
`;
const moto = `
`;
const bus = `
`;
const k = String(key||'').toLowerCase();
if (k.includes('moto')) return moto;
if (k.includes('bus')) return bus;
return car;
}
function resolveRestriction(city, cat, ymd){
const rule = cat.rule || {};
const dk = dayKeyFromYMD(ymd);
if (rule.type === "none"){
return { value:"NO APLICA", kind:"na", schedule: [] };
}
if (rule.type === "weekday_map"){
const v = rule.map && rule.map[dk] ? String(rule.map[dk]) : "SIN INFORMACIÓN";
if (v === "NO_APLICA") return { value:"NO APLICA", kind:"na", schedule: [] };
if (v === "SIN INFORMACIÓN") return { value:"SIN INFORMACIÓN", kind:"unknown", schedule: [] };
return { value: v, kind:"digits", schedule: [] };
}
return { value:"SIN INFORMACIÓN", kind:"unknown", schedule: [] };
}
function mount(root){
const tz = getTZ(root);
const key = String(root.getAttribute("data-key") || "").trim();
const cfg = window.PZ_PYP_DATA && window.PZ_PYP_DATA[key] ? window.PZ_PYP_DATA[key] : null;
if (!cfg) throw new Error(`No existe window.PZ_PYP_DATA["${key}"]. Revisa data-key.`);
const city = cfg.city;
const sw = city.switch || {};
const baseUrl = String(sw.baseUrl || "").trim();
const options = Array.isArray(sw.options) ? sw.options : [];
root.innerHTML = `
Hora: --:--
📍 ${(sw.label || 'Cambiar ciudad')} – ${String(city.name||city.id)}
Si vas a viajar, revisa el pico y placa para la fecha de tu viaje.
¿Tu placa es de Ibagué?
Esto solo cambia el horario.
Sí
No
`;
const $ = (sel) => root.querySelector(sel);
const elTitle = $('.pz-title');
const elTime = $('.pz-time');
const elCity = $('.pz-cityselect');
const elWeek = $('.pz-weekday');
const elDate = $('.pz-date');
const elDateInput = $('.pz-dateinput');
const elDateCenter = $('.pz-datecenter');
const elPrevDay = $('.pz-daybtn--left');
const elNextDay = $('.pz-daybtn--right');
const elGrid = $('.pz-grid');
const elFoot = $('.pz-foot');
const elVariantWrap = $('.pz-variant');
const elVariantBtns = root.querySelectorAll('.pz-variantBtn');
elCity.innerHTML = options
.slice()
.sort((a,b) => String(a.name||'').localeCompare(String(b.name||''), 'es'))
.map(o => ``)
.join('');
elCity.value = city.id;
let todayTZ = ymdFromParts(nowPartsInTZ(tz));
let selectedYMD = todayTZ;
let placaVariant = "ibague";
function updateHeader(){
elTitle.textContent = `PICO Y PLACA ${String(city.name || city.id).toUpperCase()}`.trim();
elTime.textContent = `Hora: ${fmtTimeNoSeconds(tz)}`;
elWeek.textContent = weekdayUpperES(tz, selectedYMD);
elDate.textContent = prettyDateES(selectedYMD);
elDateInput.value = selectedYMD;
}
function updateFooter(){
const srcName = String(city.source?.name || '').trim();
const srcUrl = safeUrl(city.source?.url || '');
elFoot.innerHTML = srcUrl
? `Fuente: ${srcName || 'Fuente oficial'}`
: (srcName ? `Fuente: ${srcName}` : '');
}
function pickSchedule(cat){
if (cat.id === "particulares" && cat.schedules){
const v = (placaVariant === "fuera") ? "fuera" : "ibague";
return Array.isArray(cat.schedules[v]) ? cat.schedules[v] : [];
}
return Array.isArray(cat.schedule) ? cat.schedule : [];
}
function render(){
const part = (city.categories || []).find(c => c.id === "particulares" && c.schedules?.ibague && c.schedules?.fuera);
elVariantWrap.style.display = part ? 'flex' : 'none';
const cats = (city.categories || []).slice().sort((a,b) => (a.priority||99)-(b.priority||99));
elGrid.innerHTML = cats.map(cat => `
${iconSvg(cat.icon || cat.id)}
${String(cat.name || cat.id).toUpperCase()}
Placas restringidas:
--
⏱
`).join('');
updateDynamic();
}
function updateDynamic(){
const nowP = nowPartsInTZ(tz);
todayTZ = ymdFromParts(nowP);
const nowMin = minutesOfDayFromParts(nowP);
const isToday = selectedYMD === todayTZ;
elTime.textContent = `Hora: ${fmtTimeNoSeconds(tz)}`;
(city.categories || []).forEach(cat => {
const card = elGrid.querySelector(`[data-cat="${cat.id}"]`);
if (!card) return;
const elValue = card.querySelector('.pz-value');
const elLabel = card.querySelector('.pz-label');
const elBand = card.querySelector('.pz-band');
const elHours = card.querySelector('.pz-bandhours');
const elCd = card.querySelector('.pz-countdown');
const elCdText = card.querySelector('.pz-counttext');
const r = resolveRestriction(city, cat, selectedYMD);
const sched = pickSchedule(cat);
if (r.kind === 'na'){
elLabel.textContent = 'Estado:';
elValue.textContent = 'NO APLICA';
elBand.classList.remove('pz-band--yellow');
elBand.classList.add('pz-band--gray');
elHours.innerHTML = `
Sin restricción
`;
elCd.style.display = 'none';
return;
}
if (r.kind === 'unknown'){
elLabel.textContent = 'Estado:';
elValue.textContent = 'SIN INFORMACIÓN';
elBand.classList.remove('pz-band--yellow');
elBand.classList.add('pz-band--gray');
elHours.innerHTML = `
Sin información
`;
elCd.style.display = 'none';
return;
}
elLabel.textContent = 'Placas restringidas:';
elValue.textContent = String(r.value);
elBand.classList.remove('pz-band--gray');
elBand.classList.add('pz-band--yellow');
elHours.innerHTML = scheduleHtmlMultiline(sched);
// Countdown solo si hoy y dentro de una franja activa
if (!isToday || !sched.length){
elCd.style.display = 'none';
return;
}
const st = getHorarioEstado(sched, nowMin);
if (st.kind !== 'active'){
elCd.style.display = 'none';
return;
}
const hm = countdownHM(st.endMin, nowMin);
elCd.style.display = 'flex';
elCdText.textContent = `Termina en: ${hm.h}h ${pad2(hm.m)}m`;
});
}
// Eventos
elCity.addEventListener('change', () => {
const nextId = String(elCity.value || '').trim();
const target = options.find(o => String(o.id) === nextId);
const url = safeUrl(target?.id ? `${baseUrl}${target.id}/` : '');
if (url) window.location.href = url;
});
elVariantBtns.forEach(btn => {
btn.addEventListener('click', () => {
placaVariant = (btn.getAttribute('data-variant') === 'fuera') ? 'fuera' : 'ibague';
elVariantBtns.forEach(b => {
const active = b === btn;
b.classList.toggle('is-active', active);
b.setAttribute('aria-checked', active ? 'true' : 'false');
});
updateDynamic();
});
});
elPrevDay.addEventListener('click', () => { selectedYMD = addDaysYMD(selectedYMD, -1); updateHeader(); render(); });
elNextDay.addEventListener('click', () => { selectedYMD = addDaysYMD(selectedYMD, +1); updateHeader(); render(); });
function openPicker(){
if (typeof elDateInput.showPicker === 'function') elDateInput.showPicker();
else elDateInput.focus();
}
elDateCenter.addEventListener('click', openPicker);
elDateInput.addEventListener('change', () => {
if (elDateInput.value){ selectedYMD = elDateInput.value; updateHeader(); render(); }
});
updateHeader();
updateFooter();
render();
root._pzTimer && clearInterval(root._pzTimer);
root._pzTimer = setInterval(() => { updateHeader(); updateDynamic(); }, 30 * 1000);
}
})();