2025-08-26 14:13:09 -06:00
|
|
|
{{ define "content" }}
|
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden" x-data="volunteerTable()">
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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
|
|
|
>
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- Search & Filters -->
|
2025-08-26 14:13:09 -06:00
|
|
|
<div
|
2025-09-11 16:54:30 -06:00
|
|
|
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
|
|
|
>
|
2025-09-11 16:54:30 -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>
|
2025-09-11 16:54:30 -06:00
|
|
|
|
|
|
|
|
<!-- Actions & Results -->
|
2025-08-26 14:13:09 -06:00
|
|
|
<div
|
2025-09-11 16:54:30 -06:00
|
|
|
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
|
|
|
>
|
2025-09-11 16:54:30 -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>
|
2025-09-11 16:54:30 -06:00
|
|
|
</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>
|
2025-09-11 16:54:30 -06:00
|
|
|
|
|
|
|
|
<!-- 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>
|
2025-09-11 16:54:30 -06:00
|
|
|
</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
|
|
|
>
|
2025-09-11 16:54:30 -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
|
|
|
>
|
2025-09-11 16:54:30 -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
|
|
|
>
|
2025-09-11 16:54:30 -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
|
2025-09-11 16:54:30 -06:00
|
|
|
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>
|
2025-09-11 16:54:30 -06:00
|
|
|
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>
|
|
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<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 }}
|