{% extends "base.html" %} {% block title %}Moderacja Forum - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Moderacja Forum

Zarządzaj tematami i odpowiedziami na forum

Analityka Zgłoszenia Usunięte treści

Wyszukiwarka

{{ total_topics }}
Tematow
{{ total_replies }}
Odpowiedzi
{{ pinned_count }}
Przypietych
{{ locked_count }}
Zamknietych
{{ status_counts.get('new', 0) }}
Nowych
{{ status_counts.get('in_progress', 0) }}
W realizacji
{{ status_counts.get('resolved', 0) }}
Rozwiazanych
{{ status_counts.get('rejected', 0) }}
Odrzuconych
{{ category_counts.get('feature_request', 0) }}
Propozycji
{{ category_counts.get('bug', 0) }}
Bledow
{{ category_counts.get('question', 0) }}
Pytan
{{ category_counts.get('announcement', 0) }}
Ogloszen

Tematy

0 zaznaczonych
{% if topics %} {% for topic in topics %} {% endfor %}
Tytul Kategoria Autor Status Data Akcje
{% if topic.is_pinned %} Przypiety {% endif %} {% if topic.is_locked %} Zamkniety {% endif %} {{ category_labels.get(topic.category, 'Pytanie') }} {{ topic.author.name or topic.author.email.split('@')[0] }} {{ status_labels.get(topic.status, 'Nowy') }} {{ topic.created_at|local_time('%d.%m.%Y') }}
{% else %}

Brak tematow na forum

{% endif %}

Ostatnie odpowiedzi

{% if recent_replies %}
{% for reply in recent_replies %}
{{ reply.content[:200] }}{% if reply.content|length > 200 %}...{% endif %}
{{ reply.author.name or reply.author.email.split('@')[0] }} w temacie {{ reply.topic.title[:30] }}{% if reply.topic.title|length > 30 %}...{% endif %} • {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }}
{% endfor %}
{% else %}

Brak odpowiedzi

{% endif %}
{% endblock %} {% block extra_js %} const csrfToken = '{{ csrf_token() }}'; let currentTopicId = null; let confirmResolve = null; function showConfirm(message, options = {}) { return new Promise(resolve => { confirmResolve = resolve; document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; document.getElementById('confirmModalMessage').innerHTML = message; document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); document.getElementById('confirmModal').classList.add('active'); }); } function closeConfirm(result) { document.getElementById('confirmModal').classList.remove('active'); if (confirmResolve) { confirmResolve(result); confirmResolve = null; } } document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true)); document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false)); document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); }); function showToast(message, type = 'info', duration = 4000) { const container = document.getElementById('toastContainer'); const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `${icons[type]||'ℹ'}${message}`; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); } function showMessage(message, type) { showToast(message, type === 'error' ? 'error' : 'success'); } // Status modal functions function openStatusModal(topicId, topicTitle, currentStatus) { currentTopicId = topicId; document.getElementById('modalTopicTitle').textContent = topicTitle; document.getElementById('newStatus').value = currentStatus; document.getElementById('statusNote').value = ''; document.getElementById('statusModal').classList.add('active'); } function closeStatusModal() { document.getElementById('statusModal').classList.remove('active'); currentTopicId = null; } async function saveStatus() { if (!currentTopicId) return; const newStatus = document.getElementById('newStatus').value; const statusNote = document.getElementById('statusNote').value; try { const response = await fetch(`/admin/forum/topic/${currentTopicId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ status: newStatus, note: statusNote }) }); const data = await response.json(); if (data.success) { location.reload(); } else { showMessage(data.error || 'Wystapil blad', 'error'); } } catch (error) { showMessage('Blad polaczenia', 'error'); } closeStatusModal(); } // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeStatusModal(); } }); // Close modal on overlay click document.getElementById('statusModal').addEventListener('click', (e) => { if (e.target.id === 'statusModal') { closeStatusModal(); } }); async function togglePin(topicId) { try { const response = await fetch(`/admin/forum/topic/${topicId}/pin`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { location.reload(); } else { showMessage(data.error || 'Wystapil blad', 'error'); } } catch (error) { showMessage('Blad polaczenia', 'error'); } } async function toggleLock(topicId) { try { const response = await fetch(`/admin/forum/topic/${topicId}/lock`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { location.reload(); } else { showMessage(data.error || 'Wystapil blad', 'error'); } } catch (error) { showMessage('Blad polaczenia', 'error'); } } async function deleteTopic(topicId, title) { const confirmed = await showConfirm(`Czy na pewno chcesz usunąć temat "${title}"?

Ta operacja usunie również wszystkie odpowiedzi i jest nieodwracalna.`, { icon: '🗑️', title: 'Usuwanie tematu', okText: 'Usuń', okClass: 'btn-danger' }); if (!confirmed) return; try { const response = await fetch(`/admin/forum/topic/${topicId}/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { document.querySelector(`tr[data-topic-id="${topicId}"]`).remove(); showToast('Temat został usunięty', 'success'); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showMessage('Błąd połączenia', 'error'); } } async function deleteReply(replyId) { const confirmed = await showConfirm('Czy na pewno chcesz usunąć tę odpowiedź?', { icon: '🗑️', title: 'Usuwanie odpowiedzi', okText: 'Usuń', okClass: 'btn-danger' }); if (!confirmed) return; try { const response = await fetch(`/admin/forum/reply/${replyId}/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { document.querySelector(`div[data-reply-id="${replyId}"]`).remove(); showToast('Odpowiedź została usunięta', 'success'); } else { showMessage(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showMessage('Błąd połączenia', 'error'); } } // ============================================================ // BULK ACTIONS // ============================================================ function getSelectedTopicIds() { return Array.from(document.querySelectorAll('.topic-checkbox:checked')).map(cb => parseInt(cb.value)); } function updateBulkSelection() { const selected = getSelectedTopicIds(); const bar = document.getElementById('bulkActionsBar'); const count = document.getElementById('selectedCount'); const selectAll = document.getElementById('selectAll'); count.textContent = selected.length; if (selected.length > 0) { bar.classList.add('visible'); } else { bar.classList.remove('visible'); } // Update select all checkbox state const allCheckboxes = document.querySelectorAll('.topic-checkbox'); selectAll.checked = allCheckboxes.length > 0 && selected.length === allCheckboxes.length; selectAll.indeterminate = selected.length > 0 && selected.length < allCheckboxes.length; // Highlight selected rows document.querySelectorAll('.topics-table tbody tr').forEach(row => { const checkbox = row.querySelector('.topic-checkbox'); if (checkbox && checkbox.checked) { row.classList.add('selected'); } else { row.classList.remove('selected'); } }); } function toggleSelectAll() { const selectAll = document.getElementById('selectAll'); document.querySelectorAll('.topic-checkbox').forEach(cb => { cb.checked = selectAll.checked; }); updateBulkSelection(); } async function bulkAction(action) { const topicIds = getSelectedTopicIds(); if (topicIds.length === 0) { showToast('Zaznacz tematy', 'warning'); return; } let confirmMessage = ''; let payload = { topic_ids: topicIds, action: action }; switch (action) { case 'pin': confirmMessage = `Przypnij ${topicIds.length} tematów?`; break; case 'unpin': confirmMessage = `Odepnij ${topicIds.length} tematów?`; break; case 'lock': confirmMessage = `Zablokuj ${topicIds.length} tematów?`; break; case 'unlock': confirmMessage = `Odblokuj ${topicIds.length} tematów?`; break; case 'status': const status = document.getElementById('bulkStatusSelect').value; if (!status) return; payload.status = status; confirmMessage = `Zmień status ${topicIds.length} tematów na "${status}"?`; document.getElementById('bulkStatusSelect').value = ''; break; case 'delete': confirmMessage = `Uwaga! Ta operacja jest nieodwracalna.

Usunąć ${topicIds.length} tematów wraz ze wszystkimi odpowiedziami?`; break; default: return; } const confirmed = await showConfirm(confirmMessage, { title: 'Akcja zbiorcza', icon: action === 'delete' ? '⚠️' : '❓', okText: action === 'delete' ? 'Usuń' : 'OK', okClass: action === 'delete' ? 'btn-danger' : 'btn-primary' }); if (!confirmed) return; try { const response = await fetch('/admin/forum/bulk-action', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { showToast(data.message || 'Operacja wykonana', 'success'); setTimeout(() => location.reload(), 1000); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } } // ============================================================ // ADMIN SEARCH // ============================================================ async function adminSearch() { const query = document.getElementById('adminSearchQuery').value.trim(); const includeDeleted = document.getElementById('includeDeleted').checked; if (query.length < 2) { showToast('Wpisz co najmniej 2 znaki', 'warning'); return; } try { const url = `/admin/forum/search?q=${encodeURIComponent(query)}${includeDeleted ? '&deleted=1' : ''}`; const response = await fetch(url, { headers: { 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { displaySearchResults(data.results, data.count); } else { showToast(data.error || 'Błąd wyszukiwania', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } } function displaySearchResults(results, count) { const container = document.getElementById('searchResults'); const list = document.getElementById('searchResultsList'); const countSpan = document.getElementById('resultCount'); countSpan.textContent = count; if (results.length === 0) { list.innerHTML = '

Brak wyników

'; } else { list.innerHTML = results.map(r => `
${r.type === 'topic' ? 'Temat' : 'Odpowiedź'}
${r.title} ${r.is_deleted ? ' (usunięty)' : ''}
${r.content_preview}
${r.author_name} • ${parseUTC(r.created_at).toLocaleString('pl-PL')} ${r.category_label ? ` • ${r.category_label}` : ''}
`).join(''); } container.style.display = 'block'; } // Search on Enter key document.getElementById('adminSearchQuery').addEventListener('keypress', (e) => { if (e.key === 'Enter') adminSearch(); }); // ============================================================ // MOVE TOPIC // ============================================================ let currentMoveTopicId = null; function openMoveModal(topicId, title, currentCategory) { currentMoveTopicId = topicId; document.getElementById('moveModalTopicTitle').textContent = title; document.getElementById('newCategorySelect').value = currentCategory; document.getElementById('moveTopicModal').classList.add('active'); } function closeMoveModal() { document.getElementById('moveTopicModal').classList.remove('active'); currentMoveTopicId = null; } async function saveMoveCategory() { if (!currentMoveTopicId) return; const newCategory = document.getElementById('newCategorySelect').value; try { const response = await fetch(`/admin/forum/topic/${currentMoveTopicId}/move`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ category: newCategory }) }); const data = await response.json(); if (data.success) { showToast(data.message, 'success'); setTimeout(() => location.reload(), 1000); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } closeMoveModal(); } document.getElementById('moveTopicModal').addEventListener('click', (e) => { if (e.target.id === 'moveTopicModal') closeMoveModal(); }); // ============================================================ // MERGE TOPICS // ============================================================ function openMergeModal() { const topicIds = getSelectedTopicIds(); if (topicIds.length < 2) { showToast('Zaznacz co najmniej 2 tematy do połączenia', 'warning'); return; } const select = document.getElementById('targetTopicSelect'); select.innerHTML = ''; // Populate select with selected topics topicIds.forEach(id => { const row = document.querySelector(`tr[data-topic-id="${id}"]`); const title = row ? row.querySelector('.topic-title a').textContent : `Temat #${id}`; const option = document.createElement('option'); option.value = id; option.textContent = title; select.appendChild(option); }); document.getElementById('mergeInfo').textContent = `${topicIds.length} tematów zaznaczonych. Wybierz temat docelowy - pozostałe zostaną do niego przeniesione.`; document.getElementById('mergeTopicsModal').classList.add('active'); } function closeMergeModal() { document.getElementById('mergeTopicsModal').classList.remove('active'); } async function executeMerge() { const topicIds = getSelectedTopicIds(); const targetId = parseInt(document.getElementById('targetTopicSelect').value); const sourceIds = topicIds.filter(id => id !== targetId); if (sourceIds.length === 0) { showToast('Nie można połączyć - brak tematów źródłowych', 'warning'); return; } const confirmed = await showConfirm(`Połączyć ${sourceIds.length} tematów z wybranym tematem docelowym?

Ta operacja przeniesie wszystkie odpowiedzi i usunie tematy źródłowe.`, { icon: '🔗', title: 'Łączenie tematów', okText: 'Połącz' }); if (!confirmed) return; try { const response = await fetch('/admin/forum/merge-topics', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, body: JSON.stringify({ target_id: targetId, source_ids: sourceIds }) }); const data = await response.json(); if (data.success) { showToast(data.message, 'success'); setTimeout(() => location.reload(), 1000); } else { showToast(data.error || 'Wystąpił błąd', 'error'); } } catch (error) { showToast('Błąd połączenia', 'error'); } closeMergeModal(); } document.getElementById('mergeTopicsModal').addEventListener('click', (e) => { if (e.target.id === 'mergeTopicsModal') closeMergeModal(); }); // ============================================================ // USER ACTIVITY // ============================================================ async function showUserActivity(userId) { document.getElementById('userActivityModal').classList.add('active'); document.getElementById('activityLog').innerHTML = '

Ładowanie...

'; try { const response = await fetch(`/admin/forum/user/${userId}/activity`, { headers: { 'X-CSRFToken': csrfToken } }); const data = await response.json(); if (data.success) { displayUserActivity(data); } else { showToast(data.error || 'Błąd ładowania aktywności', 'error'); closeActivityModal(); } } catch (error) { showToast('Błąd połączenia', 'error'); closeActivityModal(); } } function displayUserActivity(data) { document.getElementById('activityUserName').textContent = data.user.name; document.getElementById('statTopics').textContent = data.stats.topics; document.getElementById('statReplies').textContent = data.stats.replies; document.getElementById('statSolutions').textContent = data.stats.solutions; document.getElementById('statTotal').textContent = data.stats.total_posts; const log = document.getElementById('activityLog'); if (data.activity.length === 0) { log.innerHTML = '

Brak aktywności

'; } else { log.innerHTML = data.activity.map(a => `
${a.type === 'topic' ? '📝' : (a.is_solution ? '✓' : '💬')}
${a.action} ${a.title} ${a.is_deleted ? ' (usunięty)' : ''}
${parseUTC(a.created_at).toLocaleString('pl-PL')}
`).join(''); } } function closeActivityModal() { document.getElementById('userActivityModal').classList.remove('active'); } document.getElementById('userActivityModal').addEventListener('click', (e) => { if (e.target.id === 'userActivityModal') closeActivityModal(); }); // Close all modals on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeMoveModal(); closeMergeModal(); closeActivityModal(); } }); {% endblock %}