From d6d424cae883cc4bb1a59e7acf5e25acd8a30010 Mon Sep 17 00:00:00 2001 From: solocla Date: Wed, 21 Oct 2026 12:24:12 +0200 Subject: [PATCH] update kiosk --- public/userarea/class/Kiosk.php | 15 +- public/userarea/kiosk.php | 282 +++++++++++++++++++++++++++++++- public/userarea/kiosk_admin.php | 32 +++- 3 files changed, 320 insertions(+), 9 deletions(-) diff --git a/public/userarea/class/Kiosk.php b/public/userarea/class/Kiosk.php index ba8cb431..3dca6484 100644 --- a/public/userarea/class/Kiosk.php +++ b/public/userarea/class/Kiosk.php @@ -22,6 +22,7 @@ class Kiosk 'andamento' => 'Andamento 14 giorni', 'fermi' => 'Fermi della settimana', 'coda' => 'Lavoro in arrivo', + 'meteo' => 'Meteo e previsioni', 'sicurezza' => 'Giorni senza infortuni', 'obiettivi' => 'Obiettivi', 'comunicazioni' => 'Comunicazioni', @@ -51,7 +52,8 @@ class Kiosk const DEFAULTS = [ 'company' => 'Zibo', 'slide_seconds' => '15', - 'slides' => 'linee,oggi,andamento,fermi,coda,sicurezza,obiettivi,comunicazioni,benvenuti,turni,menu', + 'slide_durations' => '', + 'slides' => 'linee,oggi,andamento,fermi,coda,meteo,sicurezza,obiettivi,comunicazioni,benvenuti,turni,menu', 'scarto_target' => '3', 'last_incident' => '', 'record_days' => '', @@ -91,6 +93,16 @@ class Kiosk return $t; } + /** Secondi per tipo di slide (vuoto = durata predefinita). */ + public static function durations(array $s) + { + $out = []; + foreach ((array)json_decode((string)$s['slide_durations'], true) as $k => $v) { + if (isset(self::SLIDES[$k]) && (int)$v > 0) $out[$k] = max(4, min(300, (int)$v)); + } + return $out; + } + /* ===================== DATE ===================== */ private static function tz() @@ -366,6 +378,7 @@ class Kiosk 'settings' => [ 'slide_seconds' => max(6, min(120, (int)$s['slide_seconds'] ?: 15)), 'slides' => array_values(array_filter(explode(',', $s['slides']))), + 'durations' => (object)self::durations($s), 'scarto_target' => (float)str_replace(',', '.', $s['scarto_target']), 'city' => $s['city'], 'lat' => $s['lat'], diff --git a/public/userarea/kiosk.php b/public/userarea/kiosk.php index 6ecfd866..1c33c858 100644 --- a/public/userarea/kiosk.php +++ b/public/userarea/kiosk.php @@ -972,6 +972,173 @@ if (($_GET['a'] ?? '') === 'data') { margin-right: .8rem; } + /* meteo */ + .wxs { + flex: 1; + display: grid; + grid-template-columns: 34rem minmax(0, 1fr); + gap: 1.4rem; + min-height: 0; + } + + .wnow { + padding: 2rem 2.2rem; + display: flex; + flex-direction: column; + background: radial-gradient(600px 300px at 30% 20%, rgba(252, 211, 77, .14), transparent 70%), var(--panel); + } + + .wbig { + display: flex; + align-items: center; + gap: 1.4rem; + margin-top: 1rem; + } + + .wbig .wi { + font-size: 8rem; + line-height: 1; + } + + .wbig span { + font-size: 8.5rem; + font-weight: 900; + line-height: 1; + } + + .wdesc { + font-size: 2.2rem; + font-weight: 800; + margin-top: .8rem; + } + + .wmeta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem 1.4rem; + margin-top: auto; + padding-top: 1.4rem; + } + + .wmeta span { + display: block; + font-size: 1rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--label); + } + + .wmeta b { + font-size: 1.9rem; + font-weight: 900; + } + + .wright { + display: flex; + flex-direction: column; + gap: 1.4rem; + min-height: 0; + } + + .whours { + padding: 1.4rem 1.8rem; + } + + .hrow { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: .8rem; + margin-top: 1rem; + } + + .hc { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: .3rem; + } + + .hc .t { + font-size: 1.3rem; + font-weight: 800; + color: var(--label); + } + + .hc .wi { + font-size: 2.8rem; + } + + .hc b { + font-size: 2.2rem; + font-weight: 900; + } + + .hc .r, + .dc .dr { + font-size: 1.2rem; + font-weight: 800; + color: var(--muted); + } + + .hi { + color: #60a5fa !important; + } + + .mid { + color: #93c5fd !important; + } + + .wdays { + flex: 1; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 1.2rem; + min-height: 0; + } + + .dc { + min-width: 0; + padding: 1.4rem 1rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: .4rem; + } + + .dc .dn { + white-space: nowrap; + font-size: 1.7rem; + font-weight: 900; + } + + .dc .wi { + font-size: 4rem; + margin: .4rem 0; + } + + .dc .dt b { + font-size: 2.6rem; + font-weight: 900; + } + + .dc .dt span { + font-size: 1.8rem; + font-weight: 800; + color: var(--muted); + margin-left: .6rem; + } + + .dc .dd { + font-size: 1.1rem; + font-weight: 700; + color: var(--label); + min-height: 2.8rem; + } + .empty { margin: auto; color: var(--muted); @@ -1101,6 +1268,12 @@ if (($_GET['a'] ?? '') === 'data') { i: 'bi-list-ol', f: slideQueue }); + if (has('meteo') && WX && WX.daily && WX.hourly) S.push({ + k: 'meteo', + t: 'Meteo' + (D.settings.city ? ' · ' + D.settings.city : ''), + i: 'bi-cloud-sun-fill', + f: slideWeather + }); if (has('sicurezza') && D.safety) S.push({ k: 'sicurezza', t: 'Sicurezza', @@ -1152,7 +1325,8 @@ if (($_GET['a'] ?? '') === 'data') { return setTimeout(next, 10000); } show(seq[pos]); - const sec = D.settings.slide_seconds * (seq[pos].k === 'comunicazioni' || seq[pos].k === 'turni' || seq[pos].k === 'menu' ? 1.3 : 1); + // durata impostata per questo tipo di slide, altrimenti quella predefinita (un po' di più per i testi) + const sec = (D.settings.durations || {})[seq[pos].k] || D.settings.slide_seconds * (seq[pos].k === 'comunicazioni' || seq[pos].k === 'turni' || seq[pos].k === 'menu' ? 1.3 : 1); const p = $('#prog'); p.style.transition = 'none'; p.style.width = '0'; @@ -1461,18 +1635,116 @@ if (($_GET['a'] ?? '') === 'data') { setInterval(weather, 30 * 60000); } + // meteo: ora, prossime ore e prossimi giorni (open-meteo, gratuito e senza chiave) + let WX = null; + const WX_TXT = { + 0: 'Sereno', + 1: 'Quasi sereno', + 2: 'Parzialmente nuvoloso', + 3: 'Coperto', + 45: 'Nebbia', + 48: 'Nebbia con brina', + 51: 'Pioviggine leggera', + 53: 'Pioviggine', + 55: 'Pioviggine intensa', + 56: 'Pioviggine gelata', + 57: 'Pioviggine gelata', + 61: 'Pioggia debole', + 63: 'Pioggia', + 65: 'Pioggia forte', + 66: 'Pioggia gelata', + 67: 'Pioggia gelata forte', + 71: 'Neve debole', + 73: 'Neve', + 75: 'Neve forte', + 77: 'Nevischio', + 80: 'Rovesci leggeri', + 81: 'Rovesci', + 82: 'Rovesci violenti', + 85: 'Rovesci di neve', + 86: 'Rovesci di neve forti', + 95: 'Temporale', + 96: 'Temporale con grandine', + 99: 'Temporale con grandine' + }; + + function wxIcon(w, day = true) { + w = +w; + if (w === 0) return day ? ['bi-sun-fill', '#fcd34d'] : ['bi-moon-stars-fill', '#c7d2fe']; + if (w <= 2) return day ? ['bi-cloud-sun-fill', '#fcd34d'] : ['bi-cloud-moon-fill', '#c7d2fe']; + if (w === 3) return ['bi-cloud-fill', '#cbd5e1']; + if (w <= 48) return ['bi-cloud-fog2-fill', '#94a3b8']; + if (w <= 57) return ['bi-cloud-drizzle-fill', '#7dd3fc']; + if (w <= 67) return ['bi-cloud-rain-fill', '#60a5fa']; + if (w <= 77) return ['bi-snow', '#e0f2fe']; + if (w <= 82) return ['bi-cloud-rain-heavy-fill', '#3b82f6']; + if (w <= 86) return ['bi-cloud-snow-fill', '#e0f2fe']; + return ['bi-cloud-lightning-rain-fill', '#a78bfa']; + } + const ico = (w, day, cls = '') => { + const [i, c] = wxIcon(w, day); + return ``; + }; + function weather() { const s = D.settings; - fetch(`https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}¤t=temperature_2m,weather_code&timezone=auto`) + const q = `latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}&timezone=Europe%2FRome&forecast_days=7` + + '¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day' + + '&hourly=temperature_2m,weather_code,precipitation_probability,is_day' + + '&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset'; + fetch('https://api.open-meteo.com/v1/forecast?' + q) .then(r => r.json()).then(r => { const c = r.current; if (!c) return; - const w = +c.weather_code; - const ic = w === 0 ? 'bi-sun-fill' : w <= 2 ? 'bi-cloud-sun-fill' : w === 3 ? 'bi-cloud-fill' : w <= 48 ? 'bi-cloud-fog2-fill' : w <= 67 ? 'bi-cloud-rain-fill' : w <= 77 ? 'bi-snow' : w <= 82 ? 'bi-cloud-rain-heavy-fill' : 'bi-cloud-lightning-rain-fill'; - $('#wx').innerHTML = `
${Math.round(c.temperature_2m)}°${esc(s.city || '')}
`; + WX = r; + $('#wx').innerHTML = `${ico(c.weather_code, c.is_day !== 0)}
${Math.round(c.temperature_2m)}°${esc(s.city || '')}
`; }).catch(() => {}); } + function slideWeather() { + const c = WX.current, + d = WX.daily, + h = WX.hourly; + const hhmm = t => String(t).slice(11, 16); + // prossime 8 ore a partire dall'ora attuale + const nowH = String(c.time).slice(0, 13); + let i0 = h.time.findIndex(t => t.slice(0, 13) >= nowH); + if (i0 < 0) i0 = 0; + const hours = []; + for (let i = i0 + 1; i < h.time.length && hours.length < 8; i++) hours.push(i); + const GG = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; + const days = d.time.map((t, i) => i).slice(1, 7); + const rainCls = p => p >= 60 ? 'hi' : (p >= 30 ? 'mid' : ''); + return `
+
+
Adesso${D.settings.city ? ' a ' + esc(D.settings.city) : ''}
+
${ico(c.weather_code, c.is_day !== 0)}${Math.round(c.temperature_2m)}°
+
${esc(WX_TXT[c.weather_code] || '')}
+
+
Percepita${Math.round(c.apparent_temperature)}°
+
Min / max${Math.round(d.temperature_2m_min[0])}° / ${Math.round(d.temperature_2m_max[0])}°
+
Umidità${Math.round(c.relative_humidity_2m)}%
+
Vento${Math.round(c.wind_speed_10m)} km/h
+
Pioggia oggi${d.precipitation_probability_max[0] ?? 0}%
+
Alba · tramonto${hhmm(d.sunrise[0])} · ${hhmm(d.sunset[0])}
+
+
+
+
+
Prossime ore
+
${hours.map(i => `
${hhmm(h.time[i])}${ico(h.weather_code[i], h.is_day[i] !== 0)}${Math.round(h.temperature_2m[i])}° ${h.precipitation_probability[i] ?? 0}%
`).join('')}
+
+
${days.map(i => { + const dt = new Date(d.time[i] + 'T12:00:00'); + return `
${i === 1 ? 'Domani' : GG[dt.getDay()] + ' ' + dt.getDate()}
${ico(d.weather_code[i], true)} +
${Math.round(d.temperature_2m_max[i])}°${Math.round(d.temperature_2m_min[i])}°
+
${esc(WX_TXT[d.weather_code[i]] || '')}
+
${d.precipitation_probability_max[i] ?? 0}%
`; + }).join('')}
+
+
`; + } + /* ---------- avvio ---------- */ tick(); load().then(() => next()); diff --git a/public/userarea/kiosk_admin.php b/public/userarea/kiosk_admin.php index 7908df29..8c9ca09a 100644 --- a/public/userarea/kiosk_admin.php +++ b/public/userarea/kiosk_admin.php @@ -121,6 +121,7 @@ if ($action !== '') { 'company' => mb_substr(trim((string)($_POST['company'] ?? '')), 0, 60), 'slide_seconds' => (string)max(6, min(120, (int)($_POST['slide_seconds'] ?? 15))), 'slides' => implode(',', $slides), + 'slide_durations' => json_encode((object)array_filter(array_map('intval', array_intersect_key((array)($_POST['dur'] ?? []), Kiosk::SLIDES)), fn($v) => $v > 0)), 'scarto_target' => (string)max(0, (float)str_replace(',', '.', (string)($_POST['scarto_target'] ?? 3))), 'city' => mb_substr(trim((string)($_POST['city'] ?? '')), 0, 60), 'lat' => is_numeric($_POST['lat'] ?? '') ? (string)(float)$_POST['lat'] : '', @@ -507,6 +508,23 @@ if ($action !== '') { line-height: 1; } + .sl-row { + display: flex; + align-items: center; + gap: 10px; + padding: 3px 0; + border-bottom: 1px dashed var(--line); + } + + .sl-row .form-check { + flex: 1; + } + + .sl-sec { + width: 92px; + flex-shrink: 0; + } + .slide-list .form-check { padding: 6px 10px 6px 2.6em; border-radius: 8px; @@ -627,7 +645,7 @@ if ($action !== '') {
Impostazioni
-
+
@@ -635,7 +653,8 @@ if ($action !== '') {
- + +
Secondi vuoti = durata predefinita. Le slide con più contenuti (es. 3 comunicazioni) usano la durata per ciascuno.
@@ -1005,7 +1024,14 @@ if ($action !== '') { $('#cLon').val(s.lon); $('#geoTxt').text(s.lat ? `Posizione: ${s.lat}, ${s.lon}` : 'Scrivi la città e premi "Trova" per attivare il meteo'); const on = s.slides.split(','); - $('#slideList').html(Object.entries(L.slides).map(([k, v]) => `
`).join('')); + let dur = {}; + try { + dur = JSON.parse(s.slide_durations || '{}') || {}; + } catch (e) {} + $('#slideList').html(Object.entries(L.slides).map(([k, v]) => `
+
+
s
+
`).join('')); } // sicurezza e impostazioni si salvano insieme (stessa tabella)