feat: added a side bar

This commit is contained in:
Mann Patel
2025-09-05 15:39:06 -06:00
parent a5bdc27de0
commit 05001a53e0
28 changed files with 1631 additions and 1655 deletions

View File

@@ -208,7 +208,7 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
PageNumbers: pageNumbers, PageNumbers: pageNumbers,
} }
utils.Render(w, "address/address.html", map[string]interface{}{ utils.Render(w, "address.html", map[string]interface{}{
"Title": "Addresses", "Title": "Addresses",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": true, "ShowAdminNav": true,

View File

@@ -67,7 +67,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
housesLeftPercent = 0 // Set default value on error housesLeftPercent = 0 // Set default value on error
} }
utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{ utils.Render(w, "dashboard.html", map[string]interface{}{
"Title": "Admin Dashboard", "Title": "Admin Dashboard",
"IsAuthenticated": true, "IsAuthenticated": true,
"VolunteerCount": volunteerCount, "VolunteerCount": volunteerCount,

View File

@@ -177,20 +177,23 @@ func getAllReportDefinitions() map[string][]ReportDefinition {
return map[string][]ReportDefinition{ return map[string][]ReportDefinition{
"users": { "users": {
{ {
ID: "users_by_role", ID: "volunteer_participation_rate", // get all the appointment(done, notdone, total) poll(done, not doen, total)
Name: "Users by Role", Name: "Volunteer participation rate",
Description: "Count of users grouped by their role", Description: "Count of users grouped by their role",
SQL: `SELECT SQL: `SELECT
CASE u.user_id,
WHEN role_id = 1 THEN 'Admin' u.first_name,
WHEN role_id = 2 THEN 'Volunteer' u.last_name,
ELSE 'Unknown' COUNT(p.poll_id) AS total_polls,
END as role, COUNT(a.user_id) AS total_appointments,
COUNT(*) as user_count, case
COUNT(CASE WHEN created_at >= ?1 THEN 1 END) as new_this_period WHEN COUNT(a.user_id) = 0 THEN NULL -- avoid division by zero
FROM users ELSE ROUND(CAST(COUNT(p.poll_id) AS numeric) / COUNT(a.user_id), 2)
GROUP BY role_id END AS poll_to_appointment_rate
ORDER BY role_id`, from users u
LEFT JOIN poll p ON u.user_id = p.user_id
LEFT JOIN appointment a ON u.user_id = a.user_id
GROUP BY u.user_id, u.first_name, u.last_name;`,
}, },
{ {
ID: "volunteer_activity", ID: "volunteer_activity",

View File

@@ -81,7 +81,7 @@ func TeamBuilderHandler(w http.ResponseWriter, r *http.Request) {
unassignedVolunteers = append(unassignedVolunteers, vol) unassignedVolunteers = append(unassignedVolunteers, vol)
} }
utils.Render(w, "volunteer/team_builder.html", map[string]interface{}{ utils.Render(w, "team_builder.html", map[string]interface{}{
"Title": "Team Builder", "Title": "Team Builder",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": true, "ShowAdminNav": true,

View File

@@ -39,7 +39,7 @@ func VolunteerHandler(w http.ResponseWriter, r *http.Request) {
user = append(user, b) user = append(user, b)
} }
utils.Render(w, "volunteer/volunteer.html", map[string]interface{}{ utils.Render(w, "volunteer.html", map[string]interface{}{
"Title": "Assigned Volunteers", "Title": "Assigned Volunteers",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": true, "ShowAdminNav": true,
@@ -66,7 +66,7 @@ func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
utils.Render(w, "volunteer/edit_volunteer.html", map[string]interface{}{ utils.Render(w, "edit_volunteer.html", map[string]interface{}{
"Title": "Edit Volunteer", "Title": "Edit Volunteer",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": true, "ShowAdminNav": true,

View File

@@ -30,23 +30,6 @@ func getDefaultRedirectURL(role int) string {
} }
} }
// Helper function to render error pages with consistent data
func renderLoginError(w http.ResponseWriter, errorMsg string) {
utils.Render(w, "login.html", map[string]interface{}{
"Error": errorMsg,
"Title": "Login",
"IsAuthenticated": false,
})
}
func renderRegisterError(w http.ResponseWriter, errorMsg string) {
utils.Render(w, "register.html", map[string]interface{}{
"Error": errorMsg,
"Title": "Register",
"IsAuthenticated": false,
})
}
// Helper function to create and sign JWT token // Helper function to create and sign JWT token
func createJWTToken(userID, role int) (string, time.Time, error) { func createJWTToken(userID, role int) (string, time.Time, error) {
@@ -60,7 +43,6 @@ func createJWTToken(userID, role int) (string, time.Time, error) {
var jwtKey = []byte(jwtSecret) var jwtKey = []byte(jwtSecret)
expirationTime := time.Now().Add(12 * time.Hour) expirationTime := time.Now().Add(12 * time.Hour)
claims := &models.Claims{ claims := &models.Claims{
UserID: userID, UserID: userID,
@@ -113,7 +95,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
// Input validation // Input validation
if email == "" || password == "" { if email == "" || password == "" {
http.Redirect(w, r, "/?error=EmailAndPasswordRequired", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -130,7 +112,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("Login failed for email %s: %v", email, err) log.Printf("Login failed for email %s: %v", email, err)
http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -138,7 +120,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil { if err != nil {
log.Printf("Password verification failed for user ID %d", userID) log.Printf("Password verification failed for user ID %d", userID)
http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -146,7 +128,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
tokenString, expirationTime, err := createJWTToken(userID, role) tokenString, expirationTime, err := createJWTToken(userID, role)
if err != nil { if err != nil {
log.Printf("JWT token creation failed for user ID %d: %v", userID, err) log.Printf("JWT token creation failed for user ID %d: %v", userID, err)
http.Redirect(w, r, "/?error=InternalError", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -159,7 +141,6 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, redirectURL, http.StatusSeeOther) http.Redirect(w, r, redirectURL, http.StatusSeeOther)
} }
func RegisterHandler(w http.ResponseWriter, r *http.Request) { func RegisterHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
utils.Render(w, "layout.html", map[string]interface{}{ utils.Render(w, "layout.html", map[string]interface{}{
@@ -179,7 +160,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
// Input validation // Input validation
if firstName == "" || lastName == "" || email == "" || password == "" || role == "" { if firstName == "" || lastName == "" || email == "" || password == "" || role == "" {
renderRegisterError(w, "All fields are required") http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -187,21 +168,21 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Printf("Password hashing failed: %v", err) log.Printf("Password hashing failed: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
// Convert role to int // Convert role to int
roleID, err := strconv.Atoi(role) roleID, err := strconv.Atoi(role)
if err != nil { if err != nil {
renderRegisterError(w, "Invalid role") http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
var adminID int var adminID int
if roleID == 3 { // volunteer if roleID == 3 { // volunteer
if adminCode == "" { if adminCode == "" {
renderRegisterError(w, "Admin code is required for volunteers") http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -209,11 +190,11 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
err = models.DB.QueryRow(`SELECT user_id FROM users WHERE role_id = 1 AND admin_code = $1`, adminCode).Scan(&adminID) err = models.DB.QueryRow(`SELECT user_id FROM users WHERE role_id = 1 AND admin_code = $1`, adminCode).Scan(&adminID)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
renderRegisterError(w, "Invalid admin code") http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
log.Printf("DB error checking admin code: %v", err) log.Printf("DB error checking admin code: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
} }
@@ -227,7 +208,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
`, firstName, lastName, email, phone, string(hashedPassword), roleID).Scan(&userID) `, firstName, lastName, email, phone, string(hashedPassword), roleID).Scan(&userID)
if err != nil { if err != nil {
log.Printf("User registration failed: %v", err) log.Printf("User registration failed: %v", err)
renderRegisterError(w, "Could not create account. Email might already be in use.") http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -239,7 +220,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
`, adminID, userID) `, adminID, userID)
if err != nil { if err != nil {
log.Printf("Failed to link volunteer to admin: %v", err) log.Printf("Failed to link volunteer to admin: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
} }
@@ -248,8 +229,6 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func LogoutHandler(w http.ResponseWriter, r *http.Request) { func LogoutHandler(w http.ResponseWriter, r *http.Request) {
clearSessionCookie(w) clearSessionCookie(w)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)

View File

@@ -47,7 +47,7 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) {
volunteernav = true volunteernav = true
} }
utils.Render(w, "profile/profile.html", map[string]interface{}{ utils.Render(w, "profile.html", map[string]interface{}{
"Title": "My Profile", "Title": "My Profile",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": adminnav, "ShowAdminNav": adminnav,

View File

@@ -98,7 +98,7 @@ func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
} }
// Render template // Render template
utils.Render(w, "/appointment.html", map[string]interface{}{ utils.Render(w, "appointment.html", map[string]interface{}{
"Title": "My Profile", "Title": "My Profile",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": adminnav, "ShowAdminNav": adminnav,

View File

@@ -124,7 +124,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Volunteer viewing %d posts\n", len(posts)) fmt.Printf("Volunteer viewing %d posts\n", len(posts))
utils.Render(w, "dashboard/volunteer_dashboard.html", map[string]interface{}{ utils.Render(w, "volunteer_dashboard.html", map[string]interface{}{
"Title": "Volunteer Dashboard", "Title": "Volunteer Dashboard",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": showAdminNav, "ShowAdminNav": showAdminNav,

View File

@@ -65,7 +65,7 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
utils.Render(w, "volunteer/poll_form.html", map[string]interface{}{ utils.Render(w, "poll_form.html", map[string]interface{}{
"Title": "Poll Questions", "Title": "Poll Questions",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": true, "ShowAdminNav": true,
@@ -120,7 +120,6 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// Insert poll response // Insert poll response
_, err = models.DB.Exec(` _, err = models.DB.Exec(`
INSERT INTO poll_response ( INSERT INTO poll_response (
@@ -135,6 +134,22 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
fmt.Print(err) fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError) http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
return return
}else{
_, err := models.DB.Exec(`
UPDATE address_database
SET visited_validated = true
WHERE address_id IN (
SELECT address_id
FROM poll
WHERE poll_id = $1
)
`, pollID)
if err != nil {
fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
return
}
} }
http.Redirect(w, r, "/volunteer/Addresses", http.StatusSeeOther) http.Redirect(w, r, "/volunteer/Addresses", http.StatusSeeOther)

View File

@@ -0,0 +1,11 @@
package handlers
import (
"fmt"
"net/http"
)
func VolunteerSchedualHandler(w *http.ResponseWriter, r http.Request) {
fmt.Print("Not Implementated Yet!!!")
}

View File

@@ -0,0 +1,533 @@
{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden">
<!-- 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 -->
<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"
placeholder="Search Addresses"
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>
<!-- Pagination Controls -->
{{if .Pagination}}
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
<div class="flex items-center gap-2">
<button
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors"
onclick="window.location.href='/addresses/upload-csv'"
>
<i class="fas fa-file-import mr-2"></i>Import Data
</button>
</div>
<div class="flex items-center gap-2">
<label for="pageSize" class="text-sm text-gray-600 whitespace-nowrap">Per page:</label>
<select
id="pageSize"
onchange="changePageSize(this.value)"
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="20" {{if eq .Pagination.PageSize 20}}selected{{end}}>20</option>
<option value="50" {{if eq .Pagination.PageSize 50}}selected{{end}}>50</option>
<option value="100" {{if eq .Pagination.PageSize 100}}selected{{end}}>100</option>
</select>
</div>
<div class="flex items-center gap-2">
<button
onclick="goToPage({{.Pagination.PreviousPage}})"
{{if not .Pagination.HasPrevious}}disabled{{end}}
class="px-3 py-2 text-sm border border-gray-200 rounded-lg {{if .Pagination.HasPrevious}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}} transition-colors"
>
<i class="fas fa-chevron-left"></i>
</button>
<span class="px-3 py-2 text-sm text-gray-600 whitespace-nowrap">
{{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}
</span>
<button
onclick="goToPage({{.Pagination.NextPage}})"
{{if not .Pagination.HasNext}}disabled{{end}}
class="px-3 py-2 text-sm border border-gray-200 rounded-lg {{if .Pagination.HasNext}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}} transition-colors"
>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
{{end}}
</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">
Status
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Address
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Coordinates
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Assigned User
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
Appointment
</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">
{{ range .Addresses }}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
{{ if .VisitedValidated }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Valid
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full">
<i class="fas fa-times mr-1"></i> Invalid
</span>
{{ end }}
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ .Address }}</div>
</td>
<td class="px-6 py-4">
<a
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
target="_blank"
class="text-blue-600 hover:text-blue-800 text-sm hover:underline"
>
({{ .Latitude }}, {{ .Longitude }})
</a>
</td>
<td class="px-6 py-4">
{{ if .UserName }}
<div class="text-sm font-medium text-gray-900">{{ .UserName }}</div>
<div class="text-sm text-gray-500">{{ .UserEmail }}</div>
{{ else }}
<span class="text-sm text-gray-400">Unassigned</span>
{{ end }}
</td>
<td class="px-6 py-4">
{{ if .AppointmentDate }}
<div class="text-sm text-gray-900">{{ .AppointmentDate }}</div>
<div class="text-sm text-gray-500">{{ .AppointmentTime }}</div>
{{ else }}
<span class="text-sm text-gray-400">No appointment</span>
{{ end }}
</td>
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
{{ if .Assigned }}
<button
class="px-3 py-1 bg-gray-100 text-gray-500 text-sm rounded-md cursor-not-allowed"
disabled
>
Assigned
</button>
<form action="/remove_assigned_address" method="POST" class="inline-block">
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
<input type="hidden" name="user_id" value="{{ .UserID }}" />
<button
type="submit"
class="text-red-400 hover:text-red-600 p-1"
title="Remove assignment"
>
<i class="fas fa-trash"></i>
</button>
</form>
{{ else }}
<button
class="px-3 py-1 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition-colors"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
Assign
</button>
{{ end }}
</div>
</td>
</tr>
{{ else }}
<tr>
<td colspan="6" class="px-6 py-8 text-center text-gray-500">
No addresses found
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!-- Mobile Cards -->
<div class="lg:hidden">
<div class="space-y-4 p-4">
{{ range .Addresses }}
<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-map-marker-alt text-gray-400"></i>
<span class="text-sm font-semibold text-gray-900">Address</span>
</div>
{{ if .VisitedValidated }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Valid
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full">
<i class="fas fa-times mr-1"></i> Invalid
</span>
{{ end }}
</div>
<!-- Card Content -->
<div class="p-4 space-y-3">
<!-- Address -->
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-900">{{ .Address }}</span>
</div>
<!-- Coordinates -->
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500">Coordinates</span>
<a
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
target="_blank"
class="text-blue-600 hover:text-blue-800 text-sm hover:underline"
>
({{ .Latitude }}, {{ .Longitude }})
</a>
</div>
<!-- Assigned User -->
<div class="flex justify-between items-start">
<span class="text-sm text-gray-500">Assigned User</span>
<div class="text-right">
{{ if .UserName }}
<div class="text-sm font-medium text-gray-900">{{ .UserName }}</div>
<div class="text-sm text-gray-500">{{ .UserEmail }}</div>
{{ else }}
<span class="text-sm text-gray-400">Unassigned</span>
{{ end }}
</div>
</div>
<!-- Appointment -->
<div class="flex justify-between items-start">
<span class="text-sm text-gray-500">Appointment</span>
<div class="text-right">
{{ if .AppointmentDate }}
<div class="text-sm text-gray-900">{{ .AppointmentDate }}</div>
<div class="text-sm text-gray-500">{{ .AppointmentTime }}</div>
{{ else }}
<span class="text-sm text-gray-400">No appointment</span>
{{ end }}
</div>
</div>
<!-- Actions -->
<div class="flex justify-center space-x-4 pt-3 border-t border-gray-100">
{{ if .Assigned }}
<button
class="flex-1 px-4 py-2 bg-gray-100 text-gray-500 text-sm rounded-md cursor-not-allowed"
disabled
>
Already Assigned
</button>
<form action="/remove_assigned_address" method="POST" class="inline-block">
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
<input type="hidden" name="user_id" value="{{ .UserID }}" />
<button
type="submit"
class="px-4 py-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors text-sm font-medium"
>
<i class="fas fa-trash mr-1"></i> Remove
</button>
</form>
{{ else }}
<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"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
<i class="fas fa-user-plus mr-1"></i> Assign User
</button>
{{ end }}
</div>
</div>
</div>
{{ else }}
<div class="text-center py-12">
<div class="text-gray-400 mb-4">
<i class="fas fa-map-marker-alt text-4xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No addresses found</h3>
<p class="text-gray-500">Try adjusting your search criteria.</p>
</div>
{{ end }}
</div>
</div>
</div>
</div>
<!-- Assign Panel Overlay -->
<div
id="assignPanelOverlay"
class="fixed inset-0 bg-black bg-opacity-50 hidden z-40"
></div>
<!-- Assign Drawer Panel -->
<div
id="assignPanel"
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"></i>
<h2 class="text-lg font-semibold text-gray-900">Assign Address</h2>
</div>
<button
onclick="closeAssignPanel()"
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="assignForm" method="POST" action="/assign_address" class="flex-1 overflow-y-auto p-6 space-y-6">
<input type="hidden" name="address_id" id="panelAddressID" />
<!-- Selected Address Display -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center space-x-2 mb-2">
<i class="fas fa-map-marker-alt text-blue-500"></i>
<span class="font-medium text-gray-900">Selected Address:</span>
</div>
<div class="text-sm text-gray-700" id="panel-selected-address">None selected</div>
</div>
<!-- User Selection -->
<div>
<label for="user_id" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-user mr-2 text-gray-400"></i>Select User
</label>
<select
name="user_id"
id="user_id"
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"
required
>
<option value="">-- Select User --</option>
{{ range .Users }}
<option value="{{ .ID }}">{{ .Name }}</option>
{{ end }}
</select>
</div>
<!-- Date Selection -->
<div>
<label for="appointment-date" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar mr-2 text-gray-400"></i>Appointment Date
</label>
<input
type="date"
id="appointment-date"
name="appointment_date"
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"
min=""
/>
</div>
<!-- Time Selection -->
<div>
<label for="time" class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-clock mr-2 text-gray-400"></i>Appointment Time
</label>
<select
id="time"
name="time"
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 Time</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="closeAssignPanel()"
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="assignForm"
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> Assign
</button>
</div>
</div>
</div>
<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>
<script>
// Generate time options in 20-minute increments
function generateTimeOptions() {
const times = [];
for (let hour = 8; hour < 18; hour++) { // Business hours 8 AM to 6 PM
for (let minute = 0; minute < 60; minute += 20) {
const timeString = String(hour).padStart(2, "0") + ":" + String(minute).padStart(2, "0");
const displayTime = formatTime12Hour(hour, minute);
times.push({ value: timeString, display: displayTime });
}
}
return times;
}
// Format time to 12-hour format
function formatTime12Hour(hour, minute) {
const ampm = hour >= 12 ? "PM" : "AM";
const displayHour = hour % 12 || 12;
return displayHour + ":" + String(minute).padStart(2, "0") + " " + ampm;
}
// Populate time dropdown
function populateTimeSelect() {
const timeSelect = document.getElementById("time");
const times = generateTimeOptions();
timeSelect.innerHTML = '<option value="">Select Time</option>';
times.forEach((time) => {
const option = new Option(time.display, time.value);
timeSelect.appendChild(option);
});
}
function openAssignModal(addressID, address) {
document.getElementById("panelAddressID").value = addressID;
document.getElementById("panel-selected-address").textContent =
address || "Address ID: " + addressID;
// Set minimum date to today
const today = new Date().toISOString().split("T")[0];
document.getElementById("appointment-date").min = today;
document.getElementById("appointment-date").value = today;
// Show overlay + panel
document.getElementById("assignPanelOverlay").classList.remove("hidden");
document
.getElementById("assignPanel")
.classList.remove("translate-x-full");
setTimeout(() => {
document.getElementById("user_id").focus();
}, 100);
}
function closeAssignPanel() {
document
.getElementById("assignPanel")
.classList.add("translate-x-full");
document
.getElementById("assignPanelOverlay")
.classList.add("hidden");
document.getElementById("assignForm").reset();
document.getElementById("panel-selected-address").textContent =
"None selected";
}
// Close when clicking overlay
document
.getElementById("assignPanelOverlay")
.addEventListener("click", closeAssignPanel);
// Close on Escape key
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
closeAssignPanel();
}
});
function goToPage(page) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("page", page);
window.location.search = urlParams.toString();
}
function changePageSize(pageSize) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("pageSize", pageSize);
urlParams.set("page", 1);
window.location.search = urlParams.toString();
}
// Initialize when page loads
document.addEventListener("DOMContentLoaded", function () {
populateTimeSelect();
// Close panel when clicking outside
document.getElementById("assignPanelOverlay").addEventListener("click", function (e) {
closeAssignPanel();
});
// Close panel on Escape key
document.addEventListener("keydown", function(e) {
if (e.key === "Escape") {
const overlay = document.getElementById("assignPanelOverlay");
if (!overlay.classList.contains("invisible")) {
closeAssignPanel();
}
}
});
});
</script>
{{ end }}

View File

@@ -1,401 +0,0 @@
{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Toolbar -->
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm">
<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"
placeholder="Search Addresses"
class="w-full pl-8 pr-3 py-2 text-sm border border-gray-200 rounded bg-white"
/>
</div>
</div>
{{if .Pagination}}
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<label for="pageSize" class="text-gray-600">Per page:</label>
<select
id="pageSize"
onchange="changePageSize(this.value)"
class="px-3 py-1 text-sm border border-gray-200 rounded bg-white"
>
<option value="20" {{if eq .Pagination.PageSize 20}}selected{{end}}>
20
</option>
<option value="50" {{if eq .Pagination.PageSize 50}}selected{{end}}>
50
</option>
<option
value="100"
{{if
eq
.Pagination.PageSize
100}}selected{{end}}
>
100
</option>
</select>
</div>
<div class="flex items-center gap-2">
<button
onclick="goToPage({{.Pagination.PreviousPage}})"
{{if
not
.Pagination.HasPrevious}}disabled{{end}}
class="px-3 py-1 text-sm border border-gray-200 rounded {{if .Pagination.HasPrevious}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}}"
>
<i class="fas fa-chevron-left"></i>
</button>
<span class="px-2 text-gray-600"
>{{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}</span
>
<button
onclick="goToPage({{.Pagination.NextPage}})"
{{if
not
.Pagination.HasNext}}disabled{{end}}
class="px-3 py-1 text-sm border border-gray-200 rounded {{if .Pagination.HasNext}}hover:bg-gray-50 text-gray-700{{else}}text-gray-400 cursor-not-allowed{{end}}"
>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<!-- Table -->
<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">
<thead class="bg-gray-50 divide-gray-200 sticky top-0">
<tr
class="text-left text-gray-700 font-medium border-b border-gray-200"
>
<th class="px-6 py-3 whitespace-nowrap">Validated</th>
<th class="px-6 py-3 whitespace-nowrap">Address</th>
<th class="px-6 py-3 whitespace-nowrap">Coordinates</th>
<th class="px-6 py-3 whitespace-nowrap">Assigned User</th>
<th class="px-6 py-3 whitespace-nowrap">Appointment</th>
<th class="px-6 py-3 whitespace-nowrap">Assign</th>
<th class="px-6 py-3 whitespace-nowrap">Remove</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{{ range .Addresses }}
<tr class="hover:bg-gray-50">
<td class="px-6 py-3 whitespace-nowrap">
{{ if .VisitedValidated }}
<span
class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full"
>
<i class="fas fa-check mr-1"></i> Valid
</span>
{{ else }}
<span
class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded-full"
>
<i class="fas fa-times mr-1"></i> Invalid
</span>
{{ end }}
</td>
<td class="px-6 py-3 whitespace-nowrap">{{ .Address }}</td>
<td class="px-6 py-3 whitespace-nowrap">
<a
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
target="_blank"
class="text-blue-600 hover:underline"
>
({{ .Latitude }}, {{ .Longitude }})
</a>
</td>
<td class="px-6 py-3 whitespace-nowrap">
{{ if .UserName }}{{ .UserName }}<br /><span
class="text-xs text-gray-500"
>{{ .UserEmail }}</span
>{{ else }}<span class="text-gray-400">Unassigned</span>{{ end }}
</td>
<td class="px-6 py-3 whitespace-nowrap">
{{ if .AppointmentDate }} {{ .AppointmentDate }} {{ .AppointmentTime
}} {{ else }}
<span class="text-gray-400">No appointment</span>
{{ end }}
</td>
<td class="px-6 py-3 whitespace-nowrap">
{{ if .Assigned }}
<button
class="px-3 py-1 bg-gray-400 text-white text-sm cursor-not-allowed"
disabled
>
Assigned
</button>
{{ else }}
<button
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
Assign
</button>
{{ end }}
</td>
<td class="px-6 py-3 whitespace-nowrap">
{{ if .Assigned }}
<form
action="/remove_assigned_address"
method="POST"
class="inline-block"
>
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
<input type="hidden" name="user_id" value="{{ .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"
>
Delete
</button>
</form>
{{ else }}
<span class="text-gray-400 text-xs">-</span>
{{ end }}
</td>
</tr>
{{ else }}
<tr>
<td colspan="7" class="px-6 py-8 text-center text-gray-500">
No addresses found
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!-- Assign Modal -->
<div
id="assignModal"
class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"
>
<div class="bg-white w-full max-w-lg mx-4 shadow-lg">
<!-- Modal Header -->
<div
class="flex justify-between items-center px-6 py-4 border-b border-gray-200"
>
<h2 class="text-lg font-semibold text-gray-900">Assign Address</h2>
<button
onclick="closeAssignModal()"
class="text-gray-400 hover:text-gray-600 focus:outline-none"
>
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Modal Body -->
<form
id="assignForm"
method="POST"
action="/assign_address"
class="p-6 space-y-4"
>
<input type="hidden" name="address_id" id="modalAddressID" />
<!-- Selected Address Display -->
<div class="bg-gray-50 p-3 border border-gray-200">
<div class="flex items-center space-x-2 mb-4">
<i class="fas fa-map-marker-alt text-gray-500"></i>
<span class="font-medium text-gray-900">Selected Address:</span>
</div>
<div class="text-sm text-gray-700 mb-4" id="selected-address">
None selected
</div>
<!-- User Selection -->
<div class="mb-4">
<label
for="user_id"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-user mr-2"></i>Select User
</label>
<select
name="user_id"
id="user_id"
class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
required
>
<option value="">-- Select User --</option>
{{ range .Users }}
<option value="{{ .ID }}">{{ .Name }}</option>
{{ end }}
</select>
</div>
<!-- Date Selection -->
<div class="mb-4">
<label
for="appointment-date"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-calendar mr-2"></i>Appointment Date
</label>
<input
type="date"
id="appointment-date"
name="appointment_date"
required
class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
min=""
/>
</div>
<!-- Time Selection -->
<div class="grid grid-cols-2 gap-4">
<!-- Start Time -->
<div>
<label
for="time"
class="block text-sm font-medium text-gray-700 mb-2"
>
<i class="fas fa-clock mr-2"></i>Time
</label>
<select
id="time"
name="time"
required
onchange="updateEndTime()"
class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Select Time</option>
</select>
</div>
</div>
</div>
<!-- Modal Actions -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button
type="button"
onclick="closeAssignModal()"
class="px-4 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 font-medium"
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
>
<i class="fas fa-check mr-2"></i> Assign
</button>
</div>
</form>
</div>
</div>
</div>
<style>
/* Square corners across UI */
* {
border-radius: 0 !important;
}
input,
select,
button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
</style>
<script>
// Generate time options in 10-minute increments
function generateTimeOptions() {
const times = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 20) {
const timeString =
String(hour).padStart(2, "0") + ":" + String(minute).padStart(2, "0");
const displayTime = formatTime12Hour(hour, minute);
times.push({ value: timeString, display: displayTime });
}
}
return times;
}
// Format time to 12-hour format
function formatTime12Hour(hour, minute) {
const ampm = hour >= 12 ? "PM" : "AM";
const displayHour = hour % 12 || 12;
return displayHour + ":" + String(minute).padStart(2, "0") + " " + ampm;
}
// Populate time dropdown
function populateTimeSelect() {
const timeSelect = document.getElementById("time");
const times = generateTimeOptions();
timeSelect.innerHTML = '<option value="">Select Time</option>';
times.forEach((time) => {
const option = new Option(time.display, time.value);
timeSelect.appendChild(option);
});
}
function openAssignModal(addressID, address) {
document.getElementById("modalAddressID").value = addressID;
document.getElementById("selected-address").textContent =
address || "Address ID: " + addressID;
// Set minimum date to today
const today = new Date().toISOString().split("T")[0];
document.getElementById("appointment-date").min = today;
document.getElementById("appointment-date").value = today;
document.getElementById("assignModal").classList.remove("hidden");
document.getElementById("assignModal").classList.add("flex");
}
function closeAssignModal() {
document.getElementById("assignModal").classList.remove("flex");
document.getElementById("assignModal").classList.add("hidden");
document.getElementById("assignForm").reset();
document.getElementById("selected-address").textContent = "None selected";
}
function goToPage(page) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("page", page);
window.location.search = urlParams.toString();
}
function changePageSize(pageSize) {
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("pageSize", pageSize);
urlParams.set("page", 1);
window.location.search = urlParams.toString();
}
// Initialize when page loads
document.addEventListener("DOMContentLoaded", function () {
populateTimeSelect();
// Close modal when clicking outside
document
.getElementById("assignModal")
.addEventListener("click", function (e) {
if (e.target === this) {
closeAssignModal();
}
});
});
</script>
{{ end }}

View File

@@ -18,10 +18,10 @@
</div> </div>
</div> </div>
<a <a
href="/dashboard" href="/addresses"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
> >
<i class="fas fa-arrow-left mr-2"></i>Back to Dashboard <i class="fas fa-arrow-left mr-2"></i>Back to Addresses
</a> </a>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,387 @@
{{ define "content" }}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@7.5.2/ol.css"
/>
<script src="https://cdn.jsdelivr.net/npm/ol@7.5.2/dist/ol.js"></script>
<style>
#single-map {
width: 100%;
height: calc(100vh - 80px); /* Account for header height */
border: 1px solid #e5e7eb;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Hide map controls when sidebar is active on mobile */
@media (max-width: 768px) {
#single-map {
height: 50vh; /* Smaller height on mobile */
}
body.sidebar-open .map-controls {
display: none;
}
}
.control-button {
background: white;
border: 1px solid #ccc;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: all 0.2s;
}
.control-button:hover {
background: #f9fafb;
}
.ol-popup {
position: absolute;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
bottom: 12px;
left: -50px;
min-width: 180px;
max-width: 280px;
z-index: 200;
}
.ol-popup:after {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: #ffffff;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
/* Ensure OpenLayers controls stay below sidebar */
.ol-control {
z-index: 150 !important;
}
/* Hide popup when sidebar is open on mobile */
@media (max-width: 768px) {
body.sidebar-open .ol-popup {
display: none !important;
}
}
/* Dashboard layout */
.dashboard-container {
display: flex;
flex-direction: column;
height: calc(100vh - 80px); /* Account for header */
}
@media (min-width: 1024px) {
.dashboard-container {
flex-direction: row;
}
#single-map {
height: 100%;
}
}
.map-section {
flex: 1;
position: relative;
min-height: 50vh;
}
.stats-section {
width: 100%;
max-height: 50vh;
overflow-y: auto;
padding: 1rem;
background: #f9fafb;
}
@media (min-width: 1024px) {
.stats-section {
width: 20rem;
max-height: 100%;
}
}
</style>
<!-- Dashboard Layout -->
<div class="dashboard-container">
<!-- Left: Map -->
<div class="map-section bg-white">
<div class="map-controls">
<button class="control-button" onclick="refreshMap()" title="Refresh Map">
<i class="fas fa-sync-alt"></i>
</button>
<button class="control-button" onclick="fitAllMarkers()" title="Fit All Markers">
<i class="fas fa-expand-arrows-alt"></i>
</button>
<button class="control-button" onclick="clearAllMarkers()" title="Clear All Markers">
<i class="fas fa-trash"></i>
</button>
</div>
<div id="single-map"></div>
<div id="popup" class="ol-popup">
<a
href="#"
id="popup-closer"
class="absolute top-1 right-2 text-gray-500 hover:text-gray-800"
>×</a
>
<div id="popup-content"></div>
</div>
</div>
<!-- Right: Stats -->
<div class="stats-section">
<div class="flex flex-col gap-4">
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-blue-50 flex items-center justify-center rounded">
<i class="fas fa-users text-blue-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Active Volunteers</p>
<p class="text-xl font-bold text-gray-900">{{.VolunteerCount}}</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-green-50 flex items-center justify-center rounded">
<i class="fas fa-map-marker-alt text-green-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Addresses Visited</p>
<p class="text-xl font-bold text-gray-900">{{.ValidatedCount}}</p>
<p id="marker-count" class="text-xs text-gray-500">Loading...</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-yellow-50 flex items-center justify-center rounded">
<i class="fas fa-dollar-sign text-yellow-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Donation</p>
<p class="text-xl font-bold text-gray-900">${{.TotalDonations}}</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-red-50 flex items-center justify-center rounded">
<i class="fas fa-percentage text-red-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Houses Left</p>
<p class="text-xl font-bold text-gray-900">{{.HousesLeftPercent}}%</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables - only one set
let theMap = null;
let markerLayer = null;
let popup = null;
let initialized = false;
// Clean initialization
function initializeMap() {
if (initialized || !window.ol) {
console.log("Map already initialized or OpenLayers not ready");
return;
}
console.log("Initializing single map...");
try {
// Calgary coordinates
const center = ol.proj.fromLonLat([-114.0719, 51.0447]);
// Create the ONE AND ONLY map
theMap = new ol.Map({
target: "single-map",
layers: [
new ol.layer.Tile({
source: new ol.source.OSM(),
}),
],
view: new ol.View({
center: center,
zoom: 11,
}),
});
// Create popup
popup = new ol.Overlay({
element: document.getElementById("popup"),
positioning: "bottom-center",
stopEvent: false,
offset: [0, -50],
});
theMap.addOverlay(popup);
// Close popup handler
document.getElementById("popup-closer").onclick = function () {
popup.setPosition(undefined);
return false;
};
// Create marker layer
markerLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: new ol.style.Style({
text: new ol.style.Text({
text: "📍",
font: "24px sans-serif",
fill: new ol.style.Fill({ color: "#EF4444" }),
offsetY: -12,
}),
}),
});
theMap.addLayer(markerLayer);
// Click handler
theMap.on("click", function (event) {
const feature = theMap.forEachFeatureAtPixel(
event.pixel,
function (feature) {
return feature;
}
);
if (feature && feature.get("address_data")) {
const data = feature.get("address_data");
document.getElementById("popup-content").innerHTML = `
<div class="text-sm">
<h4 class="font-semibold text-gray-900 mb-2">Address Details</h4>
<p><strong>Address:</strong> ${data.address}</p>
<p><strong>House #:</strong> ${data.house_number}</p>
<p><strong>Street:</strong> ${data.street_name} ${data.street_type}</p>
<p><strong>ID:</strong> ${data.address_id}</p>
</div>
`;
popup.setPosition(event.coordinate);
} else {
popup.setPosition(undefined);
}
});
initialized = true;
console.log("Map initialized successfully");
// Load markers
setTimeout(loadMarkers, 500);
} catch (error) {
console.error("Map initialization error:", error);
}
}
// Load validated addresses
async function loadMarkers() {
try {
const response = await fetch("/api/validated-addresses");
const addresses = await response.json();
console.log(`Loading ${addresses.length} addresses`);
document.getElementById("marker-count").textContent = `${addresses.length} on map`;
// Clear existing markers
markerLayer.getSource().clear();
// Add new markers
const features = [];
addresses.forEach((addr) => {
if (addr.longitude && addr.latitude) {
const coords = ol.proj.fromLonLat([addr.longitude, addr.latitude]);
const feature = new ol.Feature({
geometry: new ol.geom.Point(coords),
address_data: addr,
});
features.push(feature);
}
});
markerLayer.getSource().addFeatures(features);
if (features.length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
} catch (error) {
console.error("Error loading markers:", error);
document.getElementById("marker-count").textContent = "Error loading";
}
}
// Control functions
function refreshMap() {
loadMarkers();
}
function fitAllMarkers() {
if (markerLayer && markerLayer.getSource().getFeatures().length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
}
function clearAllMarkers() {
if (markerLayer) {
markerLayer.getSource().clear();
}
if (popup) {
popup.setPosition(undefined);
}
}
// Initialize when ready
document.addEventListener("DOMContentLoaded", function () {
setTimeout(initializeMap, 1000);
});
// Listen for sidebar state changes to manage map controls visibility
function handleSidebarToggle() {
const sidebar = document.getElementById('sidebar');
const body = document.body;
if (sidebar && sidebar.classList.contains('active')) {
body.classList.add('sidebar-open');
} else {
body.classList.remove('sidebar-open');
}
}
// Override the original toggleSidebar function to handle map controls
if (typeof window.toggleSidebar === 'function') {
const originalToggleSidebar = window.toggleSidebar;
window.toggleSidebar = function() {
originalToggleSidebar();
setTimeout(handleSidebarToggle, 50);
};
}
</script>
{{ end }}

View File

@@ -1,374 +0,0 @@
{{ define "content" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.Title}}</title>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
<script src="https://cdn.tailwindcss.com"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@7.5.2/ol.css"
/>
<script src="https://cdn.jsdelivr.net/npm/ol@7.5.2/dist/ol.js"></script>
<style>
/* CRITICAL: Prevent any duplicate maps */
.ol-viewport {
max-width: 100% !important;
max-height: 700px !important;
}
#single-map {
width: 100%;
height: 700px;
border: 1px solid #e5e7eb;
}
.map-controls {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.control-button {
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.ol-popup {
position: absolute;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 15px;
border-radius: 8px;
border: 1px solid #e5e7eb;
bottom: 12px;
left: -50px;
min-width: 200px;
max-width: 300px;
}
.ol-popup:after {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-top-color: #ffffff;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Navigation -->
<div class="bg-white border-b border-gray-200 w-full">
<div class="px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-600 flex items-center justify-center">
<i class="fas fa-chart-bar text-white text-sm"></i>
</div>
<span class="text-xl font-semibold text-gray-900"
>Dashboard Overview</span
>
</div>
<div class="flex items-center gap-4">
<button
class="px-6 py-2.5 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
onclick="refreshMap()"
>
<i class="fas fa-sync-alt mr-2"></i>Refresh Map
</button>
<button
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors"
onclick="window.location.href='/addresses/upload-csv'"
>
<i class="fas fa-upload mr-2"></i>Import Data
</button>
</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 bg-white border-b border-gray-200"
>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-50 flex items-center justify-center">
<i class="fas fa-users text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Active Volunteers
</p>
<p class="text-2xl font-bold text-gray-900">{{.VolunteerCount}}</p>
</div>
</div>
</div>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-green-50 flex items-center justify-center">
<i class="fas fa-map-marker-alt text-green-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Addresses Visited
</p>
<p class="text-2xl font-bold text-gray-900">{{.ValidatedCount}}</p>
<p id="marker-count" class="text-xs text-gray-500">Loading...</p>
</div>
</div>
</div>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-yellow-50 flex items-center justify-center">
<i class="fas fa-dollar-sign text-yellow-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Donation</p>
<p class="text-2xl font-bold text-gray-900">${{.TotalDonations}}</p>
</div>
</div>
</div>
<div class="p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-red-50 flex items-center justify-center">
<i class="fas fa-percentage text-red-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Houses Left</p>
<p class="text-2xl font-bold text-gray-900">
{{.HousesLeftPercent}}%
</p>
</div>
</div>
</div>
</div>
<!-- SINGLE MAP SECTION -->
<div class="bg-white w-full relative">
<div class="map-controls">
<button class="control-button" onclick="refreshMap()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button class="control-button" onclick="fitAllMarkers()">
<i class="fas fa-expand-arrows-alt"></i> Fit All
</button>
<button class="control-button" onclick="clearAllMarkers()">
<i class="fas fa-trash"></i> Clear
</button>
</div>
<!-- THIS IS THE ONLY MAP CONTAINER -->
<div id="single-map"></div>
<div id="popup" class="ol-popup">
<a
href="#"
id="popup-closer"
style="
position: absolute;
top: 8px;
right: 8px;
text-decoration: none;
"
>×</a
>
<div id="popup-content"></div>
</div>
</div>
<script>
// Global variables - only one set
let theMap = null;
let markerLayer = null;
let popup = null;
let initialized = false;
// Clean initialization
function initializeMap() {
if (initialized || !window.ol) {
console.log("Map already initialized or OpenLayers not ready");
return;
}
console.log("Initializing single map...");
try {
// Calgary coordinates
const center = ol.proj.fromLonLat([-114.0719, 51.0447]);
// Create the ONE AND ONLY map
theMap = new ol.Map({
target: "single-map",
layers: [
new ol.layer.Tile({
source: new ol.source.OSM(),
}),
],
view: new ol.View({
center: center,
zoom: 11,
}),
});
// Create popup
popup = new ol.Overlay({
element: document.getElementById("popup"),
positioning: "bottom-center",
stopEvent: false,
offset: [0, -50],
});
theMap.addOverlay(popup);
// Close popup handler
document.getElementById("popup-closer").onclick = function () {
popup.setPosition(undefined);
return false;
};
// Create marker layer
markerLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: new ol.style.Style({
text: new ol.style.Text({
text: "📍",
font: "24px sans-serif",
fill: new ol.style.Fill({ color: "#EF4444" }),
offsetY: -12, // Adjust vertical position so pin points to location
}),
}),
});
theMap.addLayer(markerLayer);
// Click handler
theMap.on("click", function (event) {
const feature = theMap.forEachFeatureAtPixel(
event.pixel,
function (feature) {
return feature;
}
);
if (feature && feature.get("address_data")) {
const data = feature.get("address_data");
document.getElementById("popup-content").innerHTML = `
<div class="text-sm">
<h4 class="font-semibold text-gray-900 mb-2">Address Details</h4>
<p><strong>Address:</strong> ${data.address}</p>
<p><strong>House #:</strong> ${data.house_number}</p>
<p><strong>Street:</strong> ${data.street_name} ${data.street_type}</p>
<p><strong>ID:</strong> ${data.address_id}</p>
</div>
`;
popup.setPosition(event.coordinate);
} else {
popup.setPosition(undefined);
}
});
initialized = true;
console.log("Map initialized successfully");
// Load markers
setTimeout(loadMarkers, 500);
} catch (error) {
console.error("Map initialization error:", error);
}
}
// Load validated addresses
async function loadMarkers() {
try {
const response = await fetch("/api/validated-addresses");
const addresses = await response.json();
console.log(`Loading ${addresses.length} addresses`);
document.getElementById(
"marker-count"
).textContent = `${addresses.length} on map`;
// Clear existing markers
markerLayer.getSource().clear();
// Add new markers
const features = [];
addresses.forEach((addr) => {
if (addr.longitude && addr.latitude) {
const coords = ol.proj.fromLonLat([
addr.longitude,
addr.latitude,
]);
const feature = new ol.Feature({
geometry: new ol.geom.Point(coords),
address_data: addr,
});
features.push(feature);
}
});
markerLayer.getSource().addFeatures(features);
if (features.length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
} catch (error) {
console.error("Error loading markers:", error);
document.getElementById("marker-count").textContent = "Error loading";
}
}
// Control functions
function refreshMap() {
loadMarkers();
}
function fitAllMarkers() {
if (markerLayer && markerLayer.getSource().getFeatures().length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
}
function clearAllMarkers() {
if (markerLayer) {
markerLayer.getSource().clear();
}
if (popup) {
popup.setPosition(undefined);
}
}
// Initialize when ready
document.addEventListener("DOMContentLoaded", function () {
setTimeout(initializeMap, 1000);
});
</script>
</body>
</html>
{{ end }}

View File

@@ -9,615 +9,479 @@
<title>{{if .Title}}{{.Title}}{{else}}Poll System{{end}}</title> <title>{{if .Title}}{{.Title}}{{else}}Poll System{{end}}</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="//unpkg.com/alpinejs" defer></script> <script src="//unpkg.com/alpinejs" defer></script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
/> />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
"custom-gray": "#f8fafc",
"sidebar-gray": "#ffffff",
"border-gray": "#e2e8f0",
"text-primary": "#1e293b",
"text-secondary": "#64748b",
"blue-primary": "#3b82f6",
"blue-light": "#eff6ff",
},
},
},
};
</script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
font-size: 14px;
}
/* Mobile sidebar overlay */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 40;
}
.sidebar-overlay.active {
display: block;
}
/* Desktop layout - sidebar always visible */
@media (min-width: 768px) {
.main-content-container {
margin-left: 240px; /* Match sidebar width */
}
}
/* Mobile sidebar positioning */
@media (max-width: 767px) {
#sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 50;
transform: translateX(-100%);
transition: transform 0.3s ease-in-out;
}
#sidebar.active {
transform: translateX(0);
}
.main-content-container {
margin-left: 0;
}
}
/* Ensure sidebar has fixed positioning on desktop */
@media (min-width: 768px) {
#sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 30;
}
}
/* Hide scrollbars but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
</style>
</head> </head>
<body class="bg-gray-50 font-sans"> <body class="bg-custom-gray">
{{ if .IsAuthenticated }} {{ if .IsAuthenticated }}
<!-- Authenticated User Interface --> <!-- Authenticated User Interface -->
<div class="min-h-screen" x-data="{ mobileMenuOpen: false }"> <div class="min-h-screen">
<!-- Simple Navigation Bar --> <!-- Mobile sidebar overlay -->
<nav class="bg-gray-700 border-b border-gray-600 fixed top-0 left-0 w-full z-50"> <div id="sidebar-overlay" class="sidebar-overlay" onclick="toggleSidebar()"></div>
<div class="max-w-9xl mx-auto px-4 sm:px-6 lg:px-8 ">
<div class="flex justify-between items-center h-14 ">
<!-- Left: Logo and Navigation Links --> <!-- Sidebar -->
<div class="flex items-center space-x-12"> <div id="sidebar" class="w-60 bg-sidebar-gray border-r border-border-gray flex flex-col">
<!-- Logo --> <!-- Logo/Header -->
<div class="flex items-center space-x-4"> <div class="flex items-center justify-between p-6 border-b border-border-gray">
<img src="../../static/icon-512.png" alt="Logo" class="w-6 h-6"/> <div class="flex items-center">
<span class="text-xl font-semibold text-white">Poll System</span> <div class="w-7 h-7 bg-blue-primary rounded-full flex items-center justify-center mr-3">
<img src="../../static/icon-512.png" alt="Logo" class="w-4 h-4"/>
</div> </div>
<span class="font-semibold text-text-primary text-base">Poll System</span>
</div> </div>
<!-- Mobile close button -->
<!-- Desktop Navigation Links --> <button id="sidebar-close" class="md:hidden text-text-secondary hover:text-text-primary" onclick="toggleSidebar()">
<div class="hidden md:flex items-center space-x-10"> <i class="fas fa-times text-lg"></i>
{{ if .ShowAdminNav }}
<a href="/dashboard" class="text-sm font-medium {{if eq .ActiveSection "dashboard"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Dashboard
</a>
<a href="/volunteers" class="text-sm font-medium {{if eq .ActiveSection "volunteer"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Volunteers
</a>
<a href="/team_builder" class="text-sm font-medium {{if eq .ActiveSection "team_builder"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Team Builder
</a>
<a href="/addresses" class="text-sm font-medium {{if eq .ActiveSection "address"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Addresses
</a>
<a href="/posts" class="text-sm font-medium {{if eq .ActiveSection "post"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Posts
</a>
<a href="/reports" class="text-sm font-medium {{if eq .ActiveSection "report"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Reports
</a>
{{ end }}
{{ if .ShowVolunteerNav }}
<a href="/volunteer/dashboard" class="text-sm font-medium {{if eq .ActiveSection "dashboard"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Dashboard
</a>
<a href="/volunteer/Addresses" class="text-sm font-medium {{if eq .ActiveSection "address"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Assigned Address
</a>
<!-- <a href="/volunteer/schedual" class="text-sm font-medium {{if eq .ActiveSection "schedual"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
My Schedule
</a> -->
{{ end }}
<a href="/profile" class="text-sm font-medium {{if eq .ActiveSection "profile"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
Profile
</a>
</div>
<!-- Right: User Info and Actions -->
<div class="flex items-center space-x-4">
<!-- User Avatar and Name -->
<div class="hidden sm:flex items-center space-x-3">
<span class="text-sm text-gray-300">{{.UserName}}</span>
<div class="w-8 h-8 bg-blue-500 flex items-center justify-center text-white font-medium rounded-full">
{{slice .UserName 0 1}}
</div>
</div>
<!-- Logout Button -->
<a href="/logout" class="hidden sm:flex items-center px-3 py-2 text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-600 rounded-md">
<i class="fas fa-sign-out-alt mr-2"></i>
Logout
</a>
<!-- Mobile Menu Button -->
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden p-2 rounded-md text-gray-300 hover:text-white hover:bg-gray-600">
<i class="fas fa-bars" x-show="!mobileMenuOpen"></i>
<i class="fas fa-times" x-show="mobileMenuOpen"></i>
</button> </button>
</div> </div>
</div>
<!-- Mobile Navigation Menu --> <!-- Navigation -->
<div x-show="mobileMenuOpen" <nav class="flex-1 py-4">
x-transition:enter="transition ease-out duration-200" <div class="space-y-1 px-3">
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="md:hidden border-t border-gray-600"
@click.outside="mobileMenuOpen = false">
<div class="px-4 py-4 bg-gray-800">
<!-- User Info Mobile -->
<div class="flex items-center space-x-3 pb-4 mb-4 border-b border-gray-600">
<div class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-medium rounded-full">
{{slice .UserName 0 1}}
</div>
<div>
<p class="text-sm font-medium text-white">{{.UserName}}</p>
<p class="text-xs text-gray-400">Logged in</p>
</div>
</div>
<!-- Mobile Navigation Links -->
<div class="space-y-2">
{{ if .ShowAdminNav }} {{ if .ShowAdminNav }}
<a href="/dashboard" @click="mobileMenuOpen = false" <a href="/dashboard" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "dashboard"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "dashboard"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-chart-pie w-5 {{if eq .ActiveSection "dashboard"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-chart-pie mr-3 w-4"></i>Dashboard <span {{if eq .ActiveSection "dashboard"}}class="font-medium"{{end}}>Dashboard</span>
</a> </a>
<a href="/volunteers" @click="mobileMenuOpen = false" <a href="/volunteers" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "volunteer"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "volunteer"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-users w-5 {{if eq .ActiveSection "volunteer"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-users mr-3 w-4"></i>Volunteers <span {{if eq .ActiveSection "volunteer"}}class="font-medium"{{end}}>Volunteers</span>
</a> </a>
<a href="/team_builder" @click="mobileMenuOpen = false" <a href="/team_builder" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "team_builder"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "team_builder"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-user-friends w-5 {{if eq .ActiveSection "team_builder"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-user-friends mr-3 w-4"></i>Team Builder <span {{if eq .ActiveSection "team_builder"}}class="font-medium"{{end}}>Team Builder</span>
</a> </a>
<a href="/addresses" @click="mobileMenuOpen = false" <a href="/addresses" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "address"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "address"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-map-marked-alt w-5 {{if eq .ActiveSection "address"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-map-marked-alt mr-3 w-4"></i>Addresses <span {{if eq .ActiveSection "address"}}class="font-medium"{{end}}>Addresses</span>
</a> </a>
<a href="/posts" @click="mobileMenuOpen = false" <a href="/posts" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "posts"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "post"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-list w-5 {{if eq .ActiveSection "posts"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-blog mr-3 w-4"></i>Posts <span {{if eq .ActiveSection "posts"}}class="font-medium"{{end}}>Posts</span>
</a> </a>
<a href="/reports" @click="mobileMenuOpen = false" <a href="/reports" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "reports"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "report"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-table w-5 {{if eq .ActiveSection "reports"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-table mr-3 w-4"></i>Reports <span {{if eq .ActiveSection "reports"}}class="font-medium"{{end}}>Reports</span>
</a> </a>
{{ end }} {{ end }}
{{ if .ShowVolunteerNav }} {{ if .ShowVolunteerNav }}
<a href="/volunteer/dashboard" @click="mobileMenuOpen = false" <a href="/volunteer/dashboard" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "dashboard"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "dashboard"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-chart-pie w-5 {{if eq .ActiveSection "dashboard"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-chart-pie mr-3 w-4"></i>Dashboard <span {{if eq .ActiveSection "dashboard"}}class="font-medium"{{end}}>Dashboard</span>
</a> </a>
<a href="/volunteer/Addresses" @click="mobileMenuOpen = false" <a href="/volunteer/Addresses" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "address"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "address"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-home w-5 {{if eq .ActiveSection "address"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-home mr-3 w-4"></i>Assigned Address <span {{if eq .ActiveSection "address"}}class="font-medium"{{end}}>Assigned Address</span>
</a>
<a href="/volunteer/schedual" @click="mobileMenuOpen = false"
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "schedual"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}">
<i class="fas fa-calendar-alt mr-3 w-4"></i>My Schedule
</a> </a>
{{ end }} {{ end }}
<a href="/profile" @click="mobileMenuOpen = false" <a href="/profile" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "profile"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
class="block px-3 py-2 rounded-md text-base font-medium {{if eq .ActiveSection "profile"}}text-blue-300 bg-gray-700{{else}}text-gray-300 hover:text-white hover:bg-gray-700{{end}}"> <i class="fas fa-user-circle w-5 {{if eq .ActiveSection "profile"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
<i class="fas fa-user-circle mr-3 w-4"></i>Profile <span {{if eq .ActiveSection "profile"}}class="font-medium"{{end}}>Profile</span>
</a> </a>
<!-- Logout for Mobile --> <a href="/logout" class="flex items-center px-3 py-2.5 text-sm text-text-secondary hover:bg-gray-50 rounded-md group">
<div class="border-t border-gray-600 pt-2 mt-4"> <i class="fas fa-sign-out-alt w-5 text-gray-400 mr-3"></i>
<a href="/logout" class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700"> <span>Logout</span>
<i class="fas fa-sign-out-alt mr-3 w-4"></i>Logout
</a> </a>
</div> </div>
</div>
</div>
</div>
</div>
</nav> </nav>
</div>
<!-- Main Content Area --> <!-- Main Content Container -->
<main class="flex-1 mt-14"> <div class="main-content-container min-h-screen flex flex-col bg-custom-gray">
<!--sm:px-4 lg:px-6--> <!-- Top Header -->
<div class="max-w-9xl mx-auto overflow-hidden "> <div class="bg-white border-b border-border-gray px-4 md:px-6 py-4">
<div class="flex items-center justify-between">
<!-- Hamburger (left aligned with consistent spacing) -->
<div class="flex items-center">
<button id="menu-toggle" class="md:hidden text-text-secondary hover:text-text-primary p-2 -ml-2" onclick="toggleSidebar()">
<i class="fas fa-bars text-lg"></i>
</button>
</div>
<!-- Right side -->
<div class="flex items-center space-x-2 md:space-x-4">
<!-- Dark mode -->
<button class="text-text-secondary hover:text-text-primary p-2">
<i class="fas fa-moon text-lg"></i>
</button>
<!-- Profile (hover dropdown on desktop, click on mobile) -->
<div class="relative group cursor-pointer">
<div class="flex items-center space-x-2 md:space-x-3" onclick="toggleProfileMenu()">
<span class="text-sm font-medium text-text-primary hidden sm:block">{{.UserName}}</span>
<div class="w-8 h-8 rounded-full bg-blue-primary flex items-center justify-center text-white font-medium">
{{slice .UserName 0 1}}
</div>
</div>
<!-- Dropdown -->
<div id="profile-menu" class="absolute right-0 mt-2 w-40 bg-white border border-border-gray rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
<a href="/profile" class="block px-4 py-2 text-sm text-text-primary hover:bg-gray-100">Profile</a>
<a href="#" class="block px-4 py-2 text-sm text-text-primary hover:bg-gray-100">Settings</a>
<a href="/logout" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">Logout</a>
</div>
</div>
</div>
</div>
</div>
<!-- Page Content -->
<div class="flex-1">
{{ template "content" . }} {{ template "content" . }}
</div> </div>
</main> </div>
</div> </div>
{{else}} {{else}}
<!-- Landing Page (unchanged) --> <!-- Split Screen Login/Register Page -->
<!DOCTYPE html> <div class="min-h-screen flex" x-data="{ isLogin: true }">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linq - Poll System</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body class="bg-gray-50">
<div class="min-h-screen" x-data="{ mobileMenuOpen: false }">
<!-- Navigation -->
<nav class="bg-gray-700 border-b border-gray-600">
<div class="max-w-7xl mx-auto px-6">
<div class="flex justify-between items-center h-14">
<!-- Logo -->
<div class="flex items-center gap-3">
<img src="../../static/icon-512.png" alt="Logo" class="w-8 h-8"/>
<span class="text-xl font-semibold text-white">Linq</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center gap-8">
<a href="#home" class="text-gray-300 hover:text-white py-3 px-1">Home</a>
<a href="#products" class="text-gray-300 hover:text-white py-3 px-1">Products</a>
<a href="#about" class="text-gray-300 hover:text-white py-3 px-1">About</a>
</div>
<!-- Auth Buttons -->
<div class="hidden md:flex items-center gap-3">
<button onclick="openLoginModal()" class="px-4 py-2 text-gray-300 hover:text-white">
Sign In
</button>
<button onclick="openRegisterModal()" class="px-6 py-2 bg-blue-500 text-white hover:bg-blue-600 rounded">
Get Started
</button>
</div>
<!-- Mobile Menu Button -->
<button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden p-2 text-gray-300 hover:text-white hover:bg-gray-600 rounded-md">
<i class="fas fa-bars" x-show="!mobileMenuOpen"></i>
<i class="fas fa-times" x-show="mobileMenuOpen"></i>
</button>
</div>
<!-- Mobile Menu -->
<div x-show="mobileMenuOpen" class="md:hidden border-t border-gray-600 bg-gray-800">
<div class="px-6 py-4 space-y-3">
<a href="#home" @click="mobileMenuOpen = false" class="block py-2 text-gray-300 hover:text-white">Home</a>
<a href="#products" @click="mobileMenuOpen = false" class="block py-2 text-gray-300 hover:text-white">Products</a>
<a href="#about" @click="mobileMenuOpen = false" class="block py-2 text-gray-300 hover:text-white">About</a>
<div class="border-t border-gray-600 pt-3 space-y-2">
<button onclick="openLoginModal(); document.querySelector('[x-data]').__x.$data.mobileMenuOpen = false"
class="block w-full text-left py-2 text-gray-300 hover:text-white">
Sign In
</button>
<button onclick="openRegisterModal(); document.querySelector('[x-data]').__x.$data.mobileMenuOpen = false"
class="block w-full py-2 bg-blue-500 text-white hover:bg-blue-600 rounded">
Get Started
</button>
</div>
</div>
</div>
</div>
</nav>
<!-- Hero -->
<section id="home" class="pt-40 pb-16 px-6 min-h-[600px]">
<div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl font-bold text-gray-900 mb-6">
Simple Polling
<span class="text-blue-600">Management</span>
</h1>
<p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
Manage volunteers and track polling operations efficiently.
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<button onclick="openRegisterModal()" class="px-8 py-3 bg-blue-600 text-white hover:bg-blue-700 font-medium rounded">
Start Free
</button>
<button onclick="openLoginModal()" class="px-8 py-3 border border-gray-300 text-gray-700 hover:bg-gray-50 font-medium rounded">
Sign In
</button>
</div>
</div>
</section>
<!-- Products Section -->
<section id="products" class="py-16 bg-white min-h-[600px]">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 mb-4">Our Products</h2>
<p class="text-lg text-gray-600">Tools built for modern polling operations</p>
</div>
<div class="grid md:grid-cols-3 gap-8">
<div class="p-6 border border-gray-200 rounded-lg">
<img src="../../static/icon-512.png" alt="Poll Manager" class="w-12 h-12 mb-4"/>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Poll Manager</h3>
<p class="text-gray-600 mb-4">Complete polling campaign management with real-time tracking and coordination tools.</p>
<ul class="text-sm text-gray-500 space-y-1">
<li>• Volunteer coordination</li>
<li>• Address management</li>
<li>• Progress tracking</li>
</ul>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<img src="../../static/icon-512.png" alt="Analytics Suite" class="w-12 h-12 mb-4"/>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Analytics Suite</h3>
<p class="text-gray-600 mb-4">Advanced reporting and analytics dashboard for data-driven insights.</p>
<ul class="text-sm text-gray-500 space-y-1">
<li>• Performance metrics</li>
<li>• Custom reports</li>
<li>• Data visualization</li>
</ul>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<img src="../../static/icon-512.png" alt="Team Builder" class="w-12 h-12 mb-4"/>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Team Builder</h3>
<p class="text-gray-600 mb-4">Organize teams and assign roles with automated scheduling and notifications.</p>
<ul class="text-sm text-gray-500 space-y-1">
<li>• Role assignment</li>
<li>• Schedule management</li>
<li>• Team communication</li>
</ul>
</div>
</div>
</div>
</section>
<!-- About -->
<section id="about" class="py-16 bg-gray-50 min-h-[600px]">
<div class="max-w-6xl mx-auto px-6">
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div>
<h2 class="text-3xl font-bold text-gray-900 mb-6">About Linq</h2>
<p class="text-lg text-gray-600 mb-6">
Built for organizations that need efficient polling management.
Our platform simplifies volunteer coordination and progress tracking.
</p>
<div class="grid grid-cols-3 gap-6 mb-8">
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">500+</div>
<div class="text-sm text-gray-500">Volunteers</div>
</div>
<!-- <div class="text-center">
<div class="text-2xl font-bold text-gray-900">50+</div>
<div class="text-sm text-gray-500">Organizations</div>
</div> -->
<div class="text-center">
<div class="text-2xl font-bold text-gray-900">400,000+</div>
<div class="text-sm text-gray-500">Addresses</div>
</div>
</div>
</div>
<div class="bg-white p-8 rounded-lg border border-gray-200">
<img src="../../static/feature-mobile4.jpg" alt="Dashboard Preview" class="w-full h-48 object-cover rounded mb-6 bg-gray-100"/>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Simple Dashboard</h3>
<p class="text-gray-600">
Everything you need in one place. Track progress, manage teams, and generate reports.
</p>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-white border-t border-gray-200 py-8">
<div class="max-w-6xl mx-auto px-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-3">
<img src="../../static/icon-512.png" alt="Logo" class="w-6 h-6"/>
<span class="font-semibold text-gray-900">Linq</span>
</div>
<div class="flex items-center gap-6 text-sm text-gray-500">
<a href="#" class="hover:text-gray-900">Privacy</a>
<a href="#" class="hover:text-gray-900">Terms</a>
<a href="#" class="hover:text-gray-900">Contact</a>
</div>
<p class="text-sm text-gray-500">&copy; 2025 Linq. All rights reserved.</p>
</div>
</div>
</footer>
</div>
<!-- Login Modal -->
<div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4">
<div class="bg-white shadow-2xl max-w-4xl w-full overflow-hidden rounded-lg">
<div class="flex flex-col lg:flex-row min-h-[500px]">
<!-- Left Side - Image --> <!-- Left Side - Image -->
<div class="hidden lg:flex flex-1 bg-gradient-to-br from-blue-500 to-blue-700 flex-col items-center justify-center"> <div class="hidden lg:flex flex-1 relative overflow-hidden">
<img src="../../static/feature-mobile2.jpg" alt="Welcome Image" class="object-cover h-full rounded-lg shadow-lg"> <!-- Background overlay for better text readability -->
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-blue-700/40 z-10"></div>
<!-- Background Image -->
<img src="../../static/feature-mobile1.jpg" alt="Welcome to Poll System" class="object-cover w-full h-full"/>
<!-- Logo and branding overlay -->
<div class="absolute top-8 left-8 z-20 flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
<img src="../../static/icon-512.png" alt="Logo" class="w-6 h-6"/>
</div> </div>
<!-- Right Side - Form --> <span class="text-2xl font-bold text-white">Linq</span>
<div class="flex-1 p-6 sm:p-8"> </div>
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl sm:text-2xl font-bold text-gray-900">Sign In</h3> <!-- Welcome text overlay -->
<button onclick="closeLoginModal()" class="text-gray-400 hover:text-gray-600"> <div class="absolute bottom-8 left-8 right-8 z-20 text-white">
<i class="fas fa-times text-xl"></i> <h1 class="text-4xl font-bold mb-4">Welcome to Poll System</h1>
<p class="text-xl opacity-90">Streamline your polling operations with our comprehensive management platform</p>
</div>
</div>
<!-- Right Side - Login/Register Forms -->
<div class="flex-1 flex items-center justify-center p-6 lg:p-12 bg-white">
<div class="w-full max-w-md">
<!-- Mobile Logo (visible only on small screens) -->
<div class="lg:hidden flex items-center justify-center gap-3 mb-8">
<div class="w-10 h-10 bg-blue-primary rounded-full flex items-center justify-center">
<img src="../../static/icon-512.png" alt="Logo" class="w-6 h-6"/>
</div>
<span class="text-2xl font-bold text-text-primary">Linq</span>
</div>
<!-- Toggle Buttons -->
<div class="flex justify-center gap-1 mb-8 p-1 bg-gray-100 rounded-lg">
<button @click="isLogin = true" :class="isLogin ? 'bg-white text-blue-primary shadow-sm' : 'text-gray-600'" class="flex-1 px-4 py-2 rounded-md font-medium transition-all duration-200">
Sign In
</button>
<button @click="isLogin = false" :class="!isLogin ? 'bg-white text-blue-primary shadow-sm' : 'text-gray-600'" class="flex-1 px-4 py-2 rounded-md font-medium transition-all duration-200">
Register
</button> </button>
</div> </div>
<form method="POST" action="/login" class="space-y-6">
<div> <!-- Login Form -->
<label for="login_email" class="block text-sm font-medium text-gray-700 mb-2">Email</label> <div x-show="isLogin" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform translate-x-4" x-transition:enter-end="opacity-100 transform translate-x-0">
<input type="email" name="email" id="login_email" required <form action="/login" method="POST" class="space-y-6">
class="w-full px-4 py-3 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> <div class="text-center mb-6">
<h2 class="text-3xl font-bold text-text-primary">Welcome back</h2>
<p class="text-text-secondary mt-2">Please sign in to your account</p>
</div> </div>
<div> <div>
<label for="login_password" class="block text-sm font-medium text-gray-700 mb-2">Password</label> <label for="login_email" class="block text-sm font-medium text-text-primary mb-2">Email Address</label>
<input type="password" name="password" id="login_password" required <input type="email"
class="w-full px-4 py-3 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> id="login_email"
name="email"
required
placeholder="Enter your email"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors rounded">
<div>
<label for="login_password" class="block text-sm font-medium text-text-primary mb-2">Password</label>
<input type="password"
id="login_password"
name="password"
required
placeholder="Enter your password"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<input type="checkbox" class="h-4 w-4 text-blue-primary focus:ring-blue-primary border-gray-300 rounded">
<span class="ml-2 text-sm text-text-secondary">Remember me</span>
</label>
<a href="#" class="text-sm text-blue-primary hover:text-blue-600">Forgot password?</a>
</div>
<button type="submit" class="w-full bg-blue-primary text-white py-3 rounded-lg hover:bg-blue-600 focus:ring-2 focus:ring-blue-primary focus:ring-offset-2 transition-colors font-medium">
Sign In Sign In
</button> </button>
</form> </form>
<p class="text-center text-sm text-gray-600 mt-6">
Don't have an account?
<button onclick="switchToRegister()" class="text-blue-600 hover:text-blue-700 font-medium">Sign up</button>
</p>
</div>
</div>
</div>
</div> </div>
<!-- Register Modal --> <!-- Register Form -->
<div id="registerModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4"> <div x-show="!isLogin" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform translate-x-4" x-transition:enter-end="opacity-100 transform translate-x-0">
<div class="bg-white shadow-2xl max-w-4xl w-full overflow-hidden rounded-lg"> <form action="/register" method="POST" class="space-y-6">
<div class="flex flex-col lg:flex-row min-h-[600px]"> <div class="text-center mb-6">
<!-- Left Side - Image --> <h2 class="text-3xl font-bold text-text-primary">Create Account</h2>
<div class="hidden lg:flex flex-1 bg-gradient-to-br from-blue-600 to-blue-800 flex-col items-center justify-center"> <p class="text-text-secondary mt-2">Join our polling platform today</p>
<img src="../../static/feature-mobile1.jpg" alt="Welcome Image" class="object-cover h-full rounded-lg shadow-lg">
</div> </div>
<!-- Right Side - Form -->
<div class="flex-1 p-6 sm:p-8 overflow-y-auto">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl sm:text-2xl font-bold text-gray-900">Create Account</h3>
<button onclick="closeRegisterModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form method="POST" action="/register" class="space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label for="first_name" class="block text-sm font-medium text-gray-700 mb-1">First Name</label> <label class="block text-sm font-medium text-text-primary mb-2">First Name</label>
<input type="text" name="first_name" id="first_name" required <input type="text"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> name="first_name"
required
placeholder="First Name"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
<div> <div>
<label for="last_name" class="block text-sm font-medium text-gray-700 mb-1">Last Name</label> <label class="block text-sm font-medium text-text-primary mb-2">Last Name</label>
<input type="text" name="last_name" id="last_name" required <input type="text"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> name="last_name"
required
placeholder="Last Name"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
</div> </div>
<div> <div>
<label for="register_email" class="block text-sm font-medium text-gray-700 mb-1">Email</label> <label class="block text-sm font-medium text-text-primary mb-2">Email Address</label>
<input type="email" name="email" id="register_email" required <input type="email"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> name="email"
required
placeholder="Enter your email"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
<div> <div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">Phone</label> <label class="block text-sm font-medium text-text-primary mb-2">Phone</label>
<input type="tel" name="phone" id="phone" <input type="tel"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> name="phone"
required
placeholder="Phone number"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
<div> <div>
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">Role</label> <label class="block text-sm font-medium text-text-primary mb-2">Role</label>
<select name="role" id="role" required <select name="role"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors" required
onchange="toggleAdminCodeField()"> onchange="toggleAdminCodeField()"
<option value="">Select role</option> id="role"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors">
<option value="">Select Role</option>
<option value="1">Admin</option> <option value="1">Admin</option>
<option value="3">Volunteer</option> <option value="3">Volunteer</option>
</select> </select>
</div> </div>
<!-- Admin Code field (hidden by default) --> <!-- Admin/Team Leader Code Field (hidden by default) -->
<div id="adminCodeField" class="hidden"> <div id="adminCodeField" class="hidden">
<label for="admin_code" class="block text-sm font-medium text-gray-700 mb-1">Admin Code</label> <label class="block text-sm font-medium text-text-primary mb-2">Access Code</label>
<input type="text" name="admin_code" id="admin_code" <input type="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors" name="admin_code"
placeholder="Enter your admin's code"> placeholder="Enter access code"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
<p class="text-xs text-text-secondary mt-1">Required for Admin and Team Leader roles</p>
</div> </div>
<div> <div>
<label for="register_password" class="block text-sm font-medium text-gray-700 mb-1">Password</label> <label class="block text-sm font-medium text-text-primary mb-2">Password</label>
<input type="password" name="password" id="register_password" required <input type="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"> name="password"
required
placeholder="Create a password"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors rounded mt-6">
<button type="submit" class="w-full bg-blue-primary text-white py-3 rounded-lg hover:bg-blue-600 focus:ring-2 focus:ring-blue-primary focus:ring-offset-2 transition-colors font-medium">
Create Account Create Account
</button> </button>
</form> </form>
<p class="text-center text-sm text-gray-600 mt-4">
Already have an account?
<button onclick="switchToLogin()" class="text-blue-600 hover:text-blue-700 font-medium">Sign in</button>
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{end}}
<script> <script>
// Initialize Alpine.js data for mobile menu // Sidebar functionality for authenticated users
document.addEventListener('alpine:init', () => { function toggleSidebar() {
Alpine.data('sidebar', () => ({ const sidebar = document.getElementById('sidebar');
open: false const sidebarOverlay = document.getElementById('sidebar-overlay');
}));
});
// Smooth scrolling for navigation links if (sidebar && sidebarOverlay) {
document.addEventListener('DOMContentLoaded', function() { sidebar.classList.toggle('active');
const links = document.querySelectorAll('a[href^="#"]'); sidebarOverlay.classList.toggle('active');
for (const link of links) {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
const offsetTop = targetElement.offsetTop - 80; // Account for fixed navbar
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
});
}
});
function openLoginModal() {
document.getElementById('loginModal').classList.remove('hidden');
document.getElementById('loginModal').classList.add('flex');
document.body.style.overflow = 'hidden';
}
function closeLoginModal() {
document.getElementById('loginModal').classList.add('hidden');
document.getElementById('loginModal').classList.remove('flex');
document.body.style.overflow = 'auto';
}
function openRegisterModal() {
document.getElementById('registerModal').classList.remove('hidden');
document.getElementById('registerModal').classList.add('flex');
document.body.style.overflow = 'hidden';
}
function closeRegisterModal() {
document.getElementById('registerModal').classList.add('hidden');
document.getElementById('registerModal').classList.remove('flex');
document.body.style.overflow = 'auto';
}
function switchToRegister() {
closeLoginModal();
setTimeout(() => openRegisterModal(), 100);
}
function switchToLogin() {
closeRegisterModal();
setTimeout(() => openLoginModal(), 100);
}
// Close modal when clicking outside
window.onclick = function(event) {
const loginModal = document.getElementById('loginModal');
const registerModal = document.getElementById('registerModal');
if (event.target === loginModal) {
closeLoginModal();
}
if (event.target === registerModal) {
closeRegisterModal();
} }
} }
// Profile menu toggle
function toggleProfileMenu() {
const menu = document.getElementById('profile-menu');
if (menu) {
menu.classList.toggle('opacity-0');
menu.classList.toggle('invisible');
menu.classList.toggle('opacity-100');
menu.classList.toggle('visible');
}
}
// Admin code field toggle for registration
function toggleAdminCodeField() { function toggleAdminCodeField() {
const role = document.getElementById("role").value; const role = document.getElementById("role");
const field = document.getElementById("adminCodeField"); const field = document.getElementById("adminCodeField");
field.classList.toggle("hidden", role !== "3" && role !== "2"); // show only if Volunteer or Team Leader
if (role && field) {
const roleValue = role.value;
if (roleValue === "1") { // Admin or Team Leader
field.classList.add("hidden");
} else {
field.classList.remove("hidden");
}
}
} }
// Handle escape key // Close sidebar on ESC key
document.addEventListener('keydown', function(event) { document.addEventListener('keydown', (e) => {
if (event.key === 'Escape') { if (e.key === 'Escape') {
closeLoginModal(); const sidebar = document.getElementById('sidebar');
closeRegisterModal(); const sidebarOverlay = document.getElementById('sidebar-overlay');
const body = document.body;
if (sidebar && sidebarOverlay && sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
sidebarOverlay.classList.remove('active');
body.style.overflow = '';
} }
});
// Close mobile menu when clicking outside (for landing page)
document.addEventListener('click', function(event) {
const mobileMenuButton = event.target.closest('[\\@click="mobileMenuOpen = !mobileMenuOpen"]');
const mobileMenu = event.target.closest('.md\\:hidden .bg-white');
if (!mobileMenuButton && !mobileMenu) {
// This will be handled by Alpine.js automatically
} }
}); });
// Handle window resize to ensure proper mobile behavior // Handle window resize to ensure proper mobile behavior
window.addEventListener('resize', function() { window.addEventListener('resize', function() {
if (window.innerWidth >= 1024) { if (window.innerWidth >= 768) {
// Close mobile menus on desktop const sidebar = document.getElementById('sidebar');
const sidebarComponent = document.querySelector('[x-data]'); const sidebarOverlay = document.getElementById('sidebar-overlay');
if (sidebarComponent && sidebarComponent.__x) { const body = document.body;
sidebarComponent.__x.$data.sidebarOpen = false;
sidebarComponent.__x.$data.mobileMenuOpen = false; if (sidebar && sidebarOverlay) {
sidebar.classList.remove('active');
sidebarOverlay.classList.remove('active');
body.style.overflow = '';
} }
} }
}); });
// Close sidebar when clicking on overlay
document.addEventListener('DOMContentLoaded', function() {
const sidebarOverlay = document.getElementById('sidebar-overlay');
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', function() {
toggleSidebar();
});
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,6 +1,5 @@
{{ define "content" }} {{ define "content" }}
<div class="min-h-screen bg-gray-100"> <div class="min-h-screen bg-gray-100">
<!-- Header -->
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<!-- Create Post Form --> <!-- Create Post Form -->

View File

@@ -16,10 +16,9 @@
> >
<option value="">Select Category</option> <option value="">Select Category</option>
<option value="users" {{if eq .Category "users"}}selected{{end}}>Users & Teams</option> <option value="users" {{if eq .Category "users"}}selected{{end}}>Users & Teams</option>
<option value="addresses" {{if eq .Category "addresses"}}selected{{end}}>Addresses</option> <option value="address" {{if eq .Category "address"}}selected{{end}}>Addresses</option>
<option value="appointments" {{if eq .Category "appointments"}}selected{{end}}>Appointments</option> <option value="appointments" {{if eq .Category "appointments"}}selected{{end}}>Appointments</option>
<option value="polls" {{if eq .Category "polls"}}selected{{end}}>Polls</option> <option value="polls" {{if eq .Category "polls"}}selected{{end}}>Polls</option>
<option value="responses" {{if eq .Category "responses"}}selected{{end}}>Poll Responses</option>
<option value="availability" {{if eq .Category "availability"}}selected{{end}}>Availability</option> <option value="availability" {{if eq .Category "availability"}}selected{{end}}>Availability</option>
</select> </select>
</div> </div>
@@ -41,30 +40,15 @@
</select> </select>
</div> </div>
<!-- Date Range (optional) --> <!-- Date Range -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="date_from" class="text-gray-700 font-medium">From:</label> <label for="date_from" class="text-gray-700 font-medium">From:</label>
<input <input type="date" name="date_from" id="date_from" value="{{.DateFrom}}" class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"/>
type="date"
name="date_from"
id="date_from"
value="{{.DateFrom}}"
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
/>
<label for="date_to" class="text-gray-700 font-medium">To:</label> <label for="date_to" class="text-gray-700 font-medium">To:</label>
<input <input type="date" name="date_to" id="date_to" value="{{.DateTo}}" class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"/>
type="date"
name="date_to"
id="date_to"
value="{{.DateTo}}"
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
/>
</div> </div>
<button <button type="submit" class="px-4 py-2 bg-purple-600 text-white font-medium hover:bg-purple-700 transition-all duration-200 text-sm">
type="submit"
class="px-4 py-2 bg-purple-600 text-white font-medium hover:bg-purple-700 transition-all duration-200 text-sm"
>
<i class="fas fa-chart-bar mr-2"></i>Generate Report <i class="fas fa-chart-bar mr-2"></i>Generate Report
</button> </button>
</form> </form>
@@ -76,16 +60,10 @@
<div class="text-gray-600"> <div class="text-gray-600">
<span>{{.Result.Count}} results</span> <span>{{.Result.Count}} results</span>
</div> </div>
<button <button onclick="exportResults()" class="px-3 py-1.5 bg-green-600 text-white hover:bg-green-700 transition-colors">
onclick="exportResults()"
class="px-3 py-1.5 bg-green-600 text-white hover:bg-green-700 transition-colors"
>
<i class="fas fa-download mr-1"></i>Export CSV <i class="fas fa-download mr-1"></i>Export CSV
</button> </button>
<button <button onclick="printReport()" class="px-3 py-1.5 bg-blue-600 text-white hover:bg-blue-700 transition-colors">
onclick="printReport()"
class="px-3 py-1.5 bg-blue-600 text-white hover:bg-blue-700 transition-colors"
>
<i class="fas fa-print mr-1"></i>Print <i class="fas fa-print mr-1"></i>Print
</button> </button>
</div> </div>
@@ -93,43 +71,31 @@
</div> </div>
</div> </div>
<!-- Main Content Area --> <!-- Main Content -->
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
{{if .Result}} {{if .Result}}
{{if .Result.Error}} {{if .Result.Error}}
<!-- Error State -->
<div class="p-6"> <div class="p-6">
<div class="bg-red-50 border border-red-200 p-6"> <div class="bg-red-50 border border-red-200 p-6">
<div class="flex items-start">
<div class="w-10 h-10 bg-red-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-exclamation-triangle text-red-600"></i>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-red-800 mb-2">Report Error</h3> <h3 class="text-lg font-semibold text-red-800 mb-2">Report Error</h3>
<p class="text-red-700">{{.Result.Error}}</p> <p class="text-red-700">{{.Result.Error}}</p>
</div> </div>
</div> </div>
</div>
</div>
{{else}} {{else}}
<!-- Report Header --> <!-- Report Header -->
<div class="bg-white border-b border-gray-200 px-6 py-4"> <div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div class="flex items-center justify-between">
<div> <div>
<h2 class="text-xl font-semibold text-gray-900">{{.ReportTitle}}</h2> <h2 class="text-xl font-semibold text-gray-900">{{.ReportTitle}}</h2>
<p class="text-sm text-gray-600 mt-1">{{.ReportDescription}}</p> <p class="text-sm text-gray-600 mt-1">{{.ReportDescription}}</p>
</div> </div>
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500">Generated: {{.GeneratedAt}}</div>
Generated: {{.GeneratedAt}}
</div>
</div>
</div> </div>
<!-- Results Table --> <!-- Results Table -->
{{if gt .Result.Count 0}} {{if gt .Result.Count 0}}
<div class="flex-1 overflow-x-auto overflow-y-auto bg-white"> <div class="flex-1 overflow-x-auto overflow-y-auto bg-white">
<table class="w-full divide-gray-200 text-sm table-auto"> <table class="w-full divide-gray-200 text-sm table-auto">
<thead class="bg-gray-50 divide-gray-200 sticky top-0"> <thead class="bg-gray-50 sticky top-0">
<tr class="text-left text-gray-700 font-medium border-b border-gray-200"> <tr class="text-left text-gray-700 font-medium border-b border-gray-200">
{{range .Result.Columns}} {{range .Result.Columns}}
<th class="px-6 py-3 whitespace-nowrap">{{formatColumnName .}}</th> <th class="px-6 py-3 whitespace-nowrap">{{formatColumnName .}}</th>
@@ -148,7 +114,7 @@
</table> </table>
</div> </div>
<!-- Summary Stats (if available) --> <!-- Summary Stats -->
{{if .SummaryStats}} {{if .SummaryStats}}
<div class="bg-gray-50 border-t border-gray-200 px-6 py-4"> <div class="bg-gray-50 border-t border-gray-200 px-6 py-4">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Summary Statistics</h4> <h4 class="text-sm font-semibold text-gray-700 mb-3">Summary Statistics</h4>
@@ -164,187 +130,75 @@
{{end}} {{end}}
{{else}} {{else}}
<!-- No Results State -->
<div class="flex-1 flex items-center justify-center"> <div class="flex-1 flex items-center justify-center">
<div class="text-center py-12">
<div class="w-16 h-16 bg-gray-100 flex items-center justify-center mx-auto mb-4">
<i class="fas fa-chart-bar text-gray-400 text-xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No Data Found</h3>
<p class="text-gray-500">No results match your selected criteria</p> <p class="text-gray-500">No results match your selected criteria</p>
</div> </div>
</div>
{{end}} {{end}}
{{end}} {{end}}
{{else}} {{else}}
<!-- Welcome State -->
<div class="flex-1 flex items-center justify-center"> <div class="flex-1 flex items-center justify-center">
<div class="text-center py-12 max-w-4xl mx-auto px-6"> <p class="text-gray-600">Select a category and report to generate results</p>
<div class="mb-8">
<div class="w-20 h-20 bg-gradient-to-br from-purple-600 to-purple-700 flex items-center justify-center mx-auto mb-4">
<i class="fas fa-chart-line text-white text-2xl"></i>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">Campaign Reports</h1>
<p class="text-gray-600 text-lg">Generate detailed reports across all your campaign data</p>
</div>
<!-- Report Categories Overview -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-left">
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
<div class="font-medium text-gray-900 text-sm mb-1">Users & Teams</div>
<div class="text-xs text-gray-500">Volunteer performance, team stats, role distribution</div>
</div>
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
<div class="font-medium text-gray-900 text-sm mb-1">Address Reports</div>
<div class="text-xs text-gray-500">Coverage areas, visit status, geographical insights</div>
</div>
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
<div class="font-medium text-gray-900 text-sm mb-1">Appointments</div>
<div class="text-xs text-gray-500">Schedule analysis, completion rates, time trends</div>
</div>
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
<div class="font-medium text-gray-900 text-sm mb-1">Poll Analytics</div>
<div class="text-xs text-gray-500">Response rates, donation tracking, engagement metrics</div>
</div>
</div>
<div class="mt-6 text-sm text-gray-500">
Select a category above to see available reports
</div>
</div>
</div> </div>
{{end}} {{end}}
</div> </div>
</div> </div>
<style>
/* Square corners across UI */
* {
border-radius: 0 !important;
}
input, select, button {
transition: all 0.2s ease;
font-weight: 500;
letter-spacing: 0.025em;
}
@media print {
.no-print {
display: none !important;
}
/* Print-specific styles */
body {
background: white !important;
}
.bg-gray-50 {
background: white !important;
}
}
</style>
<script> <script>
// Report definitions for each category
const reportDefinitions = { const reportDefinitions = {
users: [ users: [
{ id: 'users_by_role', name: 'Users by Role' }, { id: 'participation', name: 'Volunteer Participation Rate' },
{ id: 'volunteer_activity', name: 'Volunteer Activity Summary' }, { id: 'top_performers', name: 'Top Performing Volunteers' },
{ id: 'team_performance', name: 'Team Performance Report' }, { id: 'efficiency', name: 'Response-to-Donation Ratio' },
{ id: 'admin_workload', name: 'Admin Workload Analysis' }, { id: 'coverage', name: 'User Address Coverage' }
{ id: 'inactive_users', name: 'Inactive Users Report' }
], ],
addresses: [ address: [
{ id: 'coverage_by_area', name: 'Coverage by Area' }, { id: 'responses_by_address', name: 'Total Responses by Address' },
{ id: 'visits_by_postal', name: 'Visits by Postal Code' }, { id: 'donations_by_address', name: 'Total Donations by Address' },
{ id: 'unvisited_addresses', name: 'Unvisited Addresses' }, { id: 'street_breakdown', name: 'Street-Level Breakdown' },
{ id: 'donations_by_location', name: 'Donations by Location' }, { id: 'quadrant_summary', name: 'Quadrant Summary' }
{ id: 'address_validation_status', name: 'Address Validation Status' }
], ],
appointments: [ appointments: [
{ id: 'appointments_by_day', name: 'Appointments by Day' }, { id: 'upcoming', name: 'Upcoming Appointments' },
{ id: 'completion_rates', name: 'Completion Rates' }, { id: 'completion', name: 'Appointments Completion Rate' },
{ id: 'volunteer_schedules', name: 'Volunteer Schedules' }, { id: 'geo_distribution', name: 'Appointments by Quadrant' },
{ id: 'missed_appointments', name: 'Missed Appointments' }, { id: 'lead_time', name: 'Average Lead Time' }
{ id: 'peak_hours', name: 'Peak Activity Hours' }
], ],
polls: [ polls: [
{ id: 'poll_creation_stats', name: 'Poll Creation Statistics' }, { id: 'distribution', name: 'Response Distribution' },
{ id: 'donation_analysis', name: 'Donation Analysis' }, { id: 'average', name: 'Average Poll Response' },
{ id: 'active_vs_inactive', name: 'Active vs Inactive Polls' }, { id: 'donations_by_poll', name: 'Donations by Poll' },
{ id: 'poll_trends', name: 'Poll Activity Trends' }, { id: 'correlation', name: 'Response-to-Donation Correlation' }
{ id: 'creator_performance', name: 'Creator Performance' }
],
responses: [
{ id: 'voter_status', name: 'Voter Status Report' },
{ id: 'sign_requests', name: 'Sign Requests Summary' },
{ id: 'feedback_analysis', name: 'Feedback Analysis' },
{ id: 'response_trends', name: 'Response Trends' },
{ id: 'repeat_voters', name: 'Repeat Voters Analysis' }
], ],
availability: [ availability: [
{ id: 'volunteer_availability', name: 'Volunteer Availability' }, { id: 'by_date', name: 'Volunteer Availability by Date' },
{ id: 'peak_availability', name: 'Peak Availability Times' }, { id: 'gaps', name: 'Coverage Gaps' },
{ id: 'coverage_gaps', name: 'Coverage Gaps' }, { id: 'overlaps', name: 'Volunteer Overlaps' },
{ id: 'schedule_conflicts', name: 'Schedule Conflicts' } { id: 'fulfillment', name: 'Volunteer Fulfillment' }
] ]
}; };
// Update reports dropdown when category changes
function updateReports() { function updateReports() {
const categorySelect = document.getElementById('category'); const category = document.getElementById('category').value;
const reportSelect = document.getElementById('report'); const reportSelect = document.getElementById('report');
const category = categorySelect.value;
// Clear existing options
reportSelect.innerHTML = '<option value="">Select Report</option>'; reportSelect.innerHTML = '<option value="">Select Report</option>';
if (reportDefinitions[category]) {
if (category && reportDefinitions[category]) { reportDefinitions[category].forEach(r => {
reportDefinitions[category].forEach(report => { const opt = document.createElement('option');
const option = document.createElement('option'); opt.value = r.id;
option.value = report.id; opt.textContent = r.name;
option.textContent = report.name; reportSelect.appendChild(opt);
reportSelect.appendChild(option);
}); });
} }
} }
// Export results
function exportResults() { function exportResults() {
const form = document.querySelector('form'); const params = new URLSearchParams(new FormData(document.querySelector('form')));
const formData = new FormData(form);
const params = new URLSearchParams(formData);
params.set('export', 'csv'); params.set('export', 'csv');
window.location.href = `/reports/export?${params.toString()}`; window.location.href = `/reports?${params.toString()}`;
} }
// Print report function printReport() { window.print(); }
function printReport() {
window.print();
}
// Initialize on page load document.addEventListener("DOMContentLoaded", updateReports);
document.addEventListener("DOMContentLoaded", function () {
updateReports();
// Set default date range (last 30 days)
const dateFrom = document.getElementById('date_from');
const dateTo = document.getElementById('date_to');
if (!dateFrom.value) {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
dateFrom.value = thirtyDaysAgo.toISOString().split('T')[0];
}
if (!dateTo.value) {
dateTo.value = new Date().toISOString().split('T')[0];
}
});
</script> </script>
{{ end }} {{ end }}

View File

@@ -0,0 +1,208 @@
{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden ">
<!-- Main Content -->
<div class="flex-1 p-4 md:p-6 overflow-auto">
<div class="space-y-6">
{{ range .TeamLeads }}
{{ $teamLeadID := .ID }}
<!-- Team Lead Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<!-- Team Lead Header -->
<div class="bg-gray-50 border-b border-gray-200 px-6 py-4">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<!-- Team Lead Info -->
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
<i class="fas fa-user-tie text-white"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">{{ .Name }}</h3>
<p class="text-sm text-gray-600">
{{ if .Volunteers }}
{{ len .Volunteers }} volunteer{{ if ne (len .Volunteers) 1 }}s{{ end }} assigned
{{ else }}
No volunteers assigned
{{ end }}
</p>
</div>
</div>
<!-- Add Volunteer Form -->
{{ if $.UnassignedVolunteers }}
<form action="/team_builder" method="POST" class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 w-full lg:w-auto">
<input type="hidden" name="team_lead_id" value="{{ .ID }}" />
<select
name="volunteer_id"
class="px-4 py-2 text-sm 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 min-w-0 flex-1 sm:w-64"
required
>
<option value="">Select volunteer to assign</option>
{{ range $.UnassignedVolunteers }}
<option value="{{ .ID }}">{{ .Name }}</option>
{{ end }}
</select>
<button
type="submit"
class="flex items-center justify-center px-4 py-2 bg-blue-500 text-white text-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium rounded-lg transition-colors whitespace-nowrap"
>
<i class="fas fa-plus mr-2"></i> Add Volunteer
</button>
</form>
{{ else }}
<div class="text-sm text-gray-500 bg-gray-100 px-4 py-2 rounded-lg">
<i class="fas fa-info-circle mr-2"></i>
All volunteers have been assigned
</div>
{{ end }}
</div>
</div>
<!-- Desktop Volunteer List -->
<div class="hidden md:block">
{{ if .Volunteers }}
<div class="overflow-x-auto">
<table class="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">
Volunteer
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</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">
{{ range .Volunteers }}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center">
<i class="fas fa-user text-gray-500 text-sm"></i>
</div>
<div class="text-sm font-medium text-gray-900">{{ .Name }}</div>
</div>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Assigned
</span>
</td>
<td class="px-6 py-4">
<form action="/team_builder/remove_volunteer" method="POST" class="inline-block">
<input type="hidden" name="team_lead_id" value="{{ $teamLeadID }}" />
<input type="hidden" name="volunteer_id" value="{{ .ID }}" />
<button
type="submit"
class="text-red-400 hover:text-red-600 p-1"
title="Remove {{ .Name }} from team"
onclick="return confirm('Remove {{ .Name }} from this team?')"
>
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ else }}
<div class="px-6 py-8 text-center">
<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 assigned</h3>
<p class="text-gray-500">Use the form above to assign volunteers to this team lead.</p>
</div>
{{ end }}
</div>
<!-- Mobile Volunteer Cards -->
<div class="md:hidden">
{{ if .Volunteers }}
<div class="divide-y divide-gray-100">
{{ range .Volunteers }}
<div class="px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1 min-w-0">
<div class="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center flex-shrink-0">
<i class="fas fa-user text-gray-500"></i>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 truncate">{{ .Name }}</div>
<div class="flex items-center mt-1">
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i class="fas fa-check mr-1"></i> Assigned
</span>
</div>
</div>
</div>
<form action="/team_builder/remove_volunteer" method="POST" class="ml-4">
<input type="hidden" name="team_lead_id" value="{{ $teamLeadID }}" />
<input type="hidden" name="volunteer_id" value="{{ .ID }}" />
<button
type="submit"
class="px-3 py-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors text-sm font-medium"
onclick="return confirm('Remove {{ .Name }} from this team?')"
>
<i class="fas fa-trash mr-1"></i> Remove
</button>
</form>
</div>
</div>
{{ end }}
</div>
{{ else }}
<div class="px-6 py-8 text-center">
<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 assigned</h3>
<p class="text-gray-500 text-sm">Use the form above to assign volunteers to this team lead.</p>
</div>
{{ end }}
</div>
</div>
{{ end }}
<!-- No Team Leads State -->
{{ if not .TeamLeads }}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-12 text-center">
<div class="text-gray-400 mb-4">
<i class="fas fa-user-tie text-6xl"></i>
</div>
<h3 class="text-xl font-medium text-gray-900 mb-2">No Team Leads Available</h3>
<p class="text-gray-500">Add team leads to start building teams and assigning volunteers.</p>
</div>
</div>
{{ end }}
</div>
</div>
</div>
<style>
/* Consistent styling with addresses component */
input, select, button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
/* Ensure proper responsive behavior */
@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 }}

View File

@@ -1,102 +0,0 @@
{{ define "content" }}
<div class="min-h-screen bg-gray-50">
<!-- Main Content -->
<div class="p-6 space-y-6">
{{range .TeamLeads}} {{ $teamLeadID := .ID }}
<!-- store team lead ID -->
<div class="bg-white border border-gray-200 shadow-sm">
<!-- Team Lead Header -->
<div
class="flex justify-between items-center px-4 py-3 border-b border-gray-200"
>
<div class="flex items-center space-x-3">
<i class="fas fa-user-tie text-blue-600"></i>
<span class="font-semibold text-gray-900">{{.Name}}</span>
</div>
<form
action="/team_builder"
method="POST"
class="flex items-center space-x-3"
>
<input type="hidden" name="team_lead_id" value="{{.ID}}" />
<select
name="volunteer_id"
class="px-3 py-2 border border-gray-300 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">--Select Volunteer--</option>
{{range $.UnassignedVolunteers}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
</select>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
>
<i class="fas fa-plus mr-2"></i> Add
</button>
</form>
</div>
<!-- Assigned Volunteers -->
<div class="px-6 py-4">
{{if .Volunteers}}
<ul class="space-y-2">
{{range .Volunteers}}
<li
class="flex items-center justify-between text-gray-800 border-b border-gray-200 py-2"
>
<div class="flex items-center space-x-2">
<i class="fas fa-user text-gray-500"></i>
<span>{{.Name}}</span>
</div>
<form
action="/team_builder/remove_volunteer"
method="POST"
class="flex-shrink-0"
>
<input
type="hidden"
name="team_lead_id"
value="{{ $teamLeadID }}"
/>
<input type="hidden" name="volunteer_id" value="{{.ID}}" />
<button
type="submit"
aria-label="Remove {{.Name}}"
class="px-3 py-1 bg-red-600 text-white hover:bg-red-700 focus:outline-none focus:ring-1 focus:ring-red-500"
>
<i class="fas fa-times"></i> Remove
</button>
</form>
</li>
{{end}}
</ul>
{{else}}
<p class="text-gray-500 italic">No volunteers assigned yet.</p>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
<style>
/* Square corners across UI */
* {
border-radius: 0 !important;
}
input,
select,
button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
</style>
{{ end }}

View File

@@ -110,7 +110,7 @@ func schedualHandler(w http.ResponseWriter, r *http.Request) {
} }
func HomeHandler(w http.ResponseWriter, r *http.Request) { func HomeHandler(w http.ResponseWriter, r *http.Request) {
utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{ utils.Render(w, "dashboard.html", map[string]interface{}{
"Title": "Admin Dashboard", "Title": "Admin Dashboard",
"IsAuthenticated": false, "IsAuthenticated": false,
"ActiveSection": "dashboard", "ActiveSection": "dashboard",
@@ -167,6 +167,6 @@ func main() {
// Poll routes (volunteer only) // Poll routes (volunteer only)
http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler)) http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler))
log.Println("Server started on localhost:8080") log.Println("Server started on http://localhost:8080")
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil)) log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
} }