Lade Menüdaten...
/** * Kantine Wrapper – Client-Only Bookmarklet * Replaces Bessa page content with enhanced weekly menu view. * All API calls go directly to api.bessa.app (same origin). * Data stored in localStorage (flags, theme, auth). */ (function () { 'use strict'; // Prevent double injection if (window.__KANTINE_LOADED) return; window.__KANTINE_LOADED = true; // === Constants === const API_BASE = 'https://api.bessa.app/v1'; const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131'; const CLIENT_VERSION = '1.7.0_prod/2026-01-26'; const VENUE_ID = 591; const MENU_ID = 7; const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes // === State === let allWeeks = []; let currentWeekNumber = getISOWeek(new Date()); let currentYear = new Date().getFullYear(); let displayMode = 'this-week'; let authToken = sessionStorage.getItem('kantine_authToken'); let currentUser = sessionStorage.getItem('kantine_currentUser'); let orderMap = new Map(); let userFlags = new Set(JSON.parse(localStorage.getItem('kantine_flags') || '[]')); let pollIntervalId = null; // === API Helpers === function apiHeaders(token) { return { 'Authorization': `Token ${token || GUEST_TOKEN}`, 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Client-Version': CLIENT_VERSION }; } // === Inject UI === function injectUI() { // Replace entire page content document.title = 'Kantine Weekly Menu'; // Inject Google Fonts if not already present if (!document.querySelector('link[href*="fonts.googleapis.com/css2?family=Inter"]')) { const fontLink = document.createElement('link'); fontLink.rel = 'stylesheet'; fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'; document.head.appendChild(fontLink); } if (!document.querySelector('link[href*="Material+Icons+Round"]')) { const iconLink = document.createElement('link'); iconLink.rel = 'stylesheet'; iconLink.href = 'https://fonts.googleapis.com/icon?family=Material+Icons+Round'; document.head.appendChild(iconLink); } document.body.innerHTML = `
Lade Menüdaten...
Keine Menüdaten für KW ${targetWeek} (${targetYear}) verfügbar.
Versuchen Sie eine andere Woche oder schauen Sie später vorbei.${escapeHtml(item.description)}
`; // Event: Order const orderBtn = itemEl.querySelector('.btn-order'); if (orderBtn) { orderBtn.addEventListener('click', (e) => { e.stopPropagation(); const btn = e.currentTarget; btn.disabled = true; btn.classList.add('loading'); placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '') .finally(() => { btn.disabled = false; btn.classList.remove('loading'); }); }); } // Event: Cancel const cancelBtn = itemEl.querySelector('.btn-cancel'); if (cancelBtn) { cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); const btn = e.currentTarget; btn.disabled = true; cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name) .finally(() => { btn.disabled = false; }); }); } // Event: Flag const flagBtn = itemEl.querySelector('.btn-flag'); if (flagBtn) { flagBtn.addEventListener('click', (e) => { e.stopPropagation(); const btn = e.currentTarget; toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); }); } body.appendChild(itemEl); }); card.appendChild(body); return card; } // === Version Check (periodic, every hour) === async function checkForUpdates() { console.log('[Kantine] Starting update check...'); const currentVersion = '{{VERSION}}'; const versionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt'; const installerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html'; try { console.log(`[Kantine] Fetching ${versionUrl}...`); const resp = await fetch(versionUrl, { cache: 'no-cache' }); console.log(`[Kantine] Fetch status: ${resp.status}`); if (!resp.ok) { console.warn(`[Kantine] Version Check HTTP Error: ${resp.status}`); return; } const remoteVersion = (await resp.text()).trim(); console.log(`[Kantine] Version Check: Local [${currentVersion}] vs Remote [${remoteVersion}]`); // Check if remote is NEWER (simple semver check) const isNewer = (remote, local) => { if (!remote || !local) return false; const r = remote.replace(/^v/, '').split('.').map(Number); const l = local.replace(/^v/, '').split('.').map(Number); for (let i = 0; i < Math.max(r.length, l.length); i++) { if ((r[i] || 0) > (l[i] || 0)) return true; if ((r[i] || 0) < (l[i] || 0)) return false; } return false; }; if (!isNewer(remoteVersion, currentVersion)) { console.log('[Kantine] No update needed (Remote is not newer).'); return; } console.log(`[Kantine] Update verfügbar: ${remoteVersion}`); // Show 🆕 icon in header (only once) const headerTitle = document.querySelector('.header-left h1'); if (headerTitle && !headerTitle.querySelector('.update-icon')) { const icon = document.createElement('a'); icon.className = 'update-icon'; icon.href = installerUrl; icon.target = '_blank'; icon.innerHTML = '🆕'; icon.title = `Update verfügbar: ${remoteVersion} — Klick zum Installieren`; icon.style.cssText = 'margin-left:8px;font-size:1em;text-decoration:none;cursor:pointer;vertical-align:middle;'; headerTitle.appendChild(icon); } } catch (e) { console.warn('[Kantine] Version check failed:', e); } } // === Order Countdown === function updateCountdown() { const now = new Date(); const currentDay = now.getDay(); // Skip weekends (0=Sun, 6=Sat) if (currentDay === 0 || currentDay === 6) { removeCountdown(); return; } const todayStr = now.toISOString().split('T')[0]; // 1. Check if we already ordered for today let hasOrder = false; // Optimization: Check orderMap for today's date // Keys are "YYYY-MM-DD_ArticleID" for (const key of orderMap.keys()) { if (key.startsWith(todayStr)) { hasOrder = true; break; } } if (hasOrder) { removeCountdown(); return; } // 2. Calculate time to cutoff (10:00 AM) const cutoff = new Date(); cutoff.setHours(10, 0, 0, 0); const diff = cutoff - now; // If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show? // User req: "heute noch keine bestellung... countdown erscheinen" // Let's show it if within valid order window (e.g. 00:00 - 10:00) if (diff <= 0) { removeCountdown(); return; } // 3. Render Countdown const diffHrs = Math.floor(diff / 3600000); const diffMins = Math.floor((diff % 3600000) / 60000); const headerCenter = document.querySelector('.header-center-wrapper'); if (!headerCenter) return; let countdownEl = document.getElementById('order-countdown'); if (!countdownEl) { countdownEl = document.createElement('div'); countdownEl.id = 'order-countdown'; // Insert before cost display or append headerCenter.insertBefore(countdownEl, headerCenter.firstChild); } countdownEl.innerHTML = `Bestellschluss: ${diffHrs}h ${diffMins}m`; // Red Alert if < 1 hour if (diff < 3600000) { // 1 hour countdownEl.classList.add('urgent'); // Notification logic (One time) const notifiedKey = `kantine_notified_${todayStr}`; if (!sessionStorage.getItem(notifiedKey)) { if (Notification.permission === 'granted') { new Notification('Kantine: Bestellschluss naht!', { body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!', icon: '⏳' }); } else if (Notification.permission === 'default') { Notification.requestPermission(); } sessionStorage.setItem(notifiedKey, 'true'); } } else { countdownEl.classList.remove('urgent'); } } function removeCountdown() { const el = document.getElementById('order-countdown'); if (el) el.remove(); } // Update countdown every minute setInterval(updateCountdown, 60000); // Also update on load setTimeout(updateCountdown, 1000); // === Helpers === function getISOWeek(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); } function getWeekYear(d) { const date = new Date(d.getTime()); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); return date.getFullYear(); } function translateDay(englishDay) { const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' }; return map[englishDay] || englishDay; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text || ''; return div.innerHTML; } // === Bootstrap === injectUI(); bindEvents(); updateAuthUI(); cleanupExpiredFlags(); // Load cached data first for instant UI, then refresh from API const hadCache = loadMenuCache(); if (hadCache) { // Hide loading spinner since cache is shown document.getElementById('loading').classList.add('hidden'); } loadMenuDataFromAPI(); // Auto-start polling if already logged in if (authToken) { startPolling(); } // Check for updates (now + every hour) checkForUpdates(); setInterval(checkForUpdates, 60 * 60 * 1000); console.log('Kantine Wrapper loaded ✅'); })(); // === Error Modal === function showErrorModal(title, htmlContent, btnText, url) { const modalId = 'error-modal'; let modal = document.getElementById(modalId); if (modal) modal.remove(); modal = document.createElement('div'); modal.id = modalId; modal.className = 'modal hidden'; modal.innerHTML = ` `; document.body.appendChild(modal); document.getElementById('btn-error-redirect').addEventListener('click', () => { window.location.href = url; }); requestAnimationFrame(() => { modal.classList.remove('hidden'); }); }