/******/ (() => { // 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; if (!token) { const bellBtn = document.getElementById('alarm-bell'); if (bellBtn) bellBtn.classList.remove('refreshing'); return; } // 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) { if (!tag) return false; tag = tag.trim(); if (tag.length < 2) { showToast('Tag muss mindestens 2 Zeichen lang sein.', 'error'); return false; } if (tag.length > 20) { showToast('Tag darf maximal 20 Zeichen lang sein.', 'error'); return false; } // Only allow alphanumeric characters, spaces and common special chars for food if (!/^[a-zA-Z0-9äöüÄÖÜß\s\-\.]+$/.test(tag)) { showToast('Ungültige Zeichen im Tag.', 'error'); return false; } if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.includes(tag)) return false; const newTags = [..._state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz, tag]; (0,_state_js__WEBPACK_IMPORTED_MODULE_0__/* .setHighlightTags */ .iw)(newTags); saveHighlightTags(); return true; } 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'); if (!list) return; list.innerHTML = ''; // Clear existing content _state_js__WEBPACK_IMPORTED_MODULE_0__/* .highlightTags */ .yz.forEach(tag => { const badge = document.createElement('span'); badge.className = 'tag-badge'; const label = document.createElement('span'); label.textContent = tag; badge.appendChild(label); const removeBtn = document.createElement('span'); removeBtn.className = 'tag-remove'; removeBtn.innerHTML = '×'; removeBtn.title = (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('removeTagTooltip') || 'Entfernen'; removeBtn.onclick = () => { removeHighlightTag(tag); renderTagsList(); }; badge.appendChild(removeBtn); list.appendChild(badge); }); } 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; if (!token) { loading.classList.add('hidden'); return; } 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.', error.message, 'Zur Original-Seite', 'https://web.bessa.app/knapp-kantine' ); }); } finally { loading.classList.add('hidden'); } } let lastUpdatedTimestamp = null; let lastUpdatedIntervalId = null; function updateLastUpdatedTime(isoTimestamp) { const subtitle = document.getElementById('last-updated-subtitle'); if (!isoTimestamp) return; lastUpdatedTimestamp = isoTimestamp; localStorage.setItem('kantine_last_updated', isoTimestamp); localStorage.setItem('kantine_last_checked', isoTimestamp); 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' }); const ago = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getRelativeTime */ .gs)(date); subtitle.textContent = `Aktualisiert: ${dateStr} ${timeStr} (${ago})`; } catch (e) { subtitle.textContent = ''; } if (!lastUpdatedIntervalId) { lastUpdatedIntervalId = setInterval(() => { if (lastUpdatedTimestamp) { updateLastUpdatedTime(lastUpdatedTimestamp); (0,_ui_helpers_js__WEBPACK_IMPORTED_MODULE_4__/* .updateAlarmBell */ .Mb)(); } }, 60 * 1000); } } 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 = `${icon}${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(message)}`; container.appendChild(toast); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } /***/ }, /***/ 672 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ H: () => (/* binding */ apiHeaders), /* harmony export */ O: () => (/* binding */ githubHeaders) /* harmony export */ }); /* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(521); /** * API header factories for the Bessa REST API and GitHub API. * All fetch calls in the app route through these helpers to ensure * consistent auth and versioning headers. */ /** * Returns request headers for the Bessa REST API. * @param {string|null} token - Auth token. * @returns {Object} HTTP headers for fetch() */ function apiHeaders(token) { const headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Client-Version': _constants_js__WEBPACK_IMPORTED_MODULE_0__/* .CLIENT_VERSION */ .fZ }; if (token) { headers['Authorization'] = `Token ${token}`; } return headers; } /** * Returns request headers for the GitHub REST API v3. * Used for version checks and release listing. * @returns {Object} HTTP headers for fetch() */ function githubHeaders() { return { 'Accept': 'application/vnd.github.v3+json' }; } /***/ }, /***/ 521 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ LS: () => (/* binding */ LS), /* harmony export */ YU: () => (/* binding */ MENU_ID), /* harmony export */ d_: () => (/* binding */ INSTALLER_BASE), /* harmony export */ eW: () => (/* binding */ VENUE_ID), /* harmony export */ fZ: () => (/* binding */ CLIENT_VERSION), /* harmony export */ fv: () => (/* binding */ POLL_INTERVAL_MS), /* harmony export */ pe: () => (/* binding */ GITHUB_API), /* harmony export */ tE: () => (/* binding */ API_BASE) /* harmony export */ }); /* unused harmony export GITHUB_REPO */ /** * Application-wide constants. * All API endpoints, IDs and timing parameters are centralized here * to make changes easy and avoid magic numbers scattered across the codebase. */ /** Base URL for the Bessa REST API (v1). */ const API_BASE = 'https://api.bessa.app/v1'; /** The client version injected into every API request header. */ const CLIENT_VERSION = '{{VERSION}}'; /** Bessa venue ID for Knapp-Kantine. */ const VENUE_ID = 591; /** Bessa menu ID for the weekly lunch menu. */ const MENU_ID = 7; /** Polling interval for flagged-menu availability checks (5 minutes). */ const POLL_INTERVAL_MS = 5 * 60 * 1000; /** GitHub repository identifier for update checks and release links. */ const GITHUB_REPO = 'TauNeutrino/kantine-overview'; /** GitHub REST API base URL for this repository. */ const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}`; /** Base URL for htmlpreview-hosted installer pages. */ const INSTALLER_BASE = `https://htmlpreview.github.io/?https://github.com/${GITHUB_REPO}/blob`; /** * Centralized localStorage key registry. * Always use these constants instead of raw strings to avoid typos and ease renaming. */ const LS = { AUTH_TOKEN: 'kantine_authToken', CURRENT_USER: 'kantine_currentUser', FIRST_NAME: 'kantine_firstName', LAST_NAME: 'kantine_lastName', LANG: 'kantine_lang', FLAGS: 'kantine_flags', FLAGGED_LAST_CHECKED: 'kantine_flagged_items_last_checked', LAST_CHECKED: 'kantine_last_checked', MENU_CACHE: 'kantine_menuCache', MENU_CACHE_TS: 'kantine_menuCacheTs', HISTORY_CACHE: 'kantine_history_cache', HIGHLIGHT_TAGS: 'kantine_highlightTags', LAST_UPDATED: 'kantine_last_updated', VERSION_CACHE: 'kantine_version_cache', DEV_MODE: 'kantine_dev_mode', }; /***/ }, /***/ 646 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ t: () => (/* binding */ t) /* harmony export */ }); /* unused harmony export getUILang */ /* unused harmony import specifier */ var langMode; /* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(901); /** * Internationalization (i18n) module for the Kantine Wrapper UI. * Provides translations for all static UI text based on the current language mode. * German (de) is the default; English (en) is fully supported. * When langMode is 'all', German labels are used for the GUI. */ const TRANSLATIONS = { de: { // Navigation thisWeek: 'Diese Woche', nextWeek: 'Nächste Woche', nextWeekTooltipDefault: 'Menü nächster Woche anzeigen', thisWeekTooltip: 'Menü dieser Woche anzeigen', // Header appTitle: 'Kantinen Übersicht', updatedAt: 'Aktualisiert', langTooltip: 'Sprache der Menübeschreibung', weekLabel: 'Woche', // Action buttons refresh: 'Menüdaten neu laden', history: 'Bestellhistorie', highlights: 'Persönliche Highlights verwalten', themeTooltip: 'Erscheinungsbild (Hell/Dunkel) wechseln', login: 'Anmelden', loginTooltip: 'Mit Bessa.app Account anmelden', logout: 'Abmelden', logoutTooltip: 'Von Bessa.app abmelden', // Login modal loginTitle: 'Login', employeeId: 'Mitarbeiternummer', employeeIdPlaceholder: 'z.B. 2041', employeeIdHelp: 'Deine offizielle Knapp Mitarbeiternummer.', password: 'Passwort', passwordPlaceholder: 'Bessa Passwort', passwordHelp: 'Das Passwort für deinen Bessa Account.', loginButton: 'Einloggen', loggingIn: 'Wird eingeloggt...', // Highlights modal highlightsTitle: 'Meine Highlights', highlightsDesc: 'Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.', tagInputPlaceholder: 'z.B. Schnitzel, Vegetarisch...', tagInputTooltip: 'Neues Schlagwort zum Hervorheben eingeben', addTag: 'Hinzufügen', addTagTooltip: 'Schlagwort zur Liste hinzufügen', removeTagTooltip: 'Schlagwort entfernen', // History modal historyTitle: 'Bestellhistorie', loadingHistory: 'Lade Historie...', noOrders: 'Keine Bestellungen gefunden.', orders: 'Bestellungen', historyMonthToggle: 'Klicken, um die Bestellungen für diesen Monat ein-/auszublenden', // Menu item labels available: 'Verfügbar', soldOut: 'Ausverkauft', ordered: 'Bestellt', orderButton: 'Bestellen', orderAgainTooltip: 'nochmal bestellen', orderTooltip: 'bestellen', cancelOrder: 'Bestellung stornieren', cancelOneOrder: 'Eine Bestellung stornieren', flagActivate: 'Benachrichtigen wenn verfügbar', flagDeactivate: 'Benachrichtigung deaktivieren', // Alarm bell alarmTooltipNone: 'Keine beobachteten Menüs', alarmLastChecked: 'Zuletzt geprüft', // Version modal versionsTitle: '📦 Versionen', currentVersion: 'Aktuell', devModeLabel: 'Dev-Mode (alle Tags anzeigen)', loadingVersions: 'Lade Versionen...', noVersions: 'Keine Versionen gefunden.', installed: '✓ Installiert', newVersion: '⬆ Neu!', installLink: 'Installieren', reportBug: 'Fehler melden', reportBugTooltip: 'Melde einen Fehler auf GitHub', featureRequest: 'Feature vorschlagen', featureRequestTooltip: 'Schlage ein neues Feature auf GitHub vor', clearCache: 'Lokalen Cache leeren', clearCacheTooltip: 'Löscht alle lokalen Daten & erzwingt einen Neuladen', clearCacheConfirm: 'Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.', versionMenuTooltip: 'Klick für Versionsmenü', // Progress modal progressTitle: 'Menüdaten aktualisieren', progressInit: 'Initialisierung...', // Empty state noMenuData: 'Keine Menüdaten für KW', noMenuDataHint: 'Versuchen Sie eine andere Woche oder schauen Sie später vorbei.', // Weekly cost // Countdown orderDeadline: 'Bestellschluss', // Toast messages flagRemoved: 'Flag entfernt für', flagActivated: 'Benachrichtigung aktiviert für', menuChecked: 'geprüft', menuSingular: 'Menü', menuPlural: 'Menüs', newMenuDataAvailable: 'Neue Menüdaten für nächste Woche verfügbar!', orderSuccess: 'Bestellt', cancelSuccess: 'Storniert', bgSyncFailed: 'Hintergrund-Synchronisation fehlgeschlagen', historyLoadError: 'Fehler beim Laden der Historie.', historyLoadingFull: 'Lade Bestellhistorie...', historyLoadingDelta: 'Suche nach neuen Bestellungen...', historyLoadingItem: 'Lade Bestellung', historyLoadingOf: 'von', historyLoadingNew: 'neue/geänderte Bestellungen gefunden...', // Badge tooltip parts badgeOrdered: 'bestellt', badgeOrderable: 'bestellbar', badgeTotal: 'gesamt', badgeHighlights: 'Highlights gefunden', // History item states stateCancelled: 'Storniert', stateCompleted: 'Abgeschlossen', stateTransferred: 'Übertragen', // Close button close: 'Schließen', // Error modal noConnection: 'Keine Verbindung', toOriginalPage: 'Zur Original-Seite', // Misc loggedIn: 'Angemeldet', }, en: { // Navigation thisWeek: 'This Week', nextWeek: 'Next Week', nextWeekTooltipDefault: 'Show next week\'s menu', thisWeekTooltip: 'Show this week\'s menu', // Header appTitle: 'Canteen Overview', updatedAt: 'Updated', langTooltip: 'Menu description language', weekLabel: 'Week', // Action buttons refresh: 'Reload menu data', history: 'Order history', highlights: 'Manage personal highlights', themeTooltip: 'Toggle appearance (Light/Dark)', login: 'Sign in', loginTooltip: 'Sign in with Bessa.app account', logout: 'Sign out', logoutTooltip: 'Sign out from Bessa.app', // Login modal loginTitle: 'Login', employeeId: 'Employee ID', employeeIdPlaceholder: 'e.g. 2041', employeeIdHelp: 'Your official Knapp employee number.', password: 'Password', passwordPlaceholder: 'Bessa password', passwordHelp: 'The password for your Bessa account.', loginButton: 'Log in', loggingIn: 'Logging in...', // Highlights modal highlightsTitle: 'My Highlights', highlightsDesc: 'Automatically highlight menus containing these keywords.', tagInputPlaceholder: 'e.g. Schnitzel, Vegetarian...', tagInputTooltip: 'Enter new keyword to highlight', addTag: 'Add', addTagTooltip: 'Add keyword to list', removeTagTooltip: 'Remove keyword', // History modal historyTitle: 'Order History', loadingHistory: 'Loading history...', noOrders: 'No orders found.', orders: 'Orders', historyMonthToggle: 'Click to expand/collapse orders for this month', // Menu item labels available: 'Available', soldOut: 'Sold out', ordered: 'Ordered', orderButton: 'Order', orderAgainTooltip: 'order again', orderTooltip: 'order', cancelOrder: 'Cancel order', cancelOneOrder: 'Cancel one order', flagActivate: 'Notify when available', flagDeactivate: 'Deactivate notification', // Alarm bell alarmTooltipNone: 'No flagged menus', alarmLastChecked: 'Last checked', // Version modal versionsTitle: '📦 Versions', currentVersion: 'Current', devModeLabel: 'Dev mode (show all tags)', loadingVersions: 'Loading versions...', noVersions: 'No versions found.', installed: '✓ Installed', newVersion: '⬆ New!', installLink: 'Install', reportBug: 'Report a bug', reportBugTooltip: 'Report a bug on GitHub', featureRequest: 'Request a feature', featureRequestTooltip: 'Suggest a new feature on GitHub', clearCache: 'Clear local cache', clearCacheTooltip: 'Deletes all local data & forces a reload', clearCacheConfirm: 'Do you really want to delete all local data (including login session, cache, and settings)? The page will reload afterwards.', versionMenuTooltip: 'Click for version menu', // Progress modal progressTitle: 'Updating menu data', progressInit: 'Initializing...', // Empty state noMenuData: 'No menu data for CW', noMenuDataHint: 'Try another week or check back later.', // Weekly cost // Countdown orderDeadline: 'Order deadline', // Toast messages flagRemoved: 'Flag removed for', flagActivated: 'Notification activated for', menuChecked: 'checked', menuSingular: 'menu', menuPlural: 'menus', newMenuDataAvailable: 'New menu data available for next week!', orderSuccess: 'Ordered', cancelSuccess: 'Cancelled', bgSyncFailed: 'Background synchronisation failed', historyLoadError: 'Error loading history.', historyLoadingFull: 'Loading order history...', historyLoadingDelta: 'Checking for new orders...', historyLoadingItem: 'Loading order', historyLoadingOf: 'of', historyLoadingNew: 'new/updated orders found...', // Badge tooltip parts badgeOrdered: 'ordered', badgeOrderable: 'orderable', badgeTotal: 'total', badgeHighlights: 'highlights found', // History item states stateCancelled: 'Cancelled', stateCompleted: 'Completed', stateTransferred: 'Transferred', // Close button close: 'Close', // Error modal noConnection: 'No connection', toOriginalPage: 'Go to original page', // Misc loggedIn: 'Logged in', } }; /** * Returns the translated string for the given key. * Uses the current langMode (en = English, anything else = German). * Falls back to German if a key is missing in the target language. * @param {string} key - Translation key * @returns {string} Translated text */ function t(key) { const lang = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .langMode */ .Kl === 'en' ? 'en' : 'de'; return TRANSLATIONS[lang][key] || TRANSLATIONS['de'][key] || key; } /** * Returns the effective UI language code ('en' or 'de'). * 'all' mode uses German for the GUI. */ function getUILang() { return langMode === 'en' ? 'en' : 'de'; } /***/ }, /***/ 901 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ BT: () => (/* binding */ currentWeekNumber), /* harmony export */ BY: () => (/* binding */ userFlags), /* harmony export */ K8: () => (/* binding */ pollIntervalId), /* harmony export */ Kl: () => (/* binding */ langMode), /* harmony export */ L: () => (/* binding */ orderMap), /* harmony export */ Ny: () => (/* binding */ currentUser), /* harmony export */ O5: () => (/* binding */ setAuthToken), /* harmony export */ UD: () => (/* binding */ setLangMode), /* harmony export */ Xt: () => (/* binding */ setCurrentWeekNumber), /* harmony export */ cc: () => (/* binding */ setPollIntervalId), /* harmony export */ di: () => (/* binding */ setOrderMap), /* harmony export */ gX: () => (/* binding */ authToken), /* harmony export */ iw: () => (/* binding */ setHighlightTags), /* harmony export */ lt: () => (/* binding */ setCurrentUser), /* harmony export */ pK: () => (/* binding */ setCurrentYear), /* harmony export */ p_: () => (/* binding */ allWeeks), /* harmony export */ qo: () => (/* binding */ setDisplayMode), /* harmony export */ sw: () => (/* binding */ displayMode), /* harmony export */ tn: () => (/* binding */ setAllWeeks), /* harmony export */ vW: () => (/* binding */ currentYear), /* harmony export */ yz: () => (/* binding */ highlightTags) /* harmony export */ }); /* unused harmony export setUserFlags */ /* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(413); /* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(521); let allWeeks = []; let currentWeekNumber = (0,_utils_js__WEBPACK_IMPORTED_MODULE_0__/* .getISOWeek */ .sn)(new Date()); let currentYear = new Date().getFullYear(); let displayMode = 'this-week'; let authToken = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_1__.LS.AUTH_TOKEN); let currentUser = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_1__.LS.CURRENT_USER); let orderMap = new Map(); let userFlags = new Set(JSON.parse(localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_1__.LS.FLAGS) || '[]')); let pollIntervalId = null; let langMode = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_1__.LS.LANG) || 'de'; let highlightTags = JSON.parse(localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_1__.LS.HIGHLIGHT_TAGS) || '[]'); function setAllWeeks(weeks) { allWeeks = weeks; } function setCurrentWeekNumber(week) { currentWeekNumber = week; } function setCurrentYear(year) { currentYear = year; } function setAuthToken(token) { authToken = token; } function setCurrentUser(user) { currentUser = user; } function setOrderMap(map) { orderMap = map; } function setUserFlags(flags) { userFlags = flags; } function setPollIntervalId(id) { pollIntervalId = id; } function setHighlightTags(tags) { highlightTags = tags; } /** Only 'this-week' and 'next-week' are valid display modes. */ function setDisplayMode(mode) { if (mode !== 'this-week' && mode !== 'next-week') { console.warn(`[state] Invalid displayMode: "${mode}". Ignoring.`); return; } displayMode = mode; } /** Only 'de', 'en', and 'all' are valid language modes. */ function setLangMode(lang) { if (!['de', 'en', 'all'].includes(lang)) { console.warn(`[state] Invalid langMode: "${lang}". Ignoring.`); return; } langMode = lang; } /***/ }, /***/ 842 (__unused_webpack_module, __webpack_exports__, __webpack_require__) { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ Gk: () => (/* binding */ openVersionMenu), /* harmony export */ Mb: () => (/* binding */ updateAlarmBell), /* harmony export */ OR: () => (/* binding */ renderVisibleWeeks), /* harmony export */ Ux: () => (/* binding */ checkForUpdates), /* harmony export */ gJ: () => (/* binding */ updateNextWeekBadge), /* harmony export */ showErrorModal: () => (/* binding */ showErrorModal), /* harmony export */ wy: () => (/* binding */ syncMenuItemHeights) /* harmony export */ }); /* unused harmony exports createDayCard, fetchVersions, updateCountdown, removeCountdown */ /* 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 _actions_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(367); /* harmony import */ var _i18n_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(646); /** * Updates the "Next Week" button tooltip and glow state. * Tooltip shows order status summary and highlight count. * Glow activates only if Mon-Thu have orderable menus without orders (Friday exempt). */ function updateNextWeekBadge() { const btnNextWeek = document.getElementById('btn-next-week'); let nextWeek = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentWeekNumber */ .BT + 1; let nextYear = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentYear */ .vW; if (nextWeek > 52) { nextWeek = 1; nextYear++; } const nextWeekData = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.find(w => w.weekNumber === nextWeek && w.year === nextYear); let totalDataCount = 0; let orderableCount = 0; let daysWithOrders = 0; let monThuOrderableNoOrder = 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 (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.has(key) && _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.get(key).length > 0) hasOrder = true; }); if (hasOrder) daysWithOrders++; // Feature 5: Only Mon(1)-Thu(4) count for glow logic, Friday(5) is exempt const dayOfWeek = new Date(day.date).getDay(); if (dayOfWeek >= 1 && dayOfWeek <= 4 && isOrderable && !hasOrder) { monThuOrderableNoOrder++; } } }); } // Remove any old visible badge element (Feature 3: numbers hidden) const existingBadge = btnNextWeek.querySelector('.nav-badge'); if (existingBadge) existingBadge.remove(); if (totalDataCount > 0) { // Count highlight menus in next week let highlightCount = 0; if (nextWeekData && nextWeekData.days) { nextWeekData.days.forEach(day => { day.items.forEach(item => { const nameMatches = (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .checkHighlight */ .BM)(item.name); const descMatches = (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .checkHighlight */ .BM)(item.description); if (nameMatches.length > 0 || descMatches.length > 0) { highlightCount++; } }); }); } // Feature 3: All info goes to button tooltip instead of visible badge let tooltipParts = [`${daysWithOrders} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('badgeOrdered')} / ${orderableCount} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('badgeOrderable')} / ${totalDataCount} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('badgeTotal')}`]; if (highlightCount > 0) { tooltipParts.push(`${highlightCount} ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('badgeHighlights')}`); } btnNextWeek.title = tooltipParts.join(' • '); // Feature 5: Glow only if Mon-Thu have orderable days without existing orders if (monThuOrderableNoOrder > 0) { btnNextWeek.classList.add('new-week-available'); const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`; if (!localStorage.getItem(storageKey)) { localStorage.setItem(storageKey, 'true'); (0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .showToast */ .P0)((0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('newMenuDataAvailable'), 'info'); } } else { btnNextWeek.classList.remove('new-week-available'); } } else { btnNextWeek.title = (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('nextWeekTooltipDefault'); btnNextWeek.classList.remove('new-week-available'); } } function renderVisibleWeeks() { const menuContainer = document.getElementById('menu-container'); if (!menuContainer) return; menuContainer.innerHTML = ''; let targetWeek = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentWeekNumber */ .BT; let targetYear = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .currentYear */ .vW; if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .displayMode */ .sw === 'next-week') { targetWeek++; if (targetWeek > 52) { targetWeek = 1; targetYear++; } } const allDays = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .allWeeks */ .p_.flatMap(w => w.days || []); const daysInTargetWeek = allDays.filter(day => { const d = new Date(day.date); return (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getISOWeek */ .sn)(d) === targetWeek && (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .getWeekYear */ .Ao)(d) === targetYear; }); if (daysInTargetWeek.length === 0) { menuContainer.innerHTML = `

${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('noMenuData')} ${targetWeek} (${targetYear}).

${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('noMenuDataHint')}
`; return; } const headerWeekInfo = document.getElementById('header-week-info'); const weekTitle = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .displayMode */ .sw === 'this-week' ? (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('thisWeek') : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('nextWeek'); headerWeekInfo.innerHTML = `
${weekTitle}
${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('weekLabel')} ${targetWeek} • ${targetYear}
`; const grid = document.createElement('div'); grid.className = 'days-grid'; daysInTargetWeek.sort((a, b) => a.date.localeCompare(b.date)); const workingDays = daysInTargetWeek.filter(d => { const date = new Date(d.date); const day = date.getDay(); return day !== 0 && day !== 6; }); workingDays.forEach(day => { const card = createDayCard(day); if (card) grid.appendChild(card); }); menuContainer.appendChild(grid); setTimeout(() => syncMenuItemHeights(grid), 0); } function syncMenuItemHeights(grid) { const cards = grid.querySelectorAll('.menu-card'); if (cards.length === 0) return; // 1. Gather all menu-item groups (rows) across cards const itemRows = []; let maxItems = 0; const cardItems = Array.from(cards).map(card => { const items = Array.from(card.querySelectorAll('.menu-item')); maxItems = Math.max(maxItems, items.length); return items; }); for (let i = 0; i < maxItems; i++) { // Collect i-th item from each card (forming a "row") itemRows[i] = cardItems.map(items => items[i]).filter(item => !!item); } // 2. Batch Reset (Write phase) - clear old heights to let them flow naturally itemRows.flat().forEach(item => { item.style.height = 'auto'; }); // 3. Batch Read (Read phase) - measure all heights in one pass to avoid layout thrashing const rowMaxHeights = itemRows.map(row => { return Math.max(...row.map(item => item.offsetHeight)); }); // 4. Batch Apply (Write phase) - set synchronized heights itemRows.forEach((row, i) => { const height = `${rowMaxHeights[i]}px`; row.forEach(item => { item.style.height = height; }); }); } function createDayCard(day) { if (!day.items || day.items.length === 0) return null; const card = document.createElement('div'); card.className = 'menu-card'; const now = new Date(); const cardDate = new Date(day.date); let isPastCutoff = false; if (day.orderCutoff) { isPastCutoff = now >= new Date(day.orderCutoff); } else { const today = new Date(); today.setHours(0, 0, 0, 0); const cd = new Date(day.date); cd.setHours(0, 0, 0, 0); isPastCutoff = cd < today; } if (isPastCutoff) card.classList.add('past-day'); const menuBadges = []; if (day.items) { day.items.forEach(item => { const articleId = item.articleId || parseInt(item.id.split('_')[1]); const orderKey = `${day.date}_${articleId}`; const orders = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.get(orderKey) || []; const count = orders.length; if (count > 0) { const match = item.name.match(/([M][1-9][Ff]?)/); if (match) { let code = match[1]; if (count > 1) code += '+'; menuBadges.push(code); } } }); } const header = document.createElement('div'); header.className = 'card-header'; const dateStr = cardDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const badgesHtml = menuBadges.reduce((acc, code) => acc + `${code}`, ''); let headerClass = ''; const hasAnyOrder = day.items && day.items.some(item => { const articleId = item.articleId || parseInt(item.id.split('_')[1]); const key = `${day.date}_${articleId}`; return _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.has(key) && _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.get(key).length > 0; }); const hasOrderable = day.items && day.items.some(item => item.available); if (hasAnyOrder) { headerClass = 'header-violet'; } else if (hasOrderable && !isPastCutoff) { headerClass = 'header-green'; } else { headerClass = 'header-red'; } if (headerClass) header.classList.add(headerClass); header.innerHTML = `
${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .translateDay */ .FS)(day.weekday)}
${badgesHtml}
${dateStr}`; card.appendChild(header); const body = document.createElement('div'); body.className = 'card-body'; const todayDateStr = new Date().toISOString().split('T')[0]; const isToday = day.date === todayDateStr; const sortedItems = [...day.items].sort((a, b) => { if (isToday) { const aId = a.articleId || parseInt(a.id.split('_')[1]); const bId = b.articleId || parseInt(b.id.split('_')[1]); const aOrdered = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.has(`${day.date}_${aId}`); const bOrdered = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.has(`${day.date}_${bId}`); if (aOrdered && !bOrdered) return -1; if (!aOrdered && bOrdered) return 1; } return a.name.localeCompare(b.name); }); sortedItems.forEach(item => { const itemEl = document.createElement('div'); itemEl.className = 'menu-item'; const articleId = item.articleId || parseInt(item.id.split('_')[1]); const orderKey = `${day.date}_${articleId}`; const orderIds = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .orderMap */ .L.get(orderKey) || []; const orderCount = orderIds.length; let statusBadge = ''; if (item.available) { statusBadge = item.amountTracking ? `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('available')} (${item.availableAmount})` : `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('available')}`; } else { statusBadge = `${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('soldOut')}`; } let orderedBadge = ''; if (orderCount > 0) { const countBadge = orderCount > 1 ? `${orderCount}` : ''; orderedBadge = `check_circle ${(0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('ordered')}${countBadge}`; itemEl.classList.add('ordered'); if (new Date(day.date).toDateString() === now.toDateString()) { itemEl.classList.add('today-ordered'); } } const flagId = `${day.date}_${articleId}`; const isFlagged = _state_js__WEBPACK_IMPORTED_MODULE_0__/* .userFlags */ .BY.has(flagId); if (isFlagged) { itemEl.classList.add(item.available ? 'flagged-available' : 'flagged-sold-out'); } const matchedTags = [...new Set([...(0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .checkHighlight */ .BM)(item.name), ...(0,_actions_js__WEBPACK_IMPORTED_MODULE_4__/* .checkHighlight */ .BM)(item.description)])]; if (matchedTags.length > 0) { itemEl.classList.add('highlight-glow'); } let orderButton = ''; let cancelButton = ''; let flagButton = ''; if (_state_js__WEBPACK_IMPORTED_MODULE_0__/* .authToken */ .gX && !isPastCutoff) { const flagIcon = isFlagged ? 'notifications_active' : 'notifications_none'; const flagClass = isFlagged ? 'btn-flag active' : 'btn-flag'; const flagTitle = isFlagged ? (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('flagDeactivate') : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('flagActivate'); if (!item.available || isFlagged) { flagButton = ``; } if (item.available) { if (orderCount > 0) { orderButton = ``; } else { orderButton = ``; } } if (orderCount > 0) { const cancelIcon = orderCount === 1 ? 'close' : 'remove'; const cancelTitle = orderCount === 1 ? (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('cancelOrder') : (0,_i18n_js__WEBPACK_IMPORTED_MODULE_5__.t)('cancelOneOrder'); cancelButton = ``; } } let tagsHtml = ''; if (matchedTags.length > 0) { const badges = matchedTags.reduce((acc, t) => acc + `star${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(t)}`, ''); tagsHtml = `
${badges}
`; } itemEl.innerHTML = `
${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(item.name)} ${item.price.toFixed(2)} €
${orderedBadge} ${cancelButton} ${orderButton} ${flagButton}
${statusBadge}
${tagsHtml}

${(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 = ''; const list = container.querySelector('.version-list'); versions.forEach(v => { const isCurrent = v.tag === currentVersion; const isNew = (0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .isNewer */ .U4)(v.tag, currentVersion); const li = document.createElement('li'); li.className = 'version-item' + (isCurrent ? ' current' : ''); let badge = ''; if (isCurrent) badge = '✓ Installiert'; else if (isNew) badge = '⬆ Neu!'; let action = ''; if (!isCurrent) { action = `Installieren`; } li.innerHTML = `
${(0,_utils_js__WEBPACK_IMPORTED_MODULE_1__/* .escapeHtml */ .ZD)(v.tag)} ${badge}
${action} `; list.appendChild(li); }); } try { const cachedRaw = localStorage.getItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.VERSION_CACHE); let cached = null; if (cachedRaw) { try { cached = JSON.parse(cachedRaw); } catch (e) { } } if (cached && cached.devMode === dm && cached.versions) { renderVersionsList(cached.versions); } const liveVersions = await fetchVersions(dm); const liveVersionsStr = JSON.stringify(liveVersions); const cachedVersionsStr = cached ? JSON.stringify(cached.versions) : ''; if (liveVersionsStr !== cachedVersionsStr) { localStorage.setItem(_constants_js__WEBPACK_IMPORTED_MODULE_2__.LS.VERSION_CACHE, JSON.stringify({ timestamp: Date.now(), devMode: dm, versions: liveVersions })); renderVersionsList(liveVersions); } } catch (e) { 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, message, details, 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'; // Removed hidden because we are showing it now const content = document.createElement('div'); content.className = 'modal-content'; const header = document.createElement('div'); header.className = 'modal-header'; const h2 = document.createElement('h2'); h2.style.cssText = 'color: var(--error-color); display: flex; align-items: center; gap: 10px;'; const icon = document.createElement('span'); icon.className = 'material-icons-round'; icon.textContent = 'signal_wifi_off'; h2.appendChild(icon); const titleSpan = document.createElement('span'); titleSpan.textContent = title; h2.appendChild(titleSpan); header.appendChild(h2); content.appendChild(header); const body = document.createElement('div'); body.style.padding = '20px'; const p = document.createElement('p'); p.style.cssText = 'margin-bottom: 15px; color: var(--text-primary);'; p.textContent = message; body.appendChild(p); if (details) { const small = document.createElement('small'); small.style.cssText = 'display: block; margin-top: 10px; color: var(--text-secondary);'; small.textContent = details; body.appendChild(small); } const footer = document.createElement('div'); footer.style.cssText = 'margin-top: 20px; display: flex; justify-content: center;'; const btn = document.createElement('button'); btn.style.cssText = ` background-color: var(--accent-color); color: white; padding: 12px 24px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 12px rgba(233, 69, 96, 0.3); `; btn.textContent = btnText || 'Zur Original-Seite'; btn.onclick = () => { window.open(url || 'https://web.bessa.app/knapp-kantine', '_blank'); modal.classList.add('hidden'); }; footer.appendChild(btn); body.appendChild(footer); content.appendChild(body); modal.appendChild(content); document.body.appendChild(modal); } 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 */ sg: () => (/* binding */ debounce), /* 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; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /***/ } /******/ }); /************************************************************************/ /******/ // 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 = `
Logo

Kantinen Übersicht {{VERSION}}

Lade Menüdaten...

`; document.body.innerHTML = htmlContent; } // EXTERNAL MODULE: ./src/actions.js var actions = __webpack_require__(367); // EXTERNAL MODULE: ./src/ui_helpers.js var ui_helpers = __webpack_require__(842); // EXTERNAL MODULE: ./src/constants.js var constants = __webpack_require__(521); // EXTERNAL MODULE: ./src/api.js var api = __webpack_require__(672); // EXTERNAL MODULE: ./src/i18n.js var i18n = __webpack_require__(646); // EXTERNAL MODULE: ./src/utils.js var utils = __webpack_require__(413); ;// ./src/events.js /** * Updates all static UI labels/tooltips to match the current language. * Called when the user switches the language toggle. */ function updateUILanguage() { // Navigation buttons const btnThisWeek = document.getElementById('btn-this-week'); const btnNextWeek = document.getElementById('btn-next-week'); if (btnThisWeek) { btnThisWeek.textContent = (0,i18n.t)('thisWeek'); btnThisWeek.title = (0,i18n.t)('thisWeekTooltip'); } if (btnNextWeek) { btnNextWeek.textContent = (0,i18n.t)('nextWeek'); // Tooltip will be re-set by updateNextWeekBadge() } // Header title const appTitle = document.querySelector('.header-left h1'); if (appTitle) { const versionTag = appTitle.querySelector('.version-tag'); const updateIcon = appTitle.querySelector('.update-icon'); appTitle.textContent = (0,i18n.t)('appTitle') + ' '; if (versionTag) appTitle.appendChild(versionTag); if (updateIcon) appTitle.appendChild(updateIcon); } // Action button tooltips const btnRefresh = document.getElementById('btn-refresh'); if (btnRefresh) btnRefresh.setAttribute('aria-label', (0,i18n.t)('refresh')); if (btnRefresh) btnRefresh.title = (0,i18n.t)('refresh'); const btnHistory = document.getElementById('btn-history'); if (btnHistory) btnHistory.setAttribute('aria-label', (0,i18n.t)('history')); if (btnHistory) btnHistory.title = (0,i18n.t)('history'); const btnHighlights = document.getElementById('btn-highlights'); if (btnHighlights) btnHighlights.setAttribute('aria-label', (0,i18n.t)('highlights')); if (btnHighlights) btnHighlights.title = (0,i18n.t)('highlights'); const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) themeToggle.title = (0,i18n.t)('themeTooltip'); // Login/Logout const btnLoginOpen = document.getElementById('btn-login-open'); if (btnLoginOpen) { btnLoginOpen.title = (0,i18n.t)('loginTooltip'); const loginText = btnLoginOpen.querySelector('span:last-child'); if (loginText && !loginText.classList.contains('material-icons-round')) { loginText.textContent = (0,i18n.t)('login'); } } const btnLogout = document.getElementById('btn-logout'); if (btnLogout) btnLogout.title = (0,i18n.t)('logoutTooltip'); // Language toggle tooltip const langToggle = document.getElementById('lang-toggle'); if (langToggle) langToggle.title = (0,i18n.t)('langTooltip'); // Modal headers const highlightsHeader = document.querySelector('#highlights-modal .modal-header h2'); if (highlightsHeader) highlightsHeader.textContent = (0,i18n.t)('highlightsTitle'); const highlightsDesc = document.querySelector('#highlights-modal .modal-body > p'); if (highlightsDesc) highlightsDesc.textContent = (0,i18n.t)('highlightsDesc'); const tagInput = document.getElementById('tag-input'); if (tagInput) { tagInput.placeholder = (0,i18n.t)('tagInputPlaceholder'); tagInput.title = (0,i18n.t)('tagInputTooltip'); } const btnAddTag = document.getElementById('btn-add-tag'); if (btnAddTag) { btnAddTag.textContent = (0,i18n.t)('addTag'); btnAddTag.title = (0,i18n.t)('addTagTooltip'); } const historyHeader = document.querySelector('#history-modal .modal-header h2'); if (historyHeader) historyHeader.textContent = (0,i18n.t)('historyTitle'); const loginHeader = document.querySelector('#login-modal .modal-header h2'); if (loginHeader) loginHeader.textContent = (0,i18n.t)('loginTitle'); // Alarm bell const alarmBell = document.getElementById('alarm-bell'); if (alarmBell && state/* userFlags */.BY.size === 0) { alarmBell.title = (0,i18n.t)('alarmTooltipNone'); } // Re-render dynamic parts that may use t() (0,ui_helpers/* renderVisibleWeeks */.OR)(); (0,ui_helpers/* updateNextWeekBadge */.gJ)(); (0,ui_helpers/* updateAlarmBell */.Mb)(); } function bindEvents() { const btnThisWeek = document.getElementById('btn-this-week'); const btnNextWeek = document.getElementById('btn-next-week'); const btnRefresh = document.getElementById('btn-refresh'); const themeToggle = document.getElementById('theme-toggle'); const btnLoginOpen = document.getElementById('btn-login-open'); const btnLoginClose = document.getElementById('btn-login-close'); const btnLogout = document.getElementById('btn-logout'); const loginForm = document.getElementById('login-form'); const loginModal = document.getElementById('login-modal'); const btnHighlights = document.getElementById('btn-highlights'); const highlightsModal = document.getElementById('highlights-modal'); const btnHighlightsClose = document.getElementById('btn-highlights-close'); const btnAddTag = document.getElementById('btn-add-tag'); const tagInput = document.getElementById('tag-input'); const btnHistory = document.getElementById('btn-history'); const historyModal = document.getElementById('history-modal'); const btnHistoryClose = document.getElementById('btn-history-close'); const btnLangToggle = document.getElementById('btn-lang-toggle'); const langDropdown = document.getElementById('lang-dropdown'); if (btnLangToggle && langDropdown) { btnLangToggle.addEventListener('click', (e) => { e.stopPropagation(); langDropdown.classList.toggle('hidden'); }); } document.querySelectorAll('.lang-btn').forEach(btn => { btn.addEventListener('click', () => { (0,state/* setLangMode */.UD)(btn.dataset.lang); localStorage.setItem(constants.LS.LANG, btn.dataset.lang); document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); if (langDropdown) langDropdown.classList.add('hidden'); updateUILanguage(); }); }); if (btnHighlights) { btnHighlights.addEventListener('click', () => { (0,actions/* renderTagsList */.Y1)(); highlightsModal.classList.remove('hidden'); }); } if (btnHighlightsClose) { btnHighlightsClose.addEventListener('click', () => { highlightsModal.classList.add('hidden'); }); } btnHistory.addEventListener('click', () => { if (!state/* authToken */.gX) { loginModal.classList.remove('hidden'); return; } historyModal.classList.remove('hidden'); (0,actions/* fetchFullOrderHistory */.Aq)(); }); btnHistoryClose.addEventListener('click', () => { historyModal.classList.add('hidden'); }); window.addEventListener('click', (e) => { if (e.target === historyModal) historyModal.classList.add('hidden'); if (e.target === highlightsModal) highlightsModal.classList.add('hidden'); if (langDropdown && !langDropdown.classList.contains('hidden') && !e.target.closest('#lang-toggle')) { langDropdown.classList.add('hidden'); } }); const versionTag = document.querySelector('.version-tag'); const versionModal = document.getElementById('version-modal'); const btnVersionClose = document.getElementById('btn-version-close'); if (versionTag) { versionTag.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); (0,ui_helpers/* openVersionMenu */.Gk)(); }); } if (btnVersionClose) { btnVersionClose.addEventListener('click', () => { versionModal.classList.add('hidden'); }); } const btnClearCache = document.getElementById('btn-clear-cache'); if (btnClearCache) { btnClearCache.addEventListener('click', () => { if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) { Object.keys(localStorage).forEach(key => { if (key.startsWith('kantine_')) { localStorage.removeItem(key); } }); window.location.reload(); } }); } window.addEventListener('click', (e) => { if (e.target === versionModal) versionModal.classList.add('hidden'); }); btnAddTag.addEventListener('click', () => { const tag = tagInput.value; if ((0,actions/* addHighlightTag */.oL)(tag)) { tagInput.value = ''; (0,actions/* renderTagsList */.Y1)(); } }); tagInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { btnAddTag.click(); } }); const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const themeIcon = themeToggle.querySelector('.theme-icon'); if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.documentElement.setAttribute('data-theme', 'dark'); themeIcon.textContent = 'dark_mode'; } else { document.documentElement.setAttribute('data-theme', 'light'); themeIcon.textContent = 'light_mode'; } themeToggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('theme', next); themeIcon.textContent = next === 'dark' ? 'dark_mode' : 'light_mode'; }); btnThisWeek.addEventListener('click', () => { if (state/* displayMode */.sw !== 'this-week') { (0,state/* setDisplayMode */.qo)('this-week'); btnThisWeek.classList.add('active'); btnNextWeek.classList.remove('active'); (0,ui_helpers/* renderVisibleWeeks */.OR)(); } }); btnNextWeek.addEventListener('click', () => { btnNextWeek.classList.remove('new-week-available'); if (state/* displayMode */.sw !== 'next-week') { (0,state/* setDisplayMode */.qo)('next-week'); btnNextWeek.classList.add('active'); btnThisWeek.classList.remove('active'); (0,ui_helpers/* renderVisibleWeeks */.OR)(); } }); btnRefresh.addEventListener('click', () => { if (!state/* authToken */.gX) { loginModal.classList.remove('hidden'); return; } (0,actions/* loadMenuDataFromAPI */.m9)(); }); const bellBtn = document.getElementById('alarm-bell'); if (bellBtn) { bellBtn.addEventListener('click', () => { (0,actions/* refreshFlaggedItems */.A0)(); }); } btnLoginOpen.addEventListener('click', () => { loginModal.classList.remove('hidden'); document.getElementById('login-error').classList.add('hidden'); loginForm.reset(); }); btnLoginClose.addEventListener('click', () => { loginModal.classList.add('hidden'); }); window.addEventListener('click', (e) => { if (e.target === loginModal) loginModal.classList.add('hidden'); }); loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const employeeId = document.getElementById('employee-id').value.trim(); const password = document.getElementById('password').value; const loginError = document.getElementById('login-error'); const submitBtn = loginForm.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = 'Wird eingeloggt...'; try { const email = `knapp-${employeeId}@bessa.app`; const response = await fetch(`${constants/* API_BASE */.tE}/auth/login/`, { method: 'POST', headers: (0,api/* apiHeaders */.H)(), body: JSON.stringify({ email, password }) }); const data = await response.json(); if (response.ok) { (0,state/* setAuthToken */.O5)(data.key); (0,state/* setCurrentUser */.lt)(employeeId); localStorage.setItem(constants.LS.AUTH_TOKEN, data.key); localStorage.setItem(constants.LS.CURRENT_USER, employeeId); try { const userResp = await fetch(`${constants/* API_BASE */.tE}/auth/user/`, { headers: (0,api/* apiHeaders */.H)(data.key) }); if (userResp.ok) { const userData = await userResp.json(); if (userData.first_name) localStorage.setItem(constants.LS.FIRST_NAME, userData.first_name); if (userData.last_name) localStorage.setItem(constants.LS.LAST_NAME, userData.last_name); } } catch (err) { console.error('Failed to fetch user info:', err); } (0,actions/* updateAuthUI */.i_)(); loginModal.classList.add('hidden'); (0,actions/* fetchOrders */.Gb)(); loginForm.reset(); (0,actions/* startPolling */.g8)(); (0,actions/* loadMenuDataFromAPI */.m9)(); } else { loginError.textContent = data.non_field_errors?.[0] || data.error || 'Login fehlgeschlagen'; loginError.classList.remove('hidden'); } } catch (error) { console.error('Login error:', error); loginError.textContent = 'Ein Fehler ist aufgetreten'; loginError.classList.remove('hidden'); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } }); btnLogout.addEventListener('click', () => { // Secure Logout (FR-006): Clear all application-related data from localStorage Object.keys(localStorage).forEach(key => { if (key.startsWith('kantine_')) { localStorage.removeItem(key); } }); (0,state/* setAuthToken */.O5)(null); (0,state/* setCurrentUser */.lt)(null); (0,state/* setOrderMap */.di)(new Map()); (0,actions/* stopPolling */.Et)(); (0,actions/* updateAuthUI */.i_)(); (0,ui_helpers/* renderVisibleWeeks */.OR)(); }); // Sync heights on window resize (FR-Performance) window.addEventListener('resize', (0,utils/* debounce */.sg)(() => { const grid = document.querySelector('.days-grid'); if (grid) (0,ui_helpers/* syncMenuItemHeights */.wy)(grid); }, 150)); } ;// ./src/index.js if (!window.__KANTINE_LOADED) { window.__KANTINE_LOADED = true; injectUI(); bindEvents(); (0,actions/* updateAuthUI */.i_)(); (0,actions/* cleanupExpiredFlags */.H)(); const hadCache = (0,actions/* loadMenuCache */.KG)(); if (hadCache) { document.getElementById('loading').classList.add('hidden'); if (!(0,actions/* isCacheFresh */.VL)()) { (0,actions/* loadMenuDataFromAPI */.m9)(); } } else { (0,actions/* loadMenuDataFromAPI */.m9)(); } if (state/* authToken */.gX) { (0,actions/* startPolling */.g8)(); } (0,ui_helpers/* checkForUpdates */.Ux)(); setInterval(ui_helpers/* checkForUpdates */.Ux, 60 * 60 * 1000); } /******/ })() ;