Files
Poll-system/app/internal/templates/volunteer.html

682 lines
28 KiB
HTML
Raw Normal View History

2025-08-26 14:13:09 -06:00
{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden" x-data="volunteerTable()">
<!-- Toolbar -->
<div class="bg-white border-b border-gray-200 px-4 md:px-6 py-4">
<div
class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"
2025-08-26 14:13:09 -06:00
>
<!-- Search & Filters -->
2025-08-26 14:13:09 -06:00
<div
class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto"
2025-08-26 14:13:09 -06:00
>
<!-- Search -->
<div class="relative w-full sm:w-auto">
<i
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"
></i>
<input
type="text"
x-model="searchTerm"
placeholder="Search volunteers..."
class="w-full sm:w-80 pl-10 pr-4 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Role Filter -->
<div class="flex items-center gap-2">
<label
for="roleFilter"
class="text-sm text-gray-600 whitespace-nowrap"
>Role:</label
>
<select
x-model="roleFilter"
class="px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Roles</option>
<option value="1">Admin</option>
<option value="2">Team Leader</option>
<option value="3">Volunteer</option>
</select>
</div>
<!-- Clear Filters -->
<button
@click="clearFilters()"
class="px-3 py-2 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<i class="fas fa-times mr-1"></i>Clear
</button>
2025-08-26 14:13:09 -06:00
</div>
<!-- Actions & Results -->
2025-08-26 14:13:09 -06:00
<div
class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto"
2025-08-26 14:13:09 -06:00
>
<button
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors rounded-lg"
onclick="openAddVolunteerPanel()"
>
<i class="fas fa-user-plus mr-2"></i>Add Volunteer
</button>
<!-- Results Count -->
<div class="text-sm text-gray-600 whitespace-nowrap">
Showing <span x-text="filteredVolunteers.length"></span> of
<span x-text="volunteers.length"></span> volunteers
</div>
2025-08-26 14:13:09 -06:00
</div>
</div>
</div>
<!-- Table Container -->
<div class="flex-1 p-4 md:p-6 overflow-auto">
<div
class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"
>
<!-- Desktop Table -->
<div class="hidden lg:block overflow-x-auto">
<table class="w-full min-w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap"
>
<div
class="flex items-center gap-2 cursor-pointer"
@click="sortBy('UserID')"
>
ID
<i
class="fas"
:class="getSortIcon('UserID')"
></i>
</div>
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<div
class="flex items-center gap-2 cursor-pointer"
@click="sortBy('FirstName')"
>
Name
<i
class="fas"
:class="getSortIcon('FirstName')"
></i>
</div>
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<div
class="flex items-center gap-2 cursor-pointer"
@click="sortBy('Email')"
>
Contact
<i
class="fas"
:class="getSortIcon('Email')"
></i>
</div>
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap"
>
<div
class="flex items-center gap-2 cursor-pointer"
@click="sortBy('RoleID')"
>
Role
<i
class="fas"
:class="getSortIcon('RoleID')"
></i>
</div>
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
<template
x-for="volunteer in filteredVolunteers"
:key="volunteer.UserID"
>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
<div
class="text-sm font-medium text-gray-900"
x-text="volunteer.UserID"
></div>
</td>
<td class="px-6 py-4">
<div
class="text-sm font-medium text-gray-900"
x-text="volunteer.FirstName + ' ' + volunteer.LastName"
></div>
</td>
<td class="px-6 py-4">
<div
class="text-sm text-gray-900"
x-text="volunteer.Email"
></div>
<div
class="text-sm text-gray-500"
x-text="volunteer.Phone"
></div>
</td>
<td class="px-6 py-4">
<span
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full"
:class="getRoleBadgeClass(volunteer.RoleID)"
x-text="getRoleName(volunteer.RoleID)"
></span>
</td>
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
<button
class="text-blue-600 hover:text-blue-800 p-1"
@click="editVolunteer(volunteer)"
title="Edit volunteer"
>
<i class="fas fa-edit"></i>
</button>
<button
class="text-red-600 hover:text-red-800 p-1"
@click="deleteVolunteer(volunteer.UserID)"
title="Delete volunteer"
>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
<!-- No Results - Desktop -->
<div
x-show="filteredVolunteers.length === 0"
class="px-6 py-8 text-center text-gray-500"
>
No volunteers found
</div>
2025-08-26 14:13:09 -06:00
</div>
<!-- Mobile Cards -->
<div class="lg:hidden">
<div class="space-y-4 p-4">
<template
x-for="volunteer in filteredVolunteers"
:key="volunteer.UserID"
>
<div
class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden"
>
<!-- Card Header -->
<div
class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex items-center justify-between"
>
<div class="flex items-center space-x-2">
<i class="fas fa-user text-gray-400"></i>
<span
class="text-sm font-semibold text-gray-900"
>Volunteer #<span
x-text="volunteer.UserID"
></span
></span>
</div>
<span
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full"
:class="getRoleBadgeClass(volunteer.RoleID)"
x-text="getRoleName(volunteer.RoleID)"
></span>
</div>
<!-- Card Content -->
<div class="p-4 space-y-3">
<!-- Name -->
<div class="flex flex-col">
<span
class="text-sm font-medium text-gray-900"
x-text="volunteer.FirstName + ' ' + volunteer.LastName"
></span>
</div>
<!-- Email -->
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500"
>Email</span
>
<span
class="text-sm text-gray-900"
x-text="volunteer.Email"
></span>
</div>
<!-- Phone -->
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500"
>Phone</span
>
<span
class="text-sm text-gray-900"
x-text="volunteer.Phone"
></span>
</div>
<!-- Actions -->
<div
class="flex justify-center space-x-4 pt-3 border-t border-gray-100"
>
<button
class="flex-1 px-4 py-2 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition-colors font-medium"
@click="editVolunteer(volunteer)"
>
<i class="fas fa-edit mr-1"></i> Edit
</button>
<button
class="px-4 py-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors text-sm font-medium"
@click="deleteVolunteer(volunteer.UserID)"
>
<i class="fas fa-trash mr-1"></i> Delete
</button>
</div>
</div>
</div>
</template>
<!-- No Results - Mobile -->
<div
x-show="filteredVolunteers.length === 0"
class="text-center py-12"
>
<div class="text-gray-400 mb-4">
<i class="fas fa-users text-4xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">
No volunteers found
</h3>
<p class="text-gray-500">
Try adjusting your search or filter criteria.
</p>
</div>
</div>
2025-08-26 14:13:09 -06:00
</div>
</div>
</div>
</div>
<!-- Panel Overlay -->
<div
id="volunteerPanelOverlay"
class="fixed inset-0 bg-black bg-opacity-50 hidden z-40"
></div>
<!-- Add/Edit Volunteer Panel -->
<div
id="volunteerPanel"
class="fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-xl transform translate-x-full transition-transform duration-300 ease-in-out z-50 flex flex-col"
>
<!-- Panel Header -->
<div
class="flex justify-between items-center px-6 py-4 border-b border-gray-200 bg-gray-50"
>
<div class="flex items-center space-x-2">
<i class="fas fa-user-plus text-blue-500" id="panelIcon"></i>
<h2 class="text-lg font-semibold text-gray-900" id="panelTitle">
Add Volunteer
</h2>
</div>
<button
onclick="closeVolunteerPanel()"
class="text-gray-400 hover:text-gray-600 focus:outline-none p-1"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Panel Body -->
<form
id="volunteerForm"
method="POST"
action="/volunteer/add"
class="flex-1 overflow-y-auto p-6 space-y-6"
>
<input type="hidden" name="id" id="volunteerId" />
<!-- First Name -->
<div>
<label
for="firstName"
class="block text-sm font-medium text-gray-700 mb-2"
2025-08-26 14:13:09 -06:00
>
<i class="fas fa-user mr-2 text-gray-400"></i>First Name
</label>
<input
type="text"
id="firstName"
name="first_name"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Last Name -->
<div>
<label
for="lastName"
class="block text-sm font-medium text-gray-700 mb-2"
2025-08-26 14:13:09 -06:00
>
<i class="fas fa-user mr-2 text-gray-400"></i>Last Name
</label>
<input
type="text"
id="lastName"
name="last_name"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Email -->
<div>
<label
for="email"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-envelope mr-2 text-gray-400"></i>Email
</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Phone -->
<div>
<label
for="phone"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-phone mr-2 text-gray-400"></i>Phone
</label>
<input
type="tel"
id="phone"
name="phone"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Role Selection -->
<div>
<label
for="role"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-user-tag mr-2 text-gray-400"></i>Role
</label>
<select
name="role_id"
id="role"
required
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">-- Select Role --</option>
<option value="1">Admin</option>
<option value="2">Team Leader</option>
<option value="3">Volunteer</option>
</select>
</div>
</form>
<!-- Panel Footer -->
<div
class="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50"
>
<button
type="button"
onclick="closeVolunteerPanel()"
class="px-6 py-2 border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg font-medium transition-colors"
2025-08-26 14:13:09 -06:00
>
Cancel
</button>
<button
type="submit"
form="volunteerForm"
id="submitBtn"
class="px-6 py-2 bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-lg font-medium transition-colors"
>
<i class="fas fa-check mr-2"></i>
<span id="submitText">Add Volunteer</span>
</button>
2025-08-26 14:13:09 -06:00
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
defer
2025-08-26 14:13:09 -06:00
></script>
<script>
function volunteerTable() {
return {
searchTerm: '',
roleFilter: '',
sortColumn: '',
sortDirection: 'asc',
volunteers: [
{{ range .Users }}
{
UserID: {{ .UserID }},
FirstName: "{{ .FirstName }}",
LastName: "{{ .LastName }}",
Email: "{{ .Email }}",
Phone: "{{ .Phone }}",
RoleID: {{ .RoleID }}
},
{{ end }}
],
get filteredVolunteers() {
let filtered = this.volunteers.filter(volunteer => {
// Search filter
const searchMatch = !this.searchTerm ||
volunteer.FirstName.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
volunteer.LastName.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
volunteer.Email.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
volunteer.Phone.includes(this.searchTerm);
// Role filter
const roleMatch = !this.roleFilter || volunteer.RoleID.toString() === this.roleFilter;
return searchMatch && roleMatch;
});
// Sort filtered results
if (this.sortColumn) {
filtered.sort((a, b) => {
let aValue = a[this.sortColumn];
let bValue = b[this.sortColumn];
// Handle string comparison
if (typeof aValue === 'string') {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
let comparison = 0;
if (aValue > bValue) comparison = 1;
if (aValue < bValue) comparison = -1;
return this.sortDirection === 'asc' ? comparison : -comparison;
});
}
return filtered;
},
sortBy(column) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
},
getSortIcon(column) {
if (this.sortColumn !== column) {
return 'fa-sort text-gray-400';
}
return this.sortDirection === 'asc' ? 'fa-sort-up text-blue-600' : 'fa-sort-down text-blue-600';
},
getRoleName(roleId) {
switch (roleId) {
case 1: return 'Admin';
case 2: return 'Team Leader';
case 3: return 'Volunteer';
default: return 'Unknown';
}
},
getRoleBadgeClass(roleId) {
switch (roleId) {
case 1: return 'bg-red-100 text-red-700'; // Admin - Red
case 2: return 'bg-blue-100 text-blue-700'; // Team Leader - Blue
case 3: return 'bg-green-100 text-green-700'; // Volunteer - Green
default: return 'bg-gray-100 text-gray-700';
}
},
clearFilters() {
this.searchTerm = '';
this.roleFilter = '';
this.sortColumn = '';
this.sortDirection = 'asc';
},
editVolunteer(volunteer) {
openEditVolunteerPanel(volunteer);
},
deleteVolunteer(volunteerId) {
if (confirm('Are you sure you want to delete this volunteer?')) {
// Create and submit form for deletion
const form = document.createElement('form');
form.method = 'POST';
form.action = '/volunteer/delete';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'id';
input.value = volunteerId;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
}
}
// Panel functions
function openAddVolunteerPanel() {
document.getElementById('panelTitle').textContent = 'Add Volunteer';
document.getElementById('panelIcon').className = 'fas fa-user-plus text-blue-500';
document.getElementById('submitText').textContent = 'Add Volunteer';
document.getElementById('volunteerForm').action = '/volunteer/add';
document.getElementById('volunteerForm').reset();
document.getElementById('volunteerId').value = '';
showPanel();
}
function openEditVolunteerPanel(volunteer) {
document.getElementById('panelTitle').textContent = 'Edit Volunteer';
document.getElementById('panelIcon').className = 'fas fa-user-edit text-blue-500';
document.getElementById('submitText').textContent = 'Update Volunteer';
document.getElementById('volunteerForm').action = '/volunteer/edit';
// Populate form
document.getElementById('volunteerId').value = volunteer.UserID;
document.getElementById('firstName').value = volunteer.FirstName;
document.getElementById('lastName').value = volunteer.LastName;
document.getElementById('email').value = volunteer.Email;
document.getElementById('phone').value = volunteer.Phone;
document.getElementById('role').value = volunteer.RoleID;
showPanel();
}
function showPanel() {
document.getElementById('volunteerPanelOverlay').classList.remove('hidden');
document.getElementById('volunteerPanel').classList.remove('translate-x-full');
setTimeout(() => {
document.getElementById('firstName').focus();
}, 100);
}
function closeVolunteerPanel() {
document.getElementById('volunteerPanel').classList.add('translate-x-full');
document.getElementById('volunteerPanelOverlay').classList.add('hidden');
setTimeout(() => {
document.getElementById('volunteerForm').reset();
}, 300);
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// Close panel when clicking overlay
document.getElementById('volunteerPanelOverlay').addEventListener('click', closeVolunteerPanel);
// Close panel on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const overlay = document.getElementById('volunteerPanelOverlay');
if (!overlay.classList.contains('hidden')) {
closeVolunteerPanel();
}
}
});
});
2025-08-26 14:13:09 -06:00
</script>
<style>
/* Consistent styling */
input,
select,
button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
/* Mobile responsive adjustments */
@media (max-width: 640px) {
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
}
</style>
2025-08-26 14:13:09 -06:00
{{ end }}