Files
conference/templates/organizer/dashboard.html
T
paul a3546b4c01 Security hardening: env-based config, CSRF protection, secure code generation, plus presenter management and email communications system
- Move all credentials from hardcoded to .env with python-dotenv (add .env.example template and .gitignore)
- Add CSRF token generation and validation on all state-changing requests
- Replace random.choices with secrets.choice for all secure code generation
- Add session cookie security headers (HttpOnly, Secure, SameSite)
- Add presenter management: assign/remove presenters to breakout sessions, presenter QR scanning
- Add email communications system: templates per event, custom email sending, sent email tracking
- Add staff/organizer profile pages with staff code display
- New DB tables: event_email_templates, sent_emails, user_communications
- New DB columns: staff.reminder_count/reminder_dates, breakout_session_organizers.presenter_code
- Expand English translation strings across all new features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-26 20:34:48 +00:00

196 lines
11 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ 'dashboard'|t }} - {{ 'organizer'|t }} - NetEvents{% endblock %}
{% block content %}
<div class="dashboard organizer-dashboard">
<h1>{{ 'dashboard'|t }} - {{ 'organizer'|t }}</h1>
<div class="dashboard-actions">
<a href="{{ url_for('create_event') }}" class="btn btn-primary"><i data-lucide="plus-circle"></i> {{ 'create_event'|t }}</a>
</div>
<section class="my-events">
<h2>{{ 'my_events'|t }}</h2>
{% if events %}
<div class="events-list organizer-events-list">
{% for event in events %}
<div class="event-item organizer-event-item card-static">
<div class="event-item-header">
<h3>{{ event.name }}</h3>
<div class="event-actions-inline">
<a href="{{ url_for('event_detail', code=event.code) }}" class="btn btn-sm btn-outline"><i data-lucide="eye"></i> {{ 'details'|t }}</a>
</div>
</div>
<div class="event-info">
<p class="event-date">{{ event.start_time|localized_date if event.start_time else 'TBD' }}</p>
<p class="event-location">{{ event.location }}</p>
{% if event.max_attendees %}
{% set percent = (event.attendee_count / event.max_attendees * 100)|round|int %}
<div class="organizer-event-capacity">
<span>{{ event.attendee_count }} / {{ event.max_attendees }} {{ 'attendees'|t }}</span>
<span>({{ percent }}%)</span>
</div>
<div class="organizer-progress-bar">
<div class="progress-fill {% if percent >= 100 %}full{% elif percent >= 80 %}warning{% endif %}" style="width: {{ percent if percent <= 100 else 100 }}%"></div>
</div>
{% else %}
<div class="organizer-event-capacity">
<span>{{ event.attendee_count }} {{ 'attendees'|t }} ({{ 'unlimited'|t }})</span>
</div>
{% endif %}
</div>
{% if event.staff %}
<div class="organizer-event-section">
<div class="section-toggle collapsed" onclick="toggleSection('staff-{{ event.id }}')">
<strong><i data-lucide="users"></i> Staff ({{ event.staff|length }})</strong>
</div>
<div id="staff-{{ event.id }}" class="section-content collapsed" data-item-count="{{ event.staff|length }}" data-per-page="5">
<div class="item-list">
{% for s in event.staff %}
<div class="list-item">
<div class="list-item-info">
<strong><span class="first-name">{{ s.first_name }}</span> <span class="last-name">{{ s.last_name }}</span></strong>
<span>{{ s.email }}</span>
</div>
<div class="list-item-actions">
<a href="{{ url_for('edit_staff', event_id=event.id, staff_id=s.id) }}" class="btn btn-sm btn-outline"><i data-lucide="pencil"></i></a>
<form method="POST" action="{{ url_for('delete_staff', event_id=event.id, staff_id=s.id) }}" class="d-inline" data-confirm="Remove {{ s.first_name }} {{ s.last_name }}?">
<button type="submit" class="btn btn-sm btn-danger"><i data-lucide="trash-2"></i></button>
</form>
</div>
</div>
{% endfor %}
</div>
<div class="pagination-controls"></div>
</div>
</div>
{% endif %}
{% if event.breakout_sessions %}
<div class="organizer-event-section">
<div class="section-toggle collapsed" onclick="toggleSection('sessions-{{ event.id }}')">
<strong><i data-lucide="calendar"></i> {{ 'breakout_sessions'|t }} ({{ event.breakout_sessions|length }})</strong>
</div>
<div id="sessions-{{ event.id }}" class="section-content collapsed" data-item-count="{{ event.breakout_sessions|length }}" data-per-page="5">
<div class="item-list">
{% for session in event.breakout_sessions %}
<div class="list-item">
<div class="list-item-info">
<strong>{{ session.name }}</strong>
{% if session.max_attendees %}
<span>{{ session.rsvp_count }} / {{ session.max_attendees }}</span>
{% else %}
<span>{{ session.rsvp_count }} registered</span>
{% endif %}
</div>
<div class="list-item-actions">
<a href="{{ url_for('edit_breakout_session', session_id=session.id) }}" class="btn btn-sm btn-outline"><i data-lucide="pencil"></i></a>
<form method="POST" action="{{ url_for('delete_breakout_session', session_id=session.id) }}" class="d-inline" data-confirm="Delete {{ session.name }}?">
<button type="submit" class="btn btn-sm btn-danger"><i data-lucide="trash-2"></i></button>
</form>
</div>
</div>
{% endfor %}
</div>
<div class="pagination-controls"></div>
</div>
</div>
{% endif %}
{% if event.attendees %}
<div class="organizer-event-section">
<div class="section-toggle collapsed" onclick="toggleSection('attendees-{{ event.id }}')">
<strong><i data-lucide="users"></i> {{ 'attendees'|t }} ({{ event.attendees|length }})</strong>
</div>
<div id="attendees-{{ event.id }}" class="section-content collapsed" data-item-count="{{ event.attendees|length }}" data-per-page="5">
<div class="item-list">
{% for att in event.attendees %}
<div class="list-item">
<div class="list-item-info">
<strong><span class="first-name">{{ att.first_name }}</span> <span class="last-name">{{ att.last_name }}</span></strong>
<span>{{ att.email }}</span>
{% if att.checked_in %}
<span class="badge badge-success badge-sm"><i data-lucide="check"></i> {{ 'checked_in'|t }}</span>
{% endif %}
</div>
<div class="list-item-actions">
<a href="{{ url_for('edit_attendee', attendee_id=att.id) }}" class="btn btn-sm btn-outline"><i data-lucide="pencil"></i></a>
<form method="POST" action="{{ url_for('delete_attendee', attendee_id=att.id) }}" class="d-inline" data-confirm="Remove {{ att.first_name }} {{ att.last_name }}?">
<button type="submit" class="btn btn-sm btn-danger"><i data-lucide="trash-2"></i></button>
</form>
</div>
</div>
{% endfor %}
</div>
<div class="pagination-controls"></div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="no-events">{{ 'no_events_yet'|t }}
<a href="{{ url_for('create_event') }}">{{ 'create_first_event'|t }}</a>
</p>
{% endif %}
</section>
</div>
<script>
function toggleSection(id) {
const content = document.getElementById(id);
const toggle = content.previousElementSibling;
const wasCollapsed = content.classList.contains('collapsed');
content.classList.toggle('collapsed');
toggle.classList.toggle('collapsed');
if (wasCollapsed && content.dataset.itemCount) {
const perPage = parseInt(content.dataset.perPage) || 5;
setupPagination(id, parseInt(content.dataset.itemCount), perPage);
}
}
function showPage(sectionId, page, perPage) {
const section = document.getElementById(sectionId);
const items = section.querySelectorAll('.list-item');
items.forEach((item, i) => {
const start = (page - 1) * perPage;
const end = start + perPage;
item.style.display = i >= start && i < end ? 'flex' : 'none';
});
const totalPages = parseInt(section.dataset.totalPages);
section.querySelectorAll('.pagination-controls button').forEach(btn => {
btn.disabled = parseInt(btn.dataset.page) == page;
});
}
function setupPagination(sectionId, totalItems, perPage) {
const section = document.getElementById(sectionId);
const totalPages = Math.ceil(totalItems / perPage);
if (totalPages <= 1) return;
section.dataset.totalPages = totalPages;
const controls = section.querySelector('.pagination-controls');
controls.innerHTML = '';
for (let p = 1; p <= totalPages; p++) {
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-outline';
btn.dataset.page = p;
btn.textContent = p;
btn.onclick = () => showPage(sectionId, p, perPage);
if (p === 1) btn.disabled = true;
controls.appendChild(btn);
}
showPage(sectionId, 1, perPage);
}
// Re-initialize icons after dynamic content
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
{% endblock %}