Ansagen
Ansagen werden geladen …
= dashboard_html($announcement['end_label']) ?> - -
Keine Aushänge vorgeplant.
- - -Bibelwort für heute
Bibelwort wird geladen …
Monatsplan
Monatsplan wird geladen …
Aushänge
+Aushänge werden geladen …
diff --git a/web/intranet_repo/projects/contentpanel/insertion/insert.php b/web/intranet_repo/projects/contentpanel/insertion/insert.php index 84b23f1..b73efa5 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/insert.php +++ b/web/intranet_repo/projects/contentpanel/insertion/insert.php @@ -9,7 +9,9 @@ if (!in_array($generalPermission, [1, '1', true, 't', 'true'], true)) { exit(); } -contentpanel_cleanup_expired_news(); +$today = date('Y-m-d'); +contentpanel_cleanup_expired_news(); +contentpanel_cleanup_expired_announcements($pdo, $today); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = trim((string) ($_POST['action'] ?? '')); @@ -52,8 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $flash = $_SESSION['contentpanel_flash'] ?? null; unset($_SESSION['contentpanel_flash']); -$today = date('Y-m-d'); -$services = contentpanel_fetch_services($pdo, $today); +$services = contentpanel_fetch_services($pdo, $today); $announcements = contentpanel_fetch_announcements($pdo, $today); $newsItems = contentpanel_list_news(); $pdo = null; diff --git a/web/intranet_repo/projects/contentpanel/insertion/util/announcements.php b/web/intranet_repo/projects/contentpanel/insertion/util/announcements.php new file mode 100644 index 0000000..bc2747d --- /dev/null +++ b/web/intranet_repo/projects/contentpanel/insertion/util/announcements.php @@ -0,0 +1,13 @@ +prepare( + 'DELETE FROM ansagen WHERE COALESCE(ansage_datum_bis, ansage_datum_von) < ?' + ); + $statement->execute([$today]); +} diff --git a/web/intranet_repo/projects/contentpanel/insertion/util/contentpanel_helpers.php b/web/intranet_repo/projects/contentpanel/insertion/util/contentpanel_helpers.php index 67b9355..86bc235 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/util/contentpanel_helpers.php +++ b/web/intranet_repo/projects/contentpanel/insertion/util/contentpanel_helpers.php @@ -2,6 +2,7 @@ require_once __DIR__ . '/news.php'; require_once __DIR__ . '/news_queue.php'; +require_once __DIR__ . '/announcements.php'; function contentpanel_html($value): string { @@ -294,6 +295,6 @@ function contentpanel_fetch_announcements(PDO $pdo, string $today): array return contentpanel_query($pdo, 'SELECT id, ansage_titel AS title, ansage_untertitel AS subtitle, ansage_datum_von AS date, ansage_datum_bis AS end_date, ansage_uhrzeit AS time, ansage_ort AS location - FROM ansagen WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ? ORDER BY ansage_datum_von', - [$today, $today])->fetchAll(PDO::FETCH_ASSOC); + FROM ansagen WHERE COALESCE(ansage_datum_bis, ansage_datum_von) >= ? ORDER BY ansage_datum_von', + [$today])->fetchAll(PDO::FETCH_ASSOC); } diff --git a/web/intranet_repo/projects/dashboard/config.php b/web/intranet_repo/projects/dashboard/config.php new file mode 100644 index 0000000..54e2c80 --- /dev/null +++ b/web/intranet_repo/projects/dashboard/config.php @@ -0,0 +1,21 @@ + 39.61298125, + 'POSTER_MIN_RENDER_HEIGHT_OFFSET_PX' => 2.85866875, + 'POSTER_MAX_VISIBLE_PAGES' => 12, + 'POSTER_MIN_DWELL_TIME_S' => 12, + 'ANNOUNCEMENT_MIN_FONT_SIZE_PX' => 20, + 'ANNOUNCEMENT_META_MIN_FONT_SIZE_PX' => 15, + 'BIBLE_FONT_SIZE_PX' => 23, + 'ANNOUNCEMENT_LINE_HEIGHT' => 1.35, + 'ANNOUNCEMENT_MAX_LINE_LENGTH_CH' => 85, + 'CONTENT_MIN_CONTRAST_RATIO' => 4.5, + // Ohne Aushänge gibt es keinen Durchlauf; dann weiterhin regelmäßig prüfen. + 'EMPTY_REFRESH_MS' => 30000, + 'REQUEST_TIMEOUT_MS' => 8000, +]; diff --git a/web/intranet_repo/projects/dashboard/dashboard.js b/web/intranet_repo/projects/dashboard/dashboard.js index ce79d58..2ab82e8 100644 --- a/web/intranet_repo/projects/dashboard/dashboard.js +++ b/web/intranet_repo/projects/dashboard/dashboard.js @@ -1,236 +1,435 @@ (() => { 'use strict'; - const CONTENT_REFRESH_MS = 30_000; - const INDOOR_REFRESH_MS = 10_000; - const OUTDOOR_REFRESH_MS = 30_000; - const VERTICAL_SPEED_PX_PER_SECOND = 7; - const HORIZONTAL_SPEED_PX_PER_SECOND = 10; + const config = JSON.parse(document.getElementById('dashboard-config').textContent); + const root = document.body; + for (const [property, value] of Object.entries({ + '--announcement-detail-font': 'calc(' + config.ANNOUNCEMENT_MIN_FONT_SIZE_PX + ' * var(--dashboard-unit))', + '--announcement-meta-font': 'calc(' + config.ANNOUNCEMENT_META_MIN_FONT_SIZE_PX + ' * var(--dashboard-unit))', + '--announcement-leading': config.ANNOUNCEMENT_LINE_HEIGHT, + '--bible-font': 'calc(' + config.BIBLE_FONT_SIZE_PX + ' * var(--dashboard-unit))', + '--announcement-measure': config.ANNOUNCEMENT_MAX_LINE_LENGTH_CH + 'ch', + })) root.style.setProperty(property, value); - const WEATHER_ICONS = { - cloudy: ``, - sun: ``, - moon: ``, - cloudy_day: ``, - cloudy_night: ``, - rain_shower: ``, - rain: ``, - thunder: ``, - snowy: ``, - mist: ``, - }; + function node(tag, className, text) { + const element = document.createElement(tag); + element.className = className; + if (text !== undefined) element.textContent = text; + return element; + } - function setText(selector, value) { - const element = document.querySelector(selector); - if (element) { - element.textContent = value ?? ''; + function empty(stage, text) { + stage.replaceChildren(node('p', 'empty-state', text)); + } + + // Timeout und HTTP-Prüfung gelten für Inhalte und Bilder gleichermaßen. + async function request(url, read, options = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.REQUEST_TIMEOUT_MS); + try { + const response = await fetch(url, { ...options, signal: controller.signal }); + if (!response.ok) throw new Error('HTTP ' + response.status); + return await read(response); + } finally { + clearTimeout(timeout); } } - function updateClock() { + const aspectRatio = pages => pages.reduce((sum, item) => sum + item.width / item.height, 0); + const layoutKey = layout => [layout.width, layout.height, layout.groups.map(group => group.length)].join('/'); + + const dayFormatter = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit', + }); + function berlinDay(date = new Date()) { + const parts = Object.fromEntries(dayFormatter.formatToParts(date).map(part => [part.type, part.value])); + return parts.year + '-' + parts.month + '-' + parts.day; + } + let currentDay = berlinDay(); + + function groupDocuments(pages) { + const entries = new Map(); + for (const item of pages) { + const key = item.document ?? item.url; + if (!entries.has(key)) entries.set(key, []); + entries.get(key).push(item); + } + return [...entries.values()].map(pages => pages.sort((a, b) => a.page - b.page)); + } + + class ImageGallery { + constructor(id, message, rotate = false) { + Object.assign(this, { + message, rotate, items: [], current: [], images: new Map(), + index: 0, generation: 0, signature: '', layoutKey: '', failed: false, pending: false, + }); + this.panel = document.getElementById(id); + this.stage = this.panel.querySelector('.image-stage'); + this.progress = this.panel.querySelector('.poster-progress'); + new ResizeObserver(() => this.resize()).observe(this.stage); + } + + layout() { + // Keine Ganzzahlrundung durch clientHeight: Original-Bildhöhe exakt erhalten. + const bounds = this.stage.getBoundingClientRect(); + const style = getComputedStyle(this.stage); + const width = bounds.width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); + const height = bounds.height - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); + const gap = this.rotate ? parseFloat(style.columnGap) : 0; + const frame = this.rotate ? parseFloat(style.getPropertyValue('--poster-frame-width')) : 0; + if (!this.rotate) return { groups: [this.items], width, height, gap, frame }; + const minimum = innerHeight * config.POSTER_MIN_RENDER_HEIGHT_VH / 100 + - config.POSTER_MIN_RENDER_HEIGHT_OFFSET_PX; + const occupied = pages => minimum * aspectRatio(pages) + groupDocuments(pages).length * (2 * frame + gap); + const fits = pages => pages.length <= config.POSTER_MAX_VISIBLE_PAGES + && occupied(pages) <= width + gap; + // Nur Aushänge aufteilen, die selbst nicht in eine Ansicht passen. + const chunks = []; + for (const pages of groupDocuments(this.items)) { + let chunk = []; + for (const item of pages) { + if (chunk.length && !fits([...chunk, item])) { + chunks.push(chunk); + chunk = []; + } + chunk.push(item); + } + if (chunk.length) chunks.push(chunk); + } + // Große Dokumente zuerst, kleinere in die Lücken: Datum ist nachrangig. + chunks.sort((a, b) => occupied(b) - occupied(a)); + const groups = []; + for (const chunk of chunks) { + const group = groups.find(group => fits([...group, ...chunk])); + if (group) group.push(...chunk); + else groups.push([...chunk]); + } + // Zuerst vollständige Dokumente aus der vorigen Ansicht übernehmen, + // verbleibende Plätze nötigenfalls mit einzelnen Seiten auffüllen. + const primaryGroups = groups.map(group => [...group]); + for (let index = 0; groups.length > 1 && index < groups.length; index++) { + const current = groups[index]; + const previous = primaryGroups[(index + groups.length - 1) % groups.length]; + const available = previous.filter(item => + !current.some(entry => entry.url === item.url)); + for (const pages of groupDocuments(available).reverse()) { + if (fits([...current, ...pages])) current.unshift(...pages); + } + for (const item of [...available].reverse()) { + if (!current.some(entry => entry.url === item.url) + && fits([...current, item])) current.unshift(item); + } + groups[index] = groupDocuments(current).flat(); + } + return { groups, width, height, gap, frame }; + } + + loadImage(item) { + if (!this.images.has(item.url)) { + let localUrl; + this.images.set(item.url, request(item.url, async response => { + localUrl = URL.createObjectURL(await response.blob()); + const image = new Image(); + image.alt = item.alt; + image.dataset.source = item.url; + image.src = localUrl; + await image.decode(); + return image; + }).catch(() => { + if (localUrl) URL.revokeObjectURL(localUrl); + this.images.delete(item.url); + return null; + })); + } + return this.images.get(item.url); + } + + pruneImages() { + for (const [url, pending] of this.images) { + if (this.items.some(item => item.url === url) || this.current.some(item => item.url === url)) continue; + this.images.delete(url); + pending.then(image => { if (image) URL.revokeObjectURL(image.src); }); + } + } + + update(items, message = this.message) { + const signature = JSON.stringify([items, message]); + if (signature === this.signature && !this.failed) return; + this.signature = signature; + this.message = message; + this.items = items; + // Nur neue Bildversionen laden; unveränderte Blob-URLs wiederverwenden. + for (const item of items) this.loadImage(item); + if (!items.length) { + this.generation++; + clearTimeout(this.timer); + this.current = []; + this.pending = false; + empty(this.stage, message); + this.pruneImages(); + this.schedule(); + return; + } + // Nur abgelaufene Seiten sofort entfernen; sonst bleiben vorhandene + // Bilder bis zum vollständig geladenen Ersatz sichtbar. + if (this.rotate && this.current.some(item => item.expires < currentDay)) { + this.current = []; + empty(this.stage, this.message); + } + if (this.rotate && this.current.length) { + this.pending = true; + return; + } + return this.show(0); + } + + resize() { + if (this.items.length && layoutKey(this.layout()) !== this.layoutKey) this.show(this.index); + } + + schedule() { + clearTimeout(this.timer); + if (!this.rotate) return; + const duration = this.items.length ? config.POSTER_MIN_DWELL_TIME_S * 1000 : config.EMPTY_REFRESH_MS; + this.progressAnimation?.cancel(); + this.progress.toggleAttribute('hidden', !this.items.length); + if (this.items.length) { + this.progressAnimation = this.progress.querySelector('.poster-progress-value').animate( + [{ strokeDashoffset: 100 }, { strokeDashoffset: 0 }], + { duration, easing: 'linear', fill: 'forwards' }); + } + this.timer = setTimeout(async () => { + if (this.items.length && !this.pending && this.index + 1 < this.layout().groups.length) { + return this.show(this.index + 1); + } + // Texte und Bildbestand nach einem vollständigen Durchlauf prüfen. + await this.onCycle?.(); + if (this.items.length && (this.pending || this.failed || this.index !== 0)) this.show(0); + else this.schedule(); + }, duration); + } + + async show(index) { + if (!this.items.length) return; + this.pending = false; + const layout = this.layout(); + this.layoutKey = layoutKey(layout); + this.index = index % layout.groups.length; + const batch = layout.groups[this.index]; + const documents = this.rotate ? groupDocuments(batch) : [batch]; + const renderHeight = Math.min(layout.height - 2 * layout.frame, + (layout.width - (documents.length - 1) * layout.gap - documents.length * 2 * layout.frame) / aspectRatio(batch)); + const generation = ++this.generation; + clearTimeout(this.timer); + const sheet = node('div', 'image-sheet'); + let failures = 0; + // Gemeinsam gerahmte Seiten laden unabhängig; erst die fertige Ansicht einblenden. + await Promise.all(documents.flatMap(pages => { + const wrapper = this.rotate ? node('div', 'poster-document') : sheet; + if (this.rotate) sheet.append(wrapper); + return pages.map(async item => { + const cell = node('div', 'image-cell'); + cell.style.height = renderHeight + 'px'; + cell.style.width = (renderHeight * item.width / item.height) + 'px'; + wrapper.append(cell); + const copy = (await this.loadImage(item))?.cloneNode(); + if (copy) { + try { + await copy.decode(); + cell.append(copy); + return; + } catch { /* Die übrigen Seiten bleiben unabhängig darstellbar. */ } + } + failures++; + empty(cell, 'Seite zurzeit nicht verfügbar.'); + }); + })); + if (generation !== this.generation) return; + if (!batch.every(item => this.items.some(entry => entry.url === item.url))) return this.show(0); + this.failed = failures > 0; + // Einen vorhandenen Monatsplan bei defektem Ersatz nicht wegblenden. + if (!this.rotate && failures && this.stage.querySelector('img')) return; + this.current = batch; + this.stage.replaceChildren(sheet); + this.pruneImages(); + if (failures === batch.length) empty(this.stage, 'Bilder zurzeit nicht verfügbar.'); + this.schedule(); // Mindeststandzeit erst nach vollständigem Laden der Gruppe. + } + } + + // Eine Textseite enthält ausschließlich DOM-Text, niemals HTML aus der Datenbank. + function paragraphs(className, entries) { + const container = node('div', className); + for (const [name, text] of entries) if (text) container.append(node('p', name, text)); + return container; + } + + function announcementList(items) { + const list = node('ul', 'text-page announcement-list'); + for (const item of items) { + const entry = node('li', 'announcement-entry'); + entry.append(paragraphs('announcement-when', [ + ['announcement-date', item.date_label || 'Ohne Datum'], + ['announcement-end', item.end_label], + ]), paragraphs('announcement-content', [ + ['announcement-title', item.title], + ['announcement-subtitle', item.subtitle], + ['announcement-meta', [item.time ? item.time + ' Uhr' : '', item.place].filter(Boolean).join(', ')], + ])); + list.append(entry); + } + return list; + } + + class FixedText { + constructor(id, render = entries => paragraphs('text-page', entries)) { + this.stage = document.getElementById(id).querySelector('.text-stage'); + this.render = render; + this.signature = ''; + this.loaded = false; + new ResizeObserver(() => this.rebuild()).observe(this.stage); + } + + update(documents, message) { + this.loaded = true; + const signature = JSON.stringify([documents, message]); + if (signature === this.signature) return; + this.signature = signature; + if (documents.length) this.stage.replaceChildren(this.render(documents)); + else empty(this.stage, message); + this.rebuild(); + } + + unavailable(message) { + // Auch ein erfolgreich gelesener Leerzustand bleibt bei Abruffehlern stehen. + if (this.loaded) return; + empty(this.stage, message); + this.rebuild(); + } + + rebuild() { + const page = this.stage.querySelector('.text-page'); + this.stage.classList.toggle('is-overflowing', Boolean(page && this.stage.clientHeight + && page.getBoundingClientRect().height > this.stage.clientHeight + 1)); + } + } + + const month = new ImageGallery('month-panel', 'Kein Monatsplan vorhanden.'); + const posters = new ImageGallery('poster-panel', 'Keine aktuellen Aushänge.', true); + const announcements = new FixedText('announcements-panel', announcementList); + const bible = new FixedText('bible-panel'); + let imageData = null; + + function layoutImages() { + if (!imageData) return; + // Monatsplan dauerhaft separat: keine Vermischung mit der Aushangrotation. + const months = imageData.month_images; + if (months.length) root.style.setProperty('--month-aspect', aspectRatio(months)); + return Promise.all([ + month.update(months), + posters.update(imageData.posters.filter(item => item.expires >= currentDay)), + ]); + } + + function contentLoaded(data) { + if (data.date !== currentDay) return contentUnavailable(); + if (data.errors.includes('announcements')) announcements.unavailable('Ansagen zurzeit nicht verfügbar.'); + else announcements.update(data.announcements, 'Es sind keine Ansagen eingetragen.'); + + const service = data.service; + const blocks = service ? [ + ['bible-reference', service.reference || 'Bibelstelle nicht eingetragen.'], + ['bible-verse', service.text || 'Bibelvers nicht eingetragen.'], + ] : []; + if (data.errors.includes('service')) bible.unavailable('Bibelwort zurzeit nicht verfügbar.'); + else bible.update(blocks, 'Für heute ist kein Bibelwort eingetragen.'); + } + + function contentUnavailable() { + announcements.unavailable('Ansagen zurzeit nicht verfügbar.'); + bible.unavailable('Bibelwort zurzeit nicht verfügbar.'); + } + + function imagesUnavailable() { + if (!imageData) { + month.update([], 'Monatsplan zurzeit nicht verfügbar.'); + posters.update([], 'Aushänge zurzeit nicht verfügbar.'); + } + } + + // Unabhängige Hintergrundabrufe, ohne Ladezustand und ohne doppelte Anfragen. + function createRefresh(resource, onData, onError) { + let inFlight = null; + return () => inFlight ??= request('data.php?resource=' + resource, + async response => onData(await response.json()), { cache: 'no-store' }) + .catch(onError).finally(() => { inFlight = null; }); + } + + const timeFormat = new Intl.DateTimeFormat('de-DE', { + timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', + }); + const dateFormat = new Intl.DateTimeFormat('de-DE', { + timeZone: 'Europe/Berlin', day: '2-digit', month: 'long', year: 'numeric', + }); + function tick() { const now = new Date(); - const clock = document.getElementById('uhrzeit'); - if (clock) { - clock.textContent = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; + const nextDay = berlinDay(now); + document.getElementById('clock').textContent = timeFormat.format(now); + document.getElementById('clock').dateTime = now.toISOString(); + document.getElementById('date').textContent = dateFormat.format(now); + document.getElementById('date').dateTime = nextDay; + if (nextDay !== currentDay) { + currentDay = nextDay; + // Texte erst mit einer erfolgreichen Antwort austauschen; abgelaufene + // Aushänge weiterhin sofort entfernen. + layoutImages(); + refreshAll(); } } - function renderAnnouncements(announcements) { - const container = document.querySelector('.news-scroll'); - if (!container || !Array.isArray(announcements)) { - return; - } - - container.replaceChildren(); - announcements.forEach((announcement) => { - const box = document.createElement('div'); - box.className = 'news-box'; - - const date = document.createElement('div'); - date.className = 'news-date'; - date.append(document.createTextNode(announcement.date_label || '')); - if (announcement.end_label) { - date.append(document.createElement('br'), document.createTextNode(announcement.end_label)); - } - - const body = document.createElement('div'); - body.append(document.createTextNode(announcement.title || '')); - - if (announcement.subtitle) { - const subtitle = document.createElement('div'); - subtitle.className = 'news-subtitle'; - subtitle.textContent = announcement.subtitle; - body.append(subtitle); - } - - if (announcement.meta) { - const meta = document.createElement('div'); - meta.className = 'news-subtitle'; - const italic = document.createElement('i'); - italic.textContent = announcement.meta; - meta.append(italic); - body.append(meta); - } - - box.append(date, body); - container.append(box); - }); + const refreshContent = createRefresh('content', contentLoaded, contentUnavailable); + const refreshImages = createRefresh('images', data => { + imageData = data; + return layoutImages(); + }, imagesUnavailable); + function refreshAll() { + return Promise.all([refreshContent(), refreshImages()]); } - - function renderPosters(posters) { - const container = document.querySelector('.aushang-flex'); - if (!container || !Array.isArray(posters)) { - return; + posters.onCycle = refreshAll; + refreshAll(); + tick(); + setInterval(tick, 1000); + document.fonts.ready.then(() => { + announcements.rebuild(); + bible.rebuild(); + // Ein Mindestkontrast wird auch bei später geänderten zentralen Tokens geprüft. + const style = getComputedStyle(root); + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + const context = canvas.getContext('2d', { willReadFrequently: true }); + function luminance(value) { + context.fillStyle = value; + context.fillRect(0, 0, 1, 1); + const channels = Array.from(context.getImageData(0, 0, 1, 1).data).slice(0, 3); + return channels.map(channel => { + const c = channel / 255; + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }).reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index], 0); } - - container.replaceChildren(); - if (posters.length === 0) { - const empty = document.createElement('p'); - empty.style.textAlign = 'left'; - empty.textContent = 'Keine Aushänge vorgeplant.'; - container.append(empty); - return; + const background = Math.min(...Array.from(document.querySelectorAll( + '.dashboard-panel, .dashboard-overview, .panel-heading, .announcement-entry'), + panel => luminance(getComputedStyle(panel).backgroundColor))); + for (const [property, token] of [['--dashboard-text', '--nak-black'], ['--dashboard-meta', '--gray-700']]) { + const contrast = (background + 0.05) / (luminance(style.getPropertyValue(token)) + 0.05); + if (contrast < config.CONTENT_MIN_CONTRAST_RATIO) root.style.setProperty(property, 'var(--nak-black)'); } + }); - posters.forEach((poster) => { - const image = document.createElement('img'); - image.src = poster.url; - image.alt = poster.alt || 'Aushang'; - image.style.height = '95%'; - image.addEventListener('load', () => scheduleScrollingSetup(0), { once: true }); - container.append(image); - }); - } - - function renderMonthImages(images) { - if (!Array.isArray(images)) { - return; + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + tick(); + // Nach Rückkehr bekommt die sichtbare Seite erneut ihre volle Lesezeit. + posters.schedule(); } - - images.forEach((image) => { - const element = document.querySelector(`[data-month-page="${image.page}"]`); - if (element && image.url && element.getAttribute('src') !== image.url) { - element.src = image.url; - } - }); - } - - function renderIndoor(indoor) { - if (indoor) { - setText('#temperature_txt', `${indoor.value}${indoor.unit || '°C'}`); - } - } - - function renderOutdoor(outdoor) { - const weather = document.getElementById('wetter'); - if (!weather || !outdoor) { - return; - } - - const icon = WEATHER_ICONS[outdoor.icon] || WEATHER_ICONS.cloudy; - weather.replaceChildren(); - - const iconContainer = document.createElement('div'); - iconContainer.innerHTML = icon; - const temperature = document.createElement('div'); - temperature.className = 'text-outdoor'; - temperature.textContent = `${outdoor.temperature}°C`; - weather.append(iconContainer, temperature); - } - - function renderContent(data) { - setText('[data-dashboard-date]', data.date_label); - if (data.service) { - setText('.bible', data.service.bibelwort); - setText('.verse', data.service.bibelvers); - setText('.GB', data.service.eingang_nummer); - setText('.GB-title', data.service.eingang_titel); - } - - renderAnnouncements(data.announcements); - renderPosters(data.posters); - renderMonthImages(data.month_images); - scheduleScrollingSetup(); - } - - function startPolling(resource, interval, render) { - const refresh = async () => { - try { - const response = await fetch(`data.php?resource=${encodeURIComponent(resource)}`, { cache: 'no-store' }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const data = await response.json(); - if (data.error) { - throw new Error(data.error); - } - render(data); - } catch (error) { - console.error(`Dashboard-Aktualisierung (${resource}) fehlgeschlagen:`, error); - } finally { - window.setTimeout(refresh, interval); - } - }; - - refresh(); - } - - function applyAnimation(element, distance, speed, animationName) { - element.style.animation = 'none'; - if (distance <= 0) { - return; - } - - const variable = animationName === 'scroll-h' ? '--scroll-distance-h' : '--scroll-distance-v'; - element.style.setProperty(variable, `-${distance}px`); - element.style.animationName = animationName; - element.style.animationDuration = `${Math.max(1, distance / speed)}s`; - element.style.animationTimingFunction = 'linear'; - element.style.animationIterationCount = 'infinite'; - } - - function setupScrolling() { - const posterContainer = document.querySelector('.aushang-flex'); - if (posterContainer) { - const images = Array.from(posterContainer.querySelectorAll('img')); - const totalWidth = images.reduce((sum, image) => { - const styles = window.getComputedStyle(image); - return sum + image.offsetWidth + (parseFloat(styles.marginRight) || 0); - }, 0); - applyAnimation( - posterContainer, - Math.max(0, totalWidth - posterContainer.offsetWidth), - HORIZONTAL_SPEED_PX_PER_SECOND, - 'scroll-h' - ); - } - - const newsContainer = document.querySelector('.news-container'); - const news = document.querySelector('.news-scroll'); - if (newsContainer && news) { - applyAnimation( - news, - Math.max(0, news.scrollHeight - newsContainer.offsetHeight), - VERTICAL_SPEED_PX_PER_SECOND, - 'scroll-v' - ); - } - } - - let scrollingTimer = null; - function scheduleScrollingSetup(delay = 80) { - if (scrollingTimer !== null) { - window.clearTimeout(scrollingTimer); - } - scrollingTimer = window.setTimeout(setupScrolling, delay); - } - - updateClock(); - window.setInterval(updateClock, 1000); - window.addEventListener('load', () => scheduleScrollingSetup(50)); - window.addEventListener('resize', () => scheduleScrollingSetup(100)); - startPolling('content', CONTENT_REFRESH_MS, renderContent); - startPolling('indoor', INDOOR_REFRESH_MS, (data) => renderIndoor(data.indoor)); - startPolling('outdoor', OUTDOOR_REFRESH_MS, (data) => renderOutdoor(data.outdoor)); + }); })(); diff --git a/web/intranet_repo/projects/dashboard/dashboard.php b/web/intranet_repo/projects/dashboard/dashboard.php index 993ea0a..002fa09 100644 --- a/web/intranet_repo/projects/dashboard/dashboard.php +++ b/web/intranet_repo/projects/dashboard/dashboard.php @@ -2,139 +2,65 @@ declare(strict_types=1); -date_default_timezone_set('Europe/Berlin'); -require_once __DIR__ . '/util/dashboard_helpers.php'; - -$pdo = require __DIR__ . '/../../general_files/ini/database.php'; -$content = dashboard_fetch_content($pdo, dashboard_now()); -$pdo = null; - -$service = $content['service']; -$announcements = $content['announcements']; -$posters = $content['posters']; -$monthImages = $content['month_images']; -$stylesheetVersion = (int) filemtime(__DIR__ . '/stylo.css'); -$scriptVersion = (int) filemtime(__DIR__ . '/dashboard.js'); +// Die Anzeigefläche muss auch ohne Datenbank oder externe APIs starten können. +$config = require __DIR__ . '/config.php'; ?>
-Ansagen werden geladen …
Keine Aushänge vorgeplant.
- - -Bibelwort wird geladen …
Monatsplan wird geladen …
Aushänge werden geladen …