1505 lines
47 KiB
JavaScript
1505 lines
47 KiB
JavaScript
(function () {
|
||
let active = false;
|
||
let initialized = false;
|
||
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 LOGIN_PAGE = 'login.html';
|
||
|
||
function handleUnauthorized(response) {
|
||
if (response && response.status === 401) {
|
||
if (typeof redirectToLogin === 'function') {
|
||
redirectToLogin();
|
||
} else {
|
||
window.location.href = LOGIN_PAGE;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
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 dailyDayLabel = document.getElementById('dailyDayLabel');
|
||
const dailyDaySubLabel = document.getElementById('dailyDaySubLabel');
|
||
const dailyPrevDayBtn = document.getElementById('dailyPrevDayBtn');
|
||
const dailyNextDayBtn = document.getElementById('dailyNextDayBtn');
|
||
const dailyTodayBtn = document.getElementById('dailyTodayBtn');
|
||
const dailyRefreshBtn = document.getElementById('dailyRefreshBtn');
|
||
const dailyHeroStats = document.getElementById('dailyHeroStats');
|
||
const dailyListSummary = document.getElementById('dailyListSummary');
|
||
const dailyListStatus = document.getElementById('dailyListStatus');
|
||
const dailyTableBody = document.getElementById('dailyTableBody');
|
||
const dailyBulkCountSelect = document.getElementById('dailyBulkCountSelect');
|
||
const dailyBulkOpenBtn = document.getElementById('dailyBulkOpenBtn');
|
||
const dailyAutoOpenToggle = document.getElementById('dailyAutoOpenToggle');
|
||
const dailyAutoOpenOverlay = document.getElementById('dailyAutoOpenOverlay');
|
||
const dailyAutoOpenOverlayPanel = document.getElementById('dailyAutoOpenOverlayPanel');
|
||
const dailyAutoOpenCountdown = document.getElementById('dailyAutoOpenCountdown');
|
||
const dailyOpenCreateBtn = document.getElementById('dailyOpenCreateBtn');
|
||
|
||
const modal = document.getElementById('dailyBookmarkModal');
|
||
const dailyModalCloseBtn = document.getElementById('dailyModalCloseBtn');
|
||
const dailyModalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null;
|
||
const formEl = document.getElementById('dailyBookmarkForm');
|
||
const dailyTitleInput = document.getElementById('dailyTitleInput');
|
||
const dailyUrlInput = document.getElementById('dailyUrlInput');
|
||
const dailyNotesInput = document.getElementById('dailyNotesInput');
|
||
const dailyResetBtn = document.getElementById('dailyResetBtn');
|
||
const dailySubmitBtn = document.getElementById('dailySubmitBtn');
|
||
const dailyPreviewLink = document.getElementById('dailyPreviewLink');
|
||
const dailyFormStatus = document.getElementById('dailyFormStatus');
|
||
const dailyFormModeLabel = document.getElementById('dailyFormModeLabel');
|
||
const dailyUrlSuggestionBox = document.getElementById('dailyUrlSuggestionBox');
|
||
const dailyMarkerInput = document.getElementById('dailyMarkerInput');
|
||
const dailyActiveInput = document.getElementById('dailyActiveInput');
|
||
const markerFilterSelect = document.getElementById('dailyMarkerFilter');
|
||
const urlFilterInput = document.getElementById('dailyUrlFilter');
|
||
const dailyResetViewBtn = document.getElementById('dailyResetViewBtn');
|
||
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
|
||
const dailyOpenImportBtn = document.getElementById('dailyOpenImportBtn');
|
||
const dailyImportModal = document.getElementById('dailyImportModal');
|
||
const dailyImportCloseBtn = document.getElementById('dailyImportCloseBtn');
|
||
const dailyImportBackdrop = dailyImportModal ? dailyImportModal.querySelector('.modal__backdrop') : null;
|
||
const dailyImportForm = document.getElementById('dailyImportForm');
|
||
const dailyImportInput = document.getElementById('dailyImportInput');
|
||
const dailyImportMarkerInput = document.getElementById('dailyImportMarkerInput');
|
||
const dailyImportResetBtn = document.getElementById('dailyImportResetBtn');
|
||
const dailyImportSubmitBtn = document.getElementById('dailyImportSubmitBtn');
|
||
const dailyImportStatus = document.getElementById('dailyImportStatus');
|
||
|
||
const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;
|
||
|
||
function ensureStyles(enabled) {
|
||
const link = document.getElementById('dailyBookmarksCss');
|
||
if (link) {
|
||
link.disabled = !enabled;
|
||
}
|
||
}
|
||
|
||
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 normalizeItem(item) {
|
||
if (!item || typeof item !== 'object') {
|
||
return null;
|
||
}
|
||
return {
|
||
...item,
|
||
is_active: Number(item.is_active ?? 1) !== 0
|
||
};
|
||
}
|
||
|
||
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 (!dailyUrlSuggestionBox) {
|
||
return;
|
||
}
|
||
const suggestions = buildUrlSuggestions(dailyUrlInput ? dailyUrlInput.value : '');
|
||
dailyUrlSuggestionBox.innerHTML = '';
|
||
if (!suggestions.length) {
|
||
dailyUrlSuggestionBox.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const text = document.createElement('span');
|
||
text.className = 'suggestion-box__text';
|
||
text.textContent = 'Mögliche Platzhalter:';
|
||
dailyUrlSuggestionBox.appendChild(text);
|
||
|
||
const applySuggestion = (value) => {
|
||
if (!dailyUrlInput) {
|
||
return;
|
||
}
|
||
dailyUrlInput.value = value;
|
||
updatePreviewLink();
|
||
renderUrlSuggestions();
|
||
const end = dailyUrlInput.value.length;
|
||
dailyUrlInput.focus();
|
||
try {
|
||
dailyUrlInput.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);
|
||
|
||
dailyUrlSuggestionBox.appendChild(item);
|
||
});
|
||
|
||
dailyUrlSuggestionBox.hidden = false;
|
||
}
|
||
|
||
async function apiFetch(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
...options,
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(options.headers || {})
|
||
}
|
||
});
|
||
|
||
if (handleUnauthorized(response)) {
|
||
throw new Error('Nicht angemeldet');
|
||
}
|
||
|
||
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 (dailyDayLabel) {
|
||
dailyDayLabel.textContent = formatDayLabel(state.dayKey);
|
||
}
|
||
if (dailyDaySubLabel) {
|
||
dailyDaySubLabel.textContent = formatRelativeDay(state.dayKey);
|
||
}
|
||
updatePreviewLink();
|
||
}
|
||
|
||
function setDayKey(dayKey) {
|
||
state.dayKey = formatDayKey(parseDayKey(dayKey));
|
||
if (!active) return;
|
||
updateDayUI();
|
||
loadDailyBookmarks();
|
||
}
|
||
|
||
function setFormStatus(message, isError = false) {
|
||
if (!dailyFormStatus) {
|
||
return;
|
||
}
|
||
dailyFormStatus.textContent = message || '';
|
||
dailyFormStatus.classList.toggle('form-status--error', !!isError);
|
||
}
|
||
|
||
function setListStatus(message, isError = false) {
|
||
if (!dailyListStatus) {
|
||
return;
|
||
}
|
||
dailyListStatus.textContent = message || '';
|
||
dailyListStatus.classList.toggle('list-status--error', !!isError);
|
||
}
|
||
|
||
function updatePreviewLink() {
|
||
if (!dailyPreviewLink || !dailyUrlInput) {
|
||
return;
|
||
}
|
||
const resolved = resolveTemplate(dailyUrlInput.value || '', state.dayKey);
|
||
dailyPreviewLink.textContent = resolved || '–';
|
||
if (resolved) {
|
||
dailyPreviewLink.href = resolved;
|
||
dailyPreviewLink.target = '_blank';
|
||
dailyPreviewLink.rel = 'noopener';
|
||
} else {
|
||
dailyPreviewLink.removeAttribute('href');
|
||
}
|
||
renderUrlSuggestions();
|
||
}
|
||
|
||
function resetForm() {
|
||
editingId = null;
|
||
dailyFormModeLabel.textContent = 'Neues Bookmark';
|
||
dailySubmitBtn.textContent = 'Speichern';
|
||
formEl.reset();
|
||
if (dailyMarkerInput) {
|
||
dailyMarkerInput.value = '';
|
||
}
|
||
if (dailyActiveInput) {
|
||
dailyActiveInput.checked = true;
|
||
}
|
||
setFormStatus('');
|
||
updatePreviewLink();
|
||
renderUrlSuggestions();
|
||
}
|
||
|
||
function openModal(mode, bookmark) {
|
||
if (dailyImportModal && !dailyImportModal.hidden) {
|
||
closeImportModal();
|
||
}
|
||
if (mode === 'edit' && bookmark) {
|
||
editingId = bookmark.id;
|
||
dailyFormModeLabel.textContent = 'Bookmark bearbeiten';
|
||
dailySubmitBtn.textContent = 'Aktualisieren';
|
||
dailyTitleInput.value = bookmark.title || '';
|
||
dailyUrlInput.value = bookmark.url_template || '';
|
||
dailyNotesInput.value = bookmark.notes || '';
|
||
if (dailyMarkerInput) {
|
||
dailyMarkerInput.value = bookmark.marker || '';
|
||
}
|
||
if (dailyActiveInput) {
|
||
dailyActiveInput.checked = bookmark.is_active !== false;
|
||
}
|
||
setFormStatus('Bearbeite vorhandenes Bookmark');
|
||
} else {
|
||
resetForm();
|
||
}
|
||
|
||
if (modal) {
|
||
modal.hidden = false;
|
||
modal.focus();
|
||
}
|
||
updatePreviewLink();
|
||
if (mode === 'edit' && dailyTitleInput) {
|
||
dailyTitleInput.focus();
|
||
} else if (dailyUrlInput) {
|
||
dailyUrlInput.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 dailyMarkerFilter = state.filters.marker || '';
|
||
const dailyUrlFilter = (state.filters.url || '').toLowerCase();
|
||
return state.items.filter((item) => {
|
||
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
|
||
if (dailyMarkerFilter === '__none') {
|
||
if (currentMarker) {
|
||
return false;
|
||
}
|
||
} else if (dailyMarkerFilter) {
|
||
if (currentMarker !== dailyMarkerFilter.toLowerCase()) {
|
||
return false;
|
||
}
|
||
}
|
||
if (dailyUrlFilter) {
|
||
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
|
||
if (!urlValue.includes(dailyUrlFilter)) {
|
||
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 (!dailyTableBody) {
|
||
return;
|
||
}
|
||
|
||
dailyTableBody.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');
|
||
const isActive = item.is_active !== false;
|
||
if (isActive) {
|
||
tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open');
|
||
} else {
|
||
tr.classList.add('is-inactive');
|
||
}
|
||
|
||
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';
|
||
markerTd.textContent = '';
|
||
if (normalizeMarkerValue(item.marker)) {
|
||
const markerChip = document.createElement('span');
|
||
markerChip.className = 'chip chip--marker';
|
||
markerChip.textContent = item.marker;
|
||
markerTd.appendChild(markerChip);
|
||
} else {
|
||
const placeholder = document.createElement('span');
|
||
placeholder.textContent = '–';
|
||
placeholder.classList.add('muted');
|
||
markerTd.appendChild(placeholder);
|
||
}
|
||
if (!isActive) {
|
||
const inactiveChip = document.createElement('span');
|
||
inactiveChip.className = 'chip chip--inactive';
|
||
inactiveChip.textContent = 'Deaktiviert';
|
||
markerTd.appendChild(inactiveChip);
|
||
}
|
||
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 = isActive ? 'Öffnen' : 'Deaktiviert';
|
||
openBtn.disabled = !isActive;
|
||
openBtn.addEventListener('click', () => {
|
||
const target = item.resolved_url || item.url_template;
|
||
if (!isActive) return;
|
||
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 = isActive
|
||
? (item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt')
|
||
: 'Deaktiviert';
|
||
toggleBtn.disabled = !isActive;
|
||
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);
|
||
dailyTableBody.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);
|
||
dailyTableBody.appendChild(tr);
|
||
}
|
||
|
||
function updateHeroStats() {
|
||
const activeItems = state.items.filter((item) => item.is_active !== false);
|
||
const totalActive = activeItems.length;
|
||
const inactiveCount = state.items.length - totalActive;
|
||
const done = activeItems.filter((item) => item.completed_for_day).length;
|
||
const visibleItems = getFilteredItems().filter((item) => item.is_active !== false);
|
||
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
|
||
const filterSuffix = visibleItems.length !== totalActive ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
|
||
const inactiveSuffix = inactiveCount ? ` · ${inactiveCount} deaktiviert` : '';
|
||
const baseText = totalActive ? `${done}/${totalActive} erledigt${filterSuffix}` : 'Keine aktiven Bookmarks';
|
||
const text = `${baseText}${inactiveSuffix}`;
|
||
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 (dailyHeroStats) {
|
||
dailyHeroStats.textContent = `${text}${filterText}`;
|
||
}
|
||
if (dailyListSummary) {
|
||
dailyListSummary.textContent = `${text} – Tag ${state.dayKey}${filterText}`;
|
||
}
|
||
}
|
||
|
||
function clearAutoOpenCountdown() {
|
||
if (state.autoOpenCountdownIntervalId) {
|
||
clearInterval(state.autoOpenCountdownIntervalId);
|
||
state.autoOpenCountdownIntervalId = null;
|
||
}
|
||
}
|
||
|
||
function updateAutoOpenCountdownLabel(remainingMs) {
|
||
if (!dailyAutoOpenCountdown) return;
|
||
const safeMs = Math.max(0, remainingMs);
|
||
const seconds = safeMs / 1000;
|
||
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
|
||
dailyAutoOpenCountdown.textContent = formatted;
|
||
}
|
||
|
||
function hideAutoOpenOverlay() {
|
||
clearAutoOpenCountdown();
|
||
if (dailyAutoOpenOverlay) {
|
||
dailyAutoOpenOverlay.classList.remove('visible');
|
||
dailyAutoOpenOverlay.hidden = true;
|
||
}
|
||
}
|
||
|
||
function showAutoOpenOverlay(delayMs) {
|
||
if (!dailyAutoOpenOverlay) return;
|
||
const duration = Math.max(0, delayMs);
|
||
hideAutoOpenOverlay();
|
||
dailyAutoOpenOverlay.hidden = false;
|
||
requestAnimationFrame(() => dailyAutoOpenOverlay.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 (!active) {
|
||
hideAutoOpenOverlay();
|
||
return;
|
||
}
|
||
if (!state.autoOpenEnabled) {
|
||
hideAutoOpenOverlay();
|
||
return;
|
||
}
|
||
if (state.processingBatch) return;
|
||
if (state.autoOpenTriggered) return;
|
||
const undone = getVisibleItems().filter(
|
||
(item) => item.is_active !== false && !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() {
|
||
if (!active) return;
|
||
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)}`);
|
||
const rows = Array.isArray(data) ? data : [];
|
||
state.items = rows.map((item) => normalizeItem(item)).filter(Boolean);
|
||
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;
|
||
const target = state.items.find((item) => item.id === id);
|
||
if (target && target.is_active === false) {
|
||
setListStatus('Bookmark ist deaktiviert.', true);
|
||
return;
|
||
}
|
||
state.error = '';
|
||
try {
|
||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ day: state.dayKey })
|
||
});
|
||
const normalized = normalizeItem(updated);
|
||
state.items = state.items.map((item) => (item.id === id ? normalized || item : item));
|
||
updateHeroStats();
|
||
renderTable();
|
||
} catch (error) {
|
||
state.error = 'Konnte nicht abhaken.';
|
||
setListStatus(state.error, true);
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
async function undoDailyBookmark(id) {
|
||
if (!id) return;
|
||
const target = state.items.find((item) => item.id === id);
|
||
if (target && target.is_active === false) {
|
||
setListStatus('Bookmark ist deaktiviert.', true);
|
||
return;
|
||
}
|
||
state.error = '';
|
||
try {
|
||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const normalized = normalizeItem(updated);
|
||
state.items = state.items.map((item) => (item.id === id ? normalized || item : 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 = dailyTitleInput.value.trim();
|
||
const url = dailyUrlInput.value.trim();
|
||
const notes = dailyNotesInput.value.trim();
|
||
const marker = dailyMarkerInput ? dailyMarkerInput.value.trim() : '';
|
||
|
||
if (!url) {
|
||
setFormStatus('URL-Template ist Pflicht.', true);
|
||
dailyUrlInput.focus();
|
||
return;
|
||
}
|
||
|
||
state.saving = true;
|
||
dailySubmitBtn.disabled = true;
|
||
setFormStatus('');
|
||
|
||
const payload = {
|
||
title,
|
||
url_template: url,
|
||
notes,
|
||
marker,
|
||
is_active: dailyActiveInput ? dailyActiveInput.checked : true,
|
||
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)
|
||
});
|
||
const normalized = normalizeItem(saved);
|
||
if (editingId) {
|
||
state.items = state.items.map((item) => (item.id === editingId ? normalized || item : item));
|
||
} else if (normalized) {
|
||
state.items = [normalized, ...state.items];
|
||
}
|
||
updateHeroStats();
|
||
renderMarkerFilterOptions();
|
||
renderTable();
|
||
closeModal();
|
||
} catch (error) {
|
||
setFormStatus('Speichern fehlgeschlagen.', true);
|
||
} finally {
|
||
state.saving = false;
|
||
dailySubmitBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function openBatch({ auto = false } = {}) {
|
||
if (state.processingBatch) return;
|
||
if (!auto) {
|
||
cancelAutoOpen(false);
|
||
}
|
||
const undone = getVisibleItems().filter(
|
||
(item) => item.is_active !== false && !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 (dailyBulkOpenBtn) {
|
||
dailyBulkOpenBtn.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 })
|
||
});
|
||
const normalized = normalizeItem(updated);
|
||
state.items = state.items.map((entry) => (entry.id === item.id ? normalized || entry : entry));
|
||
} catch (error) {
|
||
setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true);
|
||
}
|
||
}
|
||
|
||
state.processingBatch = false;
|
||
if (dailyBulkOpenBtn) {
|
||
dailyBulkOpenBtn.disabled = false;
|
||
}
|
||
if (auto) {
|
||
setListStatus('');
|
||
}
|
||
updateHeroStats();
|
||
renderTable();
|
||
}
|
||
|
||
function setImportStatus(message, isError = false) {
|
||
if (!dailyImportStatus) {
|
||
return;
|
||
}
|
||
dailyImportStatus.textContent = message || '';
|
||
dailyImportStatus.classList.toggle('form-status--error', !!isError);
|
||
}
|
||
|
||
function resetImportForm() {
|
||
if (dailyImportForm) {
|
||
dailyImportForm.reset();
|
||
}
|
||
const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : '';
|
||
if (dailyImportMarkerInput) {
|
||
dailyImportMarkerInput.value = suggestedMarker;
|
||
}
|
||
setImportStatus('');
|
||
}
|
||
|
||
function openImportModal() {
|
||
if (modal && !modal.hidden) {
|
||
closeModal();
|
||
}
|
||
resetImportForm();
|
||
if (dailyImportModal) {
|
||
dailyImportModal.hidden = false;
|
||
dailyImportModal.focus();
|
||
}
|
||
if (dailyImportInput) {
|
||
dailyImportInput.focus();
|
||
}
|
||
}
|
||
|
||
function closeImportModal() {
|
||
if (dailyImportModal) {
|
||
dailyImportModal.hidden = true;
|
||
}
|
||
resetImportForm();
|
||
}
|
||
|
||
async function submitImportForm(event) {
|
||
event.preventDefault();
|
||
if (state.importing) return;
|
||
|
||
const rawText = dailyImportInput ? dailyImportInput.value.trim() : '';
|
||
const marker = dailyImportMarkerInput ? dailyImportMarkerInput.value.trim() : '';
|
||
if (!rawText) {
|
||
setImportStatus('Bitte füge mindestens eine URL ein.', true);
|
||
if (dailyImportInput) {
|
||
dailyImportInput.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 (dailyImportSubmitBtn) {
|
||
dailyImportSubmitBtn.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 importedItemsRaw = Array.isArray(result && result.items) ? result.items : [];
|
||
const importedItems = importedItemsRaw.map((item) => normalizeItem(item)).filter(Boolean);
|
||
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 (dailyImportSubmitBtn) {
|
||
dailyImportSubmitBtn.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
function setupEvents() {
|
||
if (dailyPrevDayBtn) {
|
||
dailyPrevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1))));
|
||
}
|
||
if (dailyNextDayBtn) {
|
||
dailyNextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1))));
|
||
}
|
||
if (dailyTodayBtn) {
|
||
dailyTodayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date())));
|
||
}
|
||
if (dailyRefreshBtn) {
|
||
dailyRefreshBtn.addEventListener('click', () => loadDailyBookmarks());
|
||
}
|
||
if (dailyOpenCreateBtn) {
|
||
dailyOpenCreateBtn.addEventListener('click', () => openModal('create'));
|
||
}
|
||
if (dailyModalCloseBtn) {
|
||
dailyModalCloseBtn.addEventListener('click', closeModal);
|
||
}
|
||
if (modal) {
|
||
modal.addEventListener('click', (event) => {
|
||
if (event.target === modal) {
|
||
closeModal();
|
||
}
|
||
});
|
||
}
|
||
if (dailyModalBackdrop) {
|
||
dailyModalBackdrop.addEventListener('click', closeModal);
|
||
}
|
||
document.addEventListener('keydown', (event) => {
|
||
if (!active) return;
|
||
if (event.key === 'Escape') {
|
||
if (modal && !modal.hidden) {
|
||
closeModal();
|
||
}
|
||
if (dailyImportModal && !dailyImportModal.hidden) {
|
||
closeImportModal();
|
||
}
|
||
}
|
||
});
|
||
if (dailyUrlInput) {
|
||
dailyUrlInput.addEventListener('input', updatePreviewLink);
|
||
dailyUrlInput.addEventListener('blur', renderUrlSuggestions);
|
||
}
|
||
if (formEl) {
|
||
formEl.addEventListener('submit', submitForm);
|
||
}
|
||
if (dailyResetBtn) {
|
||
dailyResetBtn.addEventListener('click', resetForm);
|
||
}
|
||
if (dailyBulkCountSelect) {
|
||
dailyBulkCountSelect.value = String(state.bulkCount);
|
||
dailyBulkCountSelect.addEventListener('change', () => {
|
||
const value = parseInt(dailyBulkCountSelect.value, 10);
|
||
if (!Number.isNaN(value)) {
|
||
state.bulkCount = value;
|
||
persistBulkCount(value);
|
||
}
|
||
});
|
||
}
|
||
if (dailyBulkOpenBtn) {
|
||
dailyBulkOpenBtn.addEventListener('click', () => openBatch());
|
||
}
|
||
if (dailyAutoOpenOverlayPanel) {
|
||
dailyAutoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true));
|
||
}
|
||
if (dailyAutoOpenToggle) {
|
||
dailyAutoOpenToggle.checked = !!state.autoOpenEnabled;
|
||
dailyAutoOpenToggle.addEventListener('change', () => {
|
||
state.autoOpenEnabled = dailyAutoOpenToggle.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 (dailyResetViewBtn) {
|
||
dailyResetViewBtn.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 (dailyOpenImportBtn) {
|
||
dailyOpenImportBtn.addEventListener('click', openImportModal);
|
||
}
|
||
if (dailyImportCloseBtn) {
|
||
dailyImportCloseBtn.addEventListener('click', closeImportModal);
|
||
}
|
||
if (dailyImportModal) {
|
||
dailyImportModal.addEventListener('click', (event) => {
|
||
if (event.target === dailyImportModal) {
|
||
closeImportModal();
|
||
}
|
||
});
|
||
}
|
||
if (dailyImportBackdrop) {
|
||
dailyImportBackdrop.addEventListener('click', closeImportModal);
|
||
}
|
||
if (dailyImportForm) {
|
||
dailyImportForm.addEventListener('submit', submitImportForm);
|
||
}
|
||
if (dailyImportResetBtn) {
|
||
dailyImportResetBtn.addEventListener('click', resetImportForm);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
if (initialized) return;
|
||
setupEvents();
|
||
initialized = true;
|
||
}
|
||
|
||
function cleanup() {
|
||
active = false;
|
||
cancelAutoOpen(false);
|
||
ensureStyles(false);
|
||
hideAutoOpenOverlay();
|
||
}
|
||
|
||
function activate() {
|
||
ensureStyles(true);
|
||
init();
|
||
active = true;
|
||
updateDayUI();
|
||
updatePreviewLink();
|
||
loadDailyBookmarks();
|
||
}
|
||
|
||
window.DailyBookmarksPage = {
|
||
activate,
|
||
deactivate: cleanup
|
||
};
|
||
|
||
const dailySection = document.querySelector('[data-view="daily-bookmarks"]');
|
||
if (dailySection && dailySection.classList.contains('app-view--active')) {
|
||
activate();
|
||
}
|
||
})();
|