Files
conference/templates/organizer/event_staff.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

148 lines
6.4 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ 'staff'|t }} - {{ event.name }}{% endblock %}
{% block content %}
<div class="event-staff">
<div class="staff-header">
<h1>{{ 'staff_for'|t }} {{ event.name }}</h1>
<a href="{{ url_for('event_detail', code=event.code) }}" class="btn btn-outline"><i data-lucide="arrow-left"></i> {{ 'back_to_event'|t }}</a>
</div>
<section class="add-staff-form card">
<h2>{{ 'add_staff_member'|t }}</h2>
<form method="POST" action="{{ url_for('manage_event_staff', event_id=event.id) }}" class="staff-form">
<div class="form-row">
<div class="form-group">
<label for="first_name">{{ 'first_name'|t }}</label>
<input type="text" id="first_name" name="first_name" required>
</div>
<div class="form-group">
<label for="last_name">{{ 'last_name'|t }}</label>
<input type="text" id="last_name" name="last_name" required>
</div>
</div>
<div class="form-group">
<label for="email">{{ 'email'|t }}</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><i data-lucide="user-plus"></i> {{ 'add_staff_member'|t }}</button>
</div>
</form>
</section>
<section class="staff-list card">
<h2>{{ 'current_staff'|t }} ({{ staff_members|length }})</h2>
{% if staff_members %}
<table class="staff-table">
<thead>
<tr>
<th data-sort="first_name" class="sortable">{{ 'first_name'|t }} <span class="sort-icon"></span></th>
<th data-sort="last_name" class="sortable">{{ 'last_name'|t }} <span class="sort-icon"></span></th>
<th data-sort="email" class="sortable">{{ 'email'|t }} <span class="sort-icon"></span></th>
<th data-sort="invite_used" class="sortable">{{ 'status'|t }} <span class="sort-icon"></span></th>
<th>{{ 'actions'|t }}</th>
</tr>
</thead>
<tbody id="staff-tbody">
{% for staff in staff_members %}
<tr data-first-name="{{ staff.first_name }}" data-last-name="{{ staff.last_name }}" data-email="{{ staff.email }}" data-status="{{ staff.invite_used }}">
<td>{{ staff.first_name }}</td>
<td>{{ staff.last_name }}</td>
<td>{{ staff.email }}</td>
<td>
{% if staff.invite_used %}
<span class="badge badge-success"><i data-lucide="check"></i> {{ 'active'|t }}</span>
{% else %}
<span class="badge badge-pending"><i data-lucide="clock"></i> {{ 'invite_pending'|t }}</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('edit_staff', event_id=event.id, staff_id=staff.id) }}" class="btn btn-sm btn-outline"><i data-lucide="pencil"></i></a>
<button type="button" class="btn btn-sm btn-danger" onclick="showDeleteModal({{ staff.id }}, '{{ staff.first_name }} {{ staff.last_name }}')"><i data-lucide="trash-2"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="no-staff">{{ 'no_staff_yet'|t }}</p>
{% endif %}
</section>
</div>
<div id="deleteModal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2>{{ 'remove_staff_member'|t }}</h2>
<p>{{ 'confirm_remove_staff'|t }} <strong id="staffName"></strong>?</p>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeDeleteModal()">{{ 'cancel'|t }}</button>
<form id="deleteForm" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger">{{ 'remove'|t }}</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
});
const sortableHeaders = document.querySelectorAll('.staff-table th.sortable');
let currentSort = { column: 'first_name', direction: 'asc' };
sortableHeaders.forEach(th => {
th.addEventListener('click', () => {
const column = th.dataset.sort;
if (!column) return;
const direction = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc';
currentSort = { column, direction };
sortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
const tbody = document.getElementById('staff-tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
let valA = a.dataset[column] || '';
let valB = b.dataset[column] || '';
if (column === 'status') {
valA = valA === 'True' ? 1 : 0;
valB = valB === 'True' ? 1 : 0;
} else {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
if (valA < valB) return direction === 'asc' ? -1 : 1;
if (valA > valB) return direction === 'asc' ? 1 : -1;
return 0;
});
rows.forEach(row => tbody.appendChild(row));
});
});
function showDeleteModal(staffId, staffName) {
document.getElementById('staffName').textContent = staffName;
document.getElementById('deleteForm').action = '{{ url_for("delete_staff", event_id=event.id, staff_id=0) }}'.replace('0', staffId);
document.getElementById('deleteModal').style.display = 'flex';
}
function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none';
}
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
</script>
{% endblock %}