758 lines
28 KiB
JavaScript
758 lines
28 KiB
JavaScript
import { authToken, currentUser, orderMap, userFlags, pollIntervalId, highlightTags, allWeeks, currentWeekNumber, currentYear, displayMode, langMode, setAuthToken, setCurrentUser, setOrderMap, setUserFlags, setPollIntervalId, setHighlightTags, setAllWeeks, setCurrentWeekNumber, setCurrentYear } from './state.js';
|
|
import { getISOWeek, getWeekYear, translateDay, escapeHtml, getRelativeTime, isNewer, getLocalizedText } from './utils.js';
|
|
import { API_BASE, GUEST_TOKEN, VENUE_ID, MENU_ID, POLL_INTERVAL_MS, GITHUB_API, INSTALLER_BASE, CLIENT_VERSION } from './constants.js';
|
|
import { apiHeaders, githubHeaders } from './api.js';
|
|
import { placeOrder, cancelOrder, toggleFlag, showToast, checkHighlight, loadMenuDataFromAPI } from './actions.js';
|
|
|
|
export function updateNextWeekBadge() {
|
|
const btnNextWeek = document.getElementById('btn-next-week');
|
|
let nextWeek = currentWeekNumber + 1;
|
|
let nextYear = currentYear;
|
|
if (nextWeek > 52) { nextWeek = 1; nextYear++; }
|
|
|
|
const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear);
|
|
let totalDataCount = 0;
|
|
let orderableCount = 0;
|
|
let daysWithOrders = 0;
|
|
let daysWithOrderableAndNoOrder = 0;
|
|
|
|
if (nextWeekData && nextWeekData.days) {
|
|
nextWeekData.days.forEach(day => {
|
|
if (day.items && day.items.length > 0) {
|
|
totalDataCount++;
|
|
const isOrderable = day.items.some(item => item.available);
|
|
if (isOrderable) orderableCount++;
|
|
|
|
let hasOrder = false;
|
|
day.items.forEach(item => {
|
|
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
|
|
const key = `${day.date}_${articleId}`;
|
|
if (orderMap.has(key) && orderMap.get(key).length > 0) hasOrder = true;
|
|
});
|
|
|
|
if (hasOrder) daysWithOrders++;
|
|
if (isOrderable && !hasOrder) daysWithOrderableAndNoOrder++;
|
|
}
|
|
});
|
|
}
|
|
|
|
let badge = btnNextWeek.querySelector('.nav-badge');
|
|
if (totalDataCount > 0) {
|
|
if (!badge) {
|
|
badge = document.createElement('span');
|
|
badge.className = 'nav-badge';
|
|
btnNextWeek.appendChild(badge);
|
|
}
|
|
|
|
badge.title = `${daysWithOrders} bestellt / ${orderableCount} bestellbar / ${totalDataCount} gesamt`;
|
|
badge.innerHTML = `<span class="ordered">${daysWithOrders}</span><span class="separator">/</span><span class="orderable">${orderableCount}</span><span class="separator">/</span><span class="total">${totalDataCount}</span>`;
|
|
|
|
badge.classList.remove('badge-violet', 'badge-green', 'badge-red', 'badge-blue');
|
|
|
|
if (daysWithOrders > 0 && daysWithOrderableAndNoOrder === 0) {
|
|
badge.classList.add('badge-violet');
|
|
} else if (daysWithOrderableAndNoOrder > 0) {
|
|
badge.classList.add('badge-green');
|
|
} else if (orderableCount === 0) {
|
|
badge.classList.add('badge-red');
|
|
} else {
|
|
badge.classList.add('badge-blue');
|
|
}
|
|
|
|
let highlightCount = 0;
|
|
if (nextWeekData && nextWeekData.days) {
|
|
nextWeekData.days.forEach(day => {
|
|
day.items.forEach(item => {
|
|
const nameMatches = checkHighlight(item.name);
|
|
const descMatches = checkHighlight(item.description);
|
|
if (nameMatches.length > 0 || descMatches.length > 0) {
|
|
highlightCount++;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (highlightCount > 0) {
|
|
badge.insertAdjacentHTML('beforeend', `<span class="highlight-count" title="${highlightCount} Highlight Menüs">(${highlightCount})</span>`);
|
|
badge.title += ` • ${highlightCount} Highlights gefunden`;
|
|
badge.classList.add('has-highlights');
|
|
}
|
|
|
|
if (daysWithOrders === 0) {
|
|
btnNextWeek.classList.add('new-week-available');
|
|
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
|
|
if (!localStorage.getItem(storageKey)) {
|
|
localStorage.setItem(storageKey, 'true');
|
|
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
|
|
}
|
|
} else {
|
|
btnNextWeek.classList.remove('new-week-available');
|
|
}
|
|
|
|
} else if (badge) {
|
|
badge.remove();
|
|
}
|
|
}
|
|
|
|
export function updateWeeklyCost(days) {
|
|
let totalCost = 0;
|
|
if (days && days.length > 0) {
|
|
days.forEach(day => {
|
|
if (day.items) {
|
|
day.items.forEach(item => {
|
|
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
|
|
const key = `${day.date}_${articleId}`;
|
|
const orders = orderMap.get(key) || [];
|
|
if (orders.length > 0) totalCost += item.price * orders.length;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const costDisplay = document.getElementById('weekly-cost-display');
|
|
if (totalCost > 0) {
|
|
costDisplay.innerHTML = `<span class="material-icons-round">shopping_bag</span> <span>Gesamt: ${totalCost.toFixed(2).replace('.', ',')} €</span>`;
|
|
costDisplay.classList.remove('hidden');
|
|
} else {
|
|
costDisplay.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
export function renderVisibleWeeks() {
|
|
const menuContainer = document.getElementById('menu-container');
|
|
if (!menuContainer) return;
|
|
menuContainer.innerHTML = '';
|
|
|
|
let targetWeek = currentWeekNumber;
|
|
let targetYear = currentYear;
|
|
|
|
if (displayMode === 'next-week') {
|
|
targetWeek++;
|
|
if (targetWeek > 52) { targetWeek = 1; targetYear++; }
|
|
}
|
|
|
|
const allDays = allWeeks.flatMap(w => w.days || []);
|
|
const daysInTargetWeek = allDays.filter(day => {
|
|
const d = new Date(day.date);
|
|
return getISOWeek(d) === targetWeek && getWeekYear(d) === targetYear;
|
|
});
|
|
|
|
if (daysInTargetWeek.length === 0) {
|
|
menuContainer.innerHTML = `
|
|
<div class="empty-state">
|
|
<p>Keine Menüdaten für KW ${targetWeek} (${targetYear}) verfügbar.</p>
|
|
<small>Versuchen Sie eine andere Woche oder schauen Sie später vorbei.</small>
|
|
</div>`;
|
|
document.getElementById('weekly-cost-display').classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
updateWeeklyCost(daysInTargetWeek);
|
|
|
|
const headerWeekInfo = document.getElementById('header-week-info');
|
|
const weekTitle = displayMode === 'this-week' ? 'Diese Woche' : 'Nächste Woche';
|
|
headerWeekInfo.innerHTML = `
|
|
<div class="header-week-title">${weekTitle}</div>
|
|
<div class="header-week-subtitle">Week ${targetWeek} • ${targetYear}</div>`;
|
|
|
|
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);
|
|
}
|
|
|
|
export function syncMenuItemHeights(grid) {
|
|
const cards = grid.querySelectorAll('.menu-card');
|
|
if (cards.length === 0) return;
|
|
let maxItems = 0;
|
|
cards.forEach(card => {
|
|
maxItems = Math.max(maxItems, card.querySelectorAll('.menu-item').length);
|
|
});
|
|
for (let i = 0; i < maxItems; i++) {
|
|
let maxHeight = 0;
|
|
const itemsAtPos = [];
|
|
cards.forEach(card => {
|
|
const items = card.querySelectorAll('.menu-item');
|
|
if (items[i]) {
|
|
items[i].style.height = 'auto';
|
|
maxHeight = Math.max(maxHeight, items[i].offsetHeight);
|
|
itemsAtPos.push(items[i]);
|
|
}
|
|
});
|
|
itemsAtPos.forEach(item => { item.style.height = `${maxHeight}px`; });
|
|
}
|
|
}
|
|
|
|
export 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 = orderMap.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 + `<span class="menu-code-badge">${code}</span>`, '');
|
|
|
|
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 orderMap.has(key) && orderMap.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 = `
|
|
<div class="day-header-left">
|
|
<span class="day-name">${translateDay(day.weekday)}</span>
|
|
<div class="day-badges">${badgesHtml}</div>
|
|
</div>
|
|
<span class="day-date">${dateStr}</span>`;
|
|
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 = orderMap.has(`${day.date}_${aId}`);
|
|
const bOrdered = orderMap.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 = orderMap.get(orderKey) || [];
|
|
const orderCount = orderIds.length;
|
|
|
|
let statusBadge = '';
|
|
if (item.available) {
|
|
statusBadge = item.amountTracking
|
|
? `<span class="badge available">Verfügbar (${item.availableAmount})</span>`
|
|
: `<span class="badge available">Verfügbar</span>`;
|
|
} else {
|
|
statusBadge = `<span class="badge sold-out">Ausverkauft</span>`;
|
|
}
|
|
|
|
let orderedBadge = '';
|
|
if (orderCount > 0) {
|
|
const countBadge = orderCount > 1 ? `<span class="order-count-badge">${orderCount}</span>` : '';
|
|
orderedBadge = `<span class="badge ordered"><span class="material-icons-round">check_circle</span> Bestellt${countBadge}</span>`;
|
|
itemEl.classList.add('ordered');
|
|
if (new Date(day.date).toDateString() === now.toDateString()) {
|
|
itemEl.classList.add('today-ordered');
|
|
}
|
|
}
|
|
|
|
const flagId = `${day.date}_${articleId}`;
|
|
const isFlagged = userFlags.has(flagId);
|
|
if (isFlagged) {
|
|
itemEl.classList.add(item.available ? 'flagged-available' : 'flagged-sold-out');
|
|
}
|
|
|
|
const matchedTags = [...new Set([...checkHighlight(item.name), ...checkHighlight(item.description)])];
|
|
if (matchedTags.length > 0) {
|
|
itemEl.classList.add('highlight-glow');
|
|
}
|
|
|
|
let orderButton = '';
|
|
let cancelButton = '';
|
|
let flagButton = '';
|
|
|
|
if (authToken && !isPastCutoff) {
|
|
const flagIcon = isFlagged ? 'notifications_active' : 'notifications_none';
|
|
const flagClass = isFlagged ? 'btn-flag active' : 'btn-flag';
|
|
const flagTitle = isFlagged ? 'Benachrichtigung deaktivieren' : 'Benachrichtigen wenn verfügbar';
|
|
if (!item.available || isFlagged) {
|
|
flagButton = `<button class="${flagClass}" data-date="${day.date}" data-article="${articleId}" data-name="${escapeHtml(item.name)}" data-cutoff="${day.orderCutoff}" title="${flagTitle}"><span class="material-icons-round">${flagIcon}</span></button>`;
|
|
}
|
|
|
|
if (item.available) {
|
|
if (orderCount > 0) {
|
|
orderButton = `<button class="btn-order btn-order-compact" data-date="${day.date}" data-article="${articleId}" data-name="${escapeHtml(item.name)}" data-price="${item.price}" data-desc="${escapeHtml(item.description || '')}" title="${escapeHtml(item.name)} nochmal bestellen"><span class="material-icons-round">add</span></button>`;
|
|
} else {
|
|
orderButton = `<button class="btn-order" data-date="${day.date}" data-article="${articleId}" data-name="${escapeHtml(item.name)}" data-price="${item.price}" data-desc="${escapeHtml(item.description || '')}" title="${escapeHtml(item.name)} bestellen"><span class="material-icons-round">add_shopping_cart</span> Bestellen</button>`;
|
|
}
|
|
}
|
|
|
|
if (orderCount > 0) {
|
|
const cancelIcon = orderCount === 1 ? 'close' : 'remove';
|
|
const cancelTitle = orderCount === 1 ? 'Bestellung stornieren' : 'Eine Bestellung stornieren';
|
|
cancelButton = `<button class="btn-cancel" data-date="${day.date}" data-article="${articleId}" data-name="${escapeHtml(item.name)}" title="${cancelTitle}"><span class="material-icons-round">${cancelIcon}</span></button>`;
|
|
}
|
|
}
|
|
|
|
let tagsHtml = '';
|
|
if (matchedTags.length > 0) {
|
|
const badges = matchedTags.reduce((acc, t) => acc + `<span class="tag-badge-small"><span class="material-icons-round" style="font-size:10px;margin-right:2px">star</span>${escapeHtml(t)}</span>`, '');
|
|
tagsHtml = `<div class="matched-tags">${badges}</div>`;
|
|
}
|
|
|
|
itemEl.innerHTML = `
|
|
<div class="item-header">
|
|
<span class="item-name">${escapeHtml(item.name)}</span>
|
|
<span class="item-price">${item.price.toFixed(2)} €</span>
|
|
</div>
|
|
<div class="item-status-row">
|
|
${orderedBadge}
|
|
${cancelButton}
|
|
${orderButton}
|
|
${flagButton}
|
|
<div class="badges">${statusBadge}</div>
|
|
</div>
|
|
${tagsHtml}
|
|
<p class="item-desc">${escapeHtml(getLocalizedText(item.description))}</p>`;
|
|
|
|
const orderBtn = itemEl.querySelector('.btn-order');
|
|
if (orderBtn) {
|
|
orderBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const btn = e.currentTarget;
|
|
btn.disabled = true;
|
|
btn.classList.add('loading');
|
|
placeOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, parseFloat(btn.dataset.price), btn.dataset.desc || '')
|
|
.finally(() => { btn.disabled = false; btn.classList.remove('loading'); });
|
|
});
|
|
}
|
|
|
|
const cancelBtn = itemEl.querySelector('.btn-cancel');
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const btn = e.currentTarget;
|
|
btn.disabled = true;
|
|
cancelOrder(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name)
|
|
.finally(() => { btn.disabled = false; });
|
|
});
|
|
}
|
|
|
|
const flagBtn = itemEl.querySelector('.btn-flag');
|
|
if (flagBtn) {
|
|
flagBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const btn = e.currentTarget;
|
|
toggleFlag(btn.dataset.date, parseInt(btn.dataset.article), btn.dataset.name, btn.dataset.cutoff);
|
|
});
|
|
}
|
|
|
|
body.appendChild(itemEl);
|
|
});
|
|
|
|
card.appendChild(body);
|
|
return card;
|
|
}
|
|
|
|
export async function fetchVersions(devMode) {
|
|
const endpoint = devMode
|
|
? `${GITHUB_API}/tags?per_page=20`
|
|
: `${GITHUB_API}/releases?per_page=20`;
|
|
|
|
const resp = await fetch(endpoint, { headers: githubHeaders() });
|
|
if (!resp.ok) {
|
|
if (resp.status === 403) {
|
|
throw new Error('API Rate Limit erreicht (403). Bitte später erneut versuchen.');
|
|
}
|
|
throw new Error(`GitHub API ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
|
|
return data.map(item => {
|
|
const tag = devMode ? item.name : item.tag_name;
|
|
return {
|
|
tag,
|
|
name: devMode ? tag : (item.name || tag),
|
|
url: `${INSTALLER_BASE}/${tag}/dist/install.html`,
|
|
body: item.body || ''
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function checkForUpdates() {
|
|
const currentVersion = '{{VERSION}}';
|
|
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
|
|
|
try {
|
|
const versions = await fetchVersions(devMode);
|
|
if (!versions.length) return;
|
|
|
|
localStorage.setItem('kantine_version_cache', JSON.stringify({
|
|
timestamp: Date.now(), devMode, versions
|
|
}));
|
|
|
|
const latest = versions[0].tag;
|
|
|
|
if (!isNewer(latest, currentVersion)) return;
|
|
|
|
const headerTitle = document.querySelector('.header-left h1');
|
|
if (headerTitle && !headerTitle.querySelector('.update-icon')) {
|
|
const icon = document.createElement('a');
|
|
icon.className = 'update-icon';
|
|
icon.href = versions[0].url;
|
|
icon.target = '_blank';
|
|
icon.innerHTML = '🆕';
|
|
icon.title = `Update: ${latest} — Klick zum Installieren`;
|
|
icon.style.cssText = 'margin-left:8px;font-size:1em;text-decoration:none;cursor:pointer;vertical-align:middle;';
|
|
headerTitle.appendChild(icon);
|
|
}
|
|
} catch (e) {
|
|
console.warn('[Kantine] Version check failed:', e);
|
|
}
|
|
}
|
|
|
|
export function openVersionMenu() {
|
|
const modal = document.getElementById('version-modal');
|
|
const container = document.getElementById('version-list-container');
|
|
const devToggle = document.getElementById('dev-mode-toggle');
|
|
const currentVersion = '{{VERSION}}';
|
|
|
|
if (!modal) return;
|
|
modal.classList.remove('hidden');
|
|
|
|
const cur = document.getElementById('version-current');
|
|
if (cur) cur.textContent = currentVersion;
|
|
|
|
const devMode = localStorage.getItem('kantine_dev_mode') === 'true';
|
|
devToggle.checked = devMode;
|
|
|
|
async function loadVersions(forceRefresh) {
|
|
const dm = devToggle.checked;
|
|
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
|
|
|
|
function renderVersionsList(versions) {
|
|
if (!versions || !versions.length) {
|
|
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '<ul class="version-list"></ul>';
|
|
const list = container.querySelector('.version-list');
|
|
|
|
versions.forEach(v => {
|
|
const isCurrent = v.tag === currentVersion;
|
|
const isNew = isNewer(v.tag, currentVersion);
|
|
const li = document.createElement('li');
|
|
li.className = 'version-item' + (isCurrent ? ' current' : '');
|
|
|
|
let badge = '';
|
|
if (isCurrent) badge = '<span class="badge-current">✓ Installiert</span>';
|
|
else if (isNew) badge = '<span class="badge-new">⬆ Neu!</span>';
|
|
|
|
let action = '';
|
|
if (!isCurrent) {
|
|
action = `<a href="${escapeHtml(v.url)}" target="_blank" class="install-link" title="${escapeHtml(v.tag)} installieren">Installieren</a>`;
|
|
}
|
|
|
|
li.innerHTML = `
|
|
<div class="version-info">
|
|
<strong>${escapeHtml(v.tag)}</strong>
|
|
${badge}
|
|
</div>
|
|
${action}
|
|
`;
|
|
list.appendChild(li);
|
|
});
|
|
}
|
|
|
|
try {
|
|
const cachedRaw = localStorage.getItem('kantine_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('kantine_version_cache', JSON.stringify({
|
|
timestamp: Date.now(), devMode: dm, versions: liveVersions
|
|
}));
|
|
renderVersionsList(liveVersions);
|
|
}
|
|
|
|
} catch (e) {
|
|
container.innerHTML = `<p style="color:#e94560;">Fehler: ${escapeHtml(e.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
loadVersions(false);
|
|
|
|
devToggle.onchange = () => {
|
|
localStorage.setItem('kantine_dev_mode', devToggle.checked);
|
|
localStorage.removeItem('kantine_version_cache');
|
|
loadVersions(true);
|
|
};
|
|
}
|
|
|
|
export function updateCountdown() {
|
|
if (!authToken || !currentUser) {
|
|
removeCountdown();
|
|
return;
|
|
}
|
|
|
|
const now = new Date();
|
|
const currentDay = now.getDay();
|
|
if (currentDay === 0 || currentDay === 6) {
|
|
removeCountdown();
|
|
return;
|
|
}
|
|
|
|
const todayStr = now.toISOString().split('T')[0];
|
|
|
|
let hasOrder = false;
|
|
for (const key of orderMap.keys()) {
|
|
if (key.startsWith(todayStr)) {
|
|
hasOrder = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasOrder) {
|
|
removeCountdown();
|
|
return;
|
|
}
|
|
|
|
const cutoff = new Date();
|
|
cutoff.setHours(10, 0, 0, 0);
|
|
|
|
const diff = cutoff - now;
|
|
|
|
if (diff <= 0) {
|
|
removeCountdown();
|
|
return;
|
|
}
|
|
|
|
const diffHrs = Math.floor(diff / 3600000);
|
|
const diffMins = Math.floor((diff % 3600000) / 60000);
|
|
|
|
const headerCenter = document.querySelector('.header-center-wrapper');
|
|
if (!headerCenter) return;
|
|
|
|
let countdownEl = document.getElementById('order-countdown');
|
|
if (!countdownEl) {
|
|
countdownEl = document.createElement('div');
|
|
countdownEl.id = 'order-countdown';
|
|
headerCenter.insertBefore(countdownEl, headerCenter.firstChild);
|
|
}
|
|
|
|
countdownEl.innerHTML = `<span>Bestellschluss:</span> <strong>${diffHrs}h ${diffMins}m</strong>`;
|
|
|
|
if (diff < 3600000) {
|
|
countdownEl.classList.add('urgent');
|
|
|
|
const notifiedKey = `kantine_notified_${todayStr}`;
|
|
if (!localStorage.getItem(notifiedKey)) {
|
|
if (Notification.permission === 'granted') {
|
|
new Notification('Kantine: Bestellschluss naht!', {
|
|
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
|
|
icon: '⏳'
|
|
});
|
|
} else if (Notification.permission === 'default') {
|
|
Notification.requestPermission();
|
|
}
|
|
localStorage.setItem(notifiedKey, 'true');
|
|
}
|
|
} else {
|
|
countdownEl.classList.remove('urgent');
|
|
}
|
|
}
|
|
|
|
export function removeCountdown() {
|
|
const el = document.getElementById('order-countdown');
|
|
if (el) el.remove();
|
|
}
|
|
|
|
setInterval(updateCountdown, 60000);
|
|
setTimeout(updateCountdown, 1000);
|
|
|
|
export function showErrorModal(title, htmlContent, btnText, url) {
|
|
const modalId = 'error-modal';
|
|
let modal = document.getElementById(modalId);
|
|
if (modal) modal.remove();
|
|
|
|
modal = document.createElement('div');
|
|
modal.id = modalId;
|
|
modal.className = 'modal hidden';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
|
|
<span class="material-icons-round">signal_wifi_off</span>
|
|
${escapeHtml(title)}
|
|
</h2>
|
|
</div>
|
|
<div style="padding: 20px;">
|
|
<p style="margin-bottom: 15px; color: var(--text-primary);">${htmlContent}</p>
|
|
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
|
<button id="btn-error-redirect" style="
|
|
background-color: var(--accent-color);
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
justify-content: center;
|
|
transition: transform 0.1s;
|
|
">
|
|
${escapeHtml(btnText)}
|
|
<span class="material-icons-round">open_in_new</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
document.getElementById('btn-error-redirect').addEventListener('click', () => {
|
|
window.location.href = url;
|
|
});
|
|
|
|
requestAnimationFrame(() => {
|
|
modal.classList.remove('hidden');
|
|
});
|
|
}
|
|
|
|
export function updateAlarmBell() {
|
|
const bellBtn = document.getElementById('alarm-bell');
|
|
const bellIcon = document.getElementById('alarm-bell-icon');
|
|
if (!bellBtn || !bellIcon) return;
|
|
|
|
if (userFlags.size === 0) {
|
|
bellBtn.classList.add('hidden');
|
|
bellBtn.style.display = 'none';
|
|
bellIcon.style.color = 'var(--text-secondary)';
|
|
bellIcon.style.textShadow = 'none';
|
|
return;
|
|
}
|
|
|
|
bellBtn.classList.remove('hidden');
|
|
bellBtn.style.display = 'inline-flex';
|
|
|
|
let anyAvailable = false;
|
|
for (const wk of allWeeks) {
|
|
if (!wk.days) continue;
|
|
for (const d of wk.days) {
|
|
if (!d.items) continue;
|
|
for (const item of d.items) {
|
|
if (item.available && userFlags.has(item.id)) {
|
|
anyAvailable = true;
|
|
break;
|
|
}
|
|
}
|
|
if (anyAvailable) break;
|
|
}
|
|
if (anyAvailable) break;
|
|
}
|
|
|
|
const lastCheckedStr = localStorage.getItem('kantine_last_checked');
|
|
const flaggedLastCheckedStr = localStorage.getItem('kantine_flagged_items_last_checked');
|
|
|
|
let latestTime = 0;
|
|
if (lastCheckedStr) latestTime = Math.max(latestTime, new Date(lastCheckedStr).getTime());
|
|
if (flaggedLastCheckedStr) latestTime = Math.max(latestTime, new Date(flaggedLastCheckedStr).getTime());
|
|
|
|
let timeStr = 'gerade eben';
|
|
if (latestTime === 0) {
|
|
const now = new Date().toISOString();
|
|
localStorage.setItem('kantine_last_checked', now);
|
|
latestTime = new Date(now).getTime();
|
|
}
|
|
|
|
timeStr = getRelativeTime(new Date(latestTime));
|
|
|
|
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
|
|
|
if (anyAvailable) {
|
|
bellIcon.style.color = '#10b981';
|
|
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
|
} else {
|
|
bellIcon.style.color = '#f59e0b';
|
|
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
|
}
|
|
}
|