Lade Menüdaten...
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 367 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ A0: () => (/* binding */ refreshFlaggedItems), /* harmony export */ Aq: () => (/* binding */ fetchFullOrderHistory), /* harmony export */ BM: () => (/* binding */ checkHighlight), /* harmony export */ Et: () => (/* binding */ stopPolling), /* harmony export */ Gb: () => (/* binding */ fetchOrders), /* harmony export */ H: () => (/* binding */ cleanupExpiredFlags), /* harmony export */ KG: () => (/* binding */ loadMenuCache), /* harmony export */ N4: () => (/* binding */ cancelOrder), /* harmony export */ P0: () => (/* binding */ showToast), /* harmony export */ PQ: () => (/* binding */ toggleFlag), /* harmony export */ VL: () => (/* binding */ isCacheFresh), /* harmony export */ Y1: () => (/* binding */ renderTagsList), /* harmony export */ g8: () => (/* binding */ startPolling), /* harmony export */ i_: () => (/* binding */ updateAuthUI), /* harmony export */ m9: () => (/* binding */ loadMenuDataFromAPI), /* harmony export */ oL: () => (/* binding */ addHighlightTag), /* harmony export */ wH: () => (/* binding */ placeOrder) /* harmony export */ }); /* unused harmony exports renderHistory, saveFlags, pollFlaggedItems, saveHighlightTags, removeHighlightTag, saveMenuCache, updateLastUpdatedTime */ /* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(901); /* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(413); /* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(521); /* harmony import */ var _api_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(672); /* harmony import */ var _ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(842); /* harmony import */ var _i18n_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(646); let fullOrderHistoryCache = null; function updateAuthUI() { if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) { try { const akita = localStorage.getItem('AkitaStores'); if (akita) { const parsed = JSON.parse(akita); if (parsed.auth && parsed.auth.token) { (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setAuthToken */ .O5)(parsed.auth.token); localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.AUTH_TOKEN, parsed.auth.token); if (parsed.auth.user) { (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentUser */ .lt)(parsed.auth.user.id || 'unknown'); localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.CURRENT_USER, parsed.auth.user.id || 'unknown'); if (parsed.auth.user.firstName) localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.FIRST_NAME, parsed.auth.user.firstName); if (parsed.auth.user.lastName) localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.LAST_NAME, parsed.auth.user.lastName); } } } } catch (e) { console.warn('Failed to parse AkitaStores:', e); } } (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setAuthToken */ .O5)(localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.AUTH_TOKEN)); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentUser */ .lt)(localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.CURRENT_USER)); const firstName = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.FIRST_NAME); const btnLoginOpen = document.getElementById('btn-login-open'); const userInfo = document.getElementById('user-info'); const userIdDisplay = document.getElementById('user-id-display'); if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) { btnLoginOpen.classList.add('hidden'); userInfo.classList.remove('hidden'); userIdDisplay.textContent = firstName || (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentUser */ .Ny ? `User ${_state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentUser */ .Ny}` : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('loggedIn')); fetchOrders(); } else { btnLoginOpen.classList.remove('hidden'); userInfo.classList.add('hidden'); userIdDisplay.textContent = ''; } (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); } async function fetchOrders() { if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; try { const response = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/user/orders/?venue=${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}&ordering=-created&limit=50`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) }); const data = await response.json(); if (response.ok) { const newOrderMap = new Map(); const results = data.results || []; for (const order of results) { if (order.order_state === 9) continue; const orderDate = order.date.split('T')[0]; for (const item of (order.items || [])) { const key = `${orderDate}_${item.article}`; if (!newOrderMap.has(key)) newOrderMap.set(key, []); newOrderMap.get(key).push(order.id); } } (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setOrderMap */ .di)(newOrderMap); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateNextWeekBadge */ .gJ)(); } } catch (error) { console.error('Error fetching orders:', error); } } async function fetchFullOrderHistory() { const historyLoading = document.getElementById('history-loading'); const historyContent = document.getElementById('history-content'); const progressFill = document.getElementById('history-progress-fill'); const progressText = document.getElementById('history-progress-text'); let localCache = []; if (fullOrderHistoryCache) { localCache = fullOrderHistoryCache; } else { const ls = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.HISTORY_CACHE); if (ls) { try { localCache = JSON.parse(ls); fullOrderHistoryCache = localCache; } catch (e) { console.warn('History cache parse error', e); } } } if (localCache.length > 0) { renderHistory(localCache); } if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; if (localCache.length === 0) { historyContent.innerHTML = ''; historyLoading.classList.remove('hidden'); } progressFill.style.width = '0%'; progressText.textContent = localCache.length > 0 ? (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingDelta') : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingFull'); if (localCache.length > 0) historyLoading.classList.remove('hidden'); let nextUrl = localCache.length > 0 ? `${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/user/orders/?venue=${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}&ordering=-created&limit=5` : `${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/user/orders/?venue=${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}&ordering=-created&limit=50`; let fetchedOrders = []; let totalCount = 0; let requiresFullFetch = localCache.length === 0; let deltaComplete = false; try { while (nextUrl && !deltaComplete) { const response = await fetch(nextUrl, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) }); if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); const data = await response.json(); if (data.count && totalCount === 0) { totalCount = data.count; } const results = data.results || []; for (const order of results) { const existingOrderIndex = localCache.findIndex(cached => cached.id === order.id); if (!requiresFullFetch && existingOrderIndex !== -1) { const existingOrder = localCache[existingOrderIndex]; if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) { deltaComplete = true; break; } } fetchedOrders.push(order); } if (!deltaComplete && requiresFullFetch) { if (totalCount > 0) { const pct = Math.round((fetchedOrders.length / totalCount) * 100); progressFill.style.width = `${pct}%`; progressText.textContent = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingItem')} ${fetchedOrders.length} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingOf')} ${totalCount}...`; } else { progressText.textContent = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingItem')} ${fetchedOrders.length}...`; } } else if (!deltaComplete) { progressText.textContent = `${fetchedOrders.length} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadingNew')}`; } nextUrl = deltaComplete ? null : data.next; } if (fetchedOrders.length > 0) { const cacheMap = new Map(localCache.map(o => [o.id, o])); for (const order of fetchedOrders) { cacheMap.set(order.id, order); } const mergedOrders = Array.from(cacheMap.values()); mergedOrders.sort((a, b) => new Date(b.created) - new Date(a.created)); fullOrderHistoryCache = mergedOrders; try { localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.HISTORY_CACHE, JSON.stringify(mergedOrders)); } catch (e) { console.warn('History cache write error', e); } renderHistory(fullOrderHistoryCache); } } catch (error) { console.error('Error in history sync:', error); if (localCache.length === 0) { historyContent.innerHTML = `
${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyLoadError')}
`; } else { showToast((0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('bgSyncFailed'), 'error'); } } finally { historyLoading.classList.add('hidden'); } } function renderHistory(orders) { const content = document.getElementById('history-content'); if (!orders || orders.length === 0) { content.innerHTML = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('noOrders')}
`; return; } const groups = {}; orders.forEach(order => { const d = new Date(order.date); const y = d.getFullYear(); const m = d.getMonth(); const monthKey = `${y}-${m.toString().padStart(2, '0')}`; const uiLocale = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en' ? 'en-US' : 'de-AT'; const monthName = d.toLocaleString(uiLocale, { month: 'long' }); const kw = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(d); if (!groups[y]) { groups[y] = { year: y, months: {} }; } if (!groups[y].months[monthKey]) { groups[y].months[monthKey] = { name: monthName, year: y, monthIndex: m, count: 0, total: 0, weeks: {} }; } if (!groups[y].months[monthKey].weeks[kw]) { groups[y].months[monthKey].weeks[kw] = { label: _state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en' ? `CW ${kw}` : `KW ${kw}`, items: [], count: 0, total: 0 }; } const items = order.items || []; items.forEach(item => { const itemPrice = parseFloat(item.price || order.total || 0); groups[y].months[monthKey].weeks[kw].items.push({ date: order.date, name: item.name || 'Menü', price: itemPrice, state: order.order_state }); if (order.order_state !== 9) { groups[y].months[monthKey].weeks[kw].count++; groups[y].months[monthKey].weeks[kw].total += itemPrice; groups[y].months[monthKey].count++; groups[y].months[monthKey].total += itemPrice; } }); }); content.innerHTML = ''; const sortedYears = Object.keys(groups).sort((a, b) => b - a); sortedYears.forEach(yKey => { const yearGroup = groups[yKey]; const yearGroupDiv = document.createElement('div'); yearGroupDiv.className = 'history-year-group'; const yearHeader = document.createElement('h2'); yearHeader.className = 'history-year-header'; yearHeader.textContent = yearGroup.year; yearGroupDiv.appendChild(yearHeader); const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a)); sortedMonths.forEach(mKey => { const monthGroup = yearGroup.months[mKey]; const monthGroupDiv = document.createElement('div'); monthGroupDiv.className = 'history-month-group'; const monthHeader = document.createElement('div'); monthHeader.className = 'history-month-header'; monthHeader.setAttribute('tabindex', '0'); monthHeader.setAttribute('role', 'button'); monthHeader.setAttribute('aria-expanded', 'false'); monthHeader.setAttribute('title', (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('historyMonthToggle')); const monthHeaderContent = document.createElement('div'); monthHeaderContent.style.display = 'flex'; monthHeaderContent.style.flexDirection = 'column'; monthHeaderContent.style.gap = '4px'; const monthNameSpan = document.createElement('span'); monthNameSpan.textContent = monthGroup.name; monthHeaderContent.appendChild(monthNameSpan); const monthSummary = document.createElement('div'); monthSummary.className = 'history-month-summary'; const monthSummarySpan = document.createElement('span'); monthSummarySpan.innerHTML = `${monthGroup.count} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('orders')} • €${monthGroup.total.toFixed(2)}`; monthSummary.appendChild(monthSummarySpan); monthHeaderContent.appendChild(monthSummary); monthHeader.appendChild(monthHeaderContent); const expandIcon = document.createElement('span'); expandIcon.className = 'material-icons-round'; expandIcon.textContent = 'expand_more'; monthHeader.appendChild(expandIcon); monthHeader.addEventListener('click', () => { const parentGroup = monthHeader.parentElement; const isOpen = parentGroup.classList.contains('open'); if (isOpen) { parentGroup.classList.remove('open'); monthHeader.setAttribute('aria-expanded', 'false'); } else { parentGroup.classList.add('open'); monthHeader.setAttribute('aria-expanded', 'true'); } }); monthGroupDiv.appendChild(monthHeader); const monthContentDiv = document.createElement('div'); monthContentDiv.className = 'history-month-content'; const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a)); sortedKWs.forEach(kw => { const week = monthGroup.weeks[kw]; const weekGroupDiv = document.createElement('div'); weekGroupDiv.className = 'history-week-group'; const weekHeader = document.createElement('div'); weekHeader.className = 'history-week-header'; const weekLabel = document.createElement('strong'); weekLabel.textContent = week.label; weekHeader.appendChild(weekLabel); const weekSummary = document.createElement('span'); weekSummary.innerHTML = `${week.count} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('orders')} • €${week.total.toFixed(2)}`; weekHeader.appendChild(weekSummary); weekGroupDiv.appendChild(weekHeader); week.items.forEach(item => { const dateObj = new Date(item.date); const uiLocale = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en' ? 'en-US' : 'de-AT'; const dayStr = dateObj.toLocaleDateString(uiLocale, { weekday: 'short', day: '2-digit', month: '2-digit' }); const historyItem = document.createElement('div'); historyItem.className = 'history-item'; if (item.state === 9) { historyItem.classList.add('history-item-cancelled'); } const dateDiv = document.createElement('div'); dateDiv.style.fontSize = '0.85rem'; dateDiv.style.color = 'var(--text-secondary)'; dateDiv.textContent = dayStr; historyItem.appendChild(dateDiv); const detailsDiv = document.createElement('div'); detailsDiv.className = 'history-item-details'; const nameSpan = document.createElement('span'); nameSpan.className = 'history-item-name'; nameSpan.textContent = item.name; detailsDiv.appendChild(nameSpan); const statusDiv = document.createElement('div'); const statusSpan = document.createElement('span'); statusSpan.className = 'history-item-status'; if (item.state === 9) { statusSpan.textContent = (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('stateCancelled'); } else if (item.state === 8) { statusSpan.textContent = (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('stateCompleted'); } else { statusSpan.textContent = (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('stateTransferred'); } statusDiv.appendChild(statusSpan); detailsDiv.appendChild(statusDiv); historyItem.appendChild(detailsDiv); const priceDiv = document.createElement('div'); priceDiv.className = 'history-item-price'; if (item.state === 9) { priceDiv.classList.add('history-item-price-cancelled'); } priceDiv.textContent = `€${item.price.toFixed(2)}`; historyItem.appendChild(priceDiv); weekGroupDiv.appendChild(historyItem); }); monthContentDiv.appendChild(weekGroupDiv); }); monthGroupDiv.appendChild(monthContentDiv); yearGroupDiv.appendChild(monthGroupDiv); }); content.appendChild(yearGroupDiv); }); } async function placeOrder(date, articleId, name, price, description) { if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; try { const userResp = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/auth/user/`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) }); if (!userResp.ok) { showToast('Fehler: Benutzerdaten konnten nicht geladen werden', 'error'); return; } const userData = await userResp.json(); const now = new Date().toISOString(); const orderPayload = { uuid: crypto.randomUUID(), created: now, updated: now, order_type: 7, items: [{ article: articleId, course_group: null, modifiers: [], uuid: crypto.randomUUID(), name: name, description: description || '', price: String(parseFloat(price)), amount: 1, vat: '10.00', comment: '' }], table: null, total: parseFloat(price), tip: 0, currency: 'EUR', venue: _constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW, states: [], order_state: 1, date: `${date}T10:30:00Z`, payment_method: 'payroll', customer: { first_name: userData.first_name, last_name: userData.last_name, email: userData.email, newsletter: false }, preorder: true, delivery_fee: 0, cash_box_table_name: null, take_away: false }; const response = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/user/orders/`, { method: 'POST', headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX), body: JSON.stringify(orderPayload) }); if (response.ok || response.status === 201) { showToast(`${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('orderSuccess')}: ${name}`, 'success'); fullOrderHistoryCache = null; await fetchOrders(); } else { const data = await response.json(); showToast(`Fehler: ${data.detail || data.non_field_errors?.[0] || 'Bestellung fehlgeschlagen'}`, 'error'); } } catch (error) { console.error('Order error:', error); showToast('Netzwerkfehler bei Bestellung', 'error'); } } async function cancelOrder(date, articleId, name) { if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; const key = `${date}_${articleId}`; const orderIds = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.get(key); if (!orderIds || orderIds.length === 0) return; const orderId = orderIds[orderIds.length - 1]; try { const response = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/user/orders/${orderId}/cancel/`, { method: 'PATCH', headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX), body: JSON.stringify({}) }); if (response.ok) { showToast(`${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('cancelSuccess')}: ${name}`, 'success'); fullOrderHistoryCache = null; await fetchOrders(); } else { const data = await response.json(); showToast(`Fehler: ${data.detail || 'Stornierung fehlgeschlagen'}`, 'error'); } } catch (error) { console.error('Cancel error:', error); showToast('Netzwerkfehler bei Stornierung', 'error'); } } function saveFlags() { localStorage.setItem('kantine_flags', JSON.stringify([..._state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY])); } async function refreshFlaggedItems() { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.size === 0) return; const token = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX || _constants_js__WEBPACK_IMPORTED_MODULE_2__/* .GUEST_TOKEN */ .f9; // Collect unique dates that have flagged items const datesToFetch = new Set(); for (const flagId of _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY) { const [dateStr] = flagId.split('_'); datesToFetch.add(dateStr); } let updated = false; const bellBtn = document.getElementById('alarm-bell'); if (bellBtn) bellBtn.classList.add('refreshing'); try { for (const dateStr of datesToFetch) { try { const resp = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/venues/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}/menu/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .MENU_ID */ .YU}/${dateStr}/`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(token) }); if (!resp.ok) continue; const data = await resp.json(); const menuGroups = data.results || []; // Build a lookup of fresh API items by article ID const apiItemMap = new Map(); for (const group of menuGroups) { if (group.items && Array.isArray(group.items)) { for (const item of group.items) { apiItemMap.set(item.id, item); } } } // Only update items that are actually flagged for (let week of _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_) { if (!week.days) continue; const dayObj = week.days.find(d => d.date === dateStr); if (!dayObj || !dayObj.items) continue; for (let i = 0; i < dayObj.items.length; i++) { const existing = dayObj.items[i]; const flagId = `${dateStr}_${existing.articleId}`; if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.has(flagId)) continue; const apiItem = apiItemMap.get(existing.articleId); if (apiItem) { const isUnlimited = apiItem.amount_tracking === false; const hasStock = parseInt(apiItem.available_amount) > 0; existing.available = isUnlimited || hasStock; existing.availableAmount = parseInt(apiItem.available_amount) || 0; existing.amountTracking = apiItem.amount_tracking !== false; updated = true; } } } } catch (e) { console.error('Error refreshing flag date', dateStr, e); } } if (updated) { saveMenuCache(); } // Always update the check timestamp and bell status localStorage.setItem('kantine_flagged_items_last_checked', new Date().toISOString()); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); showToast(`${_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.size} ${_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.size === 1 ? (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('menuSingular') : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('menuPlural')} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('menuChecked')}`, 'info'); } finally { if (bellBtn) bellBtn.classList.remove('refreshing'); } } function toggleFlag(date, articleId, name, cutoff) { const id = `${date}_${articleId}`; let flagAdded = false; if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.has(id)) { _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.delete(id); showToast(`${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('flagRemoved')} ${name}`, 'success'); } else { _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.add(id); flagAdded = true; showToast(`${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('flagActivated')} ${name}`, 'success'); if (Notification.permission === 'default') { Notification.requestPermission(); } } saveFlags(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); if (flagAdded) { refreshFlaggedItems(); } } function cleanupExpiredFlags() { const now = new Date(); const todayStr = now.toISOString().split('T')[0]; let changed = false; for (const flagId of [..._state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY]) { const [dateStr] = flagId.split('_'); let isExpired = false; if (dateStr < todayStr) { isExpired = true; } else if (dateStr === todayStr) { const cutoff = new Date(dateStr); cutoff.setHours(10, 0, 0, 0); if (now >= cutoff) { isExpired = true; } } if (isExpired) { _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.delete(flagId); changed = true; } } if (changed) saveFlags(); } function startPolling() { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .pollIntervalId */ .K8) return; if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setPollIntervalId */ .cc)(setInterval(() => pollFlaggedItems(), _constants_js__WEBPACK_IMPORTED_MODULE_2__/* .POLL_INTERVAL_MS */ .fv)); } function stopPolling() { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .pollIntervalId */ .K8) { clearInterval(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .pollIntervalId */ .K8); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setPollIntervalId */ .cc)(null); } } async function pollFlaggedItems() { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.size === 0 || !_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) return; for (const flagId of _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY) { const [date, articleIdStr] = flagId.split('_'); const articleId = parseInt(articleIdStr); try { const response = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/venues/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}/menu/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .MENU_ID */ .YU}/${date}/`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX) }); if (!response.ok) continue; const data = await response.json(); const groups = data.results || []; let foundItem = null; for (const group of groups) { if (group.items) { foundItem = group.items.find(i => i.id === articleId || i.article === articleId); if (foundItem) break; } } if (foundItem) { const isAvailable = (foundItem.amount_tracking === false) || (parseInt(foundItem.available_amount) > 0); if (isAvailable) { const itemName = foundItem.name || 'Unbekannt'; showToast(`${itemName} ist jetzt verfügbar!`, 'success'); if (Notification.permission === 'granted') { new Notification('Kantine Wrapper', { body: `${itemName} ist jetzt verfügbar!`, icon: '🍽️' }); } loadMenuDataFromAPI(); } } } catch (err) { console.error(`Poll error for ${flagId}:`, err); await new Promise(r => setTimeout(r, 200)); } } localStorage.setItem('kantine_flagged_items_last_checked', new Date().toISOString()); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); } function saveHighlightTags() { localStorage.setItem('kantine_highlightTags', JSON.stringify(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz)); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateNextWeekBadge */ .gJ)(); } function addHighlightTag(tag) { tag = tag.trim().toLowerCase(); if (tag && !_state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.includes(tag)) { const newTags = [..._state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz, tag]; (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setHighlightTags */ .iw)(newTags); saveHighlightTags(); return true; } return false; } function removeHighlightTag(tag) { const newTags = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.filter(t => t !== tag); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setHighlightTags */ .iw)(newTags); saveHighlightTags(); } function renderTagsList() { const list = document.getElementById('tags-list'); list.innerHTML = ''; _state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.forEach(tag => { const badge = document.createElement('span'); badge.className = 'tag-badge'; badge.innerHTML = `${tag} ×`; list.appendChild(badge); }); list.querySelectorAll('.tag-remove').forEach(btn => { btn.addEventListener('click', (e) => { removeHighlightTag(e.target.dataset.tag); renderTagsList(); }); }); } function checkHighlight(text) { if (!text) return []; text = text.toLowerCase(); return _state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.filter(tag => text.includes(tag)); } const CACHE_KEY = 'kantine_menuCache'; const CACHE_TS_KEY = 'kantine_menuCacheTs'; function saveMenuCache() { try { localStorage.setItem(CACHE_KEY, JSON.stringify(_state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_)); localStorage.setItem(CACHE_TS_KEY, new Date().toISOString()); } catch (e) { console.warn('Failed to cache menu data:', e); } } function loadMenuCache() { try { const cached = localStorage.getItem(CACHE_KEY); const cachedTs = localStorage.getItem(CACHE_TS_KEY); if (cached) { (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setAllWeeks */ .tn)(JSON.parse(cached)); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentWeekNumber */ .Xt)((0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(new Date())); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentYear */ .pK)(new Date().getFullYear()); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateNextWeekBadge */ .gJ)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); if (cachedTs) updateLastUpdatedTime(cachedTs); try { const uniqueMenus = new Set(); _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.forEach(w => { (w.days || []).forEach(d => { (d.items || []).forEach(item => { let text = (item.description || '').replace(/\s+/g, ' ').trim(); if (text && text.includes(' / ')) { uniqueMenus.add(text); } }); }); }); } catch (e) { } return true; } } catch (e) { console.warn('Failed to load cached menu:', e); } return false; } function isCacheFresh() { const cachedTs = localStorage.getItem(CACHE_TS_KEY); if (!cachedTs) { return false; } const ageMs = Date.now() - new Date(cachedTs).getTime(); if (ageMs > 60 * 60 * 1000) { return false; } const thisWeek = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(new Date()); const thisYear = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getWeekYear */ .Ao)(new Date()); const hasCurrentWeek = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.some(w => w.weekNumber === thisWeek && w.year === thisYear && w.days && w.days.length > 0); return hasCurrentWeek; } async function loadMenuDataFromAPI() { const loading = document.getElementById('loading'); const progressModal = document.getElementById('progress-modal'); const progressFill = document.getElementById('progress-fill'); const progressPercent = document.getElementById('progress-percent'); const progressMessage = document.getElementById('progress-message'); loading.classList.remove('hidden'); const token = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX || _constants_js__WEBPACK_IMPORTED_MODULE_2__/* .GUEST_TOKEN */ .f9; try { progressModal.classList.remove('hidden'); progressMessage.textContent = 'Hole verfügbare Daten...'; progressFill.style.width = '0%'; progressPercent.textContent = '0%'; const datesResponse = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/venues/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}/menu/dates/`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(token) }); if (!datesResponse.ok) throw new Error(`Failed to fetch dates: ${datesResponse.status}`); const datesData = await datesResponse.json(); let availableDates = datesData.results || []; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 7); const cutoffStr = cutoff.toISOString().split('T')[0]; availableDates = availableDates .filter(d => d.date >= cutoffStr) .sort((a, b) => a.date.localeCompare(b.date)) .slice(0, 30); const totalDates = availableDates.length; progressMessage.textContent = `${totalDates} Tage gefunden. Lade Details...`; const allDays = []; let completed = 0; const BATCH_SIZE = 5; for (let i = 0; i < totalDates; i += BATCH_SIZE) { const batch = availableDates.slice(i, i + BATCH_SIZE); const results = await Promise.all(batch.map(async (dateObj) => { const dateStr = dateObj.date; let dayData = null; try { const detailResp = await fetch(`${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .API_BASE */ .tE}/venues/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .VENUE_ID */ .eW}/menu/${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .MENU_ID */ .YU}/${dateStr}/`, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .apiHeaders */ .H)(token) }); if (detailResp.ok) { const detailData = await detailResp.json(); const menuGroups = detailData.results || []; let dayItems = []; for (const group of menuGroups) { if (group.items && Array.isArray(group.items)) { dayItems = dayItems.concat(group.items); } } if (dayItems.length > 0) { dayData = { date: dateStr, menu_items: dayItems, orders: dateObj.orders || [] }; } } } catch (err) { console.error(`Failed to fetch details for ${dateStr}:`, err); } finally { completed++; const pct = Math.round((completed / totalDates) * 100); progressFill.style.width = `${pct}%`; progressPercent.textContent = `${pct}%`; progressMessage.textContent = `Lade Menü für ${dateStr}...`; } return dayData; })); for (const result of results) { if (result) { allDays.push(result); } } } const weeksMap = new Map(); if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_ && _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.length > 0) { _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.forEach(w => { const key = `${w.year}-${w.weekNumber}`; try { weeksMap.set(key, { year: w.year, weekNumber: w.weekNumber, days: w.days ? w.days.map(d => ({ ...d, items: d.items ? [...d.items] : [] })) : [] }); } catch (e) { console.warn('Error hydrating week:', e); } }); } for (const day of allDays) { const d = new Date(day.date); const weekNum = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(d); const year = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getWeekYear */ .Ao)(d); const key = `${year}-${weekNum}`; if (!weeksMap.has(key)) { weeksMap.set(key, { year, weekNumber: weekNum, days: [] }); } const weekObj = weeksMap.get(key); const weekday = d.toLocaleDateString('en-US', { weekday: 'long' }); const orderCutoffDate = new Date(day.date); orderCutoffDate.setHours(10, 0, 0, 0); const newDayObj = { date: day.date, weekday: weekday, orderCutoff: orderCutoffDate.toISOString(), items: day.menu_items.map(item => { const isUnlimited = item.amount_tracking === false; const hasStock = parseInt(item.available_amount) > 0; return { id: `${day.date}_${item.id}`, articleId: item.id, name: item.name || 'Unknown', description: item.description || '', price: parseFloat(item.price) || 0, available: isUnlimited || hasStock, availableAmount: parseInt(item.available_amount) || 0, amountTracking: item.amount_tracking !== false }; }) }; const existingIndex = weekObj.days.findIndex(existing => existing.date === day.date); if (existingIndex >= 0) { weekObj.days[existingIndex] = newDayObj; } else { weekObj.days.push(newDayObj); } } const newAllWeeks = Array.from(weeksMap.values()).sort((a, b) => { if (a.year !== b.year) return a.year - b.year; return a.weekNumber - b.weekNumber; }); newAllWeeks.forEach(w => { if (w.days) w.days.sort((a, b) => a.date.localeCompare(b.date)); }); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setAllWeeks */ .tn)(newAllWeeks); saveMenuCache(); updateLastUpdatedTime(new Date().toISOString()); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentWeekNumber */ .Xt)((0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(new Date())); (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setCurrentYear */ .pK)(new Date().getFullYear()); updateAuthUI(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .renderVisibleWeeks */ .OR)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateNextWeekBadge */ .gJ)(); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); progressMessage.textContent = 'Fertig!'; setTimeout(() => progressModal.classList.add('hidden'), 500); } catch (error) { console.error('Error fetching menu:', error); progressModal.classList.add('hidden'); Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 842)).then(uiHelpers => { uiHelpers.showErrorModal( 'Keine Verbindung', `Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('noMenuData')} ${targetWeek} (${targetYear}).
${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('noMenuDataHint')}${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)((0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getLocalizedText */ .PC)(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'); (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .placeOrder */ .wH)(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; (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .cancelOrder */ .N4)(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; (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .toggleFlag */ .PQ)(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff); }); } body.appendChild(itemEl); }); card.appendChild(body); return card; } async function fetchVersions(devMode) { const endpoint = devMode ? `${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .GITHUB_API */ .pe}/tags?per_page=20` : `${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .GITHUB_API */ .pe}/releases?per_page=20`; const resp = await fetch(endpoint, { headers: (0,_api_js__WEBPACK_IMPORTED_MODULE_3__/* .githubHeaders */ .O)() }); 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: `${_constants_js__WEBPACK_IMPORTED_MODULE_2__/* .INSTALLER_BASE */ .d_}/${tag}/dist/install.html`, body: item.body || '' }; }); } async function checkForUpdates() { const currentVersion = '{{VERSION}}'; const devMode = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.DEV_MODE) === 'true'; try { const versions = await fetchVersions(devMode); if (!versions.length) return; localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.VERSION_CACHE, JSON.stringify({ timestamp: Date.now(), devMode, versions })); const latest = versions[0].tag; if (!(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .isNewer */ .U4)(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); } } 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(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.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: ${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(e.message)}
`; } } loadVersions(false); devToggle.onchange = () => { localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.DEV_MODE, devToggle.checked); localStorage.removeItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.VERSION_CACHE); loadVersions(true); }; } function updateCountdown() { if (!_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX || !_state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentUser */ .Ny) { 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 _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.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 = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('orderDeadline')}: ${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'); } } function removeCountdown() { const el = document.getElementById('order-countdown'); if (el) el.remove(); } setInterval(updateCountdown, 60000); setTimeout(updateCountdown, 1000); 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'); }); } function updateAlarmBell() { const bellBtn = document.getElementById('alarm-bell'); const bellIcon = document.getElementById('alarm-bell-icon'); if (!bellBtn || !bellIcon) return; if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.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 _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_) { if (!wk.days) continue; for (const d of wk.days) { if (!d.items) continue; for (const item of d.items) { if (item.available && _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.has(item.id)) { anyAvailable = true; break; } } if (anyAvailable) break; } if (anyAvailable) break; } const lastCheckedStr = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.LAST_CHECKED); const flaggedLastCheckedStr = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.FLAGGED_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(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.LAST_CHECKED, now); latestTime = new Date(now).getTime(); } timeStr = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getRelativeTime */ .gs)(new Date(latestTime)); bellBtn.title = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('alarmLastChecked')}: ${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)'; } } /***/ }, /***/ 413 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Ao: () => (/* binding */ getWeekYear), /* harmony export */ FS: () => (/* binding */ translateDay), /* harmony export */ PC: () => (/* binding */ getLocalizedText), /* harmony export */ U4: () => (/* binding */ isNewer), /* harmony export */ ZD: () => (/* binding */ escapeHtml), /* harmony export */ gs: () => (/* binding */ getRelativeTime), /* harmony export */ sn: () => (/* binding */ getISOWeek) /* harmony export */ }); /* unused harmony export splitLanguage */ /* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(901); 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(); } /** * Translates an English day name to the UI language. * Returns German by default; returns English when langMode is 'en'. * @param {string} englishDay - Day name in English (e.g. 'Monday') * @returns {string} Translated day name */ function translateDay(englishDay) { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en') return 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; } function 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; } function getRelativeTime(date) { const diffMs = Date.now() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return 'gerade eben'; if (diffMin === 1) return 'vor 1 min.'; if (diffMin < 60) return `vor ${diffMin} min.`; const diffH = Math.floor(diffMin / 60); if (diffH === 1) return 'vor 1 Std.'; return `vor ${diffH} Std.`; } // === Language Filter (FR-100) === const DE_STEMS = [ 'apfel', 'achtung', 'aubergine', 'auflauf', 'beere', 'blumenkohl', 'bohne', 'braten', 'brokkoli', 'brot', 'brust', 'brötchen', 'butter', 'chili', 'dessert', 'dip', 'eier', 'eintopf', 'eis', 'erbse', 'erdbeer', 'essig', 'filet', 'fisch', 'fisole', 'fleckerl', 'fleisch', 'flügel', 'frucht', 'für', 'gebraten', 'gemüse', 'gewürz', 'gratin', 'grieß', 'gulasch', 'gurke', 'himbeer', 'honig', 'huhn', 'hähnchen', 'jambalaya', 'joghurt', 'karotte', 'kartoffel', 'keule', 'kirsch', 'knacker', 'knoblauch', 'knödel', 'kompott', 'kraut', 'kräuter', 'kuchen', 'käse', 'kürbis', 'lauch', 'mandel', 'milch', 'mild', 'mit', 'mohn', 'most', 'möhre', 'natur', 'nockerl', 'nudel', 'nuss', 'nuß', 'obst', 'oder', 'olive', 'paprika', 'pfanne', 'pfannkuchen', 'pfeffer', 'pikant', 'pilz', 'plunder', 'püree', 'ragout', 'rahm', 'reis', 'rind', 'sahne', 'salami', 'salat', 'salz', 'sauer', 'scharf', 'schinken', 'schnitte', 'schnitzel', 'schoko', 'schupf', 'schwein', 'sellerie', 'senf', 'sosse', 'soße', 'spargel', 'spätzle', 'speck', 'spieß', 'spinat', 'steak', 'suppe', 'süß', 'tofu', 'tomate', 'topfen', 'torte', 'trüffel', 'und', 'vanille', 'vogerl', 'vom', 'wien', 'wurst', 'zucchini', 'zum', 'zur', 'zwiebel', 'öl' ]; const EN_STEMS = [ 'almond', 'and', 'apple', 'asparagus', 'bacon', 'baked', 'ball', 'bean', 'beef', 'berry', 'bread', 'breast', 'broccoli', 'bun', 'butter', 'cabbage', 'cake', 'caper', 'carrot', 'casserole', 'cauliflower', 'celery', 'cheese', 'cherry', 'chicken', 'chili', 'choco', 'chocolate', 'cider', 'cilantro', 'coffee', 'compote', 'cream', 'cucumber', 'curd', 'danish', 'dessert', 'dip', 'dumpling', 'egg', 'eggplant', 'filet', 'fish', 'for', 'fried', 'from', 'fruit', 'garlic', 'goulash', 'gratin', 'ham', 'herb', 'honey', 'hot', 'ice', 'jambalaya', 'leek', 'leg', 'mash', 'meat', 'mexican', 'mild', 'milk', 'mint', 'mushroom', 'mustard', 'noodle', 'nut', 'oat', 'oil', 'olive', 'onion', 'or', 'oven', 'pan', 'pancake', 'pea', 'pepper', 'plain', 'plate', 'poppy', 'pork', 'potato', 'pumpkin', 'radish', 'ragout', 'raspberry', 'rice', 'roast', 'roll', 'salad', 'salami', 'salt', 'sauce', 'sausage', 'shrimp', 'skewer', 'slice', 'soup', 'sour', 'spice', 'spicy', 'spinach', 'steak', 'stew', 'strawberr', 'strawberry', 'strudel', 'sweet', 'tart', 'thyme', 'to', 'tofu', 'tomat', 'tomato', 'truffle', 'trukey', 'turkey', 'vanilla', 'vegan', 'vegetable', 'vinegar', 'wedge', 'wing', 'with', 'wok', 'yogurt', 'zucchini' ]; function splitLanguage(text) { if (!text) return { de: '', en: '', raw: '' }; const raw = text; let formattedRaw = text.replace(/(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?=\S)(?!\s*\/)/g, '($1)\n• '); if (!formattedRaw.startsWith('• ')) { formattedRaw = '• ' + formattedRaw; } function scoreBlock(wordArray) { let de = 0, en = 0; wordArray.forEach(word => { const w = word.toLowerCase().replace(/[^a-zäöüß]/g, ''); if (w) { let bestDeMatch = 0; let bestEnMatch = 0; if (DE_STEMS.includes(w)) bestDeMatch = w.length; else DE_STEMS.forEach(s => { if (w.includes(s) && s.length > bestDeMatch) bestDeMatch = s.length; }); if (EN_STEMS.includes(w)) bestEnMatch = w.length; else EN_STEMS.forEach(s => { if (w.includes(s) && s.length > bestEnMatch) bestEnMatch = s.length; }); if (bestDeMatch > 0) de += (bestDeMatch / w.length); if (bestEnMatch > 0) en += (bestEnMatch / w.length); if (/^[A-ZÄÖÜ]/.test(word)) { de += 0.5; } } }); return { de, en }; } function heuristicSplitEnDe(fragment) { const words = fragment.trim().split(/\s+/); if (words.length < 2) return { enPart: fragment, nextDe: '' }; let bestK = -1; let maxScore = -9999; for (let k = 1; k < words.length; k++) { const left = words.slice(0, k); const right = words.slice(k); const leftScore = scoreBlock(left); const rightScore = scoreBlock(right); const rightFirstWord = right[0]; let capitalBonus = 0; if (/^[A-ZÄÖÜ]/.test(rightFirstWord)) { capitalBonus = 1.0; } const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en) + capitalBonus; const leftLooksEnglish = (leftScore.en > leftScore.de) || (leftScore.en > 0); const rightLooksGerman = (rightScore.de + capitalBonus) > rightScore.en; if (leftLooksEnglish && rightLooksGerman && score > maxScore) { maxScore = score; bestK = k; } } if (bestK !== -1) { return { enPart: words.slice(0, bestK).join(' '), nextDe: words.slice(bestK).join(' ') }; } return { enPart: fragment, nextDe: '' }; } const allergenRegex = /(.*?)(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?!\s*[/])/g; let match; const rawCourses = []; let lastScanIndex = 0; while ((match = allergenRegex.exec(text)) !== null) { if (match.index > lastScanIndex) { rawCourses.push(text.substring(lastScanIndex, match.index).trim()); } rawCourses.push(match[0].trim()); lastScanIndex = allergenRegex.lastIndex; } if (lastScanIndex < text.length) { rawCourses.push(text.substring(lastScanIndex).trim()); } if (rawCourses.length === 0 && text.trim() !== '') { rawCourses.push(text.trim()); } const deParts = []; const enParts = []; for (let course of rawCourses) { let courseMatch = course.match(/(.*?)(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*$/); let courseText = course; let allergenTxt = ""; let allergenCode = ""; if (courseMatch) { courseText = courseMatch[1].trim(); allergenCode = courseMatch[2]; allergenTxt = ` (${allergenCode})`; } const slashParts = courseText.split(/\s*\/\s*(?![A-Z,]+$)/); if (slashParts.length >= 2) { const deCandidate = slashParts[0].trim(); let enCandidate = slashParts.slice(1).join(' / ').trim(); const nestedSplit = heuristicSplitEnDe(enCandidate); if (nestedSplit.nextDe) { deParts.push(deCandidate + allergenTxt); enParts.push(nestedSplit.enPart + allergenTxt); const nestedDe = nestedSplit.nextDe + allergenTxt; deParts.push(nestedDe); enParts.push(nestedDe); } else { const enFinal = enCandidate + allergenTxt; const deFinal = deCandidate.includes(allergenTxt.trim()) ? deCandidate : (deCandidate + allergenTxt); deParts.push(deFinal); enParts.push(enFinal); } } else { const heuristicSplit = heuristicSplitEnDe(courseText); if (heuristicSplit.nextDe) { enParts.push(heuristicSplit.enPart + allergenTxt); deParts.push(heuristicSplit.nextDe + allergenTxt); } else { deParts.push(courseText + allergenTxt); enParts.push(courseText + allergenTxt); } } } let deJoined = deParts.join('\n• '); if (deParts.length > 0 && !deJoined.startsWith('• ')) deJoined = '• ' + deJoined; let enJoined = enParts.join('\n• '); if (enParts.length > 0 && !enJoined.startsWith('• ')) enJoined = '• ' + enJoined; return { de: deJoined, en: enJoined, raw: formattedRaw }; } function getLocalizedText(text) { if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'all') return text || ''; const split = splitLanguage(text); if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en') return split.en || split.raw; return split.de || split.raw; } /***/ } /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // EXTERNAL MODULE: ./src/state.js var state = __webpack_require__(901); ;// ./src/ui.js /** * UI injection module. * Renders the full Kantine Wrapper HTML skeleton into the current page, * including fonts, icon stylesheet, favicon, and all modal/panel containers. * Must be called before bindEvents() and any state-rendering logic. */ /** * Injects the full application HTML into the current tab. * Idempotent in conjunction with the __KANTINE_LOADED guard in index.js. */ function injectUI() { document.title = 'Kantine Weekly Menu'; if (document.querySelectorAll) { document.querySelectorAll('link[rel*="icon"]').forEach(el => el.remove()); } const favicon = document.createElement('link'); favicon.rel = 'icon'; favicon.type = 'image/png'; favicon.href = '{{FAVICON_DATA_URI}}'; document.head.appendChild(favicon); 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); } const htmlContent = `Lade Menüdaten...