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>
647 lines
32 KiB
HTML
647 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ event.name }} - NetEvents{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="event-detail">
|
|
<div class="event-header">
|
|
<h1>{{ event.name }}</h1>
|
|
<div class="event-actions">
|
|
<a href="{{ url_for('event_communications', event_id=event.id) }}" class="btn btn-outline"><i data-lucide="mail"></i> {{ 'communications'|t }}</a>
|
|
<a href="{{ url_for('edit_event', event_id=event.id) }}" class="btn btn-outline"><i data-lucide="pencil"></i> {{ 'edit_event'|t }}</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if event.description %}
|
|
<div class="event-description-box">
|
|
<h3>{{ 'description'|t }}</h3>
|
|
<p>{{ event.description }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="event-meta-box">
|
|
<p><strong>{{ 'start'|t }}:</strong> {{ event.start_time|localized_date if event.start_time else 'TBD' }}</p>
|
|
{% if event.end_time %}
|
|
<p><strong>{{ 'end'|t }}:</strong> {{ event.end_time|localized_date }}</p>
|
|
{% endif %}
|
|
<p><strong>{{ 'location'|t }}:</strong> {{ event.location }}</p>
|
|
<p><strong>{{ 'max_attendees'|t }}:</strong> {{ event.max_attendees or ('unlimited'|t) }}</p>
|
|
<p><strong>{{ 'registered_attendees'|t }}:</strong> <span id="attendee-count">{{ attendees|length }}</span></p>
|
|
<p><strong>{{ 'registration_link'|t }}:</strong> <span id="reg-link">{{ url_for('register_event', code=event.code, _external=True) }}</span> <button type="button" class="btn btn-sm btn-ghost" onclick="copyLink('{{ url_for('register_event', code=event.code, _external=True) }}')"><i data-lucide="copy"></i></button></p>
|
|
</div>
|
|
|
|
<section class="attendee-types-section">
|
|
<div class="section-box">
|
|
<h2>{{ 'attendee_types'|t }}</h2>
|
|
<div class="section-actions">
|
|
<a href="{{ url_for('manage_attendee_types', event_id=event.id) }}" class="btn btn-primary"><i data-lucide="tag"></i> {{ 'manage_types'|t }}</a>
|
|
</div>
|
|
|
|
{% if attendee_types %}
|
|
<table class="types-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ 'type_name'|t }}</th>
|
|
<th>{{ 'price'|t }}</th>
|
|
<th>{{ 'registration_link'|t }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for at in attendee_types %}
|
|
<tr>
|
|
<td><strong>{{ at.name }}</strong></td>
|
|
<td>
|
|
{% if at.price and at.price > 0 %}
|
|
<span class="text-success font-medium">{{ at.price|format_currency }}</span>
|
|
{% else %}
|
|
<span class="text-muted">{{ 'free'|t }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<code class="code">
|
|
{{ url_for('register_event', code=event.code, type_code=at.code, _external=True) }}
|
|
</code>
|
|
<button type="button" class="btn btn-sm btn-ghost"
|
|
onclick="copyLink('{{ url_for('register_event', code=event.code, type_code=at.code, _external=True) }}')">
|
|
<i data-lucide="copy"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="no-types">{{ 'no_attendee_types_defined'|t }} <a href="{{ url_for('manage_attendee_types', event_id=event.id) }}">{{ 'create_first_type'|t }}</a></p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staff-section">
|
|
<div class="section-box">
|
|
<h2>{{ 'staff'|t }}</h2>
|
|
<div class="section-actions">
|
|
<button type="button" class="btn btn-primary" id="add-staff-btn"><i data-lucide="user-plus"></i> {{ 'add_staff'|t }}</button>
|
|
</div>
|
|
|
|
<div id="add-staff-form" class="add-staff-form d-none">
|
|
<form method="POST" action="{{ url_for('manage_event_staff', event_id=event.id) }}" id="staff-inline-form">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="staff_first_name">{{ 'first_name'|t }}</label>
|
|
<input type="text" id="staff_first_name" name="first_name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="staff_last_name">{{ 'last_name'|t }}</label>
|
|
<input type="text" id="staff_last_name" name="last_name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="staff_email">{{ 'email'|t }}</label>
|
|
<input type="email" id="staff_email" name="email" required>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">{{ 'add_staff_member'|t }}</button>
|
|
<button type="button" class="btn btn-outline" id="cancel-staff-btn">{{ 'cancel'|t }}</button>
|
|
</form>
|
|
{% if other_events %}
|
|
<form method="POST" action="{{ url_for('batch_add_staff', event_id=event.id) }}" class="border-top pt-4 mt-4">
|
|
<div class="form-row align-items-end">
|
|
<div class="form-group">
|
|
<label for="batch_source_event">Or add staff from another event:</label>
|
|
<select name="source_event_id" id="batch_source_event" required>
|
|
<option value="">Select event...</option>
|
|
{% for other_event in other_events %}
|
|
<option value="{{ other_event.id }}">{{ other_event.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-secondary">Batch Add</button>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if staff %}
|
|
<table class="staff-table" id="event-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_token" class="sortable">{{ 'status'|t }} <span class="sort-icon"></span></th>
|
|
<th>{{ 'actions'|t }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="event-staff-tbody">
|
|
{% for member in staff %}
|
|
<tr data-first-name="{{ member.first_name }}" data-last-name="{{ member.last_name }}" data-email="{{ member.email }}" data-status="{{ member.invite_token }}">
|
|
<td>{{ member.first_name }}</td>
|
|
<td>{{ member.last_name }}</td>
|
|
<td>{{ member.email }}</td>
|
|
<td>
|
|
{% if member.invite_token %}
|
|
<span class="badge badge-pending">{{ 'invite_pending'|t }}</span>
|
|
{% else %}
|
|
<span class="badge badge-success">{{ 'active'|t }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('edit_staff', event_id=event.id, staff_id=member.id) }}" class="btn btn-sm btn-success"><i data-lucide="eye"></i></a>
|
|
<button type="button" class="btn btn-sm btn-danger" onclick="showDeleteModal({{ member.id }}, '{{ member.first_name }} {{ member.last_name }}', '{{ member.email }}')"><i data-lucide="trash-2"></i></button>
|
|
{% if member.invite_token %}
|
|
<button type="button" class="btn btn-sm btn-outline remind-btn" data-staff-id="{{ member.id }}" data-reminder-dates="{{ member.reminder_dates|tojson }}" title="{{ 'send_reminder'|t }}">
|
|
<i data-lucide="bell"></i> <span class="reminder-count">{{ member.reminder_count|default(0) }}</span>
|
|
</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if total_staff_pages > 1 %}
|
|
<div class="pagination mt-4" id="staff-pagination">
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="loadStaffPage({{ staff_page - 1 }})" {% if staff_page <= 1 %}disabled{% endif %}>
|
|
<i data-lucide="chevron-left"></i>
|
|
</button>
|
|
<span class="pagination-info">{{ 'page'|t }} <span id="staff-current-page">{{ staff_page }}</span> {{ 'of'|t }} {{ total_staff_pages }}</span>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="loadStaffPage({{ staff_page + 1 }})" {% if staff_page >= total_staff_pages %}disabled{% endif %}>
|
|
<i data-lucide="chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="no-staff">{{ 'no_staff_yet'|t }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<div id="staffDeleteModal" 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> {{ 'from_event'|t }}?</p>
|
|
<p class="text-muted text-sm">{{ 'email'|t }}: <span id="staffEmail"></span></p>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeStaffDeleteModal()">{{ 'cancel'|t }}</button>
|
|
<form id="staffDeleteForm" method="POST" class="d-inline">
|
|
<button type="submit" class="btn btn-danger">{{ 'remove'|t }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="breakout-sessions-section">
|
|
<div class="section-box">
|
|
<h2>{{ 'breakout_sessions'|t }}</h2>
|
|
<div class="section-actions">
|
|
<a href="{{ url_for('create_breakout_session', event_id=event.id) }}" class="btn btn-primary"><i data-lucide="plus"></i> {{ 'add_breakout_session'|t }}</a>
|
|
</div>
|
|
|
|
{% if breakout_sessions %}
|
|
<table class="sessions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ 'name'|t }}</th>
|
|
<th>{{ 'time'|t }}</th>
|
|
<th>{{ 'location'|t }}</th>
|
|
<th>{{ 'max_attendees'|t }}</th>
|
|
<th>{{ 'registered'|t }}</th>
|
|
<th>{{ 'actions'|t }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for session in breakout_sessions %}
|
|
<tr>
|
|
<td>{{ session.name }}</td>
|
|
<td>{{ session.start_time|localized_date }}</td>
|
|
<td>{{ session.location or '-' }}</td>
|
|
<td>{{ session.max_attendees or ('unlimited'|t) }}</td>
|
|
<td>{{ session.registered_count|default(0) }}</td>
|
|
<td>
|
|
<a href="{{ url_for('view_breakout_session', code=session.code) }}" class="btn btn-sm btn-success"><i data-lucide="eye"></i></a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="no-sessions">{{ 'no_breakout_sessions_yet'|t }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="attendees-section">
|
|
<div class="section-box">
|
|
<h2>{{ 'registered_attendees'|t }} ({{ attendees|length }})</h2>
|
|
<div class="section-actions">
|
|
<a href="{{ url_for('register_attendee', code=event.code) }}" class="btn btn-primary"><i data-lucide="user-plus"></i> {{ 'add_attendee'|t }}</a>
|
|
<a href="{{ url_for('event_badges', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="printer"></i> {{ 'print_badges'|t }}</a>
|
|
<a href="{{ url_for('download_rectangular_badges', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="tag"></i> Rectangular Labels</a>
|
|
<a href="{{ url_for('download_attendees_excel', event_id=event.id) }}" class="btn btn-secondary"><i data-lucide="download"></i> {{ 'download_excel'|t }}</a>
|
|
</div>
|
|
|
|
{% if attendees %}
|
|
<form method="POST" action="{{ url_for('batch_assign_attendee_type', event_id=event.id) }}" id="batch-assign-form">
|
|
<div class="batch-select-bar">
|
|
<input type="checkbox" id="select-all-attendees">
|
|
<label for="select-all-attendees" class="m-0 font-semibold">{{ 'select_all'|t }}</label>
|
|
<span class="ml-4">{{ 'assign_type'|t }}:</span>
|
|
<select name="attendee_type_id" id="batch-type-select" class="batch-type-select">
|
|
<option value="">{{ 'no_type'|t }}</option>
|
|
{% for at in attendee_types %}
|
|
<option value="{{ at.id }}">{{ at.name }} {% if at.price and at.price > 0 %}({{ at.price|format_currency }}){% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit" class="btn btn-sm btn-primary">{{ 'apply'|t }}</button>
|
|
<span id="selected-count" class="text-muted text-sm"></span>
|
|
</div>
|
|
</form>
|
|
|
|
<table class="attendees-table" id="attendees-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="checkbox-col"></th>
|
|
<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="organisation" class="sortable">{{ 'organisation'|t }} <span class="sort-icon"></span></th>
|
|
<th data-sort="role" class="sortable">{{ 'role'|t }} <span class="sort-icon"></span></th>
|
|
<th data-sort="attendee_type_id" class="sortable">{{ 'type'|t }} <span class="sort-icon"></span></th>
|
|
<th data-sort="checked_in" class="sortable">{{ 'status'|t }} <span class="sort-icon"></span></th>
|
|
<th>{{ 'actions'|t }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="attendees-tbody">
|
|
{% for attendee in attendees %}
|
|
<tr data-first-name="{{ attendee.first_name }}" data-last-name="{{ attendee.last_name }}" data-organisation="{{ attendee.organisation or '' }}" data-role="{{ attendee.role or '' }}" data-status="{{ attendee.checked_in }}" data-type="{{ attendee.attendee_type_name or '' }}">
|
|
<td>
|
|
<input type="checkbox" name="attendee_ids" value="{{ attendee.id }}" class="attendee-checkbox" form="batch-assign-form">
|
|
</td>
|
|
<td>{{ attendee.first_name }}</td>
|
|
<td>{{ attendee.last_name }}</td>
|
|
<td>{{ attendee.organisation if attendee.organisation else '-' }}</td>
|
|
<td>{{ attendee.role if attendee.role else '-' }}</td>
|
|
<td>
|
|
{% if attendee.attendee_type_name %}
|
|
<span class="badge badge-info">{{ attendee.attendee_type_name }}</span>
|
|
{% else %}
|
|
<span class="text-light">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if attendee.checked_in %}
|
|
<span class="badge badge-success"><i data-lucide="check"></i> {{ 'checked_in'|t }}</span>
|
|
{% else %}
|
|
<span class="badge badge-pending">{{ 'not_checked_in'|t }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if not attendee.checked_in %}
|
|
<button class="btn btn-sm btn-success checkin-btn" data-attendee-id="{{ attendee.id }}"><i data-lucide="user-check"></i></button>
|
|
{% else %}
|
|
<span class="text-muted"><i data-lucide="check"></i></span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination mt-4" id="attendees-pagination">
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="loadAttendeesPage({{ page - 1 }})" {% if page <= 1 %}disabled{% endif %}>
|
|
<i data-lucide="chevron-left"></i>
|
|
</button>
|
|
<span class="pagination-info">{{ 'page'|t }} <span id="attendees-current-page">{{ page }}</span> {{ 'of'|t }} {{ total_pages }}</span>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="loadAttendeesPage({{ page + 1 }})" {% if page >= total_pages %}disabled{% endif %}>
|
|
<i data-lucide="chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="no-attendees">{{ 'no_attendees_yet'|t }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
|
|
document.getElementById('add-staff-btn').addEventListener('click', function() {
|
|
document.getElementById('add-staff-form').classList.add('shown');
|
|
this.classList.add('d-none');
|
|
});
|
|
|
|
document.getElementById('cancel-staff-btn').addEventListener('click', function() {
|
|
document.getElementById('add-staff-form').classList.remove('shown');
|
|
document.getElementById('add-staff-btn').classList.remove('d-none');
|
|
});
|
|
|
|
document.getElementById('staff-inline-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const form = this;
|
|
const formData = new FormData(form);
|
|
|
|
fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(response => {
|
|
if (response.redirected) {
|
|
window.location.href = response.url;
|
|
} else {
|
|
location.reload();
|
|
}
|
|
}).catch(error => {
|
|
showToast('{{ "error_adding_staff"|t }}', 'error');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.checkin-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const attendeeId = this.dataset.attendeeId;
|
|
const eventId = {{ event.id }};
|
|
|
|
try {
|
|
const response = await fetch(`/organizer/event/${eventId}/checkin/${attendeeId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
const row = this.closest('tr');
|
|
row.querySelector('td:nth-child(6)').innerHTML = '<span class="badge badge-success"><i data-lucide="check"></i> {{ "checked_in"|t }}</span>';
|
|
this.remove();
|
|
document.getElementById('attendee-count').textContent = parseInt(document.getElementById('attendee-count').textContent) + 1;
|
|
lucide.createIcons();
|
|
}
|
|
} catch (error) {
|
|
showToast('{{ "error_checking_in"|t }}', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Attendee table sorting - AJAX-based with backend sorting
|
|
let currentAttendeeSort = { column: 'first_name', direction: 'asc' };
|
|
|
|
const sortableHeaders = document.querySelectorAll('#attendees-table th.sortable');
|
|
sortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
if (!column) return;
|
|
const direction = currentAttendeeSort.column === column && currentAttendeeSort.direction === 'asc' ? 'desc' : 'asc';
|
|
currentAttendeeSort = { column, direction };
|
|
|
|
sortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
|
|
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
|
|
loadAttendeesPage(1);
|
|
});
|
|
});
|
|
|
|
// Staff table sorting - AJAX-based with backend sorting
|
|
let currentStaffSort = { column: 'first_name', direction: 'asc' };
|
|
|
|
const staffSortableHeaders = document.querySelectorAll('#event-staff-table th.sortable');
|
|
staffSortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
if (!column) return;
|
|
const direction = currentStaffSort.column === column && currentStaffSort.direction === 'asc' ? 'desc' : 'asc';
|
|
currentStaffSort = { column, direction };
|
|
|
|
staffSortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
|
|
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
|
|
loadStaffPage(1);
|
|
});
|
|
});
|
|
|
|
function showDeleteModal(staffId, staffName, staffEmail) {
|
|
document.getElementById('staffName').textContent = staffName;
|
|
document.getElementById('staffEmail').textContent = staffEmail;
|
|
document.getElementById('staffDeleteForm').action = '{{ url_for("delete_staff", event_id=event.id, staff_id=0) }}'.replace('0', staffId);
|
|
document.getElementById('staffDeleteModal').classList.add('active');
|
|
}
|
|
|
|
function closeStaffDeleteModal() {
|
|
document.getElementById('staffDeleteModal').classList.remove('active');
|
|
}
|
|
|
|
document.getElementById('staffDeleteModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeStaffDeleteModal();
|
|
}
|
|
});
|
|
|
|
// Reminder button handler
|
|
document.querySelectorAll('.remind-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const staffId = this.dataset.staffId;
|
|
const countSpan = this.querySelector('.reminder-count');
|
|
|
|
try {
|
|
const response = await fetch(`{{ url_for('send_staff_reminder', event_id=event.id, staff_id=0) }}`.replace('0', staffId), {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
countSpan.textContent = data.reminder_count;
|
|
// Update the data attribute with new dates
|
|
this.dataset.reminderDates = JSON.stringify(data.reminder_dates);
|
|
showToast('{{ "reminder_sent"|t }}', 'success');
|
|
} else {
|
|
showToast(data.error || '{{ "error_sending_reminder"|t }}', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('{{ "error_sending_reminder"|t }}', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Select all attendees checkbox
|
|
document.getElementById('select-all-attendees').addEventListener('change', function() {
|
|
const checkboxes = document.querySelectorAll('.attendee-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = this.checked);
|
|
updateSelectedCount();
|
|
});
|
|
|
|
document.querySelectorAll('.attendee-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', updateSelectedCount);
|
|
});
|
|
|
|
function updateSelectedCount() {
|
|
const selected = document.querySelectorAll('.attendee-checkbox:checked').length;
|
|
document.getElementById('selected-count').textContent = selected > 0 ? `(${selected} selected)` : '';
|
|
}
|
|
|
|
updateSelectedCount();
|
|
|
|
// Staff pagination via AJAX
|
|
function loadStaffPage(pageNum) {
|
|
const currentUrl = new URL(window.location.href);
|
|
currentUrl.searchParams.set('staff_page', pageNum);
|
|
currentUrl.searchParams.set('staff_sort_by', currentStaffSort.column);
|
|
currentUrl.searchParams.set('staff_sort_order', currentStaffSort.direction);
|
|
|
|
fetch(currentUrl.pathname + '?' + currentUrl.searchParams.toString(), {
|
|
headers: { 'Accept': 'text/html' }
|
|
})
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
const staffSection = doc.querySelector('.staff-section .section-box');
|
|
const staffPagination = doc.querySelector('#staff-pagination');
|
|
|
|
if (staffSection && staffPagination) {
|
|
const currentStaffSection = document.querySelector('.staff-section .section-box');
|
|
currentStaffSection.innerHTML = staffSection.innerHTML;
|
|
|
|
const currentPagination = document.querySelector('#staff-pagination');
|
|
if (currentPagination) {
|
|
currentPagination.innerHTML = staffPagination.innerHTML;
|
|
}
|
|
|
|
lucide.createIcons();
|
|
|
|
// Re-attach staff sort handlers
|
|
const newStaffSortableHeaders = document.querySelectorAll('#event-staff-table th.sortable');
|
|
newStaffSortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
if (!column) return;
|
|
const direction = currentStaffSort.column === column && currentStaffSort.direction === 'asc' ? 'desc' : 'asc';
|
|
currentStaffSort = { column, direction };
|
|
|
|
newStaffSortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
|
|
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
|
|
loadStaffPage(1);
|
|
});
|
|
});
|
|
|
|
// Re-attach reminder handlers
|
|
document.querySelectorAll('.remind-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const staffId = this.dataset.staffId;
|
|
const countSpan = this.querySelector('.reminder-count');
|
|
|
|
try {
|
|
const response = await fetch(`{{ url_for('send_staff_reminder', event_id=event.id, staff_id=0) }}`.replace('0', staffId), {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
countSpan.textContent = data.reminder_count;
|
|
this.dataset.reminderDates = JSON.stringify(data.reminder_dates);
|
|
showToast('{{ "reminder_sent"|t }}', 'success');
|
|
} else {
|
|
showToast(data.error || '{{ "error_sending_reminder"|t }}', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('{{ "error_sending_reminder"|t }}', 'error');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
window.location.href = currentUrl.toString();
|
|
});
|
|
}
|
|
|
|
// Attendees pagination via AJAX
|
|
function loadAttendeesPage(pageNum) {
|
|
const currentUrl = new URL(window.location.href);
|
|
currentUrl.searchParams.set('page', pageNum);
|
|
currentUrl.searchParams.set('sort_by', currentAttendeeSort.column);
|
|
currentUrl.searchParams.set('sort_order', currentAttendeeSort.direction);
|
|
|
|
fetch(currentUrl.pathname + '?' + currentUrl.searchParams.toString(), {
|
|
headers: { 'Accept': 'text/html' }
|
|
})
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
const attendeesSection = doc.querySelector('.attendees-section .section-box');
|
|
const attendeesPagination = doc.querySelector('#attendees-pagination');
|
|
|
|
if (attendeesSection && attendeesPagination) {
|
|
const currentAttendeesSection = document.querySelector('.attendees-section .section-box');
|
|
currentAttendeesSection.innerHTML = attendeesSection.innerHTML;
|
|
|
|
const currentPagination = document.querySelector('#attendees-pagination');
|
|
if (currentPagination) {
|
|
currentPagination.innerHTML = attendeesPagination.innerHTML;
|
|
}
|
|
|
|
lucide.createIcons();
|
|
|
|
// Re-attach attendee sort handlers
|
|
const newSortableHeaders = document.querySelectorAll('#attendees-table th.sortable');
|
|
newSortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
if (!column) return;
|
|
const direction = currentAttendeeSort.column === column && currentAttendeeSort.direction === 'asc' ? 'desc' : 'asc';
|
|
currentAttendeeSort = { column, direction };
|
|
|
|
newSortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
|
|
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
|
|
loadAttendeesPage(1);
|
|
});
|
|
});
|
|
|
|
// Re-attach checkin handlers
|
|
document.querySelectorAll('.checkin-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const attendeeId = this.dataset.attendeeId;
|
|
const eventId = {{ event.id }};
|
|
|
|
try {
|
|
const response = await fetch(`/organizer/event/${eventId}/checkin/${attendeeId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
const row = this.closest('tr');
|
|
row.querySelector('td:nth-child(7)').innerHTML = '<span class="badge badge-success"><i data-lucide="check"></i> {{ "checked_in"|t }}</span>';
|
|
this.remove();
|
|
document.getElementById('attendee-count').textContent = parseInt(document.getElementById('attendee-count').textContent) + 1;
|
|
lucide.createIcons();
|
|
}
|
|
} catch (error) {
|
|
showToast('{{ "error_checking_in"|t }}', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Re-attach select all handler
|
|
const selectAllCheckbox = document.getElementById('select-all-attendees');
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.addEventListener('change', function() {
|
|
const checkboxes = document.querySelectorAll('.attendee-checkbox');
|
|
checkboxes.forEach(cb => cb.checked = this.checked);
|
|
updateSelectedCount();
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.attendee-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', updateSelectedCount);
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
window.location.href = currentUrl.toString();
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |