431 lines
24 KiB
HTML
431 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block content %}
|
|
<div class="container py-5">
|
|
<div class="mb-4">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'event_list' %}">Events</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">{{ event.name|default:event.event_type }}</li>
|
|
</ol>
|
|
</nav>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h1 class="h2 mb-0">{{ event.name|default:event.event_type }}</h1>
|
|
<div class="d-flex gap-2">
|
|
<a href="{% url 'event_edit' event.id %}" class="btn btn-outline-secondary btn-sm">Edit Event Info</a>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addVolunteerModal">
|
|
+ Add Volunteer
|
|
</button>
|
|
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addParticipantModal">
|
|
+ Add Participant
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Event Details Column -->
|
|
<div class="col-lg-4">
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<h5 class="card-title fw-bold mb-4">Event Details</h5>
|
|
<div class="mb-3">
|
|
<label class="small text-muted text-uppercase fw-bold d-block">Type</label>
|
|
<span>{{ event.event_type }}</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted text-uppercase fw-bold d-block">Date</label>
|
|
<span>{{ event.date|date:"F d, Y" }}</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted text-uppercase fw-bold d-block">Time</label>
|
|
<span>
|
|
{% if event.start_time %}
|
|
{{ event.start_time|time:"g:i A" }}
|
|
{% if event.end_time %} - {{ event.end_time|time:"g:i A" }}{% endif %}
|
|
{% else %}
|
|
Not specified
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="small text-muted text-uppercase fw-bold d-block">Location</label>
|
|
{% if event.location_name %}<strong>{{ event.location_name }}</strong><br>{% endif %}
|
|
<span>
|
|
{% if event.address %}
|
|
{{ event.address }}<br>
|
|
{{ event.city }}{% if event.city and event.state %}, {% endif %}{{ event.state }} {{ event.zip_code }}
|
|
{% elif event.city or event.state or event.zip_code %}
|
|
{{ event.city }}{% if event.city and event.state %}, {% endif %}{{ event.state }} {{ event.zip_code }}
|
|
{% else %}
|
|
No location provided.
|
|
{% endif %}
|
|
</span>
|
|
{% if event.latitude and event.longitude %}
|
|
<div class="mt-2 small text-muted">
|
|
<i class="bi bi-geo-alt"></i> {{ event.latitude }}, {{ event.longitude }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="mb-0">
|
|
<label class="small text-muted text-uppercase fw-bold d-block">Description</label>
|
|
<p class="mb-0 text-muted">{{ event.description|default:"No description provided." }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Volunteers Card -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0 fw-bold">Volunteers ({{ volunteers.count }})</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="ps-4">Volunteer</th>
|
|
<th>Role</th>
|
|
<th class="pe-4 text-end"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for v in volunteers %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<a href="{% url 'volunteer_detail' v.volunteer.id %}" class="fw-semibold text-primary text-decoration-none">
|
|
{{ v.volunteer.first_name }} {{ v.volunteer.last_name }}
|
|
</a>
|
|
</td>
|
|
<td><span class="small">{{ v.role }}</span></td>
|
|
<td class="pe-4 text-end">
|
|
<form action="{% url 'event_remove_volunteer' v.id %}" method="POST" onsubmit="return confirm('Remove this volunteer?')">
|
|
{% csrf_token %}
|
|
<button type="submit" class="btn btn-sm btn-link text-danger p-0">
|
|
<i class="bi bi-x-circle"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="3" class="text-center py-4 text-muted small">
|
|
No volunteers assigned.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Participants Column -->
|
|
<div class="col-lg-8">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0 fw-bold">Participants ({{ participations.count }})</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="ps-4">Voter Name</th>
|
|
<th>Status</th>
|
|
<th class="pe-4 text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for p in participations %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<a href="{% url 'voter_detail' p.voter.id %}" class="fw-semibold text-primary text-decoration-none">
|
|
{{ p.voter.first_name }} {{ p.voter.last_name }}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<span class="badge {% if p.participation_status.name == 'Attended' %}bg-success{% elif p.participation_status.name == 'Canceled' %}bg-danger{% else %}bg-info{% endif %} bg-opacity-10 text-{% if p.participation_status.name == 'Attended' %}success{% elif p.participation_status.name == 'Canceled' %}danger{% else %}info{% endif %} border border-{% if p.participation_status.name == 'Attended' %}success{% elif p.participation_status.name == 'Canceled' %}danger{% else %}info{% endif %}">
|
|
{{ p.participation_status.name }}
|
|
</span>
|
|
</td>
|
|
<td class="pe-4 text-end">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
|
Actions
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
|
<li><h6 class="dropdown-header">Update Status</h6></li>
|
|
{% for status in participation_statuses %}
|
|
{% if status != p.participation_status %}
|
|
<li>
|
|
<form action="{% url 'event_edit_participant' p.id %}" method="POST">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="participation_status" value="{{ status.id }}">
|
|
<button type="submit" class="dropdown-item">{{ status.name }}</button>
|
|
</form>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<form action="{% url 'event_delete_participant' p.id %}" method="POST" onsubmit="return confirm('Remove this participant?')">
|
|
{% csrf_token %}
|
|
<button type="submit" class="dropdown-item text-danger">Remove from Event</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="3" class="text-center py-5 text-muted">
|
|
No participants yet.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Participant Modal -->
|
|
<div class="modal fade" id="addParticipantModal" tabindex="-1" aria-labelledby="addParticipantModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header border-0 bg-primary text-white">
|
|
<h5 class="modal-title fw-bold" id="addParticipantModalLabel">Add Participant</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form action="{% url 'event_add_participant' event.id %}" method="POST" id="addParticipantForm">
|
|
{% csrf_token %}
|
|
<div class="modal-body p-4">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Search Voter</label>
|
|
<div class="input-group">
|
|
<input type="text" id="voterSearchInput" class="form-control" placeholder="Type name or ID..." autocomplete="off">
|
|
<span class="input-group-text bg-white border-start-0">
|
|
<i class="bi bi-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
<div id="voterSearchResults" class="list-group mt-2 shadow-sm d-none" style="max-height: 200px; overflow-y: auto; position: absolute; width: calc(100% - 3rem); z-index: 1000;">
|
|
<!-- Results will appear here -->
|
|
</div>
|
|
<div id="selectedVoterDisplay" class="mt-2 d-none">
|
|
<div class="alert alert-info py-2 px-3 mb-0 d-flex justify-content-between align-items-center rounded-3 border-0">
|
|
<span id="voterName" class="fw-semibold"></span>
|
|
<button type="button" class="btn-close small" id="clearSelectedVoter"></button>
|
|
</div>
|
|
</div>
|
|
<!-- Hidden field for the actual voter ID -->
|
|
<input type="hidden" name="voter" id="voter_id_hidden" required>
|
|
</div>
|
|
<div class="mb-0">
|
|
<label for="{{ add_form.participation_status.id_for_label }}" class="form-label fw-bold">Participation Status</label>
|
|
{{ add_form.participation_status }}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 p-4 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow-sm" id="submitAddParticipant" disabled>Add Participant</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Volunteer Modal -->
|
|
<div class="modal fade" id="addVolunteerModal" tabindex="-1" aria-labelledby="addVolunteerModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header border-0 bg-primary text-white">
|
|
<h5 class="modal-title fw-bold" id="addVolunteerModalLabel">Add Volunteer</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form action="{% url 'event_add_volunteer' event.id %}" method="POST" id="addVolunteerForm">
|
|
{% csrf_token %}
|
|
<div class="modal-body p-4">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Search Volunteer</label>
|
|
<div class="input-group">
|
|
<input type="text" id="volunteerSearchInput" class="form-control" placeholder="Type name or email..." autocomplete="off">
|
|
<span class="input-group-text bg-white border-start-0">
|
|
<i class="bi bi-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
<div id="volunteerSearchResults" class="list-group mt-2 shadow-sm d-none" style="max-height: 200px; overflow-y: auto; position: absolute; width: calc(100% - 3rem); z-index: 1000;">
|
|
<!-- Results will appear here -->
|
|
</div>
|
|
<div id="selectedVolunteerDisplay" class="mt-2 d-none">
|
|
<div class="alert alert-primary py-2 px-3 mb-0 d-flex justify-content-between align-items-center rounded-3 border-0">
|
|
<span id="volunteerNameDisplay" class="fw-semibold"></span>
|
|
<button type="button" class="btn-close small" id="clearSelectedVolunteer"></button>
|
|
</div>
|
|
</div>
|
|
<!-- Hidden field for the actual volunteer ID -->
|
|
<input type="hidden" name="volunteer" id="volunteer_id_hidden" required>
|
|
</div>
|
|
<div class="mb-0">
|
|
<label for="{{ add_volunteer_form.role.id_for_label }}" class="form-label fw-bold">Role</label>
|
|
{{ add_volunteer_form.role }}
|
|
<div class="form-text small">e.g., Driver, Caller, Organizer</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 p-4 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow-sm" id="submitAddVolunteer" disabled>Add Volunteer</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Voter Search Logic
|
|
const searchInput = document.getElementById('voterSearchInput');
|
|
const resultsContainer = document.getElementById('voterSearchResults');
|
|
const hiddenVoterId = document.getElementById('voter_id_hidden');
|
|
const selectedDisplay = document.getElementById('selectedVoterDisplay');
|
|
const voterNameDisplay = document.getElementById('voterName');
|
|
const clearBtn = document.getElementById('clearSelectedVoter');
|
|
const submitBtn = document.getElementById('submitAddParticipant');
|
|
|
|
let debounceTimer;
|
|
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(debounceTimer);
|
|
const query = this.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
resultsContainer.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
debounceTimer = setTimeout(() => {
|
|
fetch(`/voters/search/json/?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
resultsContainer.innerHTML = '';
|
|
if (data.results && data.results.length > 0) {
|
|
data.results.forEach(voter => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action py-2';
|
|
btn.innerHTML = `<div class="fw-bold text-dark">${voter.text}</div><div class="small text-muted">${voter.address || "No address"} | ${voter.phone || "No phone"}</div>`;
|
|
btn.addEventListener('click', () => {
|
|
selectVoter(voter.id, voter.text, voter.address, voter.phone);
|
|
});
|
|
resultsContainer.appendChild(btn);
|
|
});
|
|
resultsContainer.classList.remove('d-none');
|
|
} else {
|
|
const div = document.createElement('div');
|
|
div.className = 'list-group-item text-muted small py-2';
|
|
div.textContent = 'No results found';
|
|
resultsContainer.appendChild(div);
|
|
resultsContainer.classList.remove('d-none');
|
|
}
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
function selectVoter(id, text, address, phone) {
|
|
hiddenVoterId.value = id;
|
|
voterNameDisplay.innerHTML = `<div>${text}</div><div class="small fw-normal text-muted">${address || ""} ${phone ? "• " + phone : ""}</div>`;
|
|
selectedDisplay.classList.remove('d-none');
|
|
searchInput.parentElement.classList.add('d-none');
|
|
resultsContainer.classList.add('d-none');
|
|
submitBtn.disabled = false;
|
|
}
|
|
|
|
clearBtn.addEventListener('click', () => {
|
|
hiddenVoterId.value = '';
|
|
voterNameDisplay.textContent = '';
|
|
selectedDisplay.classList.add('d-none');
|
|
searchInput.parentElement.classList.remove('d-none');
|
|
searchInput.value = '';
|
|
submitBtn.disabled = true;
|
|
});
|
|
|
|
// Volunteer Search Logic
|
|
const volSearchInput = document.getElementById('volunteerSearchInput');
|
|
const volResultsContainer = document.getElementById('volunteerSearchResults');
|
|
const volHiddenId = document.getElementById('volunteer_id_hidden');
|
|
const volSelectedDisplay = document.getElementById('selectedVolunteerDisplay');
|
|
const volNameDisplay = document.getElementById('volunteerNameDisplay');
|
|
const volClearBtn = document.getElementById('clearSelectedVolunteer');
|
|
const volSubmitBtn = document.getElementById('submitAddVolunteer');
|
|
|
|
let volDebounceTimer;
|
|
|
|
volSearchInput.addEventListener('input', function() {
|
|
clearTimeout(volDebounceTimer);
|
|
const query = this.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
volResultsContainer.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
volDebounceTimer = setTimeout(() => {
|
|
fetch(`/volunteers/search/json/?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
volResultsContainer.innerHTML = '';
|
|
if (data.results && data.results.length > 0) {
|
|
data.results.forEach(vol => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action py-2';
|
|
btn.innerHTML = `<div class="fw-bold text-dark">${vol.text}</div><div class="small text-muted">${vol.phone || "No phone"}</div>`;
|
|
btn.addEventListener('click', () => {
|
|
selectVolunteer(vol.id, vol.text, vol.phone);
|
|
});
|
|
volResultsContainer.appendChild(btn);
|
|
});
|
|
volResultsContainer.classList.remove('d-none');
|
|
} else {
|
|
const div = document.createElement('div');
|
|
div.className = 'list-group-item text-muted small py-2';
|
|
div.textContent = 'No results found';
|
|
volResultsContainer.appendChild(div);
|
|
volResultsContainer.classList.remove('d-none');
|
|
}
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
function selectVolunteer(id, text, phone) {
|
|
volHiddenId.value = id;
|
|
volNameDisplay.innerHTML = `<div>${text}</div><div class="small fw-normal text-muted">${phone || ""}</div>`;
|
|
volSelectedDisplay.classList.remove('d-none');
|
|
volSearchInput.parentElement.classList.add('d-none');
|
|
volResultsContainer.classList.add('d-none');
|
|
volSubmitBtn.disabled = false;
|
|
}
|
|
|
|
volClearBtn.addEventListener('click', () => {
|
|
volHiddenId.value = '';
|
|
volNameDisplay.textContent = '';
|
|
volSelectedDisplay.classList.add('d-none');
|
|
volSearchInput.parentElement.classList.remove('d-none');
|
|
volSearchInput.value = '';
|
|
volSubmitBtn.disabled = true;
|
|
});
|
|
|
|
// Close results when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!resultsContainer.contains(e.target) && e.target !== searchInput) {
|
|
resultsContainer.classList.add('d-none');
|
|
}
|
|
if (!volResultsContainer.contains(e.target) && e.target !== volSearchInput) {
|
|
volResultsContainer.classList.add('d-none');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |