const API_URL = 'https://fb.srv.medeba-media.de/api'; let currentProfile = 1; let currentTab = 'pending'; let posts = []; let profilePollTimer = null; const MAX_PROFILES = 5; const PROFILE_NAMES = { 1: 'Profil 1', 2: 'Profil 2', 3: 'Profil 3', 4: 'Profil 4', 5: 'Profil 5' }; const screenshotModal = document.getElementById('screenshotModal'); const screenshotModalContent = document.getElementById('screenshotModalContent'); const screenshotModalImage = document.getElementById('screenshotModalImage'); const screenshotModalClose = document.getElementById('screenshotModalClose'); const screenshotModalBackdrop = document.getElementById('screenshotModalBackdrop'); let screenshotModalLastFocus = null; let screenshotModalPreviousOverflow = ''; let screenshotModalZoomed = false; function getProfileName(profileNumber) { if (!profileNumber) { return ''; } return PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`; } function formatDateTime(value) { if (!value) { return ''; } try { const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) { return ''; } return date.toLocaleString('de-DE'); } catch (error) { console.warn('Ungültiges Datum:', error); return ''; } } function formatUrlForDisplay(url) { if (!url) { return ''; } try { const parsed = new URL(url); const pathname = parsed.pathname === '/' ? '' : parsed.pathname; const search = parsed.search || ''; return `${parsed.hostname}${pathname}${search}`; } catch (error) { return url; } } function normalizeRequiredProfiles(post) { if (Array.isArray(post.required_profiles) && post.required_profiles.length) { return post.required_profiles .map((value) => { const parsed = parseInt(value, 10); if (Number.isNaN(parsed)) { return null; } return Math.min(MAX_PROFILES, Math.max(1, parsed)); }) .filter(Boolean); } const parsedTarget = parseInt(post.target_count, 10); const count = Number.isNaN(parsedTarget) ? 1 : Math.min(MAX_PROFILES, Math.max(1, parsedTarget)); return Array.from({ length: count }, (_, index) => index + 1); } function normalizeChecks(checks) { if (!Array.isArray(checks)) { return []; } return checks .map((check) => { if (!check) { return null; } const parsed = parseInt(check.profile_number, 10); if (Number.isNaN(parsed)) { return null; } const profileNumber = Math.min(MAX_PROFILES, Math.max(1, parsed)); return { ...check, profile_number: profileNumber, profile_name: check.profile_name || getProfileName(profileNumber), checked_at: check.checked_at || null }; }) .filter(Boolean) .sort((a, b) => { const aTime = a.checked_at ? new Date(a.checked_at).getTime() : 0; const bTime = b.checked_at ? new Date(b.checked_at).getTime() : 0; if (aTime === bTime) { return a.profile_number - b.profile_number; } return aTime - bTime; }); } function computePostStatus(post, profileNumber = currentProfile) { const requiredProfiles = normalizeRequiredProfiles(post); const checks = normalizeChecks(post.checks); const backendStatuses = Array.isArray(post.profile_statuses) ? post.profile_statuses : []; let profileStatuses = backendStatuses .map((status) => { if (!status) { return null; } const parsed = parseInt(status.profile_number, 10); if (Number.isNaN(parsed)) { return null; } const profileNumberValue = Math.min(MAX_PROFILES, Math.max(1, parsed)); let normalizedStatus = status.status; if (normalizedStatus !== 'done' && normalizedStatus !== 'available') { normalizedStatus = 'locked'; } const check = checks.find((item) => item.profile_number === profileNumberValue) || null; return { profile_number: profileNumberValue, profile_name: status.profile_name || getProfileName(profileNumberValue), status: normalizedStatus, checked_at: status.checked_at || (check ? check.checked_at : null) || null }; }) .filter(Boolean); if (profileStatuses.length !== requiredProfiles.length) { const checksByProfile = new Map(checks.map((check) => [check.profile_number, check])); const completedSet = new Set(checks.map((check) => check.profile_number)); profileStatuses = requiredProfiles.map((value, index) => { const prerequisites = requiredProfiles.slice(0, index); const prerequisitesMet = prerequisites.every((profile) => completedSet.has(profile)); const check = checksByProfile.get(value) || null; return { profile_number: value, profile_name: getProfileName(value), status: check ? 'done' : (prerequisitesMet ? 'available' : 'locked'), checked_at: check ? check.checked_at : null }; }); } else { const checksByProfile = new Map(checks.map((check) => [check.profile_number, check])); profileStatuses = requiredProfiles.map((value) => { const status = profileStatuses.find((item) => item.profile_number === value); if (!status) { const check = checksByProfile.get(value) || null; return { profile_number: value, profile_name: getProfileName(value), status: check ? 'done' : 'locked', checked_at: check ? check.checked_at : null }; } if (status.status === 'done') { const check = checksByProfile.get(value) || null; return { ...status, checked_at: status.checked_at || (check ? check.checked_at : null) || null }; } if (status.status === 'available') { return { ...status, checked_at: status.checked_at || null }; } return { ...status, status: 'locked', checked_at: status.checked_at || null }; }); } const completedProfilesSet = new Set( profileStatuses .filter((status) => status.status === 'done') .map((status) => status.profile_number) ); const checkedCount = profileStatuses.filter((status) => status.status === 'done').length; const targetCount = profileStatuses.length; const isComplete = profileStatuses.every((status) => status.status === 'done'); const nextRequiredProfile = profileStatuses.find((status) => status.status === 'available') || null; const isCurrentProfileRequired = requiredProfiles.includes(profileNumber); const isCurrentProfileDone = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'done'); const canCurrentProfileCheck = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'available'); const waitingForStatuses = profileStatuses.filter((status) => status.profile_number < profileNumber && status.status !== 'done'); const waitingForProfiles = waitingForStatuses.map((status) => status.profile_number); const waitingForNames = waitingForStatuses.map((status) => status.profile_name); return { requiredProfiles, profileStatuses, checks, completedProfilesSet, checkedCount, targetCount, isComplete, isCurrentProfileRequired, isCurrentProfileDone, canCurrentProfileCheck, waitingForProfiles, waitingForNames, nextRequiredProfile, profileNumber }; } function applyScreenshotModalSize() { if (!screenshotModalContent || !screenshotModalImage) { return; } if (screenshotModalZoomed) { return; } if (!screenshotModalImage.src) { return; } requestAnimationFrame(() => { const padding = 48; const viewportWidth = Math.max(320, window.innerWidth * 0.95); const viewportHeight = Math.max(280, window.innerHeight * 0.92); const naturalWidth = screenshotModalImage.naturalWidth || screenshotModalImage.width || 0; const naturalHeight = screenshotModalImage.naturalHeight || screenshotModalImage.height || 0; const targetWidth = Math.min(Math.max(320, naturalWidth + padding), viewportWidth); const targetHeight = Math.min(Math.max(260, naturalHeight + padding), viewportHeight); screenshotModalContent.style.width = `${targetWidth}px`; screenshotModalContent.style.height = `${targetHeight}px`; }); } async function fetchProfileState() { try { const response = await fetch(`${API_URL}/profile-state`); if (!response.ok) { return null; } const data = await response.json(); if (data && typeof data.profile_number !== 'undefined') { const parsed = parseInt(data.profile_number, 10); if (!Number.isNaN(parsed)) { return parsed; } } return null; } catch (error) { console.warn('Profilstatus konnte nicht geladen werden:', error); return null; } } async function pushProfileState(profileNumber) { try { await fetch(`${API_URL}/profile-state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile_number: profileNumber }) }); } catch (error) { console.error('Profilstatus konnte nicht gespeichert werden:', error); } } function applyProfileNumber(profileNumber, { fromBackend = false } = {}) { if (!profileNumber) { return; } document.getElementById('profileSelect').value = String(profileNumber); if (currentProfile === profileNumber) { if (!fromBackend) { pushProfileState(profileNumber); } return; } currentProfile = profileNumber; localStorage.setItem('profileNumber', currentProfile); if (!fromBackend) { pushProfileState(currentProfile); } renderPosts(); } // Load profile from localStorage function loadProfile() { fetchProfileState().then((backendProfile) => { if (backendProfile) { applyProfileNumber(backendProfile, { fromBackend: true }); } else { const saved = localStorage.getItem('profileNumber'); if (saved) { applyProfileNumber(parseInt(saved, 10) || 1, { fromBackend: true }); } else { applyProfileNumber(1, { fromBackend: true }); } } }); } // Save profile to localStorage function saveProfile(profileNumber) { applyProfileNumber(profileNumber); } function startProfilePolling() { if (profilePollTimer) { clearInterval(profilePollTimer); } profilePollTimer = setInterval(async () => { const backendProfile = await fetchProfileState(); if (backendProfile && backendProfile !== currentProfile) { applyProfileNumber(backendProfile, { fromBackend: true }); } }, 5000); } // Profile selector change handler document.getElementById('profileSelect').addEventListener('change', (e) => { saveProfile(parseInt(e.target.value, 10)); }); // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentTab = btn.dataset.tab; renderPosts(); }); }); // Fetch all posts async function fetchPosts() { try { showLoading(); const response = await fetch(`${API_URL}/posts`); if (!response.ok) { throw new Error('Failed to fetch posts'); } posts = await response.json(); renderPosts(); } catch (error) { showError('Fehler beim Laden der Beiträge. Stelle sicher, dass das Backend läuft.'); console.error('Error fetching posts:', error); } } // Render posts function renderPosts() { hideLoading(); hideError(); const container = document.getElementById('postsContainer'); const postItems = posts.map((post) => ({ post, status: computePostStatus(post) })); let filteredItems = postItems; if (currentTab === 'pending') { filteredItems = postItems.filter((item) => item.status.canCurrentProfileCheck && !item.status.isComplete); } if (filteredItems.length === 0) { container.innerHTML = `
🎉
${currentTab === 'pending' ? 'Keine offenen Beiträge!' : 'Noch keine Beiträge erfasst.'}
`; return; } container.innerHTML = filteredItems .map(({ post, status }) => createPostCard(post, status)) .join(''); filteredItems.forEach(({ post, status }) => { const card = document.getElementById(`post-${post.id}`); if (!card) { return; } const openBtn = card.querySelector('.btn-open'); if (openBtn) { openBtn.addEventListener('click', () => openPost(post.id)); } const editBtn = card.querySelector('.btn-edit-target'); if (editBtn) { editBtn.addEventListener('click', () => promptEditTarget(post.id, status.targetCount)); } const deleteBtn = card.querySelector('.btn-delete'); if (deleteBtn) { deleteBtn.addEventListener('click', () => deletePost(post.id)); } const screenshotEl = card.querySelector('.post-screenshot'); if (screenshotEl && screenshotEl.dataset.screenshot) { const url = screenshotEl.dataset.screenshot; const openHandler = () => openScreenshotModal(url); screenshotEl.addEventListener('click', openHandler); screenshotEl.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openScreenshotModal(url); } }); } }); } function openScreenshotModal(url) { if (!screenshotModal || !url) { return; } screenshotModalLastFocus = document.activeElement; screenshotModalImage.src = url; resetScreenshotZoom(); screenshotModalPreviousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; screenshotModal.removeAttribute('hidden'); screenshotModal.classList.add('open'); if (screenshotModalClose) { screenshotModalClose.focus(); } if (screenshotModalImage.complete) { applyScreenshotModalSize(); } else { const handleLoad = () => { applyScreenshotModalSize(); }; screenshotModalImage.addEventListener('load', handleLoad, { once: true }); } } function closeScreenshotModal() { if (!screenshotModal) { return; } if (!screenshotModal.classList.contains('open')) { return; } resetScreenshotZoom(); screenshotModal.classList.remove('open'); screenshotModal.setAttribute('hidden', ''); screenshotModalImage.src = ''; document.body.style.overflow = screenshotModalPreviousOverflow; if (screenshotModalLastFocus && typeof screenshotModalLastFocus.focus === 'function') { screenshotModalLastFocus.focus(); } } function resetScreenshotZoom() { screenshotModalZoomed = false; if (screenshotModalContent) { screenshotModalContent.classList.remove('zoomed'); screenshotModalContent.style.width = ''; screenshotModalContent.style.height = ''; screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' }); } if (screenshotModalImage) { screenshotModalImage.classList.remove('zoomed'); } applyScreenshotModalSize(); } function toggleScreenshotZoom() { if (!screenshotModalContent || !screenshotModalImage) { return; } screenshotModalZoomed = !screenshotModalZoomed; screenshotModalContent.classList.toggle('zoomed', screenshotModalZoomed); screenshotModalImage.classList.toggle('zoomed', screenshotModalZoomed); if (screenshotModalZoomed) { screenshotModalContent.style.width = 'min(95vw, 1300px)'; screenshotModalContent.style.height = '92vh'; screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' }); } else { screenshotModalContent.style.width = ''; screenshotModalContent.style.height = ''; applyScreenshotModalSize(); } } // Create post card HTML function createPostCard(post, status) { const createdDate = formatDateTime(post.created_at) || '—'; const resolvedScreenshotPath = post.screenshot_path ? (post.screenshot_path.startsWith('http') ? post.screenshot_path : `${API_URL.replace(/\/api$/, '')}${post.screenshot_path}`) : null; const screenshotHtml = resolvedScreenshotPath ? `
Screenshot zum Beitrag
` : ''; const profileRowsHtml = status.profileStatuses.map((profileStatus) => { const classes = ['profile-line', `profile-line--${profileStatus.status}`]; let label = 'Wartet'; if (profileStatus.status === 'done') { const doneDate = formatDateTime(profileStatus.checked_at); label = doneDate ? `Erledigt (${doneDate})` : 'Erledigt'; } else if (profileStatus.status === 'available') { label = 'Bereit'; } return `
${escapeHtml(profileStatus.profile_name)} ${escapeHtml(label)}
`; }).join(''); const infoMessages = []; if (!status.isCurrentProfileRequired) { infoMessages.push('Dieses Profil muss den Beitrag nicht bestätigen.'); } else if (status.isCurrentProfileDone) { infoMessages.push('Für dein Profil erledigt.'); } else if (status.waitingForNames.length) { infoMessages.push(`Wartet auf: ${status.waitingForNames.join(', ')}`); } const infoHtml = infoMessages.length ? `
${infoMessages.map((message) => `
${escapeHtml(message)}
`).join('')}
` : ''; const directLinkHtml = post.url ? `
Direktlink: ${escapeHtml(formatUrlForDisplay(post.url))}
` : ''; const openButtonHtml = status.canCurrentProfileCheck ? ` ` : ''; return `
${escapeHtml(post.title || '')}
${status.checkedCount}/${status.targetCount} ${status.isComplete ? '✓' : ''}
Benötigte Profile: ${status.targetCount}
${directLinkHtml}
${profileRowsHtml}
${infoHtml} ${screenshotHtml}
${openButtonHtml} ${post.url ? ` Direkt öffnen ` : ''}
`; } // Open post and auto-check async function openPost(postId) { const post = posts.find((item) => item.id === postId); if (!post) { alert('Beitrag konnte nicht gefunden werden.'); return; } if (!post.url) { alert('Für diesen Beitrag ist kein Direktlink vorhanden.'); return; } const status = computePostStatus(post); if (!status.isCurrentProfileRequired) { alert('Dieses Profil muss den Beitrag nicht bestätigen.'); return; } if (status.isCurrentProfileDone) { window.open(post.url, '_blank'); return; } if (!status.canCurrentProfileCheck) { if (status.waitingForNames.length) { alert(`Wartet auf: ${status.waitingForNames.join(', ')}`); } else { alert('Der Beitrag kann aktuell nicht abgehakt werden.'); } return; } try { const response = await fetch(`${API_URL}/check-by-url`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: post.url, profile_number: currentProfile }) }); if (!response.ok) { if (response.status === 409) { const data = await response.json().catch(() => null); if (data && data.error) { alert(data.error); return; } } throw new Error('Failed to check post'); } window.open(post.url, '_blank'); await fetchPosts(); } catch (error) { alert('Fehler beim Abhaken des Beitrags'); console.error('Error checking post:', error); } } async function promptEditTarget(postId, currentTarget) { const defaultValue = Number.isFinite(currentTarget) ? String(currentTarget) : ''; const newValue = prompt(`Neue Anzahl (1-${MAX_PROFILES}):`, defaultValue); if (newValue === null) { return; } const parsed = parseInt(newValue, 10); if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) { alert(`Bitte gib eine Zahl zwischen 1 und ${MAX_PROFILES} ein.`); return; } try { const response = await fetch(`${API_URL}/posts/${postId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target_count: parsed }) }); if (!response.ok) { const data = await response.json().catch(() => null); const message = data && data.error ? data.error : 'Fehler beim Aktualisieren der Anzahl.'; alert(message); return; } await fetchPosts(); } catch (error) { alert('Fehler beim Aktualisieren der Anzahl.'); console.error('Error updating target count:', error); } } // Delete post async function deletePost(postId) { if (!confirm('Beitrag wirklich löschen?')) { return; } try { const response = await fetch(`${API_URL}/posts/${postId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Failed to delete post'); } await fetchPosts(); } catch (error) { alert('Fehler beim Löschen des Beitrags'); console.error('Error deleting post:', error); } } // Utility functions function showLoading() { document.getElementById('loading').style.display = 'block'; document.getElementById('postsContainer').style.display = 'none'; } function hideLoading() { document.getElementById('loading').style.display = 'none'; document.getElementById('postsContainer').style.display = 'block'; } function showError(message) { const errorEl = document.getElementById('error'); errorEl.textContent = message; errorEl.style.display = 'block'; } function hideError() { document.getElementById('error').style.display = 'none'; } function escapeHtml(unsafe) { if (unsafe === null || unsafe === undefined) { unsafe = ''; } if (typeof unsafe !== 'string') { unsafe = String(unsafe); } return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Auto-check on page load if URL parameter is present function checkAutoCheck() { const urlParams = new URLSearchParams(window.location.search); const autoCheckUrl = urlParams.get('check'); if (autoCheckUrl) { // Try to check this URL automatically fetch(`${API_URL}/check-by-url`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: decodeURIComponent(autoCheckUrl), profile_number: currentProfile }) }).then(() => { // Remove the parameter from URL window.history.replaceState({}, document.title, window.location.pathname); fetchPosts(); }).catch(console.error); } } if (screenshotModalClose) { screenshotModalClose.addEventListener('click', closeScreenshotModal); } if (screenshotModalBackdrop) { screenshotModalBackdrop.addEventListener('click', closeScreenshotModal); } if (screenshotModalImage) { screenshotModalImage.addEventListener('click', (event) => { event.stopPropagation(); toggleScreenshotZoom(); }); screenshotModalImage.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleScreenshotZoom(); } }); screenshotModalImage.setAttribute('tabindex', '0'); screenshotModalImage.setAttribute('role', 'button'); screenshotModalImage.setAttribute('aria-label', 'Screenshot vergrößern oder verkleinern'); } document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { if (screenshotModalZoomed) { resetScreenshotZoom(); return; } closeScreenshotModal(); } }); window.addEventListener('resize', () => { if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) { applyScreenshotModalSize(); } }); // Initialize loadProfile(); startProfilePolling(); fetchPosts(); checkAutoCheck(); // Auto-refresh every 30 seconds setInterval(fetchPosts, 30000);