function switchAdminTab(tabName) { ['users', 'payouts', 'actions', 'stats', 'banner', 'changelog'].forEach(t => { const tabEl = document.getElementById(`admin-tab-${t}`); const btnEl = document.getElementById(`admin-tab-${t}-btn`); if (tabEl) tabEl.classList.add('hidden'); if (btnEl) { btnEl.classList.remove('bg-indigo-600', 'text-white'); btnEl.classList.add('text-slate-400'); } }); const currentTab = document.getElementById(`admin-tab-${tabName}`); const currentBtn = document.getElementById(`admin-tab-${tabName}-btn`); if (currentTab) currentTab.classList.remove('hidden'); if (currentBtn) { currentBtn.classList.add('bg-indigo-600', 'text-white'); currentBtn.classList.remove('text-slate-400'); } if (tabName === 'payouts') { loadAdminApprovalTasks(); loadAdminTaskCatalog(); loadAdminPayouts(); } if (tabName === 'stats') loadAdminStats(); if (tabName === 'changelog') loadAdminChangelogView(); } async function undoActivityLog(logId, roomName, actionText) { if (!isAdmin()) return; if (!confirm(`Möchtest du diese Aktion wirklich rückgängig machen?\n\n"${actionText}"`)) return; if (actionText.includes('als erledigt') || actionText.includes('erledigt')) { const room = allRoomsData.find(r => r.room_number === roomName); if (room && room.tasks) { const match = actionText.match(/"([^"]+)"/); const taskName = match ? match[1] : null; const task = room.tasks.find(t => taskName ? t.task_name === taskName : t.is_completed); if (task) { await supabaseClient.from('tasks').update({ is_completed: false, is_approved: false, is_paid: false, paid_amount: 0, paid_at: null }).eq('id', task.id); task.is_completed = false; task.is_approved = false; task.is_paid = false; task.paid_amount = 0; task.paid_at = null; } } } else if (actionText.includes('ausbezahlt')) { const match = actionText.match(/an ([^ ]+)/); if (match) { const alias = match[1]; const profile = allProfiles.find(p => p.alias === alias); if (profile) { const paidTasks = []; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.assigned_to === profile.id && (t.is_paid || (t.paid_amount && t.paid_amount > 0))) { paidTasks.push(t.id); } }); } }); if (paidTasks.length > 0) { await supabaseClient.from('tasks').update({ is_paid: false, paid_amount: 0, paid_at: null }).in('id', paidTasks); allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (paidTasks.includes(t.id)) { t.is_paid = false; t.paid_amount = 0; t.paid_at = null; } }); } }); } } } } await supabaseClient.from('activity_logs').delete().eq('id', logId); logActivity(null, `[ADMIN UNDO] hat die Aktion "${actionText}" rückgängig gemacht`); loadRooms(); loadAdminPayouts(); loadAdminApprovalTasks(); fetchGlobalActivityLog(); alert('Aktion wurde erfolgreich rückgängig gemacht!'); } async function publishChangelog() { if (!isAdmin()) return; const version = document.getElementById('admin-changelog-version').value.trim(); const content = document.getElementById('admin-changelog-content').value.trim(); if (!version || !content) { alert('Bitte gib eine Versionsnummer und den Inhalt des Changelogs ein.'); return; } const { error } = await supabaseClient.from('activity_logs').insert([{ user_alias: 'CHANGELOG_UPDATE', room_name: version, action_text: content }]); if (!error) { alert(`Changelog für Version ${version} wurde erfolgreich veröffentlicht!`); document.getElementById('admin-changelog-version').value = ''; document.getElementById('admin-changelog-content').value = ''; fetchLatestChangelog(); loadAdminChangelogView(); } else { alert('Fehler beim Veröffentlichen des Changelogs.'); } } async function loadAdminChangelogView() { const container = document.getElementById('admin-changelog-history'); if (!container) return; container.innerHTML = '

Lade Changelogs...

'; const { data } = await supabaseClient .from('activity_logs') .select('*') .eq('user_alias', 'CHANGELOG_UPDATE') .order('created_at', { ascending: false }); if (!data || data.length === 0) { container.innerHTML = '

Noch keine Changelogs veröffentlicht.

'; return; } container.innerHTML = ''; data.forEach(item => { const dateStr = new Date(item.created_at).toLocaleDateString('de-DE'); const div = document.createElement('div'); div.className = "bg-slate-900 border border-slate-700/80 p-3 rounded-xl space-y-1 text-xs"; div.innerHTML = `
Version ${item.room_name} ${dateStr}
${item.action_text}
`; container.appendChild(div); }); } async function openAdminUsersModal() { if (!isAdmin()) return; switchAdminTab('users'); document.getElementById('admin-users-modal').classList.remove('hidden'); const tbody = document.getElementById('admin-users-table-body'); tbody.innerHTML = 'Nutzer werden geladen...'; const { data: profiles, error } = await supabaseClient.from('profiles').select('*'); if (error || !profiles) { tbody.innerHTML = 'Fehler beim Laden der Nutzerliste.'; return; } tbody.innerHTML = ''; const now = new Date(); profiles.forEach(p => { const tr = document.createElement('tr'); tr.className = "hover:bg-slate-800/50 transition"; const lastSeenDate = p.last_seen ? new Date(p.last_seen) : null; const isOnline = lastSeenDate && (now - lastSeenDate < 2 * 60 * 1000); const statusBadge = isOnline ? ' Online' : 'Offline'; const lastSeenText = lastSeenDate ? lastSeenDate.toLocaleDateString('de-DE') + ' ' + lastSeenDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : 'Nie'; const isThisAdmin = (p.id === currentUser.id) || (p.email && p.email.toLowerCase() === ADMIN_EMAIL.toLowerCase()); let actionBtn = ''; if (!isThisAdmin) { if (p.is_disabled) { actionBtn = ``; } else { actionBtn = ``; } } else { actionBtn = 'Admin'; } tr.innerHTML = ` ${statusBadge} ${p.alias || 'Kein Name'} ${p.email || 'Registrierter Nutzer'} ${lastSeenText} ${actionBtn} `; tbody.appendChild(tr); }); } function loadAdminApprovalTasks() { const container = document.getElementById('admin-approval-tasks-container'); container.innerHTML = ''; const pendingTasks = []; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.is_completed && !t.is_approved) { let assignedName = "👥 Alle / Nicht zugewiesen"; if (t.assigned_to) { const prof = allProfiles.find(p => p.id === t.assigned_to); if (prof && prof.alias) assignedName = `👤 ${prof.alias}`; } pendingTasks.push({ ...t, roomNumber: r.room_number, assignedName }); } }); } }); if (pendingTasks.length === 0) { container.innerHTML = '

Keine unüberprüften Aufgaben vorhanden. Alle erledigten Aufgaben sind freigegeben!

'; return; } pendingTasks.forEach(t => { const item = document.createElement('div'); item.className = "flex items-center justify-between bg-slate-800 border border-amber-500/30 p-2.5 rounded-lg text-xs gap-2"; const amount = parseFloat(t.reward_amount || 0); item.innerHTML = `
${t.task_name} ${t.roomNumber}
Erledigt von: ${t.assignedName}
${amount.toFixed(2)} €
`; container.appendChild(item); }); } async function approveTask(taskId, isApproved) { if (!isAdmin()) return; const payload = isApproved ? { is_approved: true } : { is_approved: false, is_completed: false }; const { error } = await supabaseClient.from('tasks').update(payload).eq('id', taskId); if (!error) { allRoomsData.forEach(r => { if (r.tasks) { const t = r.tasks.find(x => x.id === taskId); if (t) { t.is_approved = isApproved; if (!isApproved) t.is_completed = false; } } }); loadAdminApprovalTasks(); loadAdminPayouts(); calculateUserEarnedAmounts(); renderMyTasks(); loadRooms(); } else { alert('Fehler bei der Aufgaben-Freigabe.'); } } function loadAdminTaskCatalog() { const catalogList = document.getElementById('admin-task-catalog-list'); catalogList.innerHTML = ''; const uniqueTaskMap = {}; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { const nameKey = t.task_name.trim(); if (!uniqueTaskMap[nameKey]) { uniqueTaskMap[nameKey] = { name: nameKey, currentReward: parseFloat(t.reward_amount || 0), count: 1 }; } else { uniqueTaskMap[nameKey].count++; if (t.reward_amount > uniqueTaskMap[nameKey].currentReward) { uniqueTaskMap[nameKey].currentReward = parseFloat(t.reward_amount); } } }); } }); const taskNames = Object.keys(uniqueTaskMap).sort(); if (taskNames.length === 0) { catalogList.innerHTML = '

Noch keine Aufgaben im System vorhanden.

'; return; } taskNames.forEach(nameKey => { const item = uniqueTaskMap[nameKey]; const row = document.createElement('div'); row.className = "flex items-center justify-between bg-slate-800 border border-slate-700 p-2.5 rounded-lg text-xs"; row.innerHTML = `
${item.name} (${item.count}x in Zimmern)
€
`; catalogList.appendChild(row); }); } async function applyCatalogPrice(taskName) { if (!isAdmin()) return; const input = document.getElementById(`catalog-input-${encodeURIComponent(taskName)}`); if (!input) return; const newAmount = parseFloat(input.value) || 0; const targetIds = []; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.task_name.trim() === taskName.trim()) { targetIds.push(t.id); } }); } }); if (targetIds.length === 0) return; const { error } = await supabaseClient .from('tasks') .update({ reward_amount: newAmount }) .in('id', targetIds); if (!error) { allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (targetIds.includes(t.id)) { t.reward_amount = newAmount; } }); } }); alert(`Preis von ${newAmount.toFixed(2)} € für alle "${taskName}"-Aufgaben angewendet!`); logActivity(null, `hat Vergütung für alle "${taskName}"-Aufgaben auf ${newAmount.toFixed(2)} € festgelegt`); loadAdminTaskCatalog(); loadAdminPayouts(); calculateUserEarnedAmounts(); renderMyTasks(); } else { alert('Fehler beim Aktualisieren der Vergütung.'); } } function loadAdminPayouts() { const container = document.getElementById('admin-payouts-container'); container.innerHTML = ''; const userMap = {}; allProfiles.forEach(p => { userMap[p.id] = { alias: p.alias || 'Kein Name', unpaidTasks: [], paidTasks: [], unpaidTotal: 0, paidTotal: 0 }; }); allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.is_completed && t.is_approved && t.assigned_to && userMap[t.assigned_to]) { const fullReward = parseFloat(t.reward_amount || 0); const alreadyPaid = parseFloat(t.paid_amount || 0); const openForTask = fullReward - alreadyPaid; userMap[t.assigned_to].paidTotal += alreadyPaid; if (!t.is_paid && openForTask > 0) { userMap[t.assigned_to].unpaidTasks.push({ ...t, roomNumber: r.room_number, openForTask }); userMap[t.assigned_to].unpaidTotal += openForTask; } else if (t.is_paid) { userMap[t.assigned_to].paidTasks.push({ ...t, roomNumber: r.room_number }); } } }); } }); Object.keys(userMap).forEach(uid => { const u = userMap[uid]; const card = document.createElement('div'); card.className = "bg-slate-900 border border-slate-700/80 rounded-xl p-4 space-y-3"; let unpaidTasksListHTML = ''; if (u.unpaidTasks.length > 0) { unpaidTasksListHTML = u.unpaidTasks.map(t => `
${t.task_name} (${t.roomNumber})
${t.openForTask.toFixed(2)} € offen
`).join(''); } else { unpaidTasksListHTML = '

Keine offenen Prämien vorhanden.

'; } card.innerHTML = `

👤 ${u.alias}

Offen: ${u.unpaidTotal.toFixed(2)} € Bereits ausgezahlt: ${u.paidTotal.toFixed(2)} €
${u.unpaidTotal > 0 ? ` ` : ''}
Offene Prämien-Aufgaben: ${unpaidTasksListHTML}
`; container.appendChild(card); }); } // Teilzahlungs-Split per Aufgabe (Restbeträge werden exakt als 'partially_paid' verwaltet) async function payoutCustomAmount(userId, userAlias, currentMaxUnpaid) { if (!isAdmin()) return; const inputVal = prompt(`Wie viel Euro möchtest du an ${userAlias} auszahlen? (Maximal offen: ${currentMaxUnpaid.toFixed(2)} €):`, currentMaxUnpaid.toFixed(2)); if (!inputVal) return; const amountToPay = parseFloat(inputVal.replace(',', '.')); if (isNaN(amountToPay) || amountToPay <= 0) { alert('Ungültiger Betrag.'); return; } const note = prompt('Optionales Kommentar zur Auszahlung (z. B. "Bar ausbezahlt"):', '') || ''; let remaining = amountToPay; const eligibleTasks = []; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.is_completed && t.is_approved && t.assigned_to === userId && !t.is_paid) { const fullReward = parseFloat(t.reward_amount || 0); const alreadyPaid = parseFloat(t.paid_amount || 0); const openForTask = fullReward - alreadyPaid; if (openForTask > 0) { eligibleTasks.push({ task: t, openForTask }); } } }); } }); for (const item of eligibleTasks) { if (remaining <= 0) break; const t = item.task; const currentPaid = parseFloat(t.paid_amount || 0); if (remaining >= item.openForTask) { const newPaid = currentPaid + item.openForTask; remaining -= item.openForTask; await supabaseClient.from('tasks').update({ is_paid: true, paid_amount: newPaid, paid_at: new Date().toISOString() }).eq('id', t.id); t.is_paid = true; t.paid_amount = newPaid; t.paid_at = new Date().toISOString(); } else { const newPaid = currentPaid + remaining; remaining = 0; await supabaseClient.from('tasks').update({ is_paid: false, paid_amount: newPaid }).eq('id', t.id); t.is_paid = false; t.paid_amount = newPaid; } } await logActivity(null, `hat ${amountToPay.toFixed(2)} € an ${userAlias} ausbezahlt${note ? ` (${note})` : ''}`); alert(`Auszahlung von ${amountToPay.toFixed(2)} € für ${userAlias} verbucht!`); loadAdminPayouts(); calculateUserEarnedAmounts(); renderMyTasks(); } // Admin-Quittung / Auszahlungsbeleg als PDF generieren function generateAdminPayoutReceipt(userId, userAlias) { const { jsPDF } = window.jspdf; const doc = new jsPDF(); doc.setFontSize(16); doc.text(`Auszahlungsbeleg / Quittung - ${userAlias}`, 14, 20); doc.setFontSize(10); doc.text(`Erstellt am: ${new Date().toLocaleDateString('de-DE')} | Administrator-Export`, 14, 26); const rows = []; let grandTotalPaid = 0; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.assigned_to === userId && (parseFloat(t.paid_amount || 0) > 0)) { const paid = parseFloat(t.paid_amount); grandTotalPaid += paid; rows.push([ t.task_name, r.room_number, `${parseFloat(t.reward_amount || 0).toFixed(2)} €`, `${paid.toFixed(2)} €`, t.is_paid ? 'Vollständig' : 'Teilzahlung', t.paid_at ? new Date(t.paid_at).toLocaleDateString('de-DE') : 'Teilzahlung' ]); } }); } }); if (rows.length === 0) { alert(`Keine bisherigen Auszahlungen für ${userAlias} vorhanden.`); return; } doc.autoTable({ startY: 32, head: [['Aufgabe', 'Zimmer', 'Gesamt', 'Ausbezahlt', 'Status', 'Datum']], body: rows, theme: 'striped', headStyles: { fillStyle: [79, 70, 229] } }); const finalY = doc.lastAutoTable.finalY + 10; doc.setFontSize(12); doc.text(`Summe ausbezahlt: ${grandTotalPaid.toFixed(2)} €`, 14, finalY); doc.save(`Quittung_${userAlias.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`); } async function markTaskPaid(taskId, isPaid) { if (!isAdmin()) return; let targetTask = null; allRoomsData.forEach(r => { if (r.tasks) { const t = r.tasks.find(x => x.id === taskId); if (t) targetTask = t; } }); const fullReward = targetTask ? parseFloat(targetTask.reward_amount || 0) : 0; const payload = { is_paid: isPaid, paid_amount: isPaid ? fullReward : 0, paid_at: isPaid ? new Date().toISOString() : null }; const { error } = await supabaseClient.from('tasks').update(payload).eq('id', taskId); if (!error && targetTask) { targetTask.is_paid = isPaid; targetTask.paid_amount = payload.paid_amount; targetTask.paid_at = payload.paid_at; loadAdminPayouts(); calculateUserEarnedAmounts(); renderMyTasks(); } } async function payoutAllUserTasks(userId) { if (!isAdmin()) return; const targetTasks = []; let totalAmount = 0; let targetAlias = 'Mitarbeiter'; allProfiles.forEach(p => { if(p.id === userId) targetAlias = p.alias; }); allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.is_completed && t.is_approved && t.assigned_to === userId && !t.is_paid) { const fullReward = parseFloat(t.reward_amount || 0); const alreadyPaid = parseFloat(t.paid_amount || 0); targetTasks.push({ id: t.id, fullReward }); totalAmount += (fullReward - alreadyPaid); } }); } }); if (targetTasks.length === 0) return; if (!confirm(`Sollen alle offenen Beträge (${totalAmount.toFixed(2)} €) für ${targetAlias} als ausgezahlt markiert werden?`)) return; for (const item of targetTasks) { await supabaseClient.from('tasks').update({ is_paid: true, paid_amount: item.fullReward, paid_at: new Date().toISOString() }).eq('id', item.id); } allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { const found = targetTasks.find(x => x.id === t.id); if (found) { t.is_paid = true; t.paid_amount = found.fullReward; t.paid_at = new Date().toISOString(); } }); } }); await logActivity(null, `hat ${totalAmount.toFixed(2)} € (Gesamtes Guthaben) an ${targetAlias} ausbezahlt`); loadAdminPayouts(); calculateUserEarnedAmounts(); renderMyTasks(); alert('Guthaben erfolgreich als ausgezahlt markiert!'); } async function toggleUserAccess(userId, disable) { await supabaseClient.from('profiles').update({ is_disabled: disable }).eq('id', userId); openAdminUsersModal(); } function copyInviteLink() { const url = window.location.href; navigator.clipboard.writeText(url); alert(`Einladungs-Link kopiert:\n${url}`); } function closeAdminUsersModal() { document.getElementById('admin-users-modal').classList.add('hidden'); } async function bulkAddGlobalTask() { if (!isAdmin()) return; const input = document.getElementById('admin-bulk-task-input'); const rewardInput = document.getElementById('admin-bulk-task-reward'); const taskName = input.value.trim(); const rewardAmount = parseFloat(rewardInput.value) || 0; if (!taskName) return; if (!confirm(`Soll die Aufgabe "${taskName}" wirklich zu ALLEN Räumen hinzugefügt werden?`)) return; const inserts = allRoomsData.map(r => ({ room_id: r.id, task_name: taskName, is_completed: false, reward_amount: rewardAmount })); const { error } = await supabaseClient.from('tasks').insert(inserts); if (!error) { input.value = ''; rewardInput.value = ''; alert('Aufgabe erfolgreich an alle Räume verteilt!'); logActivity(null, `hat Standard-Aufgabe "${taskName}" (${rewardAmount.toFixed(2)} €) an alle Räume verteilt`); loadRooms(); } } async function bulkUpdateFloorStatus() { if (!isAdmin()) return; const floor = document.getElementById('admin-bulk-floor').value; const newStatus = document.getElementById('admin-bulk-status').value; if (!confirm(`Sollen alle Zimmer auf "${floor}" auf "${newStatus}" gesetzt werden?`)) return; const targetRooms = allRoomsData.filter(r => r.floor === floor); const ids = targetRooms.map(r => r.id); if (ids.length === 0) { alert('Keine Zimmer auf dieser Etage gefunden.'); return; } const { error } = await supabaseClient.from('rooms').update({ status: newStatus }).in('id', ids); if (!error) { alert(`Status von ${ids.length} Zimmern auf ${floor} wurde aktualisiert.`); logActivity(null, `hat Status aller Zimmer auf "${floor}" geändert auf "${newStatus}"`); loadRooms(); } } function downloadSystemBackup() { const backupData = { timestamp: new Date().toISOString(), exported_by: currentUserAlias, rooms: allRoomsData }; const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `Mainpark_Backup_${new Date().toISOString().slice(0, 10)}.json`; link.click(); logActivity(null, 'hat System-Backup (JSON) heruntergeladen'); } function loadAdminStats() { const userTaskCounts = {}; allRoomsData.forEach(r => { if (r.tasks) { r.tasks.forEach(t => { if (t.is_completed && t.assigned_to) { userTaskCounts[t.assigned_to] = (userTaskCounts[t.assigned_to] || 0) + 1; } }); } }); let topUser = '-'; let maxTasks = 0; Object.keys(userTaskCounts).forEach(uid => { if (userTaskCounts[uid] > maxTasks) { maxTasks = userTaskCounts[uid]; const prof = allProfiles.find(p => p.id === uid); if (prof) topUser = `${prof.alias} (${maxTasks} Aufgaben)`; } }); document.getElementById('stat-top-performer').textContent = topUser; const stuckRooms = allRoomsData.filter(r => getRoomProgress(r) === 0 && r.status === 'In Arbeit').length; document.getElementById('stat-stuck-rooms').textContent = `${stuckRooms} Zimmer`; const openMat = allRoomsData.filter(r => r.material_status === 'Offen').length; document.getElementById('stat-material-open').textContent = `${openMat} Bestellanfragen`; } async function publishAnnouncement() { const input = document.getElementById('admin-announcement-input'); const msg = input.value.trim(); if (!msg) return; await supabaseClient.from('activity_logs').insert([{ user_alias: 'ANNOUNCEMENT', room_name: 'SYSTEM', action_text: msg }]); input.value = ''; checkGlobalAnnouncement(); alert('Banner veröffentlicht!'); } async function clearAnnouncement() { await supabaseClient.from('activity_logs').insert([{ user_alias: 'ANNOUNCEMENT', room_name: 'SYSTEM', action_text: 'CLEAR' }]); checkGlobalAnnouncement(); } async function checkGlobalAnnouncement() { const { data } = await supabaseClient .from('activity_logs') .select('*') .eq('user_alias', 'ANNOUNCEMENT') .order('created_at', { ascending: false }) .limit(1); const bar = document.getElementById('global-announcement-bar'); const txt = document.getElementById('announcement-text'); if (data && data.length > 0 && data[0].action_text !== 'CLEAR') { txt.textContent = data[0].action_text; bar.classList.remove('hidden'); } else { bar.classList.add('hidden'); } }