Files
Poll-system/app/internal/templates/volunteer.html
Mann Patel b21e76eed0 Update: Few issues to resolve see readme
this push will conclude the majority of pulls. this repos will now, not be actively be managed or any further code pushes will not be frequent.
2025-09-11 16:54:30 -06:00

682 lines
28 KiB
HTML

{{ 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"
>
<!-- Search & Filters -->
<div
class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto"
>
<!-- 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>
</div>
<!-- Actions & Results -->
<div
class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto"
>
<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>
</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>
</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>
</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"
>
<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"
>
<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"
>
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>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
defer
></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();
}
}
});
});
</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>
{{ end }}