feat: Dodano moderację tablicy B2B dla administratora

- Przycisk usuwania ogłoszenia z potwierdzeniem
- Przycisk aktywacji/dezaktywacji ogłoszenia
- Endpointy: /delete, /toggle-active
- Badge "Nieaktywne" dla dezaktywowanych ogłoszeń

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-30 20:31:37 +01:00
parent 8bcb339bff
commit e6acc2ec6f
2 changed files with 205 additions and 0 deletions

View File

@ -148,3 +148,52 @@ def close(classified_id):
return jsonify({'success': True, 'message': 'Ogłoszenie zamknięte'})
finally:
db.close()
@bp.route('/<int:classified_id>/delete', methods=['POST'], endpoint='classifieds_delete')
@login_required
def delete(classified_id):
"""Usuń ogłoszenie (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
classified = db.query(Classified).filter(
Classified.id == classified_id
).first()
if not classified:
return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404
db.delete(classified)
db.commit()
return jsonify({'success': True, 'message': 'Ogłoszenie usunięte'})
finally:
db.close()
@bp.route('/<int:classified_id>/toggle-active', methods=['POST'], endpoint='classifieds_toggle_active')
@login_required
def toggle_active(classified_id):
"""Aktywuj/dezaktywuj ogłoszenie (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
classified = db.query(Classified).filter(
Classified.id == classified_id
).first()
if not classified:
return jsonify({'success': False, 'error': 'Ogłoszenie nie istnieje'}), 404
classified.is_active = not classified.is_active
db.commit()
status = 'aktywowane' if classified.is_active else 'dezaktywowane'
return jsonify({'success': True, 'message': f'Ogłoszenie {status}', 'is_active': classified.is_active})
finally:
db.close()

View File

@ -202,6 +202,69 @@
.close-btn {
margin-left: auto;
}
/* Admin actions */
.admin-actions {
display: flex;
gap: var(--spacing-sm);
margin-left: auto;
}
.admin-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
border: 1px solid;
transition: all 0.2s;
}
.admin-btn svg {
width: 16px;
height: 16px;
}
.admin-btn-delete {
background: #fef2f2;
color: #dc2626;
border-color: #fecaca;
}
.admin-btn-delete:hover {
background: #fee2e2;
border-color: #f87171;
}
.admin-btn-toggle {
background: #f5f5f5;
color: #525252;
border-color: #d4d4d4;
}
.admin-btn-toggle:hover {
background: #e5e5e5;
border-color: #a3a3a3;
}
.admin-btn-toggle.inactive {
background: #fef3c7;
color: #92400e;
border-color: #fcd34d;
}
.inactive-badge {
background: #fef2f2;
color: #dc2626;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: 500;
margin-left: var(--spacing-sm);
}
</style>
{% endblock %}
@ -219,10 +282,33 @@
<div>
<span class="classified-type {{ classified.listing_type }}">{{ 'Szukam' if classified.listing_type == 'szukam' else 'Oferuje' }}</span>
<span class="classified-category category-{{ classified.category }}">{{ classified.category|replace('uslugi', 'Usługi')|replace('produkty', 'Produkty')|replace('wspolpraca', 'Współpraca')|replace('praca', 'Praca')|replace('inne', 'Inne')|replace('nieruchomosci', 'Nieruchomości') }}</span>
{% if not classified.is_active %}
<span class="inactive-badge">Nieaktywne</span>
{% endif %}
</div>
{% if classified.author_id == current_user.id %}
<button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button>
{% endif %}
{% if current_user.is_authenticated and current_user.is_admin %}
<div class="admin-actions">
<button type="button" class="admin-btn admin-btn-toggle {% if not classified.is_active %}inactive{% endif %}" onclick="toggleActive()" title="{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
{% if classified.is_active %}
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
{% else %}
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
{% endif %}
</svg>
{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}
</button>
<button type="button" class="admin-btn admin-btn-delete" onclick="deleteClassified()" title="Usuń ogłoszenie">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Usuń
</button>
</div>
{% endif %}
</div>
<h1 class="classified-title">{{ classified.title }}</h1>
@ -308,6 +394,12 @@
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
.btn-danger { background: #dc2626; color: white; border: none; }
.btn-danger:hover { background: #b91c1c; }
.btn-warning { background: #f59e0b; color: white; border: none; }
.btn-warning:hover { background: #d97706; }
.btn-success { background: #10b981; color: white; border: none; }
.btn-success:hover { background: #059669; }
</style>
{% endblock %}
@ -375,4 +467,68 @@ async function closeClassified() {
showToast('Błąd połączenia', 'error');
}
}
// Admin functions
async function deleteClassified() {
const confirmed = await showConfirm('Czy na pewno chcesz usunąć to ogłoszenie?<br><br><strong>Ta operacja jest nieodwracalna.</strong>', {
icon: '🗑️',
title: 'Usuń ogłoszenie',
okText: 'Usuń',
okClass: 'btn-danger'
});
if (!confirmed) return;
try {
const response = await fetch('{{ url_for("classifieds.classifieds_delete", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast('Ogłoszenie usunięte', 'success');
setTimeout(() => window.location.href = '{{ url_for("classifieds.classifieds_index") }}', 1500);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function toggleActive() {
const isActive = {{ 'true' if classified.is_active else 'false' }};
const action = isActive ? 'dezaktywować' : 'aktywować';
const confirmed = await showConfirm(`Czy na pewno chcesz ${action} to ogłoszenie?`, {
icon: isActive ? '🚫' : '✅',
title: isActive ? 'Dezaktywuj ogłoszenie' : 'Aktywuj ogłoszenie',
okText: isActive ? 'Dezaktywuj' : 'Aktywuj',
okClass: isActive ? 'btn-warning' : 'btn-success'
});
if (!confirmed) return;
try {
const response = await fetch('{{ url_for("classifieds.classifieds_toggle_active", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
{% endblock %}