a3546b4c01
- 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>
192 lines
8.1 KiB
HTML
192 lines
8.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ session.name }} - {{ 'breakout_session'|t }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="session-detail-page">
|
|
<div class="page-header">
|
|
<div class="header-content">
|
|
<h1>{{ session.name }}</h1>
|
|
<p class="page-subtitle">{{ session.event_name }}</p>
|
|
</div>
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('edit_breakout_session', session_id=session.id) }}" class="btn btn-outline">{{ 'edit_session'|t }}</a>
|
|
<a href="{{ url_for('list_breakout_sessions', event_id=session.event_id) }}" class="btn btn-ghost">{{ 'back_to_sessions'|t }}</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="session-meta-grid">
|
|
<div class="meta-card">
|
|
<div class="meta-icon">
|
|
<i data-lucide="calendar"></i>
|
|
</div>
|
|
<div class="meta-content">
|
|
<span class="meta-label">{{ 'time'|t }}</span>
|
|
<span class="meta-value">
|
|
{% if session.start_time %}
|
|
{{ session.start_time|localized_date('%H:%M') }} - {{ session.end_time|localized_date('%H:%M') }}
|
|
{% else %}
|
|
TBD
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="meta-card">
|
|
<div class="meta-icon">
|
|
<i data-lucide="map-pin"></i>
|
|
</div>
|
|
<div class="meta-content">
|
|
<span class="meta-label">{{ 'location'|t }}</span>
|
|
<span class="meta-value">{{ session.location or 'TBD' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="meta-card">
|
|
<div class="meta-icon">
|
|
<i data-lucide="users"></i>
|
|
</div>
|
|
<div class="meta-content">
|
|
<span class="meta-label">{{ 'capacity'|t }}</span>
|
|
<span class="meta-value">
|
|
{% if session.max_attendees %}
|
|
{{ rsvps|length }} / {{ session.max_attendees }}
|
|
{% else %}
|
|
{{ rsvps|length }} ({{ 'unlimited'|t }})
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if session.description %}
|
|
<div class="session-description-card">
|
|
<h3>{{ 'description'|t }}</h3>
|
|
<p>{{ session.description }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="session-sections">
|
|
<!-- Presenters Section -->
|
|
<section class="section-card presenters-section">
|
|
<div class="section-header">
|
|
<h2>{{ 'presenters'|t }}</h2>
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="toggleAddPresenter()">
|
|
<i data-lucide="plus"></i> {{ 'add_presenter'|t }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Presenter Form -->
|
|
<div id="add-presenter-form" class="add-presenter-form hidden">
|
|
<form method="POST" action="{{ url_for('add_presenter_to_session', session_id=session.id) }}">
|
|
<input type="hidden" name="code" value="{{ session.code }}">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="organizer_id">{{ 'select_organizer'|t }}</label>
|
|
<select id="organizer_id" name="organizer_id" required>
|
|
<option value="">{{ 'select_organizer'|t }}...</option>
|
|
{% for org in available_organizers %}
|
|
<option value="{{ org.id }}">{{ org.first_name }} {{ org.last_name }} ({{ org.email }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">{{ 'add_presenter'|t }}</button>
|
|
<button type="button" class="btn btn-ghost" onclick="toggleAddPresenter()">{{ 'cancel'|t }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{% if presenters %}
|
|
<div class="presenters-grid">
|
|
{% for presenter in presenters %}
|
|
<div class="presenter-card">
|
|
<div class="presenter-info">
|
|
<div class="presenter-avatar">
|
|
{{ presenter.first_name[0] }}{{ presenter.last_name[0] }}
|
|
</div>
|
|
<div class="presenter-details">
|
|
<h4>{{ presenter.first_name }} {{ presenter.last_name }}</h4>
|
|
<p>{{ presenter.email }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="presenter-qr">
|
|
<div class="qr-code-display" data-code="{{ presenter.presenter_code }}">
|
|
<span class="qr-label">{{ 'scan_here'|t }}</span>
|
|
<span class="qr-code">{{ presenter.presenter_code }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="presenter-actions">
|
|
<form method="POST" action="{{ url_for('remove_presenter_from_session', session_id=session.id, presenter_id=presenter.id) }}" class="inline-form">
|
|
<input type="hidden" name="code" value="{{ session.code }}">
|
|
<button type="submit" class="btn btn-sm btn-ghost btn-remove" data-confirm="{{ 'confirm_remove_presenter'|t }}">
|
|
<i data-lucide="trash-2"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i data-lucide="user-x"></i>
|
|
<p>{{ 'no_presenters_yet'|t }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Registered Attendees Section -->
|
|
<section class="section-card attendees-section">
|
|
<div class="section-header">
|
|
<h2>{{ 'registered_attendees'|t }}</h2>
|
|
<span class="badge badge-info">{{ rsvps|length }}</span>
|
|
</div>
|
|
|
|
{% if rsvps %}
|
|
<div class="table-container">
|
|
<table class="attendees-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ 'name'|t }}</th>
|
|
<th>{{ 'email'|t }}</th>
|
|
<th>{{ 'organisation'|t }}</th>
|
|
<th>{{ 'role'|t }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for rsvp in rsvps %}
|
|
<tr>
|
|
<td>
|
|
<div class="attendee-name-cell">
|
|
<span class="attendee-avatar-sm">{{ rsvp.first_name[0] }}{{ rsvp.last_name[0] }}</span>
|
|
{{ rsvp.first_name }} {{ rsvp.last_name }}
|
|
</div>
|
|
</td>
|
|
<td>{{ rsvp.email }}</td>
|
|
<td>{{ rsvp.organisation if rsvp.organisation else '-' }}</td>
|
|
<td>{{ rsvp.role if rsvp.role else '-' }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i data-lucide="users"></i>
|
|
<p>{{ 'no_attendees_registered'|t }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleAddPresenter() {
|
|
const form = document.getElementById('add-presenter-form');
|
|
form.classList.toggle('hidden');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
</script>
|
|
{% endblock %} |