`; } }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initAll); else initAll(); // Elementor sometimes mounts later window.addEventListener('elementor/frontend/init', () => setTimeout(initAll, 60)); let tries = 0; const t = setInterval(() => { tries++; initAll(); 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 toMinutes(hhmm){ if (!isHHMM(hhmm)) return null; const [h,m] = hhmm.split(':').map(Number); return h*60 + m; } 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){ const s = String(slot.start), e = String(slot.end); if (!isHHMM(s) || !isHHMM(e)) return ''; const sMin = toMinutes(s), eMin = toMinutes(e); const crosses = (sMin !== null && eMin !== null && eMin < sMin); return crosses ? `${time24to12ES(s)} – ${time24to12ES(e)} (día sig.)` : `${time24to12ES(s)} – ${time24to12ES(e)}`; } function scheduleHtml(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("")} `; } // Detecta si está activo ahora y soporta cruce de medianoche function getHorarioEstado(schedule, nowMin){ const raw = (Array.isArray(schedule) ? schedule : []) .map(s => ({ s: toMinutes(String(s.start||'')), e: toMinutes(String(s.end||'')) })) .filter(x => x.s !== null && x.e !== null); if (!raw.length) return { kind:'none', endMin:null }; const slots = []; for (const slot of raw){ if (slot.e >= slot.s) slots.push(slot); else { slots.push({ s: slot.s, e: 1440 }); slots.push({ s: 0, e: slot.e, _carry:true }); } } slots.sort((a,b) => a.s - b.s); for (const slot of slots){ if (nowMin >= slot.s && nowMin < slot.e) return { kind:'active', endMin: slot.e, carry: !!slot._carry }; } 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 : ''; } // Emojis (como pediste) function iconEmoji(key){ const k = String(key||'').toLowerCase(); if (k.includes('taxi')) return '🚕'; if (k.includes('moto')) return '🛵'; return '🚗'; } function resolveRestriction(cat, ymd){ const rule = cat.rule || {}; if (rule.type !== "month_map") return { value:"SIN INFORMACIÓN", kind:"unknown", labelMode: rule.labelMode || "terminadas" }; const ym = ymd.slice(0,7); const bucket = rule.months && rule.months[ym] ? rule.months[ym] : null; if (!bucket) return { value:"SIN INFORMACIÓN", kind:"unknown", labelMode: rule.labelMode || "terminadas" }; const noA = Array.isArray(bucket.no_aplica) ? bucket.no_aplica : []; if (noA.includes(ymd)) return { value:"NO APLICA", kind:"na", labelMode: rule.labelMode || "terminadas" }; const v = bucket.restricted && bucket.restricted[ymd] ? String(bucket.restricted[ymd]) : ""; if (!v) return { value:"SIN INFORMACIÓN", kind:"unknown", labelMode: rule.labelMode || "terminadas" }; return { value: v, kind:"digits", labelMode: rule.labelMode || "terminadas" }; } 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.
`; 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'); // Select cities 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; 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}` : ''); } let visibleNodes = new Map(); function render(){ const cats = (city.categories || []).slice().sort((a,b) => (a.priority||99)-(b.priority||99)); elGrid.innerHTML = cats.map(cat => `
${iconEmoji(cat.icon || cat.id)}
${String(cat.name || cat.id).toUpperCase()}
Placas terminadas en:
--
⏱
`).join(''); visibleNodes = new Map(); elGrid.querySelectorAll('.pz-card').forEach(card => { const catId = card.getAttribute('data-cat'); visibleNodes.set(catId, { label: card.querySelector('.pz-label'), value: card.querySelector('.pz-value'), cd: card.querySelector('.pz-countdown'), cdText: card.querySelector('.pz-counttext'), pill: card.querySelector('.pz-horariopill'), pillHours: card.querySelector('.pz-horariopill-hours') }); }); updateDynamic(); } function updateDynamic(){ const nowP = nowPartsInTZ(tz); todayTZ = ymdFromParts(nowP); const nowMin = minutesOfDayFromParts(nowP); const isToday = selectedYMD === todayTZ; elTime.textContent = `Hora: ${fmtTimeNoSeconds(tz)}`; const cats = (city.categories || []).slice().sort((a,b) => (a.priority||99)-(b.priority||99)); for (const cat of cats){ const refs = visibleNodes.get(String(cat.id)); if (!refs) continue; const r = resolveRestriction(cat, selectedYMD); const sched = Array.isArray(cat.schedule) ? cat.schedule : []; // Label correcto: iniciadas vs terminadas refs.label.textContent = (String(r.labelMode) === 'iniciadas') ? 'Placas iniciadas en:' : 'Placas terminadas en:'; // Estado if (r.kind === 'na'){ refs.value.textContent = 'NO APLICA'; refs.pill.classList.remove('pz-horariopill--yellow'); refs.pill.classList.add('pz-horariopill--gray'); refs.pillHours.innerHTML = `
Sin restricción
`; refs.cd.style.display = 'none'; refs.cdText.textContent = ''; continue; } if (r.kind === 'unknown'){ refs.value.textContent = 'SIN INFORMACIÓN'; refs.pill.classList.remove('pz-horariopill--yellow'); refs.pill.classList.add('pz-horariopill--gray'); refs.pillHours.innerHTML = `
Sin información
`; refs.cd.style.display = 'none'; refs.cdText.textContent = ''; continue; } // Restricción normal refs.value.textContent = String(r.value); refs.pill.classList.remove('pz-horariopill--gray'); refs.pill.classList.add('pz-horariopill--yellow'); refs.pillHours.innerHTML = scheduleHtml(sched); // Countdown sólo si HOY y estamos en horario activo const canCount = isToday && r.kind === 'digits' && Array.isArray(sched) && sched.length > 0; if (!canCount){ refs.cd.style.display = 'none'; refs.cdText.textContent = ''; continue; } const st = getHorarioEstado(sched, nowMin); if (st.kind !== 'active'){ refs.cd.style.display = 'none'; refs.cdText.textContent = ''; continue; } const hm = countdownHM(st.endMin, nowMin); refs.cd.style.display = 'flex'; refs.cdText.textContent = `Termina en: ${hm.h}h ${pad2(hm.m)}m`; } } // Redirección por ciudad (patrón /slug/) elCity.addEventListener('change', () => { const nextId = String(elCity.value || '').trim(); const url = safeUrl(nextId ? `${baseUrl}${nextId}/` : ''); if (url) window.location.href = url; }); 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); elDateCenter.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); 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); } })();