update to volunteer

This commit is contained in:
Mann Patel
2025-08-28 17:09:23 -06:00
parent 9dd24e71e7
commit 1955407d7c
16 changed files with 1075 additions and 306 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/uploads
.env
/Example_code
/tmp
/Example_code

View File

@@ -4,6 +4,7 @@ import (
"log"
"net/http"
"strconv"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
@@ -262,12 +263,19 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
userIDStr := r.FormValue("user_id")
addressIDStr := r.FormValue("address_id")
appointmentDate := r.FormValue("appointment_date")
startTime := r.FormValue("time")
if userIDStr == "" || addressIDStr == "" {
http.Error(w, "User ID and Address ID are required", http.StatusBadRequest)
return
}
if appointmentDate == "" || startTime == "" {
http.Error(w, "Appointment date and start time are required", http.StatusBadRequest)
return
}
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
@@ -280,6 +288,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Parse and validate the appointment date
parsedDate, err := time.Parse("2006-01-02", appointmentDate)
if err != nil {
http.Error(w, "Invalid appointment date format", http.StatusBadRequest)
return
}
// Validate that the appointment date is not in the past
today := time.Now().Truncate(24 * time.Hour)
if parsedDate.Before(today) {
http.Error(w, "Appointment date cannot be in the past", http.StatusBadRequest)
return
}
// Parse and validate the start time
_, err = time.Parse("15:04", startTime)
if err != nil {
http.Error(w, "Invalid start time format", http.StatusBadRequest)
return
}
// Verify the user exists and is associated with the current admin
currentAdminID := r.Context().Value("user_id").(int)
var userExists int
@@ -314,11 +343,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Assign the address - create appointment
// Check if the user already has an appointment at the same date and time
var timeConflict int
err = models.DB.QueryRow(`
SELECT COUNT(*) FROM appointment
WHERE user_id = $1 AND appointment_date = $2 AND appointment_time = $3
`, userID, appointmentDate, startTime).Scan(&timeConflict)
if err != nil {
log.Println("Time conflict check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if timeConflict > 0 {
http.Error(w, "User already has an appointment at this date and time", http.StatusBadRequest)
return
}
// Assign the address - create appointment with specific date and time
_, err = models.DB.Exec(`
INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
VALUES ($1, $2, CURRENT_DATE, CURRENT_TIME, NOW(), NOW())
`, userID, addressID)
VALUES ($1, $2, $3, $4, NOW(), NOW())
`, userID, addressID, appointmentDate, startTime)
if err != nil {
log.Println("Assignment error:", err)
http.Error(w, "Failed to assign address", http.StatusInternalServerError)

View File

@@ -33,7 +33,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
// 2. Total donations from polls
err = models.DB.QueryRow(`
SELECT COALESCE(SUM(amount_donated), 0)
FROM poll;
FROM poll_responce;
`).Scan(&totalDonations)
if err != nil {
log.Println("Donation query error:", err)

View File

@@ -8,73 +8,103 @@ import (
"github.com/patel-mann/poll-system/app/internal/utils"
)
func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
// Fetch appointments joined with address info
// Fetch appointments joined with address info
currentUserID := models.GetCurrentUserID(w, r)
username, _ := models.GetCurrentUserName(r)
currentUserID := models.GetCurrentUserID(w,r)
username,_ := models.GetCurrentUserName(r)
rows, err := models.DB.Query(`
SELECT
a.sched_id,
a.user_id,
ad.address_id,
ad.address,
ad.latitude,
ad.longitude,
a.appointment_date,
a.appointment_time
FROM appointment a
JOIN address_database ad ON a.address_id = ad.address_id
WHERE a.user_id = $1
`, currentUserID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
rows, err := models.DB.Query(`
SELECT
a.sched_id,
a.user_id,
ad.address,
ad.latitude,
ad.longitude,
a.appointment_date,
a.appointment_time
FROM appointment a
JOIN address_database ad ON a.address_id = ad.address_id
WHERE a.user_id = $1
`, currentUserID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
// Struct to hold appointment + address info
type AppointmentWithAddress struct {
SchedID int
UserID int
AddressID int
Address string
Latitude float64
Longitude float64
AppointmentDate time.Time
AppointmentTime time.Time
HasPollResponse bool // New field to track poll status
PollButtonText string // New field for button text
PollButtonClass string // New field for button styling
}
// Struct to hold appointment + address info
type AppointmentWithAddress struct {
SchedID int
UserID int
Address string
Latitude float64
Longitude float64
AppointmentDate time.Time
AppointmentTime time.Time
}
var appointments []AppointmentWithAddress
for rows.Next() {
var a AppointmentWithAddress
if err := rows.Scan(&a.SchedID, &a.UserID, &a.AddressID, &a.Address, &a.Latitude, &a.Longitude, &a.AppointmentDate, &a.AppointmentTime); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Check if poll response exists for this address
var pollResponseExists bool
err = models.DB.QueryRow(`
SELECT EXISTS(
SELECT 1
FROM poll p
JOIN poll_response pr ON p.poll_id = pr.poll_id
WHERE p.address_id = $1 AND p.user_id = $2
)
`, a.AddressID, currentUserID).Scan(&pollResponseExists)
if err != nil {
// If there's an error checking, default to no response
pollResponseExists = false
}
// Set button properties based on poll response status
a.HasPollResponse = pollResponseExists
if pollResponseExists {
a.PollButtonText = "Poll Taken"
a.PollButtonClass = "px-3 py-1 bg-green-600 text-white text-sm rounded cursor-not-allowed"
} else {
a.PollButtonText = "Ask Poll"
a.PollButtonClass = "px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700 rounded"
}
appointments = append(appointments, a)
}
var appointments []AppointmentWithAddress
for rows.Next() {
var a AppointmentWithAddress
if err := rows.Scan(&a.SchedID, &a.UserID, &a.Address, &a.Latitude, &a.Longitude, &a.AppointmentDate, &a.AppointmentTime); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
appointments = append(appointments, a)
}
role := r.Context().Value("user_role").(int)
adminnav := false
volunteernav := false
if role == 1{
if role == 1 {
adminnav = true
volunteernav = false
}else{
} else {
adminnav = false
volunteernav = true
}
// Render template
utils.Render(w, "/appointment.html", map[string]interface{}{
"Title": "My Profile",
"IsAuthenticated": true,
"ShowAdminNav": adminnav, // your existing variable
"ShowVolunteerNav": volunteernav, // your existing variable
"ActiveSection": "address",
"UserName": username,
"Appointments": appointments, // pass the fetched appointments
})
}
// Render template
utils.Render(w, "/appointment.html", map[string]interface{}{
"Title": "My Profile",
"IsAuthenticated": true,
"ShowAdminNav": adminnav,
"ShowVolunteerNav": volunteernav,
"ActiveSection": "address",
"UserName": username,
"Appointments": appointments,
})
}

View File

@@ -0,0 +1,138 @@
package handlers
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
func PollHandler(w http.ResponseWriter, r *http.Request) {
username, _ := models.GetCurrentUserName(r)
if r.Method == http.MethodGet {
addressID := r.URL.Query().Get("address_id")
if addressID == "" {
http.Error(w, "Address ID required", http.StatusBadRequest)
return
}
// Get address details
var address string
var userID int
fmt.Print(addressID, userID)
err := models.DB.QueryRow(`
SELECT a.address, ap.user_id
FROM appointment AS ap
JOIN address_database a ON a.address_id = ap.address_id
WHERE ap.address_id = $1
`, addressID).Scan(&address, &userID)
if err != nil {
http.Error(w, "Address not found", http.StatusNotFound)
return
}
// Check if poll already exists for this address
var pollID int
err = models.DB.QueryRow(`
SELECT poll_id
FROM poll
WHERE address_id = $1 AND user_id = $2
`, addressID, userID).Scan(&pollID)
// If no poll exists, create one
if err == sql.ErrNoRows {
err = models.DB.QueryRow(`
INSERT INTO poll (user_id, address_id, poll_title, poll_description, is_active)
VALUES ($1, $2, 'Door-to-Door Poll', 'Campaign polling questions', true)
RETURNING poll_id
`, userID, addressID).Scan(&pollID)
if err != nil {
log.Printf("Failed to create poll: %v", err)
http.Error(w, "Failed to create poll", http.StatusInternalServerError)
return
}
} else if err != nil {
log.Printf("Database error: %v", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
utils.Render(w, "volunteer/poll_form.html", map[string]interface{}{
"Title": "Poll Questions",
"IsAuthenticated": true,
"ShowAdminNav": true,
"UserName": username,
"ActiveSection": "appointments",
"PollID": pollID,
"AddressID": addressID,
"Address": address,
"PageIcon": "fas fa-poll",
})
return
}
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
pollID := r.FormValue("poll_id")
postalCode := r.FormValue("postal_code")
question5Thoughts := r.FormValue("question5_thoughts")
// Parse boolean values
var question1VotedBefore *bool
if val := r.FormValue("question1_voted_before"); val != "" {
if val == "true" {
b := true
question1VotedBefore = &b
} else if val == "false" {
b := false
question1VotedBefore = &b
}
}
var question2VoteAgain *bool
if val := r.FormValue("question2_vote_again"); val != "" {
if val == "true" {
b := true
question2VoteAgain = &b
} else if val == "false" {
b := false
question2VoteAgain = &b
}
}
// Parse integer values
question3LawnSigns, _ := strconv.Atoi(r.FormValue("question3_lawn_signs"))
question4BannerSigns, _ := strconv.Atoi(r.FormValue("question4_banner_signs"))
// Insert poll response
_, err = models.DB.Exec(`
INSERT INTO poll_response (
poll_id, respondent_postal_code, question1_voted_before,
question2_vote_again, question3_lawn_signs, question4_banner_signs,
question5_thoughts
) VALUES ($1, $2, $3, $4, $5, $6, $7)
`, pollID, postalCode, question1VotedBefore, question2VoteAgain,
question3LawnSigns, question4BannerSigns, question5Thoughts)
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)
}
}

View File

@@ -1,5 +1,4 @@
// Add this to your handlers package (create volunteer_posts.go or add to existing file)
package handlers
import (
@@ -12,7 +11,18 @@ import (
"github.com/patel-mann/poll-system/app/internal/utils"
)
// VolunteerPostsHandler - Read-only posts view for volunteers
type VolunteerStatistics struct {
AppointmentsToday int
AppointmentsThisWeek int
TotalAppointments int
PollsCompleted int
PollsRemaining int
LawnSignsRequested int
BannerSignsRequested int
PollCompletionPercent int
}
// VolunteerPostsHandler - Dashboard view for volunteers with posts and statistics
func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
// Only allow GET requests for volunteers
if r.Method != http.MethodGet {
@@ -23,7 +33,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
// Get user info from context
role := r.Context().Value("user_role").(int)
CurrentUserID := models.GetCurrentUserID(w, r)
username,_ := models.GetCurrentUserName(r)
username, _ := models.GetCurrentUserName(r)
// Fetch posts from database
rows, err := models.DB.Query(`
@@ -34,7 +44,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
JOIN admin_volunteers x ON u.user_id = x.admin_id
WHERE x.volunteer_id = $1
ORDER BY p.created_at DESC
`,CurrentUserID)
`, CurrentUserID)
if err != nil {
fmt.Printf("Database query error: %v\n", err)
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
@@ -60,20 +70,107 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Get volunteer statistics
stats, err := getVolunteerStatistics(CurrentUserID)
if err != nil {
fmt.Printf("Failed to fetch statistics: %v\n", err)
// Continue with empty stats rather than failing
stats = &VolunteerStatistics{}
}
// Get navigation flags
showAdminNav, showVolunteerNav := getNavFlags(role)
fmt.Printf("Volunteer viewing %d posts\n", len(posts))
utils.Render(w, "dashboard/volunteer_dashboard.html", map[string]interface{}{
"Title": "Community Posts",
"Title": "Volunteer Dashboard",
"IsAuthenticated": true,
"ShowAdminNav": showAdminNav,
"ShowVolunteerNav": showVolunteerNav,
"UserName": username,
"UserName": username,
"Posts": posts,
"ActiveSection": "posts",
"IsVolunteer": true, // Flag to indicate this is volunteer view
"Statistics": stats,
"ActiveSection": "dashboard",
"IsVolunteer": true,
})
}
func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
stats := &VolunteerStatistics{}
today := time.Now().Format("2006-01-02")
// Get start of current week (Monday)
now := time.Now()
weekday := now.Weekday()
if weekday == time.Sunday {
weekday = 7
}
weekStart := now.AddDate(0, 0, -int(weekday)+1).Format("2006-01-02")
// Appointments today
err := models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, today).Scan(&stats.AppointmentsToday)
if err != nil {
return nil, err
}
// Appointments this week
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
`, userID, weekStart, today).Scan(&stats.AppointmentsThisWeek)
if err != nil {
return nil, err
}
// Total appointments
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1
`, userID).Scan(&stats.TotalAppointments)
if err != nil {
return nil, err
}
// Polls completed
err = models.DB.QueryRow(`
SELECT COUNT(DISTINCT pr.poll_response_id)
FROM poll p
JOIN poll_response pr ON p.poll_id = pr.poll_id
WHERE p.user_id = $1
`, userID).Scan(&stats.PollsCompleted)
if err != nil {
return nil, err
}
// Polls remaining (appointments without poll responses)
stats.PollsRemaining = stats.TotalAppointments - stats.PollsCompleted
// Calculate completion percentage
if stats.TotalAppointments > 0 {
stats.PollCompletionPercent = (stats.PollsCompleted * 100) / stats.TotalAppointments
} else {
stats.PollCompletionPercent = 0
}
// Signs requested
err = models.DB.QueryRow(`
SELECT
COALESCE(SUM(pr.question3_lawn_signs), 0),
COALESCE(SUM(pr.question4_banner_signs), 0)
FROM poll p
JOIN poll_response pr ON p.poll_id = pr.poll_id
WHERE p.user_id = $1
`, userID).Scan(&stats.LawnSignsRequested, &stats.BannerSignsRequested)
if err != nil {
return nil, err
}
return stats, nil
}

View File

@@ -18,7 +18,7 @@ func GetCurrentUserName(r *http.Request) (string, error) {
var currentUserName string
err := DB.QueryRow(`
SELECT first_name || ' ' || last_name
SELECT first_name
FROM users
WHERE user_id = $1
`, currentUserID).Scan(&currentUserName)

View File

@@ -100,7 +100,7 @@
>
<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">Cordinates</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>
@@ -158,7 +158,7 @@
{{ else }}
<button
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
onclick="openAssignModal({{ .AddressID }})"
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
>
Assign
</button>
@@ -187,7 +187,7 @@
</tr>
{{ else }}
<tr>
<td colspan="9" class="px-6 py-8 text-center text-gray-500">
<td colspan="7" class="px-6 py-8 text-center text-gray-500">
No addresses found
</td>
</tr>
@@ -201,37 +201,125 @@
id="assignModal"
class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"
>
<div class="bg-white p-6 rounded shadow-lg w-96">
<h2 class="text-lg font-semibold mb-4">Assign Address</h2>
<form id="assignForm" method="POST" action="/assign_address">
<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" />
<label for="user_id" class="block text-sm font-medium mb-2"
>Select User</label
>
<select
name="user_id"
id="user_id"
class="w-full border border-gray-300 px-3 py-2 mb-4 rounded"
required
>
<option value="">-- Select User --</option>
{{ range .Users }}
<option value="{{ .ID }}">{{ .Name }}</option>
{{ end }}
</select>
<div class="flex justify-end gap-2">
<!-- 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>
<!-- Duration Display -->
<div class="bg-blue-50 p-3 border border-blue-200">
<div class="flex items-center space-x-2 text-blue-800">
<i class="fas fa-stopwatch"></i>
<span class="text-sm font-medium"
>Duration: <span id="duration-display">0 minutes</span></span
>
</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 bg-gray-200 rounded hover:bg-gray-300"
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 rounded hover:bg-blue-700"
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"
>
Assign
<i class="fas fa-check mr-2"></i> Assign
</button>
</div>
</form>
@@ -239,26 +327,105 @@
</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>
function openAssignModal(addressID) {
// 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

@@ -1,38 +0,0 @@
{{ define "content" }}
<div class="container mx-auto mt-6">
<h2 class="text-2xl font-bold mb-4">Assigned Addresses</h2>
<table class="min-w-full border border-gray-300 shadow-md">
<thead>
<tr class="bg-gray-200">
<th class="px-4 py-2 border">ID</th>
<th class="px-4 py-2 border">Address</th>
<th class="px-4 py-2 border">Assigned</th>
<th class="px-4 py-2 border">Volunteer</th>
<th class="px-4 py-2 border">Email</th>
<th class="px-4 py-2 border">Phone</th>
<th class="px-4 py-2 border">Appointment Date</th>
<th class="px-4 py-2 border">Appointment Time</th>
</tr>
</thead>
<tbody>
{{range .AssignedList}}
<tr class="hover:bg-gray-100">
<td class="px-4 py-2 border">{{.AddressID}}</td>
<td class="px-4 py-2 border">
{{.Address}} {{.StreetName}} {{.StreetType}} {{.StreetQuadrant}}
</td>
<td class="px-4 py-2 border">
{{if .Assigned}}✅ Yes{{else}}❌ No{{end}}
</td>
<td class="px-4 py-2 border">{{.UserName}}</td>
<td class="px-4 py-2 border">{{.UserEmail}}</td>
<td class="px-4 py-2 border">{{.UserPhone}}</td>
<td class="px-4 py-2 border">{{.AppointmentDate}}</td>
<td class="px-4 py-2 border">{{.AppointmentTime}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{ end }}

View File

@@ -8,7 +8,7 @@
<i
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-calendar-alt{{end}} text-green-600"
></i>
<span class="text-sm font-medium"> Appointments </span>
<span class="text-sm font-medium">Appointments</span>
</div>
</div>
</div>
@@ -42,7 +42,7 @@
class="text-left text-gray-700 font-medium border-b border-gray-200"
>
<th class="px-6 py-3 whitespace-nowrap">Address</th>
<th class="px-6 py-3 whitespace-nowrap">Cordinated</th>
<th class="px-6 py-3 whitespace-nowrap">Coordinates</th>
<th class="px-6 py-3 whitespace-nowrap">Appointment Date</th>
<th class="px-6 py-3 whitespace-nowrap">Appointment Time</th>
<th class="px-6 py-3 whitespace-nowrap">Poll Question</th>
@@ -68,11 +68,16 @@
{{ .AppointmentTime.Format "15:04" }}
</td>
<td class="px-6 py-3 whitespace-nowrap">
<button
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
{{ if .HasPollResponse }}
<span class="{{ .PollButtonClass }}"> {{ .PollButtonText }} </span>
{{ else }}
<a
href="/poll?address_id={{ .AddressID }}"
class="{{ .PollButtonClass }}"
>
Ask Poll
</button>
{{ .PollButtonText }}
</a>
{{ end }}
</td>
</tr>
{{ else }}

View File

@@ -1,122 +1,286 @@
{{ define "content" }}
<div class="flex flex-col min-h-screen bg-gray-100">
<!-- Optional Header -->
<header class="bg-white shadow p-4">
<h1 class="text-xl font-bold">Community</h1>
</header>
<!-- Scrollable Posts -->
<main class="flex-1 overflow-y-auto px-2 py-4 max-w-2xl mx-auto space-y-4">
<!-- Posts Feed -->
{{range .Posts}}
<article class="bg-white border-b border-gray-200">
<!-- Post Header -->
<div class="flex items-center px-6 py-4">
<div class="flex-shrink-0">
<div
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold"
>
{{slice .AuthorName 0 1}}
</div>
</div>
<div class="ml-4">
<p class="text-sm font-semibold text-gray-900">{{.AuthorName}}</p>
<p class="text-xs text-gray-500">
{{.CreatedAt.Format "Jan 2, 2006"}}
</p>
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Top Navigation -->
<div class="bg-white border-b border-gray-200 px-4 sm:px-6 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<i class="fas fa-tachometer-alt text-green-600"></i>
<span class="text-sm font-medium">Volunteer Dashboard</span>
</div>
</div>
<!-- Post Image -->
{{if .ImageURL}}
<div class="w-full">
<img
src="{{.ImageURL}}"
alt="Post image"
class="w-full max-h-96 object-cover"
onerror="this.parentElement.style.display='none'"
/>
</div>
{{end}}
<!-- Post Actions -->
<div class="px-6 py-3">
<div class="flex items-center space-x-6">
<button
class="reaction-btn flex items-center space-x-2 text-gray-600 hover:text-blue-500 transition-colors"
data-post-id="{{.PostID}}"
data-reaction="like"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V18m-7-8a2 2 0 01-2-2V7a2 2 0 012-2h3.764a2 2 0 011.789 1.106L14 8v2m-7-8V5a2 2 0 012-2h1m-5 10h3m4 3H8"
></path>
</svg>
<span class="text-sm font-medium like-count">0</span>
</button>
<button
class="reaction-btn flex items-center space-x-2 text-gray-600 hover:text-red-500 transition-colors"
data-post-id="{{.PostID}}"
data-reaction="dislike"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018c.163 0 .326.02.485.06L17 4m-7 10v-8m7 8a2 2 0 002 2v1a2 2 0 01-2 2h-3.764a2 2 0 01-1.789-1.106L10 16v-2m7 8V19a2 2 0 00-2-2h-1m5-10H12m-4-3h4"
></path>
</svg>
<span class="text-sm font-medium dislike-count">0</span>
</button>
</div>
</div>
<!-- Post Content -->
{{if .Content}}
<div class="px-6 pb-4">
<p class="text-gray-900 leading-relaxed">
<span class="font-semibold">{{.AuthorName}}</span> {{.Content}}
</p>
</div>
{{end}}
</article>
{{else}}
<div class="bg-white p-12 text-center">
<div class="max-w-sm mx-auto">
<svg
class="w-16 h-16 mx-auto text-gray-300 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
<p class="text-gray-500">
Be the first to share something with the community!
</p>
<div class="text-sm text-gray-600 hidden sm:block">
Welcome back, {{ .UserName }}!
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 overflow-hidden bg-gray-50">
<div class="h-full flex flex-col lg:flex-row gap-6 p-4 sm:p-6">
<!-- Left Column - Posts -->
<div class="flex-1 lg:flex-none lg:w-2/3 space-y-0">
{{ if .Posts }}{{range .Posts}}
<article class="bg-white border-b border-gray-200">
<!-- Post Header -->
<div class="flex items-center px-4 sm:px-6 py-4">
<div class="flex-shrink-0">
<div
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold rounded-full"
>
{{slice .AuthorName 0 1}}
</div>
</div>
<div class="ml-4">
<p class="text-sm font-semibold text-gray-900">{{.AuthorName}}</p>
<p class="text-xs text-gray-500">
{{.CreatedAt.Format "Jan 2, 2006"}}
</p>
</div>
</div>
<!-- Post Image -->
{{if .ImageURL}}
<div class="w-full">
<img
src="{{.ImageURL}}"
alt="Post image"
class="w-full max-h-96 object-cover"
onerror="this.parentElement.style.display='none'"
/>
</div>
{{end}}
<!-- Post Content -->
{{if .Content}}
<div class="px-4 sm:px-6 pt-2 pb-4">
<p class="text-gray-900 leading-relaxed">
<span class="font-semibold">{{.AuthorName}}</span> {{.Content}}
</p>
</div>
{{end}}
</article>
{{else}}
<div class="bg-white p-8 sm:p-12 text-center border-b border-gray-200">
<div class="max-w-sm mx-auto">
<svg
class="w-16 h-16 mx-auto text-gray-300 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
<p class="text-gray-500">
Be the first to share something with the community!
</p>
</div>
</div>
{{ end }} {{ else }}
<div class="bg-white border-b border-gray-200 p-8 sm:p-12 text-center">
<div class="max-w-sm mx-auto">
<div
class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"
>
<i class="fas fa-inbox text-2xl text-gray-400"></i>
</div>
<p class="text-gray-600 font-medium mb-2">No posts yet</p>
<p class="text-sm text-gray-500">
Check back later for updates from your team
</p>
</div>
</div>
{{ end }}
</div>
<!-- Right Column - Statistics -->
<div class="w-full lg:w-1/3 flex flex-col gap-4 sm:gap-6">
<!-- Today's Overview -->
<div class="bg-white border-b border-gray-200">
<div class="px-4 sm:px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900 mb-4">
Today's Overview
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-calendar-day text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Appointments Today</span>
</div>
<span class="text-lg font-semibold text-gray-900">
{{ .Statistics.AppointmentsToday }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-calendar-week text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">This Week</span>
</div>
<span class="text-lg font-semibold text-gray-900">
{{ .Statistics.AppointmentsThisWeek }}
</span>
</div>
</div>
</div>
</div>
<!-- Polling Progress -->
<div class="bg-white border-b border-gray-200">
<div class="px-4 sm:px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900 mb-4">
Polling Progress
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-check-circle text-green-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Polls Completed</span>
</div>
<span class="text-lg font-semibold text-green-600">
{{ .Statistics.PollsCompleted }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-clock text-orange-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Polls Remaining</span>
</div>
<span class="text-lg font-semibold text-orange-600">
{{ .Statistics.PollsRemaining }}
</span>
</div>
<!-- Progress Bar -->
{{ if gt .Statistics.TotalAppointments 0 }}
<div class="mt-4">
<div class="flex justify-between text-xs text-gray-600 mb-2">
<span>Progress</span>
<span
>{{ .Statistics.PollsCompleted }}/{{
.Statistics.TotalAppointments }}</span
>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-gray-600 h-2 rounded-full transition-all duration-300"
style="width: {{ .Statistics.PollCompletionPercent }}%"
></div>
</div>
</div>
{{ end }}
</div>
</div>
</div>
<!-- Signs Summary -->
<div class="bg-white border-b border-gray-200">
<div class="px-4 sm:px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900 mb-4">
Signs Requested
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-sign text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Lawn Signs</span>
</div>
<span class="text-lg font-semibold text-gray-900">
{{ .Statistics.LawnSignsRequested }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-flag text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Banner Signs</span>
</div>
<span class="text-lg font-semibold text-gray-900">
{{ .Statistics.BannerSignsRequested }}
</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white border-b border-gray-200">
<div class="px-4 sm:px-6 py-4">
<h3 class="text-sm font-semibold text-gray-900 mb-4">
Quick Actions
</h3>
<div class="space-y-2">
<a
href="/volunteer/Addresses"
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
>
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-calendar-alt text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">View Appointments</span>
</a>
<a
href="/schedual"
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
>
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-clock text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">My Schedule</span>
</a>
<a
href="/profile"
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
>
<div
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
>
<i class="fas fa-user text-gray-600 text-xs"></i>
</div>
<span class="text-sm text-gray-700">Profile Settings</span>
</a>
</div>
</div>
</div>
</div>
</div>
{{end}}
</div>
</div>
{{ end }}

View File

@@ -31,7 +31,7 @@
<i class="fas fa-bars text-gray-600"></i>
</button>
<span class="text-sm font-medium text-gray-600">{{.UserName}}</span>
<div class="w-9 h-9 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
{{slice .UserName 0 1}}
</div>
<a href="/logout" class="p-2 hover:bg-gray-200 rounded">
@@ -52,8 +52,8 @@
<!-- Right Side: User Info -->
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-600">Welcome, {{.UserName}}</span>
<div class="w-9 h-9 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
<span class="text-sm font-medium text-gray-600">Hi, {{.UserName}}</span>
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
{{slice .UserName 0 1}}
</div>
<a href="/logout" class="p-2 hover:bg-gray-200 rounded">

View File

@@ -0,0 +1,197 @@
{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Top Navigation -->
<div class="bg-white border-b border-gray-200 px-6 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<i
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-poll{{end}} text-green-600"
></i>
<span class="text-sm font-medium">Poll Questions</span>
</div>
</div>
<div class="flex items-center gap-2">
<a
href="/appointments"
class="px-3 py-1 bg-gray-500 text-white text-sm hover:bg-gray-600 rounded"
>
Back to Appointments
</a>
</div>
</div>
</div>
<!-- Form Content -->
<div class="flex-1 overflow-y-auto bg-gray-50 p-6">
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900">
Campaign Poll Questions
</h2>
<p class="text-sm text-gray-600 mt-1">Address: {{ .Address }}</p>
</div>
<form method="POST" class="space-y-6">
<input type="hidden" name="poll_id" value="{{ .PollID }}" />
<!-- Postal Code -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Respondent's Postal Code
</label>
<input
type="text"
name="postal_code"
placeholder="Enter postal code (e.g., T2P 1J9)"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<!-- Question 1 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
1. Have you voted before?
</label>
<div class="space-y-2">
<label class="flex items-center">
<input
type="radio"
name="question1_voted_before"
value="true"
class="mr-2"
/>
<span class="text-sm">Yes</span>
</label>
<label class="flex items-center">
<input
type="radio"
name="question1_voted_before"
value="false"
class="mr-2"
/>
<span class="text-sm">No</span>
</label>
</div>
</div>
<!-- Question 2 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
2. Will you vote again for this candidate?
</label>
<div class="space-y-2">
<label class="flex items-center">
<input
type="radio"
name="question2_vote_again"
value="true"
class="mr-2"
/>
<span class="text-sm">Yes</span>
</label>
<label class="flex items-center">
<input
type="radio"
name="question2_vote_again"
value="false"
class="mr-2"
/>
<span class="text-sm">No</span>
</label>
</div>
</div>
<!-- Question 3 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
3. How many lawn signs do you need?
</label>
<input
type="number"
name="question3_lawn_signs"
min="0"
max="10"
value="0"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<!-- Question 4 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
4. How many banner signs do you need?
</label>
<input
type="number"
name="question4_banner_signs"
min="0"
max="5"
value="0"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<!-- Question 5 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
5. Write your thoughts (optional)
</label>
<textarea
name="question5_thoughts"
rows="4"
placeholder="Any additional comments or feedback..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
></textarea>
</div>
<!-- Submit Button -->
<div class="flex justify-end gap-3 pt-6">
<a
href="/appointments"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Cancel
</a>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700"
>
Submit Poll Response
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// Show delivery address section if user needs signs
document.addEventListener("DOMContentLoaded", function () {
const lawnSignsInput = document.querySelector(
'input[name="question3_lawn_signs"]'
);
const bannerSignsInput = document.querySelector(
'input[name="question4_banner_signs"]'
);
const deliverySection = document.getElementById("delivery-section");
function toggleDeliverySection() {
const lawnSigns = parseInt(lawnSignsInput.value) || 0;
const bannerSigns = parseInt(bannerSignsInput.value) || 0;
if (lawnSigns > 0 || bannerSigns > 0) {
deliverySection.style.display = "block";
} else {
deliverySection.style.display = "none";
}
}
lawnSignsInput.addEventListener("input", toggleDeliverySection);
bannerSignsInput.addEventListener("input", toggleDeliverySection);
});
</script>
{{ end }}

View File

@@ -1,14 +1,10 @@
// Add this debugging code to your main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
@@ -20,30 +16,10 @@ import (
_ "github.com/lib/pq" // use PostgreSQL
)
// Custom file server with logging
func loggingFileServer(dir string) http.Handler {
fs := http.FileServer(http.Dir(dir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Log the request
log.Printf("File request: %s", r.URL.Path)
// Check if file exists
filePath := filepath.Join(dir, r.URL.Path)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
log.Printf("File not found: %s", filePath)
http.NotFound(w, r)
return
}
log.Printf("Serving file: %s", filePath)
fs.ServeHTTP(w, r)
})
}
// Helper function to determine navigation visibility based on role
func getNavFlags(role int) (bool, bool, bool) {
showAdminNav := role == 1 // Admin role
showLeaderNav := role == 2 // Volunteer role
showLeaderNav := role == 2 // Team Leader role
showVolunteerNav := role == 3 // Volunteer role
return showAdminNav, showVolunteerNav, showLeaderNav
}
@@ -70,18 +46,14 @@ func createTemplateData(title, activeSection string, role int, isAuthenticated b
}
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
err := godotenv.Load() // or specify path: godotenv.Load("/path/to/.env")
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
// Get individual components from environment variables
jwtSecret := os.Getenv("JWT_SECRET")
var jwtKey = []byte(jwtSecret)
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
@@ -124,7 +96,6 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
return authMiddleware(func(w http.ResponseWriter, r *http.Request) {
role, ok := r.Context().Value("user_role").(int)
if !ok || (role != 3 && role != 2) {
fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@@ -132,36 +103,29 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
})
}
// Updated handler functions using the helper
func schedualHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
// currentUserID := r.Context().Value("user_id").(int)
data := createTemplateData("My Schedule", "schedual", role, true, nil)
utils.Render(w, "Schedual/schedual.html", data)
}
func HomeHandler(w http.ResponseWriter, r *http.Request) {
utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{
"Title": "Admin Dashboard",
"IsAuthenticated": false,
"ActiveSection": "dashboard",
})
"Title": "Admin Dashboard",
"IsAuthenticated": false,
"ActiveSection": "dashboard",
})
}
func main() {
models.InitDB()
// Static file servers with logging
// Static file servers
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Use logging file server for uploads
http.Handle("/uploads/", http.StripPrefix("/uploads/", loggingFileServer("uploads")))
uploadsFs := http.FileServer(http.Dir("uploads"))
http.Handle("/uploads/", http.StripPrefix("/uploads/", uploadsFs))
// Public HTML Routes
http.HandleFunc("/", HomeHandler)
@@ -187,17 +151,16 @@ func main() {
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
//--- Volunteer-only routes
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
// Poll routes (volunteer only)
http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler))
log.Println("Server started on localhost:8080")
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}
}

View File

@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

Binary file not shown.