64ab1d0412
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
653 lines
26 KiB
HTML
653 lines
26 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ event.name }} - NetEvents{% endblock %}
|
|
|
|
{% block content %}
|
|
<div style="padding: 20px;">
|
|
<style>
|
|
.section-box {
|
|
background: #fff;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.section-box h2 {
|
|
margin-top: 0;
|
|
margin-bottom: 15px;
|
|
color: #2c3e50;
|
|
}
|
|
.section-actions {
|
|
margin-bottom: 15px;
|
|
}
|
|
.staff-table,
|
|
.sessions-table,
|
|
.attendees-table {
|
|
width: 100%;
|
|
}
|
|
.form-row {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.form-group {
|
|
flex: 1;
|
|
}
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
}
|
|
.form-group input {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
th.sortable {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
th.sortable:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
.sort-icon::before {
|
|
content: '\2195';
|
|
margin-left: 5px;
|
|
opacity: 0.4;
|
|
}
|
|
th.sort-asc .sort-icon::before {
|
|
content: '\2191';
|
|
opacity: 1;
|
|
}
|
|
th.sort-desc .sort-icon::before {
|
|
content: '\2193';
|
|
opacity: 1;
|
|
}
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.modal-content {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
.modal-header h2 {
|
|
margin-top: 0;
|
|
color: #dc3545;
|
|
}
|
|
.modal-actions {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
/* Wide screen - spread content */
|
|
@media (min-width: 1400px) {
|
|
.event-detail {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.event-detail .section-box {
|
|
padding: 30px 40px;
|
|
}
|
|
|
|
.event-meta-box {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 15px;
|
|
}
|
|
|
|
.staff-section .section-box,
|
|
.breakout-sessions-section .section-box,
|
|
.attendees-section .section-box {
|
|
padding: 25px 30px;
|
|
}
|
|
}
|
|
|
|
/* Responsive styles */
|
|
@media (max-width: 768px) {
|
|
.event-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.event-meta-box p {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.form-row {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.staff-table,
|
|
.sessions-table,
|
|
.attendees-table {
|
|
display: block;
|
|
overflow-x: auto;
|
|
white-space: nowrap;
|
|
width: 100%;
|
|
}
|
|
|
|
.section-box {
|
|
padding: 15px;
|
|
}
|
|
|
|
.modal-content {
|
|
padding: 16px;
|
|
margin: 10px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
</style>
|
|
<div class="event-detail">
|
|
<div class="event-header">
|
|
<h1>{{ event.name }}</h1>
|
|
<div class="event-actions">
|
|
<a href="{{ url_for('edit_event', event_id=event.id) }}" class="btn btn-outline">{{ '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" style="padding: 2px 6px; font-size: 11px;" onclick="navigator.clipboard.writeText('{{ url_for('register_event', code=event.code, _external=True) }}').then(() => this.textContent = 'Copied!')">Copy</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">{{ '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 style="color: #27ae60; font-weight: 500;">{{ at.price|format_currency }}</span>
|
|
{% else %}
|
|
<span style="color: #7f8c8d;">{{ 'free'|t }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<code style="background: #f0f0f0; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
|
|
{{ url_for('register_event', code=event.code, type_code=at.code, _external=True) }}
|
|
</code>
|
|
<button type="button" class="btn btn-sm" style="padding: 2px 8px; margin-left: 8px;"
|
|
onclick="copyLink('{{ url_for('register_event', code=event.code, type_code=at.code, _external=True) }}')">
|
|
{{ 'copy'|t }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="no-types" style="color: #888;">{{ '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">{{ 'add_staff'|t }}</button>
|
|
</div>
|
|
|
|
<div id="add-staff-form" style="display: none; margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px;">
|
|
<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) }}" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;">
|
|
<div class="form-row" style="align-items: flex-end;">
|
|
<div class="form-group" style="flex: 1;">
|
|
<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="name" class="sortable">{{ '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="status" 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-name="{{ member.first_name }} {{ member.last_name }}" data-email="{{ member.email }}" data-status="{{ member.invite_token }}">
|
|
<td>{{ member.first_name }} {{ 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">{{ 'view'|t }}</a>
|
|
<button type="button" class="btn btn-sm btn-danger" onclick="showDeleteModal({{ member.id }}, '{{ member.first_name }} {{ member.last_name }}', '{{ member.email }}')">{{ 'remove'|t }}</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="no-staff">{{ 'no_staff_yet'|t }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<div id="staffDeleteModal" class="modal-overlay" style="display: none;">
|
|
<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 style="color: #666; font-size: 14px;">{{ '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" style="display: 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">{{ '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">{{ 'view'|t }}</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 }}</h2>
|
|
<div class="section-actions">
|
|
<a href="{{ url_for('register_attendee', code=event.code) }}" class="btn btn-primary">{{ 'add_attendee'|t }}</a>
|
|
<a href="{{ url_for('event_badges', event_id=event.id) }}" class="btn btn-secondary">🖨️ {{ 'print_badges'|t }}</a>
|
|
<a href="{{ url_for('download_rectangular_badges', event_id=event.id) }}" class="btn btn-secondary">🏷️ Rectangular Labels (80x50mm)</a>
|
|
<a href="{{ url_for('download_attendees_excel', event_id=event.id) }}" class="btn btn-secondary">📥 {{ '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 style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; display: flex; gap: 10px; align-items: center;">
|
|
<input type="checkbox" id="select-all-attendees" style="width: auto;">
|
|
<label for="select-all-attendees" style="margin: 0; font-weight: 500;">{{ 'select_all'|t }}</label>
|
|
<span style="margin-left: 20px;">{{ 'assign_type'|t }}:</span>
|
|
<select name="attendee_type_id" id="batch-type-select" style="padding: 5px 10px;">
|
|
<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" style="color: #888; font-size: 12px;"></span>
|
|
</div>
|
|
</form>
|
|
|
|
<table class="attendees-table" id="attendees-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 40px;"></th>
|
|
<th data-sort="name" class="sortable">{{ '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="type" class="sortable">{{ 'type'|t }} <span class="sort-icon"></span></th>
|
|
<th data-sort="status" 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-name="{{ attendee.first_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 }} {{ attendee.last_name }}</td>
|
|
<td>{{ (attendee.organisation)|spacify if attendee.organisation else '-' }}</td>
|
|
<td>{{ (attendee.role)|spacify if attendee.role else '-' }}</td>
|
|
<td>
|
|
{% if attendee.attendee_type_name %}
|
|
<span class="badge" style="background: #3498db; color: white;">{{ attendee.attendee_type_name }}</span>
|
|
{% else %}
|
|
<span style="color: #aaa;">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if attendee.checked_in %}
|
|
<span class="badge badge-success">{{ '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 }}">{{ 'check_in'|t }}</button>
|
|
{% else %}
|
|
<span class="text-muted">{{ 'checked_in'|t }}</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="no-attendees">{{ 'no_attendees_yet'|t }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById('add-staff-btn').addEventListener('click', function() {
|
|
document.getElementById('add-staff-form').style.display = 'block';
|
|
this.style.display = 'none';
|
|
});
|
|
|
|
document.getElementById('cancel-staff-btn').addEventListener('click', function() {
|
|
document.getElementById('add-staff-form').style.display = 'none';
|
|
document.getElementById('add-staff-btn').style.display = 'inline-block';
|
|
});
|
|
|
|
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 => {
|
|
alert('{{ "error_adding_staff"|t }}');
|
|
});
|
|
});
|
|
</script>
|
|
</div>
|
|
|
|
<script>
|
|
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) {
|
|
// Update status cell (6th td) and remove checkin button
|
|
const row = this.closest('tr');
|
|
row.querySelector('td:nth-child(6)').innerHTML = '<span class="badge badge-success">{{ "checked_in"|t }}</span>';
|
|
this.remove();
|
|
document.getElementById('attendee-count').textContent = parseInt(document.getElementById('attendee-count').textContent) + 1;
|
|
}
|
|
} catch (error) {
|
|
alert('{{ "error_checking_in"|t }}');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Attendee table sorting
|
|
const sortableHeaders = document.querySelectorAll('#attendees-table th.sortable');
|
|
let currentSort = { column: null, direction: 'asc' };
|
|
|
|
sortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
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('attendees-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));
|
|
});
|
|
});
|
|
|
|
// Staff table sorting
|
|
const staffSortableHeaders = document.querySelectorAll('#event-staff-table th.sortable');
|
|
let staffCurrentSort = { column: null, direction: 'asc' };
|
|
|
|
staffSortableHeaders.forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const column = th.dataset.sort;
|
|
const direction = staffCurrentSort.column === column && staffCurrentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
staffCurrentSort = { column, direction };
|
|
|
|
staffSortableHeaders.forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
|
|
th.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
|
|
const tbody = document.getElementById('event-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 === '' ? 1 : 0;
|
|
valB = valB === '' ? 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, 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').style.display = 'flex';
|
|
}
|
|
|
|
function closeStaffDeleteModal() {
|
|
document.getElementById('staffDeleteModal').style.display = 'none';
|
|
}
|
|
|
|
// 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'|t }})` : '';
|
|
}
|
|
|
|
// Copy link function
|
|
function copyLink(url) {
|
|
// Try modern clipboard API first
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(url).then(function() {
|
|
alert('{{ 'link_copied'|t }}');
|
|
}, function(err) {
|
|
// Fallback for HTTP sites
|
|
fallbackCopy(url);
|
|
});
|
|
} else {
|
|
// Fallback for older browsers or HTTP sites
|
|
fallbackCopy(url);
|
|
}
|
|
}
|
|
|
|
function fallbackCopy(url) {
|
|
var textArea = document.createElement('textarea');
|
|
textArea.value = url;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-9999px';
|
|
textArea.style.top = '-9999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
try {
|
|
var successful = document.execCommand('copy');
|
|
if (successful) {
|
|
alert('{{ 'link_copied'|t }}');
|
|
} else {
|
|
alert('{{ 'copy_failed'|t }}');
|
|
}
|
|
} catch (err) {
|
|
alert('{{ 'copy_failed'|t }}');
|
|
}
|
|
document.body.removeChild(textArea);
|
|
}
|
|
|
|
// Initial selected count
|
|
updateSelectedCount();
|
|
</script>
|
|
</div>
|
|
{% endblock %}
|