300 lines
10 KiB
HTML
300 lines
10 KiB
HTML
|
|
{{ define "content" }}
|
||
|
|
<!-- Main Content -->
|
||
|
|
<div class="flex-1 flex flex-col overflow-hidden" x-data="volunteerTable()">
|
||
|
|
<!-- Top Navigation -->
|
||
|
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<div class="flex items-center gap-4">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<i
|
||
|
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||
|
|
></i>
|
||
|
|
<span class="text-sm font-medium">Volunteers</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Toolbar -->
|
||
|
|
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
|
||
|
|
<div class="flex items-center gap-4 text-sm">
|
||
|
|
<!-- Search -->
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<div class="relative">
|
||
|
|
<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-64 pl-8 pr-3 py-2 text-sm border border-gray-200 bg-white focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Role Filter -->
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<label for="roleFilter" class="text-gray-600 font-medium">Role:</label>
|
||
|
|
<select
|
||
|
|
x-model="roleFilter"
|
||
|
|
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||
|
|
>
|
||
|
|
<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 transition-colors"
|
||
|
|
>
|
||
|
|
<i class="fas fa-times mr-1"></i>Clear
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<!-- Results Count -->
|
||
|
|
<div class="ml-auto">
|
||
|
|
<span class="text-gray-600 text-sm">
|
||
|
|
Showing <span x-text="filteredVolunteers.length"></span> of
|
||
|
|
<span x-text="volunteers.length"></span> volunteers
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Table Wrapper -->
|
||
|
|
<div
|
||
|
|
class="flex-1 overflow-x-auto overflow-y-auto bg-white border border-gray-100"
|
||
|
|
>
|
||
|
|
<table class="w-full divide-gray-200 text-sm table-auto">
|
||
|
|
<!-- Table Head -->
|
||
|
|
<thead class="bg-gray-50 divide-gray-200">
|
||
|
|
<tr
|
||
|
|
class="text-left text-gray-700 font-medium border-b border-gray-200"
|
||
|
|
>
|
||
|
|
<th class="px-4 py-3 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 whitespace-nowrap">
|
||
|
|
<div
|
||
|
|
class="flex items-center gap-2 cursor-pointer"
|
||
|
|
@click="sortBy('FirstName')"
|
||
|
|
>
|
||
|
|
First Name <i class="fas" :class="getSortIcon('FirstName')"></i>
|
||
|
|
</div>
|
||
|
|
</th>
|
||
|
|
<th class="px-6 py-3 whitespace-nowrap">
|
||
|
|
<div
|
||
|
|
class="flex items-center gap-2 cursor-pointer"
|
||
|
|
@click="sortBy('LastName')"
|
||
|
|
>
|
||
|
|
Last Name <i class="fas" :class="getSortIcon('LastName')"></i>
|
||
|
|
</div>
|
||
|
|
</th>
|
||
|
|
<th class="px-6 py-3 whitespace-nowrap">
|
||
|
|
<div
|
||
|
|
class="flex items-center gap-2 cursor-pointer"
|
||
|
|
@click="sortBy('Email')"
|
||
|
|
>
|
||
|
|
Email <i class="fas" :class="getSortIcon('Email')"></i>
|
||
|
|
</div>
|
||
|
|
</th>
|
||
|
|
<th class="px-6 py-3 whitespace-nowrap">
|
||
|
|
<div
|
||
|
|
class="flex items-center gap-2 cursor-pointer"
|
||
|
|
@click="sortBy('Phone')"
|
||
|
|
>
|
||
|
|
Phone <i class="fas" :class="getSortIcon('Phone')"></i>
|
||
|
|
</div>
|
||
|
|
</th>
|
||
|
|
<th class="px-6 py-3 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 whitespace-nowrap">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
|
||
|
|
<!-- Table Body -->
|
||
|
|
<tbody class="divide-y divide-gray-200">
|
||
|
|
<template
|
||
|
|
x-for="volunteer in filteredVolunteers"
|
||
|
|
:key="volunteer.UserID"
|
||
|
|
>
|
||
|
|
<tr class="hover:bg-gray-50">
|
||
|
|
<td
|
||
|
|
class="px-6 py-3 whitespace-nowrap"
|
||
|
|
x-text="volunteer.UserID"
|
||
|
|
></td>
|
||
|
|
<td
|
||
|
|
class="px-6 py-3 whitespace-nowrap"
|
||
|
|
x-text="volunteer.FirstName"
|
||
|
|
></td>
|
||
|
|
<td
|
||
|
|
class="px-6 py-3 whitespace-nowrap"
|
||
|
|
x-text="volunteer.LastName"
|
||
|
|
></td>
|
||
|
|
<td
|
||
|
|
class="px-6 py-3 whitespace-nowrap"
|
||
|
|
x-text="volunteer.Email"
|
||
|
|
></td>
|
||
|
|
<td
|
||
|
|
class="px-6 py-3 whitespace-nowrap"
|
||
|
|
x-text="volunteer.Phone"
|
||
|
|
></td>
|
||
|
|
<td class="px-6 py-3 whitespace-nowrap">
|
||
|
|
<span
|
||
|
|
class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800"
|
||
|
|
x-text="getRoleName(volunteer.RoleID)"
|
||
|
|
></span>
|
||
|
|
</td>
|
||
|
|
<td class="px-6 py-3 whitespace-nowrap">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<a
|
||
|
|
:href="`/volunteer/edit?id=${volunteer.UserID}`"
|
||
|
|
class="text-blue-600 hover:text-blue-800 font-medium text-xs px-2 py-1 hover:bg-blue-50 transition-colors"
|
||
|
|
>Edit</a
|
||
|
|
>
|
||
|
|
<form
|
||
|
|
action="/volunteer/delete"
|
||
|
|
method="POST"
|
||
|
|
class="inline-block"
|
||
|
|
>
|
||
|
|
<input type="hidden" name="id" :value="volunteer.UserID" />
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
class="text-red-600 hover:text-red-800 font-medium text-xs px-2 py-1 hover:bg-red-50 transition-colors"
|
||
|
|
@click="return confirm('Are you sure you want to delete this volunteer?')"
|
||
|
|
>
|
||
|
|
Delete
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</template>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
|
||
|
|
<!-- No Results Message -->
|
||
|
|
<div x-show="filteredVolunteers.length === 0" class="text-center py-12">
|
||
|
|
<i class="fas fa-search text-gray-400 text-3xl mb-4"></i>
|
||
|
|
<p class="text-gray-600 text-lg mb-2">No volunteers found</p>
|
||
|
|
<p class="text-gray-500 text-sm">
|
||
|
|
Try adjusting your search or filter criteria
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</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';
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
clearFilters() {
|
||
|
|
this.searchTerm = '';
|
||
|
|
this.roleFilter = '';
|
||
|
|
this.sortColumn = '';
|
||
|
|
this.sortDirection = 'asc';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
{{ end }}
|