nordabiz/templates/admin/announcements.html
Maciej Pienczyn 6e00291a88 feat: AI usage user details + styled modals across app
- Add /admin/ai-usage/user/<id> route for detailed AI usage per user
- Add ai_usage_user.html template with stats, usage breakdown, logs
- Make user names clickable in AI usage dashboard ranking
- Replace all native browser dialogs (alert, confirm) with styled modals/toasts:
  - admin/fees.html, forum.html, recommendations.html, announcements.html, debug.html
  - calendar/admin.html, event.html
  - company_detail.html, company/recommend.html
  - forum/new_topic.html, topic.html
  - classifieds/view.html
  - auth/reset_password.html

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 10:30:35 +01:00

260 lines
10 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Zarzadzanie Ogloszeniami - Norda Biznes Hub{% endblock %}
{% block extra_css %}
<style>
.admin-header {
margin-bottom: var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.announcements-table {
width: 100%;
border-collapse: collapse;
}
.announcements-table th,
.announcements-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.announcements-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
background: var(--background);
}
.announcements-table tr:hover {
background: var(--background);
}
.status-badge {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: 600;
}
.status-published { background: var(--success-bg); color: var(--success); }
.status-draft { background: var(--warning-bg); color: var(--warning); }
.status-expired { background: var(--surface-secondary); color: var(--text-secondary); }
.type-badge {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
background: var(--primary-bg);
color: var(--primary);
}
.type-fees { background: var(--warning-bg); color: var(--warning); }
.type-important { background: var(--error-bg); color: var(--error); }
.type-urgent { background: var(--error); color: white; }
.pinned-icon {
color: var(--warning);
margin-left: var(--spacing-xs);
}
.btn-small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-xs);
}
.actions-cell {
white-space: nowrap;
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="admin-header">
<h1>Zarzadzanie Ogloszeniami</h1>
<a href="{{ url_for('admin_announcements_new') }}" class="btn btn-primary">
+ Nowe ogloszenie
</a>
</div>
<div class="section">
{% if announcements %}
<table class="announcements-table">
<thead>
<tr>
<th>Tytul</th>
<th>Typ</th>
<th>Status</th>
<th>Autor</th>
<th>Utworzono</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for ann in announcements %}
<tr>
<td>
{{ ann.title }}
{% if ann.is_pinned %}<span class="pinned-icon" title="Przypiety">&#128204;</span>{% endif %}
</td>
<td>
<span class="type-badge type-{{ ann.announcement_type }}">
{% if ann.announcement_type == 'general' %}Ogolne
{% elif ann.announcement_type == 'fees' %}Skladki
{% elif ann.announcement_type == 'event' %}Wydarzenie
{% elif ann.announcement_type == 'important' %}Wazne
{% elif ann.announcement_type == 'urgent' %}Pilne
{% else %}{{ ann.announcement_type }}
{% endif %}
</span>
</td>
<td>
{% if not ann.is_published %}
<span class="status-badge status-draft">Wersja robocza</span>
{% elif ann.expire_date and ann.expire_date < now %}
<span class="status-badge status-expired">Wygaslo</span>
{% else %}
<span class="status-badge status-published">Opublikowane</span>
{% endif %}
</td>
<td>{{ ann.author.name if ann.author else '-' }}</td>
<td>{{ ann.created_at.strftime('%Y-%m-%d %H:%M') if ann.created_at else '-' }}</td>
<td class="actions-cell">
<a href="{{ url_for('admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small">
Edytuj
</a>
<button class="btn btn-error btn-small" onclick="deleteAnnouncement({{ ann.id }})">
Usun
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Brak ogloszen. Utworz pierwsze ogloszenie klikajac przycisk powyzej.</p>
</div>
{% endif %}
</div>
</div>
<!-- Universal Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 420px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
<div class="modal-icon" id="confirmModalIcon" style="font-size: 3em; margin-bottom: var(--spacing-md);"></div>
<h3 id="confirmModalTitle" style="margin-bottom: var(--spacing-sm);">Potwierdzenie</h3>
<p class="modal-description" id="confirmModalMessage" style="color: var(--text-secondary);"></p>
</div>
<div class="modal-actions" style="display: flex; gap: var(--spacing-sm); justify-content: center;">
<button type="button" class="btn btn-secondary" id="confirmModalCancel">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmModalOk">OK</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.modal-overlay#confirmModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#confirmModal.active { display: flex; }
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.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; } }
</style>
{% endblock %}
{% block extra_js %}
const now = new Date();
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 = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
async function deleteAnnouncement(id) {
const confirmed = await showConfirm('Czy na pewno chcesz usunąć to ogłoszenie?', {
icon: '🗑️',
title: 'Usuwanie ogłoszenia',
okText: 'Usuń',
okClass: 'btn-error'
});
if (!confirmed) return;
try {
const response = await fetch('/admin/announcements/' + id + '/delete', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
showToast('Ogłoszenie zostało usunięte', 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast('Błąd: ' + data.error, 'error');
}
} catch (err) {
showToast('Błąd: ' + err, 'error');
}
}
{% endblock %}