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...
Fehler beim Laden der Menüdaten: ${error.message}
`; progressModal.classList.add('hidden'); } finally { loading.classList.add('hidden'); } } // === Last Updated Display === function updateLastUpdatedTime(isoTimestamp) { const subtitle = document.getElementById('last-updated-subtitle'); if (!isoTimestamp) return; try { const date = new Date(isoTimestamp); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr}`; } catch (e) { subtitle.textContent = ''; } } // === Toast Notification === function showToast(message, type = 'info') { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icon = type === 'success' ? 'check_circle' : type === 'error' ? 'error' : 'info'; toast.innerHTML = `${message}`; container.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // === Next Week Badge === function updateNextWeekBadge() { const btnNextWeek = document.getElementById('btn-next-week'); let nextWeek = currentWeekNumber + 1; let nextYear = currentYear; if (nextWeek > 52) { nextWeek = 1; nextYear++; } const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear); let totalDataCount = 0; let orderableCount = 0; if (nextWeekData && nextWeekData.days) { nextWeekData.days.forEach(day => { if (day.items && day.items.length > 0) { totalDataCount++; if (day.items.some(item => item.available)) orderableCount++; } }); } let badge = btnNextWeek.querySelector('.nav-badge'); if (totalDataCount > 0) { if (!badge) { badge = document.createElement('span'); badge.className = 'nav-badge'; btnNextWeek.appendChild(badge); } badge.title = `${orderableCount} Tage bestellbar / ${totalDataCount} Tage mit Menüdaten`; badge.innerHTML = `${orderableCount}/${totalDataCount}`; } else if (badge) { badge.remove(); } } // === Weekly Cost === function updateWeeklyCost(days) { let totalCost = 0; if (days && days.length > 0) { days.forEach(day => { if (day.items) { day.items.forEach(item => { const articleId = item.articleId || parseInt(item.id.split('_')[1]); const key = `${day.date}_${articleId}`; const orders = orderMap.get(key) || []; if (orders.length > 0) totalCost += item.price * orders.length; }); } }); } const costDisplay = document.getElementById('weekly-cost-display'); if (totalCost > 0) { costDisplay.innerHTML = ` Gesamt: ${totalCost.toFixed(2).replace('.', ',')} €`; costDisplay.classList.remove('hidden'); } else { costDisplay.classList.add('hidden'); } } // === Render Weeks === function renderVisibleWeeks() { const menuContainer = document.getElementById('menu-container'); if (!menuContainer) return; menuContainer.innerHTML = ''; let targetWeek = currentWeekNumber; let targetYear = currentYear; if (displayMode === 'next-week') { targetWeek++; if (targetWeek > 52) { targetWeek = 1; targetYear++; } } // Flatten & filter by week + year const allDays = allWeeks.flatMap(w => w.days || []); const daysInTargetWeek = allDays.filter(day => { const d = new Date(day.date); return getISOWeek(d) === targetWeek && getWeekYear(d) === targetYear; }); if (daysInTargetWeek.length === 0) { menuContainer.innerHTML = `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; } // === 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(); } console.log('Kantine Wrapper loaded ✅'); })();