(function () { const API_URL = window.API_URL || 'https://fb.srv.medeba-media.de/api'; const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount'; const FILTER_STORAGE_KEY = 'dailyBookmarkFilters'; const SORT_STORAGE_KEY = 'dailyBookmarkSort'; const AUTO_OPEN_STORAGE_KEY = 'dailyBookmarkAutoOpen'; const DEFAULT_BULK_COUNT = 5; const DEFAULT_SORT = { column: 'last_completed_at', direction: 'desc' }; const AUTO_OPEN_DELAY_MS = 1500; const state = { dayKey: formatDayKey(new Date()), items: [], loading: false, saving: false, processingBatch: false, error: '', bulkCount: loadBulkCount(), filters: loadFilters(), sort: loadSort(), importing: false, autoOpenEnabled: loadAutoOpenEnabled(), autoOpenTriggered: false, autoOpenTimerId: null, autoOpenCountdownIntervalId: null }; let editingId = null; const dayLabel = document.getElementById('dayLabel'); const daySubLabel = document.getElementById('daySubLabel'); const prevDayBtn = document.getElementById('prevDayBtn'); const nextDayBtn = document.getElementById('nextDayBtn'); const todayBtn = document.getElementById('todayBtn'); const refreshBtn = document.getElementById('refreshBtn'); const heroStats = document.getElementById('heroStats'); const listSummary = document.getElementById('listSummary'); const listStatus = document.getElementById('listStatus'); const tableBody = document.getElementById('tableBody'); const bulkCountSelect = document.getElementById('bulkCountSelect'); const bulkOpenBtn = document.getElementById('bulkOpenBtn'); const autoOpenToggle = document.getElementById('autoOpenToggle'); const autoOpenOverlay = document.getElementById('autoOpenOverlay'); const autoOpenOverlayPanel = document.getElementById('autoOpenOverlayPanel'); const autoOpenCountdown = document.getElementById('autoOpenCountdown'); const openCreateBtn = document.getElementById('openCreateBtn'); const modal = document.getElementById('bookmarkModal'); const modalCloseBtn = document.getElementById('modalCloseBtn'); const modalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null; const formEl = document.getElementById('bookmarkForm'); const titleInput = document.getElementById('titleInput'); const urlInput = document.getElementById('urlInput'); const notesInput = document.getElementById('notesInput'); const resetBtn = document.getElementById('resetBtn'); const submitBtn = document.getElementById('submitBtn'); const previewLink = document.getElementById('previewLink'); const formStatus = document.getElementById('formStatus'); const formModeLabel = document.getElementById('formModeLabel'); const urlSuggestionBox = document.getElementById('urlSuggestionBox'); const markerInput = document.getElementById('markerInput'); const markerFilterSelect = document.getElementById('markerFilter'); const urlFilterInput = document.getElementById('urlFilter'); const resetViewBtn = document.getElementById('resetViewBtn'); const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]')); const openImportBtn = document.getElementById('openImportBtn'); const importModal = document.getElementById('importModal'); const importCloseBtn = document.getElementById('importCloseBtn'); const importBackdrop = importModal ? importModal.querySelector('.modal__backdrop') : null; const importForm = document.getElementById('importForm'); const importInput = document.getElementById('importInput'); const importMarkerInput = document.getElementById('importMarkerInput'); const importResetBtn = document.getElementById('importResetBtn'); const importSubmitBtn = document.getElementById('importSubmitBtn'); const importStatus = document.getElementById('importStatus'); const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi; function formatDayKey(date) { const d = date instanceof Date ? date : new Date(); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function parseDayKey(dayKey) { if (typeof dayKey !== 'string') { return new Date(); } const parts = dayKey.split('-').map((part) => parseInt(part, 10)); if (parts.length !== 3 || parts.some(Number.isNaN)) { return new Date(); } return new Date(parts[0], parts[1] - 1, parts[2]); } function addDays(date, delta) { const base = date instanceof Date ? date : new Date(); const next = new Date(base); next.setDate(base.getDate() + delta); return next; } function formatDayLabel(dayKey) { const date = parseDayKey(dayKey); return date.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }); } function formatRelativeDay(dayKey) { const target = parseDayKey(dayKey); const today = new Date(); const todayKey = formatDayKey(today); const diff = Math.round((target.setHours(0, 0, 0, 0) - new Date(todayKey).setHours(0, 0, 0, 0)) / 86400000); if (diff === 0) return 'Heute'; if (diff === 1) return 'Morgen'; if (diff === -1) return 'Gestern'; if (diff > 1) return `In ${diff} Tagen`; return `${Math.abs(diff)} Tage her`; } function loadBulkCount() { try { const stored = localStorage.getItem(BULK_COUNT_STORAGE_KEY); const value = parseInt(stored, 10); if (!Number.isNaN(value) && [1, 5, 10, 15, 20].includes(value)) { return value; } } catch (error) { // ignore } return DEFAULT_BULK_COUNT; } function persistBulkCount(value) { try { localStorage.setItem(BULK_COUNT_STORAGE_KEY, String(value)); } catch (error) { // ignore } } function loadFilters() { try { const raw = localStorage.getItem(FILTER_STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); return { marker: typeof parsed.marker === 'string' ? parsed.marker : '', url: typeof parsed.url === 'string' ? parsed.url : '' }; } } catch (error) { // ignore } return { marker: '', url: '' }; } function persistFilters(filters) { try { localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(filters || {})); } catch (error) { // ignore } } function loadSort() { try { const raw = localStorage.getItem(SORT_STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); const allowedColumns = ['url_template', 'marker', 'created_at', 'updated_at', 'last_completed_at']; const allowedDirections = ['asc', 'desc']; if ( parsed && allowedColumns.includes(parsed.column) && allowedDirections.includes(parsed.direction) ) { return parsed; } } } catch (error) { // ignore } return { ...DEFAULT_SORT }; } function persistSort(sort) { try { localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(sort || DEFAULT_SORT)); } catch (error) { // ignore } } function loadAutoOpenEnabled() { try { return localStorage.getItem(AUTO_OPEN_STORAGE_KEY) === '1'; } catch (error) { return false; } } function persistAutoOpenEnabled(enabled) { try { localStorage.setItem(AUTO_OPEN_STORAGE_KEY, enabled ? '1' : '0'); } catch (error) { // ignore } } function resolveTemplate(template, dayKey) { if (typeof template !== 'string') { return ''; } const baseDate = parseDayKey(dayKey); return template.replace(PLACEHOLDER_PATTERN, (_match, contentRaw) => { const content = (contentRaw || '').trim(); if (!content) { return ''; } const counterMatch = content.match(/^counter:\s*([+-]?\d+)([+-]\d+)?$/i); if (counterMatch) { const base = parseInt(counterMatch[1], 10); const offset = counterMatch[2] ? parseInt(counterMatch[2], 10) || 0 : 0; if (Number.isNaN(base)) { return ''; } const date = addDays(baseDate, offset); return String(base + date.getDate()); } const placeholderMatch = content.match(/^(date|day|dd|mm|month|yyyy|yy)([+-]\d+)?$/i); if (!placeholderMatch) { return content; } const token = String(placeholderMatch[1] || '').toLowerCase(); const offset = placeholderMatch[2] ? parseInt(placeholderMatch[2], 10) || 0 : 0; const date = addDays(baseDate, offset); switch (token) { case 'date': return formatDayKey(date); case 'day': return String(date.getDate()); case 'dd': return String(date.getDate()).padStart(2, '0'); case 'month': case 'mm': return String(date.getMonth() + 1).padStart(2, '0'); case 'yyyy': return String(date.getFullYear()); case 'yy': return String(date.getFullYear()).slice(-2); default: return token; } }); } function isDelimiter(char) { return !char || /[\\/_\\-\\.\\?#&=]/.test(char); } function buildUrlSuggestions(urlValue) { const suggestions = []; if (!urlValue || typeof urlValue !== 'string') { return suggestions; } const raw = urlValue.trim(); if (!raw) { return suggestions; } const today = parseDayKey(state.dayKey); const day = today.getDate(); const yearFull = String(today.getFullYear()); const yearShort = yearFull.slice(-2); const monthNumber = today.getMonth() + 1; const tokens = []; const addToken = (text, placeholder) => { if (!text) return; tokens.push({ text, placeholder }); }; const dayText = String(day); addToken(dayText, '{{day}}'); const dayPadded = dayText.padStart(2, '0'); if (dayPadded !== dayText) { addToken(dayPadded, '{{dd}}'); } const monthText = String(monthNumber); addToken(monthText, '{{mm}}'); const monthPadded = monthText.padStart(2, '0'); if (monthPadded !== monthText) { addToken(monthPadded, '{{mm}}'); } addToken(yearFull, '{{yyyy}}'); addToken(yearShort, '{{yy}}'); const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); function findOccurrences(text, tokenText) { const pattern = new RegExp(`(^|[^0-9])(${escapeRegex(tokenText)})(?=$|[^0-9])`, 'g'); const occurrences = []; let match; while ((match = pattern.exec(text)) !== null) { const prefixLength = (match[1] || '').length; const startIndex = match.index + prefixLength; occurrences.push({ index: startIndex, length: tokenText.length }); } return occurrences; } for (const token of tokens) { const occ = findOccurrences(raw, token.text); if (!occ.length) { continue; } const candidate = occ[occ.length - 1]; // prefer last occurrence (z.B. am URL-Ende) const replaced = `${raw.slice(0, candidate.index)}${token.placeholder}${raw.slice(candidate.index + candidate.length)}`; const label = `Ersetze „${token.text}“ durch ${token.placeholder}`; if (!suggestions.some((s) => s.label === label && s.value === replaced)) { suggestions.push({ label, value: replaced }); } } return suggestions; } function renderUrlSuggestions() { if (!urlSuggestionBox) { return; } const suggestions = buildUrlSuggestions(urlInput ? urlInput.value : ''); urlSuggestionBox.innerHTML = ''; if (!suggestions.length) { urlSuggestionBox.hidden = true; return; } const text = document.createElement('span'); text.className = 'suggestion-box__text'; text.textContent = 'Mögliche Platzhalter:'; urlSuggestionBox.appendChild(text); const applySuggestion = (value) => { if (!urlInput) { return; } urlInput.value = value; updatePreviewLink(); renderUrlSuggestions(); const end = urlInput.value.length; urlInput.focus(); try { urlInput.setSelectionRange(end, end); } catch (error) { // ignore if not supported } }; suggestions.forEach((sugg) => { const item = document.createElement('div'); item.className = 'suggestion-box__item'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'suggestion-btn'; btn.textContent = sugg.label; btn.addEventListener('click', () => { applySuggestion(sugg.value); }); item.appendChild(btn); const preview = document.createElement('a'); preview.className = 'suggestion-preview'; preview.href = resolveTemplate(sugg.value, state.dayKey) || sugg.value; preview.target = '_blank'; preview.rel = 'noopener'; preview.title = resolveTemplate(sugg.value, state.dayKey) || sugg.value; preview.textContent = resolveTemplate(sugg.value, state.dayKey) || sugg.value; preview.addEventListener('click', (event) => { event.preventDefault(); applySuggestion(sugg.value); }); item.appendChild(preview); urlSuggestionBox.appendChild(item); }); urlSuggestionBox.hidden = false; } async function apiFetch(url, options = {}) { const response = await fetch(url, { ...options, credentials: 'include', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }); if (!response.ok) { const message = `Fehler: HTTP ${response.status}`; throw new Error(message); } if (response.status === 204) { return null; } const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch (error) { return null; } } function updateDayUI() { if (dayLabel) { dayLabel.textContent = formatDayLabel(state.dayKey); } if (daySubLabel) { daySubLabel.textContent = formatRelativeDay(state.dayKey); } updatePreviewLink(); } function setDayKey(dayKey) { state.dayKey = formatDayKey(parseDayKey(dayKey)); updateDayUI(); loadDailyBookmarks(); } function setFormStatus(message, isError = false) { if (!formStatus) { return; } formStatus.textContent = message || ''; formStatus.classList.toggle('form-status--error', !!isError); } function setListStatus(message, isError = false) { if (!listStatus) { return; } listStatus.textContent = message || ''; listStatus.classList.toggle('list-status--error', !!isError); } function updatePreviewLink() { if (!previewLink || !urlInput) { return; } const resolved = resolveTemplate(urlInput.value || '', state.dayKey); previewLink.textContent = resolved || '–'; if (resolved) { previewLink.href = resolved; previewLink.target = '_blank'; previewLink.rel = 'noopener'; } else { previewLink.removeAttribute('href'); } renderUrlSuggestions(); } function resetForm() { editingId = null; formModeLabel.textContent = 'Neues Bookmark'; submitBtn.textContent = 'Speichern'; formEl.reset(); if (markerInput) { markerInput.value = ''; } setFormStatus(''); updatePreviewLink(); renderUrlSuggestions(); } function openModal(mode, bookmark) { if (importModal && !importModal.hidden) { closeImportModal(); } if (mode === 'edit' && bookmark) { editingId = bookmark.id; formModeLabel.textContent = 'Bookmark bearbeiten'; submitBtn.textContent = 'Aktualisieren'; titleInput.value = bookmark.title || ''; urlInput.value = bookmark.url_template || ''; notesInput.value = bookmark.notes || ''; if (markerInput) { markerInput.value = bookmark.marker || ''; } setFormStatus('Bearbeite vorhandenes Bookmark'); } else { resetForm(); } if (modal) { modal.hidden = false; modal.focus(); } updatePreviewLink(); if (mode === 'edit' && titleInput) { titleInput.focus(); } else if (urlInput) { urlInput.focus(); } } function closeModal() { if (modal) { modal.hidden = true; } resetForm(); } function formatRelativeTimestamp(value) { if (!value) return 'Noch nie erledigt'; const date = new Date(value); if (Number.isNaN(date.getTime())) { return 'Zeitpunkt unbekannt'; } const diffMs = Date.now() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); if (diffMin < 1) return 'Gerade erledigt'; if (diffMin < 60) return `Vor ${diffMin} Min`; const diffHours = Math.floor(diffMin / 60); if (diffHours < 24) return `Vor ${diffHours} Std`; const diffDays = Math.floor(diffHours / 24); return `Vor ${diffDays} Tagen`; } function formatDateStamp(value) { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) { return '—'; } return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function normalizeMarkerValue(value) { return (value || '').trim(); } function getFilteredItems() { const markerFilter = state.filters.marker || ''; const urlFilter = (state.filters.url || '').toLowerCase(); return state.items.filter((item) => { const currentMarker = normalizeMarkerValue(item.marker).toLowerCase(); if (markerFilter === '__none') { if (currentMarker) { return false; } } else if (markerFilter) { if (currentMarker !== markerFilter.toLowerCase()) { return false; } } if (urlFilter) { const urlValue = (item.resolved_url || item.url_template || '').toLowerCase(); if (!urlValue.includes(urlFilter)) { return false; } } return true; }); } function sortItems(items) { const sorted = [...items]; const activeSort = state.sort || DEFAULT_SORT; const direction = activeSort.direction === 'asc' ? 1 : -1; const safeTime = (value) => { const time = value ? new Date(value).getTime() : 0; return Number.isNaN(time) ? 0 : time; }; sorted.sort((a, b) => { switch (activeSort.column) { case 'url_template': return a.url_template.localeCompare(b.url_template, 'de', { sensitivity: 'base' }) * direction; case 'marker': return normalizeMarkerValue(a.marker).localeCompare(normalizeMarkerValue(b.marker), 'de', { sensitivity: 'base' }) * direction; case 'created_at': return (safeTime(a.created_at) - safeTime(b.created_at)) * direction; case 'updated_at': return (safeTime(a.updated_at) - safeTime(b.updated_at)) * direction; case 'last_completed_at': default: return (safeTime(a.last_completed_at) - safeTime(b.last_completed_at)) * direction; } }); return sorted; } function getVisibleItems() { return sortItems(getFilteredItems()); } function updateSortIndicators() { if (!sortButtons || !sortButtons.length) { return; } const activeSort = state.sort || DEFAULT_SORT; sortButtons.forEach((btn) => { const key = btn.dataset.sortKey; const isActive = key === activeSort.column; btn.classList.toggle('is-active', isActive); btn.setAttribute('data-sort-direction', isActive ? (activeSort.direction === 'asc' ? '↑' : '↓') : '↕'); }); } function renderMarkerFilterOptions() { if (!markerFilterSelect) { return; } const activeValue = state.filters.marker || ''; const markers = Array.from( new Set( state.items .map((item) => normalizeMarkerValue(item.marker)) .filter(Boolean) ) ).sort((a, b) => a.localeCompare(b, 'de', { sensitivity: 'base' })); markerFilterSelect.innerHTML = ''; const addOption = (value, label) => { const opt = document.createElement('option'); opt.value = value; opt.textContent = label; markerFilterSelect.appendChild(opt); }; addOption('', 'Alle Marker'); addOption('__none', 'Ohne Marker'); markers.forEach((marker) => addOption(marker, marker)); if (activeValue && activeValue !== '__none' && !markers.includes(activeValue)) { addOption(activeValue, `${activeValue} (kein Treffer)`); } const nextValue = activeValue || ''; markerFilterSelect.value = nextValue; state.filters.marker = nextValue; persistFilters(state.filters); } function renderTable() { if (!tableBody) { return; } tableBody.innerHTML = ''; updateSortIndicators(); if (state.loading) { appendSingleRow('Lade Bookmarks...', 'loading-state'); return; } if (state.error) { appendSingleRow(state.error, 'error-state'); return; } if (!state.items.length) { appendSingleRow('Keine Bookmarks vorhanden. Lege dein erstes Bookmark an.', 'empty-state'); return; } const visibleItems = getVisibleItems(); if (!visibleItems.length) { appendSingleRow('Keine Bookmarks für diesen Filter.', 'empty-state'); return; } visibleItems.forEach((item) => { const tr = document.createElement('tr'); tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open'); const urlTd = document.createElement('td'); urlTd.className = 'url-cell'; const link = document.createElement('a'); const resolvedUrl = item.resolved_url || item.url_template; link.href = resolvedUrl; link.target = '_blank'; link.rel = 'noopener'; link.title = resolvedUrl; link.textContent = resolvedUrl; urlTd.appendChild(link); tr.appendChild(urlTd); const markerTd = document.createElement('td'); markerTd.className = 'marker-cell'; if (normalizeMarkerValue(item.marker)) { const markerChip = document.createElement('span'); markerChip.className = 'chip chip--marker'; markerChip.textContent = item.marker; markerTd.appendChild(markerChip); } else { markerTd.textContent = '–'; markerTd.classList.add('muted'); } tr.appendChild(markerTd); const createdTd = document.createElement('td'); createdTd.textContent = formatDateStamp(item.created_at); tr.appendChild(createdTd); const timeTd = document.createElement('td'); timeTd.textContent = formatRelativeTimestamp(item.last_completed_at); tr.appendChild(timeTd); const actionsTd = document.createElement('td'); actionsTd.className = 'table-actions'; const openBtn = document.createElement('button'); openBtn.className = 'ghost-btn'; openBtn.type = 'button'; openBtn.textContent = '🔗'; openBtn.title = 'Öffnen'; openBtn.addEventListener('click', () => { const target = item.resolved_url || item.url_template; if (target) { window.open(target, '_blank', 'noopener'); } }); actionsTd.appendChild(openBtn); const toggleBtn = document.createElement('button'); toggleBtn.className = 'ghost-btn'; toggleBtn.type = 'button'; toggleBtn.textContent = item.completed_for_day ? '↩️' : '✅'; toggleBtn.title = item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt'; toggleBtn.addEventListener('click', () => { if (item.completed_for_day) { undoDailyBookmark(item.id); } else { completeDailyBookmark(item.id); } }); actionsTd.appendChild(toggleBtn); const editBtn = document.createElement('button'); editBtn.className = 'ghost-btn'; editBtn.type = 'button'; editBtn.textContent = '✏️'; editBtn.title = 'Bearbeiten'; editBtn.addEventListener('click', () => openModal('edit', item)); actionsTd.appendChild(editBtn); const deleteBtn = document.createElement('button'); deleteBtn.className = 'ghost-btn danger'; deleteBtn.type = 'button'; deleteBtn.textContent = '🗑️'; deleteBtn.title = 'Löschen'; deleteBtn.addEventListener('click', () => { if (window.confirm('Bookmark wirklich löschen?')) { deleteDailyBookmark(item.id); } }); actionsTd.appendChild(deleteBtn); tr.appendChild(actionsTd); tableBody.appendChild(tr); }); } function appendSingleRow(text, className) { const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 5; td.className = className; td.textContent = text; tr.appendChild(td); tableBody.appendChild(tr); } function updateHeroStats() { const total = state.items.length; const done = state.items.filter((item) => item.completed_for_day).length; const visibleItems = getFilteredItems(); const visibleDone = visibleItems.filter((item) => item.completed_for_day).length; const filterSuffix = visibleItems.length !== total ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : ''; const text = total ? `${done}/${total} erledigt${filterSuffix}` : 'Keine Bookmarks vorhanden'; const filterParts = []; if (state.filters.marker) { filterParts.push(state.filters.marker === '__none' ? 'ohne Marker' : `Marker: ${state.filters.marker}`); } if (state.filters.url) { filterParts.push(`URL enthält „${state.filters.url}“`); } const filterText = filterParts.length ? ` · Filter: ${filterParts.join(' · ')}` : ''; if (heroStats) { heroStats.textContent = `${text}${filterText}`; } if (listSummary) { listSummary.textContent = `${text} – Tag ${state.dayKey}${filterText}`; } } function clearAutoOpenCountdown() { if (state.autoOpenCountdownIntervalId) { clearInterval(state.autoOpenCountdownIntervalId); state.autoOpenCountdownIntervalId = null; } } function updateAutoOpenCountdownLabel(remainingMs) { if (!autoOpenCountdown) return; const safeMs = Math.max(0, remainingMs); const seconds = safeMs / 1000; const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1); autoOpenCountdown.textContent = formatted; } function hideAutoOpenOverlay() { clearAutoOpenCountdown(); if (autoOpenOverlay) { autoOpenOverlay.classList.remove('visible'); autoOpenOverlay.hidden = true; } } function showAutoOpenOverlay(delayMs) { if (!autoOpenOverlay) return; const duration = Math.max(0, delayMs); hideAutoOpenOverlay(); autoOpenOverlay.hidden = false; requestAnimationFrame(() => autoOpenOverlay.classList.add('visible')); updateAutoOpenCountdownLabel(duration); const start = Date.now(); state.autoOpenCountdownIntervalId = setInterval(() => { const remaining = Math.max(0, duration - (Date.now() - start)); updateAutoOpenCountdownLabel(remaining); if (remaining <= 0) { clearAutoOpenCountdown(); } }, 100); } function cancelAutoOpen(showMessage = false) { if (state.autoOpenTimerId) { clearTimeout(state.autoOpenTimerId); state.autoOpenTimerId = null; } state.autoOpenTriggered = false; hideAutoOpenOverlay(); if (showMessage) { setListStatus('Automatisches Öffnen abgebrochen.', false); } } function maybeAutoOpen(reason = '', delayMs = AUTO_OPEN_DELAY_MS) { if (!state.autoOpenEnabled) { hideAutoOpenOverlay(); return; } if (state.processingBatch) return; if (state.autoOpenTriggered) return; const undone = getVisibleItems().filter((item) => !item.completed_for_day); if (!undone.length) { hideAutoOpenOverlay(); return; } if (state.autoOpenTimerId) { clearTimeout(state.autoOpenTimerId); state.autoOpenTimerId = null; } hideAutoOpenOverlay(); state.autoOpenTriggered = true; const delay = typeof delayMs === 'number' ? Math.max(0, delayMs) : AUTO_OPEN_DELAY_MS; if (delay === 0) { if (state.autoOpenEnabled) { openBatch({ auto: true }); } else { state.autoOpenTriggered = false; } return; } showAutoOpenOverlay(delay); state.autoOpenTimerId = setTimeout(() => { state.autoOpenTimerId = null; hideAutoOpenOverlay(); if (state.autoOpenEnabled) { openBatch({ auto: true }); } else { state.autoOpenTriggered = false; } }, delay); } async function loadDailyBookmarks() { state.loading = true; if (state.autoOpenTimerId) { clearTimeout(state.autoOpenTimerId); state.autoOpenTimerId = null; } hideAutoOpenOverlay(); state.autoOpenTriggered = false; state.error = ''; setListStatus(''); renderTable(); try { const data = await apiFetch(`${API_URL}/daily-bookmarks?day=${encodeURIComponent(state.dayKey)}`); state.items = Array.isArray(data) ? data : []; state.loading = false; updateHeroStats(); renderMarkerFilterOptions(); renderTable(); maybeAutoOpen('load'); } catch (error) { state.loading = false; state.error = 'Konnte Bookmarks nicht laden.'; setListStatus(state.error, true); renderTable(); } } async function completeDailyBookmark(id) { if (!id) return; state.error = ''; try { const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, { method: 'POST', body: JSON.stringify({ day: state.dayKey }) }); state.items = state.items.map((item) => (item.id === id ? updated : item)); updateHeroStats(); renderTable(); } catch (error) { state.error = 'Konnte nicht abhaken.'; setListStatus(state.error, true); renderTable(); } } async function undoDailyBookmark(id) { if (!id) return; state.error = ''; try { const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, { method: 'DELETE' }); state.items = state.items.map((item) => (item.id === id ? updated : item)); updateHeroStats(); renderTable(); } catch (error) { state.error = 'Konnte nicht zurücksetzen.'; setListStatus(state.error, true); renderTable(); } } async function deleteDailyBookmark(id) { if (!id) return; state.error = ''; try { await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}`, { method: 'DELETE' }); state.items = state.items.filter((item) => item.id !== id); if (editingId === id) { resetForm(); } updateHeroStats(); renderMarkerFilterOptions(); renderTable(); } catch (error) { state.error = 'Konnte Bookmark nicht löschen.'; setListStatus(state.error, true); renderTable(); } } async function submitForm(event) { event.preventDefault(); if (state.saving) return; const title = titleInput.value.trim(); const url = urlInput.value.trim(); const notes = notesInput.value.trim(); const marker = markerInput ? markerInput.value.trim() : ''; if (!url) { setFormStatus('URL-Template ist Pflicht.', true); urlInput.focus(); return; } state.saving = true; submitBtn.disabled = true; setFormStatus(''); const payload = { title, url_template: url, notes, marker, day: state.dayKey }; const targetUrl = editingId ? `${API_URL}/daily-bookmarks/${encodeURIComponent(editingId)}` : `${API_URL}/daily-bookmarks`; const method = editingId ? 'PUT' : 'POST'; try { const saved = await apiFetch(targetUrl, { method, body: JSON.stringify(payload) }); if (editingId) { state.items = state.items.map((item) => (item.id === editingId ? saved : item)); } else { state.items = [saved, ...state.items]; } updateHeroStats(); renderMarkerFilterOptions(); renderTable(); closeModal(); } catch (error) { setFormStatus('Speichern fehlgeschlagen.', true); } finally { state.saving = false; submitBtn.disabled = false; } } async function openBatch({ auto = false } = {}) { if (state.processingBatch) return; if (!auto) { cancelAutoOpen(false); } const undone = getVisibleItems().filter((item) => !item.completed_for_day); if (!undone.length) { if (!auto) { setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true); } return; } const count = state.bulkCount || DEFAULT_BULK_COUNT; const selection = undone.slice(0, count); state.processingBatch = true; if (bulkOpenBtn) { bulkOpenBtn.disabled = true; } if (!auto) { setListStatus(''); } else { setListStatus(`Öffne automatisch ${selection.length} Links...`, false); } for (const item of selection) { const target = item.resolved_url || item.url_template; if (target) { window.open(target, '_blank', 'noopener'); } try { const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(item.id)}/check`, { method: 'POST', body: JSON.stringify({ day: state.dayKey }) }); state.items = state.items.map((entry) => (entry.id === item.id ? updated : entry)); } catch (error) { setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true); } } state.processingBatch = false; if (bulkOpenBtn) { bulkOpenBtn.disabled = false; } if (auto) { setListStatus(''); } updateHeroStats(); renderTable(); } function setImportStatus(message, isError = false) { if (!importStatus) { return; } importStatus.textContent = message || ''; importStatus.classList.toggle('form-status--error', !!isError); } function resetImportForm() { if (importForm) { importForm.reset(); } const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : ''; if (importMarkerInput) { importMarkerInput.value = suggestedMarker; } setImportStatus(''); } function openImportModal() { if (modal && !modal.hidden) { closeModal(); } resetImportForm(); if (importModal) { importModal.hidden = false; importModal.focus(); } if (importInput) { importInput.focus(); } } function closeImportModal() { if (importModal) { importModal.hidden = true; } resetImportForm(); } async function submitImportForm(event) { event.preventDefault(); if (state.importing) return; const rawText = importInput ? importInput.value.trim() : ''; const marker = importMarkerInput ? importMarkerInput.value.trim() : ''; if (!rawText) { setImportStatus('Bitte füge mindestens eine URL ein.', true); if (importInput) { importInput.focus(); } return; } const urls = rawText .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (!urls.length) { setImportStatus('Keine gültigen Zeilen gefunden.', true); return; } state.importing = true; if (importSubmitBtn) { importSubmitBtn.disabled = true; } setImportStatus('Import läuft...'); try { const result = await apiFetch(`${API_URL}/daily-bookmarks/import`, { method: 'POST', body: JSON.stringify({ urls, marker, day: state.dayKey }) }); const importedItems = Array.isArray(result && result.items) ? result.items : []; const importedIds = new Set(importedItems.map((entry) => entry.id)); const remaining = state.items.filter((item) => !importedIds.has(item.id)); state.items = [...importedItems, ...remaining]; const summaryParts = []; const createdCount = result && typeof result.created === 'number' ? result.created : importedItems.length; summaryParts.push(`${createdCount} neu`); if (result && result.skipped_existing) summaryParts.push(`${result.skipped_existing} bereits vorhanden`); if (result && result.skipped_invalid) summaryParts.push(`${result.skipped_invalid} ungültig`); if (result && result.skipped_duplicates) summaryParts.push(`${result.skipped_duplicates} Duplikate`); setListStatus(`Import fertig: ${summaryParts.join(' · ')}`); updateHeroStats(); renderMarkerFilterOptions(); renderTable(); closeImportModal(); } catch (error) { setImportStatus(error && error.message ? error.message : 'Import fehlgeschlagen.', true); } finally { state.importing = false; if (importSubmitBtn) { importSubmitBtn.disabled = false; } } } function setupEvents() { if (prevDayBtn) { prevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1)))); } if (nextDayBtn) { nextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1)))); } if (todayBtn) { todayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date()))); } if (refreshBtn) { refreshBtn.addEventListener('click', () => loadDailyBookmarks()); } if (openCreateBtn) { openCreateBtn.addEventListener('click', () => openModal('create')); } if (modalCloseBtn) { modalCloseBtn.addEventListener('click', closeModal); } if (modal) { modal.addEventListener('click', (event) => { if (event.target === modal) { closeModal(); } }); } if (modalBackdrop) { modalBackdrop.addEventListener('click', closeModal); } document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { if (modal && !modal.hidden) { closeModal(); } if (importModal && !importModal.hidden) { closeImportModal(); } } }); if (urlInput) { urlInput.addEventListener('input', updatePreviewLink); urlInput.addEventListener('blur', renderUrlSuggestions); } if (formEl) { formEl.addEventListener('submit', submitForm); } if (resetBtn) { resetBtn.addEventListener('click', resetForm); } if (bulkCountSelect) { bulkCountSelect.value = String(state.bulkCount); bulkCountSelect.addEventListener('change', () => { const value = parseInt(bulkCountSelect.value, 10); if (!Number.isNaN(value)) { state.bulkCount = value; persistBulkCount(value); } }); } if (bulkOpenBtn) { bulkOpenBtn.addEventListener('click', () => openBatch()); } if (autoOpenOverlayPanel) { autoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true)); } if (autoOpenToggle) { autoOpenToggle.checked = !!state.autoOpenEnabled; autoOpenToggle.addEventListener('change', () => { state.autoOpenEnabled = autoOpenToggle.checked; persistAutoOpenEnabled(state.autoOpenEnabled); state.autoOpenTriggered = false; if (!state.autoOpenEnabled && state.autoOpenTimerId) { cancelAutoOpen(false); } if (state.autoOpenEnabled) { maybeAutoOpen('toggle'); } else { hideAutoOpenOverlay(); } }); } document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && state.autoOpenEnabled) { // Reset the guard so that returning to the tab can trigger the next batch. if (state.autoOpenTimerId) { clearTimeout(state.autoOpenTimerId); state.autoOpenTimerId = null; } state.autoOpenTriggered = false; maybeAutoOpen('visibility', AUTO_OPEN_DELAY_MS); } }); if (markerFilterSelect) { markerFilterSelect.addEventListener('change', () => { state.filters.marker = markerFilterSelect.value || ''; persistFilters(state.filters); updateHeroStats(); renderTable(); }); } if (urlFilterInput) { urlFilterInput.value = state.filters.url || ''; urlFilterInput.addEventListener('input', () => { state.filters.url = urlFilterInput.value.trim(); persistFilters(state.filters); updateHeroStats(); renderTable(); }); } if (resetViewBtn) { resetViewBtn.addEventListener('click', () => { state.filters = { marker: '', url: '' }; state.sort = { ...DEFAULT_SORT }; persistFilters(state.filters); persistSort(state.sort); renderMarkerFilterOptions(); if (urlFilterInput) { urlFilterInput.value = ''; } updateHeroStats(); renderTable(); }); } if (sortButtons && sortButtons.length) { sortButtons.forEach((btn) => { btn.addEventListener('click', () => { const key = btn.dataset.sortKey; if (!key) return; if (state.sort.column === key) { state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc'; } else { const defaultDirection = ['last_completed_at', 'updated_at', 'created_at'].includes(key) ? 'desc' : 'asc'; state.sort = { column: key, direction: defaultDirection }; } persistSort(state.sort); renderTable(); }); }); } if (openImportBtn) { openImportBtn.addEventListener('click', openImportModal); } if (importCloseBtn) { importCloseBtn.addEventListener('click', closeImportModal); } if (importModal) { importModal.addEventListener('click', (event) => { if (event.target === importModal) { closeImportModal(); } }); } if (importBackdrop) { importBackdrop.addEventListener('click', closeImportModal); } if (importForm) { importForm.addEventListener('submit', submitImportForm); } if (importResetBtn) { importResetBtn.addEventListener('click', resetImportForm); } } function init() { updateDayUI(); setupEvents(); loadDailyBookmarks(); updatePreviewLink(); } init(); })();