import { authToken, currentUser, orderMap, userFlags, pollIntervalId, highlightTags, allWeeks, currentWeekNumber, currentYear, displayMode, langMode, setAuthToken, setCurrentUser, setOrderMap, setUserFlags, setPollIntervalId, setHighlightTags, setAllWeeks, setCurrentWeekNumber, setCurrentYear } from './state.js'; import { getISOWeek, getWeekYear, translateDay, escapeHtml, getRelativeTime, isNewer, getLocalizedText } from './utils.js'; import { API_BASE, GUEST_TOKEN, VENUE_ID, MENU_ID, POLL_INTERVAL_MS, GITHUB_API, INSTALLER_BASE, CLIENT_VERSION } from './constants.js'; import { apiHeaders, githubHeaders } from './api.js'; import { placeOrder, cancelOrder, toggleFlag, showToast, checkHighlight, loadMenuDataFromAPI } from './actions.js'; export 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; let daysWithOrders = 0; let daysWithOrderableAndNoOrder = 0; if (nextWeekData && nextWeekData.days) { nextWeekData.days.forEach(day => { if (day.items && day.items.length > 0) { totalDataCount++; const isOrderable = day.items.some(item => item.available); if (isOrderable) orderableCount++; let hasOrder = false; day.items.forEach(item => { const articleId = item.articleId || parseInt(item.id.split('_')[1]); const key = `${day.date}_${articleId}`; if (orderMap.has(key) && orderMap.get(key).length > 0) hasOrder = true; }); if (hasOrder) daysWithOrders++; if (isOrderable && !hasOrder) daysWithOrderableAndNoOrder++; } }); } let badge = btnNextWeek.querySelector('.nav-badge'); if (totalDataCount > 0) { if (!badge) { badge = document.createElement('span'); badge.className = 'nav-badge'; btnNextWeek.appendChild(badge); } badge.title = `${daysWithOrders} bestellt / ${orderableCount} bestellbar / ${totalDataCount} gesamt`; badge.innerHTML = `${daysWithOrders}/${orderableCount}/${totalDataCount}`; badge.classList.remove('badge-violet', 'badge-green', 'badge-red', 'badge-blue'); if (daysWithOrders > 0 && daysWithOrderableAndNoOrder === 0) { badge.classList.add('badge-violet'); } else if (daysWithOrderableAndNoOrder > 0) { badge.classList.add('badge-green'); } else if (orderableCount === 0) { badge.classList.add('badge-red'); } else { badge.classList.add('badge-blue'); } let highlightCount = 0; if (nextWeekData && nextWeekData.days) { nextWeekData.days.forEach(day => { day.items.forEach(item => { const nameMatches = checkHighlight(item.name); const descMatches = checkHighlight(item.description); if (nameMatches.length > 0 || descMatches.length > 0) { highlightCount++; } }); }); } if (highlightCount > 0) { badge.insertAdjacentHTML('beforeend', `(${highlightCount})`); badge.title += ` • ${highlightCount} Highlights gefunden`; badge.classList.add('has-highlights'); } if (daysWithOrders === 0) { btnNextWeek.classList.add('new-week-available'); const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`; if (!localStorage.getItem(storageKey)) { localStorage.setItem(storageKey, 'true'); showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info'); } } else { btnNextWeek.classList.remove('new-week-available'); } } else if (badge) { badge.remove(); } } export 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'); } } export 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++; } } 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(getLocalizedText(item.description))}
`; 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'); }); }); } 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; }); }); } 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; } export async function fetchVersions(devMode) { const endpoint = devMode ? `${GITHUB_API}/tags?per_page=20` : `${GITHUB_API}/releases?per_page=20`; const resp = await fetch(endpoint, { headers: githubHeaders() }); if (!resp.ok) { if (resp.status === 403) { throw new Error('API Rate Limit erreicht (403). Bitte später erneut versuchen.'); } throw new Error(`GitHub API ${resp.status}`); } const data = await resp.json(); return data.map(item => { const tag = devMode ? item.name : item.tag_name; return { tag, name: devMode ? tag : (item.name || tag), url: `${INSTALLER_BASE}/${tag}/dist/install.html`, body: item.body || '' }; }); } export async function checkForUpdates() { const currentVersion = '{{VERSION}}'; const devMode = localStorage.getItem('kantine_dev_mode') === 'true'; try { const versions = await fetchVersions(devMode); if (!versions.length) return; localStorage.setItem('kantine_version_cache', JSON.stringify({ timestamp: Date.now(), devMode, versions })); const latest = versions[0].tag; if (!isNewer(latest, currentVersion)) return; const headerTitle = document.querySelector('.header-left h1'); if (headerTitle && !headerTitle.querySelector('.update-icon')) { const icon = document.createElement('a'); icon.className = 'update-icon'; icon.href = versions[0].url; icon.target = '_blank'; icon.innerHTML = '🆕'; icon.title = `Update: ${latest} — 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); } } export function openVersionMenu() { const modal = document.getElementById('version-modal'); const container = document.getElementById('version-list-container'); const devToggle = document.getElementById('dev-mode-toggle'); const currentVersion = '{{VERSION}}'; if (!modal) return; modal.classList.remove('hidden'); const cur = document.getElementById('version-current'); if (cur) cur.textContent = currentVersion; const devMode = localStorage.getItem('kantine_dev_mode') === 'true'; devToggle.checked = devMode; async function loadVersions(forceRefresh) { const dm = devToggle.checked; container.innerHTML = 'Lade Versionen...
'; function renderVersionsList(versions) { if (!versions || !versions.length) { container.innerHTML = 'Keine Versionen gefunden.
'; return; } container.innerHTML = 'Fehler: ${escapeHtml(e.message)}
`; } } loadVersions(false); devToggle.onchange = () => { localStorage.setItem('kantine_dev_mode', devToggle.checked); localStorage.removeItem('kantine_version_cache'); loadVersions(true); }; } export function updateCountdown() { if (!authToken || !currentUser) { removeCountdown(); return; } const now = new Date(); const currentDay = now.getDay(); if (currentDay === 0 || currentDay === 6) { removeCountdown(); return; } const todayStr = now.toISOString().split('T')[0]; let hasOrder = false; for (const key of orderMap.keys()) { if (key.startsWith(todayStr)) { hasOrder = true; break; } } if (hasOrder) { removeCountdown(); return; } const cutoff = new Date(); cutoff.setHours(10, 0, 0, 0); const diff = cutoff - now; if (diff <= 0) { removeCountdown(); return; } 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'; headerCenter.insertBefore(countdownEl, headerCenter.firstChild); } countdownEl.innerHTML = `Bestellschluss: ${diffHrs}h ${diffMins}m`; if (diff < 3600000) { countdownEl.classList.add('urgent'); const notifiedKey = `kantine_notified_${todayStr}`; if (!localStorage.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(); } localStorage.setItem(notifiedKey, 'true'); } } else { countdownEl.classList.remove('urgent'); } } export function removeCountdown() { const el = document.getElementById('order-countdown'); if (el) el.remove(); } setInterval(updateCountdown, 60000); setTimeout(updateCountdown, 1000); export 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'); }); } export function updateAlarmBell() { const bellBtn = document.getElementById('alarm-bell'); const bellIcon = document.getElementById('alarm-bell-icon'); if (!bellBtn || !bellIcon) return; if (userFlags.size === 0) { bellBtn.classList.add('hidden'); bellBtn.style.display = 'none'; bellIcon.style.color = 'var(--text-secondary)'; bellIcon.style.textShadow = 'none'; return; } bellBtn.classList.remove('hidden'); bellBtn.style.display = 'inline-flex'; let anyAvailable = false; for (const wk of allWeeks) { if (!wk.days) continue; for (const d of wk.days) { if (!d.items) continue; for (const item of d.items) { if (item.available && userFlags.has(item.id)) { anyAvailable = true; break; } } if (anyAvailable) break; } if (anyAvailable) break; } const lastCheckedStr = localStorage.getItem('kantine_last_checked'); const flaggedLastCheckedStr = localStorage.getItem('kantine_flagged_items_last_checked'); let latestTime = 0; if (lastCheckedStr) latestTime = Math.max(latestTime, new Date(lastCheckedStr).getTime()); if (flaggedLastCheckedStr) latestTime = Math.max(latestTime, new Date(flaggedLastCheckedStr).getTime()); let timeStr = 'gerade eben'; if (latestTime === 0) { const now = new Date().toISOString(); localStorage.setItem('kantine_last_checked', now); latestTime = new Date(now).getTime(); } timeStr = getRelativeTime(new Date(latestTime)); bellBtn.title = `Zuletzt geprüft: ${timeStr}`; if (anyAvailable) { bellIcon.style.color = '#10b981'; bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)'; } else { bellIcon.style.color = '#f59e0b'; bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)'; } }