// Netz-Verbindungsüberwachung & PWA Registrierung if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js').catch(err => { console.warn('ServiceWorker Info:', err.message); }); }); } window.addEventListener('online', handleNetworkStatus); window.addEventListener('offline', handleNetworkStatus); function handleNetworkStatus() { const offlineBar = document.getElementById('offline-status-bar'); if (!navigator.onLine) { if (offlineBar) offlineBar.classList.remove('hidden'); } else { if (offlineBar) offlineBar.classList.add('hidden'); syncOfflineQueue(); } } function queueOfflineAction(action) { const queue = JSON.parse(localStorage.getItem('mainpark_offline_queue') || '[]'); queue.push(action); localStorage.setItem('mainpark_offline_queue', JSON.stringify(queue)); } async function syncOfflineQueue() { const queue = JSON.parse(localStorage.getItem('mainpark_offline_queue') || '[]'); if (queue.length === 0) return; for (const item of queue) { try { if (item.type === 'toggleTask') { await supabaseClient.from('tasks').update({ is_completed: item.isCompleted }).eq('id', item.taskId); } else if (item.type === 'addTask') { delete item.taskData.id; delete item.taskData.is_offline_pending; await supabaseClient.from('tasks').insert([item.taskData]); } else if (item.type === 'updateStatus') { await supabaseClient.from('rooms').update({ status: item.status }).eq('id', item.roomId); } else if (item.type === 'updateNotes') { await supabaseClient.from('rooms').update({ notes: item.notes }).eq('id', item.roomId); } } catch (e) { console.error('Fehler beim Nach-Synchronisieren:', e); } } localStorage.removeItem('mainpark_offline_queue'); loadRooms(); } // Dynamische Changelog-Steuerung async function fetchLatestChangelog() { if (!navigator.onLine) return; const { data } = await supabaseClient .from('activity_logs') .select('*') .eq('user_alias', 'CHANGELOG_UPDATE') .order('created_at', { ascending: false }); const footerBtn = document.getElementById('footer-changelog-btn'); const profileLink = document.getElementById('profile-changelog-link'); if (data && data.length > 0) { window.publishedChangelogs = data; if (footerBtn) footerBtn.classList.remove('hidden'); if (profileLink) profileLink.classList.remove('hidden'); const latest = data[0]; const versionLabel = document.getElementById('footer-version-label'); if (versionLabel) versionLabel.textContent = `v${latest.room_name}`; } else { window.publishedChangelogs = []; if (footerBtn) footerBtn.classList.add('hidden'); if (profileLink) profileLink.classList.add('hidden'); } } function openChangelogModal() { const list = document.getElementById('changelog-modal-list'); list.innerHTML = ''; const changelogs = window.publishedChangelogs || []; if (changelogs.length === 0) { list.innerHTML = '
Kein veröffentlichter Changelog vorhanden.
'; } else { changelogs.forEach(item => { const dateStr = new Date(item.created_at).toLocaleDateString('de-DE'); const card = document.createElement('div'); card.className = "bg-slate-900 border border-slate-700/80 p-4 rounded-xl space-y-2 text-xs"; card.innerHTML = `Noch keine Auszahlungen erhalten.
'; } else { paidHistoryList.forEach(item => { const row = document.createElement('div'); row.className = "flex items-center justify-between text-xs bg-slate-900 border border-slate-700/60 p-2.5 rounded-lg"; row.innerHTML = `Aktuell liegt kein offener Materialbedarf vor. Alle Materialien stehen bereit!
'; } else { materialRooms.forEach(r => { const statusBadge = r.material_status === 'Offen' ? 'Offen / Benötigt' : 'Bestellt'; const item = document.createElement('div'); item.className = "bg-slate-900 border border-slate-700/80 rounded-xl p-4 space-y-2"; item.innerHTML = `${r.notes ? r.notes : 'Keine Notizen/Details eingetragen'}
`; container.appendChild(item); }); } document.getElementById('shopping-list-modal').classList.remove('hidden'); } function closeShoppingListModal() { document.getElementById('shopping-list-modal').classList.add('hidden'); } function checkNotifications() { const notifications = []; const userTag = `@${currentUserAlias.toLowerCase()}`; allRoomsData.forEach(room => { if (room.notes && room.notes.toLowerCase().includes(userTag)) { notifications.push({ type: 'mention', title: `Erwähnung in ${room.room_number}`, text: room.notes, room: room }); } if (room.tasks) { room.tasks.forEach(t => { if (t.assigned_to === currentUser.id && !t.is_completed) { notifications.push({ type: 'task', title: `Offene Aufgabe in ${room.room_number}`, text: t.task_name, room: room }); } }); } }); const badge = document.getElementById('notification-badge'); if (notifications.length > 0) { badge.textContent = notifications.length; badge.classList.remove('hidden'); } else { badge.classList.add('hidden'); } window.currentNotifications = notifications; } function toggleNotificationsModal() { const modal = document.getElementById('notifications-modal'); const list = document.getElementById('notifications-list'); if (modal.classList.contains('hidden')) { list.innerHTML = ''; const notifications = window.currentNotifications || []; if (notifications.length === 0) { list.innerHTML = 'Keine neuen Benachrichtigungen vorhanden.
'; } else { notifications.forEach(n => { const item = document.createElement('div'); item.className = "bg-slate-900 border border-slate-700/80 p-3 rounded-lg cursor-pointer hover:border-indigo-500 transition"; item.onclick = () => { toggleNotificationsModal(); openModal(n.room); }; item.innerHTML = `${n.text}
`; list.appendChild(item); }); } modal.classList.remove('hidden'); } else { modal.classList.add('hidden'); } } async function logActivity(room, actionText) { const roomName = room ? room.room_number : 'System'; if (room) { if (!room.history) room.history = []; const timestampStr = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); room.history.unshift(`[${timestampStr}] ${currentUserAlias}: ${actionText}`); renderRoomHistory(room); } if (!navigator.onLine) return; const { error } = await supabaseClient .from('activity_logs') .insert([{ user_alias: currentUserAlias, room_name: roomName, action_text: actionText }]); if (!error) { fetchGlobalActivityLog(); } } async function fetchGlobalActivityLog() { if (!navigator.onLine) return; const { data, error } = await supabaseClient .from('activity_logs') .select('*') .order('created_at', { ascending: false }) .limit(50); if (error) return; renderGlobalActivityLog(data || []); } function renderGlobalActivityLog(logs) { const feed = document.getElementById('global-activity-feed'); const modalList = document.getElementById('modal-global-log-list'); feed.innerHTML = ''; modalList.innerHTML = ''; const filteredLogs = logs.filter(item => item.user_alias !== 'ANNOUNCEMENT' && item.user_alias !== 'CHANGELOG_UPDATE'); if (filteredLogs.length === 0) { const emptyState = 'Noch keine Aktivitäten im gesamten System aufgezeichnet.
'; feed.innerHTML = emptyState; modalList.innerHTML = emptyState; return; } filteredLogs.forEach(item => { const logTime = new Date(item.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const logDate = new Date(item.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const isUndoable = isAdmin() && !item.action_text.includes('[ADMIN UNDO]'); const undoBtnHTML = isUndoable ? `` : ''; const row = document.createElement('div'); row.className = "bg-slate-900/80 border border-slate-800 p-2.5 rounded-lg flex items-center justify-between gap-3"; row.innerHTML = `Noch keine Aktivitäten aufgezeichnet.
'; return; } room.history.forEach(h => { const item = document.createElement('div'); item.className = "text-slate-400 border-b border-slate-800 pb-1 last:border-0"; item.textContent = h; list.appendChild(item); }); } function renderMyTasks() { const openContainer = document.getElementById('my-open-tasks-list'); const completedContainer = document.getElementById('my-completed-tasks-list'); openContainer.innerHTML = ''; completedContainer.innerHTML = ''; let openCount = 0; let completedCount = 0; allRoomsData.forEach(room => { if (room.tasks) { room.tasks.forEach(task => { const isForMe = task.assigned_to === currentUser.id; const isForAll = !task.assigned_to; if (isForMe || isForAll) { const taskEl = document.createElement('div'); taskEl.className = "flex items-center justify-between bg-slate-900 border border-slate-700/80 p-3 rounded-lg text-xs"; const typeBadge = isForAll ? '👥 Alle' : `👤 ${currentUserAlias}`; const rewardBadge = parseFloat(task.reward_amount || 0) > 0 ? `+${parseFloat(task.reward_amount).toFixed(2)} €` : ''; let approvalStatusBadge = ''; if (task.is_completed && parseFloat(task.reward_amount || 0) > 0) { if (!task.is_approved) { approvalStatusBadge = '🔍 Wartet auf Admin-Prüfung'; } else if (task.is_paid) { approvalStatusBadge = '✅ Ausbezahlt'; } else { approvalStatusBadge = '💰 Bestätigt (Guthaben)'; } } if (task.is_completed) { completedCount++; taskEl.innerHTML = ` ${room.room_number} `; completedContainer.appendChild(taskEl); } else { openCount++; taskEl.innerHTML = ` ${room.room_number} (${room.floor}) `; openContainer.appendChild(taskEl); } } }); } }); if (openCount === 0) { openContainer.innerHTML = 'Keine offenen Aufgaben vorhanden.
'; } if (completedCount === 0) { completedContainer.innerHTML = 'Noch keine erledigten Aufgaben.
'; } } async function loadRooms() { if (navigator.onLine) { const { data, error } = await supabaseClient .from('rooms') .select('*, tasks(*)') .order('position', { ascending: true }); if (!error && data) { allRoomsData = data; localStorage.setItem('mainpark_rooms_cache', JSON.stringify(data)); } } else { const cached = localStorage.getItem('mainpark_rooms_cache'); if (cached) { allRoomsData = JSON.parse(cached); } } document.getElementById('room-count-badge').textContent = `${allRoomsData.length} Zimmer`; updateOverallProgress(); calculateUserEarnedAmounts(); checkNotifications(); fetchGlobalActivityLog(); applyFilterAndSort(); } function updateOverallProgress() { let totalTasks = 0; let completedTasks = 0; allRoomsData.forEach(room => { if (room.tasks) { totalTasks += room.tasks.length; completedTasks += room.tasks.filter(t => t.is_completed).length; } }); const overallPct = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; document.getElementById('overall-progress-text').textContent = `${overallPct}% (${completedTasks}/${totalTasks} Aufgaben)`; document.getElementById('overall-progress-bar').style.width = `${overallPct}%`; } function applyFilterAndSort() { let result = [...allRoomsData]; if (currentFilterStatus === 'my-rooms') { result = result.filter(r => r.tasks && r.tasks.some(t => (!t.assigned_to || t.assigned_to === currentUser.id) && !t.is_completed)); } else if (currentFilterStatus !== 'all') { result = result.filter(r => r.status === currentFilterStatus); } const sortBy = document.getElementById('sort-select').value; const sortFn = (a, b) => { if (sortBy === 'manual') return (a.position || 0) - (b.position || 0); if (sortBy === 'name-asc') return a.room_number.localeCompare(b.room_number, undefined, { numeric: true, sensitivity: 'base' }); if (sortBy === 'name-desc') return b.room_number.localeCompare(a.room_number, undefined, { numeric: true, sensitivity: 'base' }); if (sortBy === 'status') { const order = { 'Geplant': 1, 'In Arbeit': 2, 'Fertig': 3 }; return (order[a.status] || 0) - (order[b.status] || 0); } if (sortBy === 'progress-desc') return getRoomProgress(b) - getRoomProgress(a); if (sortBy === 'progress-asc') return getRoomProgress(a) - getRoomProgress(b); return 0; }; const pinned = result.filter(r => r.is_pinned).sort(sortFn); const unpinned = result.filter(r => !r.is_pinned).sort(sortFn); result = [...pinned, ...unpinned]; renderRooms(result); } function setFilter(status, btnElement) { currentFilterStatus = status; document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.remove('bg-indigo-600', 'text-white'); btn.classList.add('text-slate-300'); }); btnElement.classList.remove('text-slate-300'); btnElement.classList.add('bg-indigo-600', 'text-white'); applyFilterAndSort(); } async function togglePin(roomId, pinStatus) { if (navigator.onLine) { await supabaseClient.from('rooms').update({ is_pinned: pinStatus }).eq('id', roomId); } const room = allRoomsData.find(r => r.id === roomId); if (room) { room.is_pinned = pinStatus; logActivity(room, pinStatus ? 'hat Zimmer angepinnt' : 'hat Anpinnung aufgehoben'); } localStorage.setItem('mainpark_rooms_cache', JSON.stringify(allRoomsData)); applyFilterAndSort(); } async function saveNewOrder() { const grid = document.getElementById('rooms-grid'); const cards = Array.from(grid.children); const updates = cards.map((card, index) => { const roomId = parseInt(card.dataset.id); const room = allRoomsData.find(r => r.id === roomId); if (room) room.position = index + 1; if (navigator.onLine) { return supabaseClient .from('rooms') .update({ position: index + 1 }) .eq('id', roomId); } }); if (navigator.onLine) { await Promise.all(updates); } localStorage.setItem('mainpark_rooms_cache', JSON.stringify(allRoomsData)); logActivity(null, 'hat Reihung/Sortierung der Zimmer per Drag & Drop geändert'); } function openAddRoomModal() { document.getElementById('new-room-name').value = ''; document.getElementById('add-room-modal').classList.remove('hidden'); } function closeAddRoomModal() { document.getElementById('add-room-modal').classList.add('hidden'); } async function createNewRoom() { const nameInput = document.getElementById('new-room-name'); const floorSelect = document.getElementById('new-room-floor'); const roomName = nameInput.value.trim(); const floor = floorSelect.value; if (!roomName) return; const nextPos = allRoomsData.length + 1; if (navigator.onLine) { const { data, error } = await supabaseClient .from('rooms') .insert([{ room_number: roomName, floor: floor, status: 'Geplant', position: nextPos, is_pinned: false }]) .select(); if (!error && data && data.length > 0) { closeAddRoomModal(); const newRoom = data[0]; logActivity(newRoom, `hat das Zimmer neu angelegt`); loadRooms(); } } else { alert('Neuanlegen von Räumen erfordert eine aktive Internetverbindung.'); } } async function reassignTask(taskId, newAssigneeId) { const assigneeValue = (newAssigneeId && newAssigneeId.trim() !== '') ? newAssigneeId : null; if (navigator.onLine) { await supabaseClient.from('tasks').update({ assigned_to: assigneeValue }).eq('id', taskId); } let assignedPersonName = '👥 Alle (Team)'; if (assigneeValue) { const foundProf = allProfiles.find(p => p.id === assigneeValue); if (foundProf && foundProf.alias) { assignedPersonName = `👤 ${foundProf.alias}`; } } allRoomsData.forEach(r => { if (r.tasks) { const task = r.tasks.find(t => t.id === taskId); if (task) { task.assigned_to = assigneeValue; logActivity(r, `hat Aufgabe "${task.task_name}" neu zugewiesen an ${assignedPersonName}`); } } }); localStorage.setItem('mainpark_rooms_cache', JSON.stringify(allRoomsData)); calculateUserEarnedAmounts(); renderMyTasks(); checkNotifications(); } function closeModal() { document.getElementById('room-modal').classList.add('hidden'); loadRooms(); } async function deleteCurrentRoom() { if (!isAdmin()) { alert('Nur der Administrator darf Zimmer löschen.'); return; } const room = allRoomsData.find(r => r.id === currentRoomId); if (!room) return; const confirmDelete = confirm(`Möchten Sie "${room.room_number}" wirklich samt allen Aufgaben und Notizen löschen?`); if (!confirmDelete) return; if (navigator.onLine) { const { error } = await supabaseClient.from('rooms').delete().eq('id', currentRoomId); if (!error) { closeModal(); loadRooms(); } } else { alert('Löschen von Räumen ist nur im Online-Modus verfügbar.'); } } async function updateRoomDetails() { const newName = document.getElementById('modal-room-number').value.trim(); if (!newName) return; const room = allRoomsData.find(r => r.id === currentRoomId); if (!room) return; const oldName = room.room_number; if (navigator.onLine) { await supabaseClient.from('rooms').update({ room_number: newName }).eq('id', currentRoomId); } room.room_number = newName; document.getElementById('modal-title').textContent = `${newName} (${room.floor})`; logActivity(room, `hat das Zimmer von "${oldName}" in "${newName}" umbenannt`); localStorage.setItem('mainpark_rooms_cache', JSON.stringify(allRoomsData)); applyFilterAndSort(); } // Automatische Vergütung aus dem Katalog oder bestehenden Aufgaben beim Tippen ermitteln document.getElementById('new-task-input')?.addEventListener('input', function(e) { const inputVal = e.target.value.trim().toLowerCase(); const rewardInput = document.getElementById('new-task-reward'); if (!rewardInput || !inputVal) return; // 1. Suche im globalen Admin-Katalog if (window.taskCatalog && window.taskCatalog[inputVal] !== undefined) { rewardInput.value = window.taskCatalog[inputVal]; } else { // 2. Fallback: Suche in allen bereits existierenden Aufgaben im System let foundReward = null; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.task_name && t.task_name.trim().toLowerCase() === inputVal && parseFloat(t.reward_amount || 0) > 0) { foundReward = parseFloat(t.reward_amount); } }); } }); if (foundReward !== null) { rewardInput.value = foundReward; } } }); // NEUE AUFGABE ANLEGEN async function addTask() { const input = document.getElementById('new-task-input'); const assigneeSelect = document.getElementById('new-task-assignee'); const rewardInput = document.getElementById('new-task-reward'); if (!input || !input.value.trim()) { alert('Bitte gib einen Namen für die Aufgabe ein.'); if (input) input.focus(); return; } const taskName = input.value.trim(); const taskNameLower = taskName.toLowerCase(); const assignedTo = (assigneeSelect && assigneeSelect.value.trim() !== '') ? assigneeSelect.value : null; let rewardAmount = 0; // 1. Manuell eingegebenen Betrag bevorzugen if (rewardInput && rewardInput.value.trim() !== '') { const parsed = parseFloat(rewardInput.value.replace(',', '.')); rewardAmount = isNaN(parsed) ? 0 : parsed; } // 2. Falls leer: Automatisch aus dem Admin-Katalog ziehen else if (window.taskCatalog && window.taskCatalog[taskNameLower] !== undefined) { rewardAmount = window.taskCatalog[taskNameLower]; } // 3. Falls nicht im Katalog: Aus existierenden Aufgaben ermitteln else { allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.task_name && t.task_name.trim().toLowerCase() === taskNameLower && parseFloat(t.reward_amount || 0) > 0) { rewardAmount = parseFloat(t.reward_amount); } }); } }); } const newTaskData = { room_id: currentRoomId, task_name: taskName, is_completed: false, assigned_to: assignedTo, reward_amount: rewardAmount }; if (navigator.onLine) { const { data, error } = await supabaseClient.from('tasks').insert([newTaskData]).select(); if (!error && data && data.length > 0) { input.value = ''; if (rewardInput) rewardInput.value = ''; if (assigneeSelect) assigneeSelect.selectedIndex = 0; const room = allRoomsData.find(r => r.id === currentRoomId); if (room) { if (!room.tasks) room.tasks = []; room.tasks.push(data[0]); logActivity(room, `hat Aufgabe "${taskName}" hinzugefügt (${rewardAmount > 0 ? rewardAmount.toFixed(2) + ' €' : 'Keine Vergütung'})`); } localStorage.setItem('mainpark_rooms_cache', JSON.stringify(allRoomsData)); renderModalTasks(room ? room.tasks : []); renderMyTasks(); updateOverallProgress(); } else { console.error('Fehler beim Einfügen der Aufgabe:', error); alert(`Aufgabe konnte nicht gespeichert werden.\n\nFehlerursache: ${error ? error.message : 'Datenbankzugriff verweigert'}`); } } else { newTaskData.id = 'offline_' + Date.now(); newTaskData.is_offline_pending = true; queueOfflineAction({ type: 'addTask', taskData: newTaskData }); const room = allRoomsData.find(r => r.id === currentRoomId); if (room) { if (!room.tasks) room.tasks = []; room.tasks.push(newTaskData); renderModalTasks(room.tasks); } input.value = ''; if (rewardInput) rewardInput.value = ''; renderMyTasks(); } } function renderModalTasks(tasks) { const container = document.getElementById('modal-tasks-list'); container.innerHTML = ''; if (!tasks || tasks.length === 0) { container.innerHTML = 'Keine Aufgaben vorhanden.
'; return; } tasks.forEach(t => { const div = document.createElement('div'); div.className = "flex items-center justify-between bg-slate-900 border border-slate-700/80 p-2.5 rounded-lg text-xs gap-2 flex-wrap"; const offlineBadge = t.is_offline_pending ? '📡 Offline' : ''; let priorityBadge = ''; if (t.priority === 'high') { priorityBadge = '⚠️ Dringend'; } else if (t.priority === 'low') { priorityBadge = 'Niedrig'; } const dueDateBadge = t.due_date ? `📅 ${new Date(t.due_date).toLocaleDateString('de-DE')}` : ''; const rewardAmount = parseFloat(t.reward_amount || 0); const rewardBadge = rewardAmount > 0 ? `+${rewardAmount.toFixed(2)} €` : ''; const paidAmount = parseFloat(t.paid_amount || 0); let splitBadge = ''; if (paidAmount > 0 && !t.is_paid) { splitBadge = `Teilbezahlt: ${paidAmount.toFixed(2)} / ${rewardAmount.toFixed(2)} €`; } else if (t.is_paid) { splitBadge = `Bezahlt (${paidAmount.toFixed(2)} €)`; } const isUnassigned = !t.assigned_to; let assigneeOptions = ``; allProfiles.forEach(p => { if (p.alias) { const selected = (t.assigned_to === p.id) ? 'selected' : ''; assigneeOptions += ``; } }); div.innerHTML = `