nordabiz/templates/calendar/admin.html
Maciej Pienczyn c6326d9760
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix(calendar): include guests in attendee counts everywhere
Event attendee counts were inconsistent - event detail page showed total
(members + guests = 42) but event list and homepage showed only members (39).
Now all views use total_attendee_count including guests (osoby towarzyszące).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:29:05 +02:00

281 lines
11 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 %}Zarządzanie wydarzeniami - 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);
}
.events-table {
width: 100%;
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border-collapse: collapse;
overflow: hidden;
}
.events-table th,
.events-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.events-table th {
background: var(--background);
font-weight: 600;
color: var(--text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
}
.events-table tr:hover {
background: var(--background);
}
.event-title-cell {
font-weight: 500;
}
.event-title-cell a {
color: var(--text-primary);
text-decoration: none;
}
.event-title-cell a:hover {
color: var(--primary);
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
}
.badge-past {
background: var(--border);
color: var(--text-secondary);
}
.badge-upcoming {
background: #dcfce7;
color: #166534;
}
.action-buttons {
display: flex;
gap: var(--spacing-xs);
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: var(--transition);
}
.btn-icon:hover {
background: var(--background);
}
.btn-icon.danger:hover {
background: var(--error);
border-color: var(--error);
color: white;
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
</style>
{% endblock %}
{% block content %}
<div class="admin-header">
<div>
<h1>Zarządzanie wydarzeniami</h1>
<p class="text-muted">Tworzenie i edycja wydarzeń Norda Biznes</p>
</div>
<a href="{{ url_for('admin.admin_calendar_new') }}" class="btn btn-primary">Dodaj wydarzenie</a>
</div>
{% if events %}
<table class="events-table">
<thead>
<tr>
<th>Tytul</th>
<th>Data</th>
<th>Typ</th>
<th>Miejsce</th>
<th>Uczestnicy</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr data-event-id="{{ event.id }}">
<td class="event-title-cell">
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>{% if event.is_external %} <span style="background:#94a3b8;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;">ZEWNĘTRZNE</span>{% endif %}{% if event.access_level == 'admin_only' %} <span style="background:#ef4444;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;">UKRYTE</span>{% elif event.access_level == 'rada_only' %} <span style="background:#f59e0b;color:#92400e;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;">IZBA</span>{% endif %}
</td>
<td>{{ event.event_date.strftime('%d.%m.%Y') }}</td>
<td>{{ event.event_type }}</td>
<td>{{ event.location or '-' }}</td>
<td>{{ event.total_attendee_count }}</td>
<td>
{% if event.is_past %}
<span class="badge badge-past">Zakonczone</span>
{% else %}
<span class="badge badge-upcoming">Nadchodzace</span>
{% endif %}
</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('admin.admin_calendar_edit', event_id=event.id) }}" class="btn-icon" title="Edytuj" style="color: var(--primary);">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</a>
{% if event.is_paid %}
<a href="{{ url_for('admin.admin_event_payments', event_id=event.id) }}" class="btn-icon" title="Platnosci" style="color: var(--success);">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>
</svg>
</a>
{% endif %}
<button class="btn-icon danger" onclick="deleteEvent({{ event.id }}, '{{ event.title|e }}')" title="Usun">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<p>Brak wydarzeń. Dodaj pierwsze wydarzenie!</p>
<a href="{{ url_for('admin.admin_calendar_new') }}" class="btn btn-primary mt-2">Dodaj wydarzenie</a>
</div>
{% endif %}
<!-- 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 csrfToken = '{{ csrf_token() }}';
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 deleteEvent(eventId, title) {
const confirmed = await showConfirm(`Czy na pewno chcesz usunąć wydarzenie "<strong>${title}</strong>"?`, {
icon: '🗑️',
title: 'Usuwanie wydarzenia',
okText: 'Usuń',
okClass: 'btn-danger'
});
if (!confirmed) return;
try {
const response = await fetch(`/admin/kalendarz/${eventId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
document.querySelector(`tr[data-event-id="${eventId}"]`).remove();
showToast('Wydarzenie zostało usunięte', 'success');
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
{% endblock %}