37769-vm/core/templates/core/event_detail.html
2026-01-30 04:40:56 +00:00

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 %}