- Add edit tracking (24h limit), soft delete, and JSONB reactions to ForumTopic/ForumReply - Create ForumTopicSubscription, ForumReport, ForumEditHistory models - Add 15 new API endpoints for user actions and admin moderation - Implement reactions (👍❤️🎉), topic subscriptions, content reporting - Add solution marking, restore deleted content, edit history for admins - Create forum_reports.html and forum_deleted.html admin templates - Integrate notifications for replies, reactions, solutions, and reports - Add SQL migration 024_forum_modernization.sql Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
290 lines
8.2 KiB
HTML
290 lines
8.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Zgłoszenia Forum - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.admin-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.stat-pill {
|
|
background: var(--surface);
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
border-radius: 20px;
|
|
box-shadow: var(--shadow);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.stat-pill.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.stat-pill .count {
|
|
font-weight: 700;
|
|
margin-left: var(--spacing-xs);
|
|
}
|
|
|
|
.reports-list {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.report-card {
|
|
padding: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.report-card:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.report-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.report-meta {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.report-reason {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.reason-spam { background: #fef2f2; color: #991b1b; }
|
|
.reason-offensive { background: #fef3c7; color: #92400e; }
|
|
.reason-off-topic { background: #eff6ff; color: #1e40af; }
|
|
.reason-other { background: #f5f5f5; color: #525252; }
|
|
|
|
.report-content {
|
|
background: var(--background);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.report-content .label {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.report-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.btn-review {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
border: 1px solid;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-review.accept {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
border-color: #86efac;
|
|
}
|
|
|
|
.btn-review.accept:hover {
|
|
background: #bbf7d0;
|
|
}
|
|
|
|
.btn-review.dismiss {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
border-color: #fecaca;
|
|
}
|
|
|
|
.btn-review.dismiss:hover {
|
|
background: #fecaca;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.nav-tabs {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.nav-tabs a {
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
border-radius: var(--radius);
|
|
text-decoration: none;
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.nav-tabs a.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.back-link {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.back-link a {
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.back-link a:hover {
|
|
color: var(--primary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="back-link">
|
|
<a href="{{ url_for('admin_forum') }}">← Powrót do moderacji forum</a>
|
|
</div>
|
|
|
|
<div class="admin-header">
|
|
<h1>Zgłoszenia Forum</h1>
|
|
</div>
|
|
|
|
<div class="nav-tabs">
|
|
<a href="{{ url_for('admin_forum_reports', status='pending') }}" class="{% if status_filter == 'pending' %}active{% endif %}">
|
|
Oczekujące ({{ pending_count }})
|
|
</a>
|
|
<a href="{{ url_for('admin_forum_reports', status='reviewed') }}" class="{% if status_filter == 'reviewed' %}active{% endif %}">
|
|
Rozpatrzone ({{ reviewed_count }})
|
|
</a>
|
|
<a href="{{ url_for('admin_forum_reports', status='dismissed') }}" class="{% if status_filter == 'dismissed' %}active{% endif %}">
|
|
Odrzucone ({{ dismissed_count }})
|
|
</a>
|
|
</div>
|
|
|
|
{% if reports %}
|
|
<div class="reports-list">
|
|
{% for report in reports %}
|
|
<div class="report-card" id="report-{{ report.id }}">
|
|
<div class="report-header">
|
|
<div>
|
|
<span class="report-reason reason-{{ report.reason }}">{{ reason_labels.get(report.reason, report.reason) }}</span>
|
|
<span class="report-meta">
|
|
Zgłoszone przez {{ report.reporter.name or report.reporter.email.split('@')[0] }}
|
|
• {{ report.created_at.strftime('%d.%m.%Y %H:%M') }}
|
|
</span>
|
|
</div>
|
|
<span class="report-meta">
|
|
{{ report.content_type|capitalize }} #{{ report.topic_id or report.reply_id }}
|
|
</span>
|
|
</div>
|
|
|
|
{% if report.description %}
|
|
<div class="report-content">
|
|
<div class="label">Opis zgłoszenia:</div>
|
|
{{ report.description }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="report-content">
|
|
<div class="label">Zgłoszona treść:</div>
|
|
{% if report.content_type == 'topic' and report.topic %}
|
|
<strong>{{ report.topic.title }}</strong><br>
|
|
{{ report.topic.content[:300] }}{% if report.topic.content|length > 300 %}...{% endif %}
|
|
<br><a href="{{ url_for('forum_topic', topic_id=report.topic.id) }}" target="_blank">Zobacz temat →</a>
|
|
{% elif report.content_type == 'reply' and report.reply %}
|
|
{{ report.reply.content[:300] }}{% if report.reply.content|length > 300 %}...{% endif %}
|
|
<br><a href="{{ url_for('forum_topic', topic_id=report.reply.topic_id) }}#reply-{{ report.reply.id }}" target="_blank">Zobacz odpowiedź →</a>
|
|
{% else %}
|
|
<em>Treść niedostępna</em>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if report.status == 'pending' %}
|
|
<div class="report-actions">
|
|
<button class="btn-review accept" onclick="reviewReport({{ report.id }}, 'reviewed')">
|
|
✓ Rozpatrzone
|
|
</button>
|
|
<button class="btn-review dismiss" onclick="reviewReport({{ report.id }}, 'dismissed')">
|
|
✕ Odrzuć
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="report-meta">
|
|
{% if report.reviewed_by %}
|
|
Rozpatrzone przez {{ report.reviewer.name or report.reviewer.email.split('@')[0] }}
|
|
• {{ report.reviewed_at.strftime('%d.%m.%Y %H:%M') }}
|
|
{% endif %}
|
|
{% if report.review_note %}
|
|
<br>Notatka: {{ report.review_note }}
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="reports-list">
|
|
<div class="empty-state">
|
|
Brak zgłoszeń w tej kategorii.
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
function reviewReport(reportId, status) {
|
|
const note = status === 'dismissed' ? prompt('Opcjonalna notatka (powód odrzucenia):') : '';
|
|
|
|
fetch(`/admin/forum/report/${reportId}/review`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({ status: status, note: note || '' })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('report-' + reportId).remove();
|
|
location.reload();
|
|
} else {
|
|
alert(data.error || 'Błąd');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert('Błąd połączenia');
|
|
});
|
|
}
|
|
{% endblock %}
|