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

1
.gitignore vendored
View File

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

View File

@@ -4,6 +4,7 @@ import (
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/patel-mann/poll-system/app/internal/models" "github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils" "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") userIDStr := r.FormValue("user_id")
addressIDStr := r.FormValue("address_id") addressIDStr := r.FormValue("address_id")
appointmentDate := r.FormValue("appointment_date")
startTime := r.FormValue("time")
if userIDStr == "" || addressIDStr == "" { if userIDStr == "" || addressIDStr == "" {
http.Error(w, "User ID and Address ID are required", http.StatusBadRequest) http.Error(w, "User ID and Address ID are required", http.StatusBadRequest)
return return
} }
if appointmentDate == "" || startTime == "" {
http.Error(w, "Appointment date and start time are required", http.StatusBadRequest)
return
}
userID, err := strconv.Atoi(userIDStr) userID, err := strconv.Atoi(userIDStr)
if err != nil { if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest) http.Error(w, "Invalid user ID", http.StatusBadRequest)
@@ -280,6 +288,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return 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 // Verify the user exists and is associated with the current admin
currentAdminID := r.Context().Value("user_id").(int) currentAdminID := r.Context().Value("user_id").(int)
var userExists int var userExists int
@@ -314,11 +343,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return 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(` _, err = models.DB.Exec(`
INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at) INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
VALUES ($1, $2, CURRENT_DATE, CURRENT_TIME, NOW(), NOW()) VALUES ($1, $2, $3, $4, NOW(), NOW())
`, userID, addressID) `, userID, addressID, appointmentDate, startTime)
if err != nil { if err != nil {
log.Println("Assignment error:", err) log.Println("Assignment error:", err)
http.Error(w, "Failed to assign address", http.StatusInternalServerError) 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 // 2. Total donations from polls
err = models.DB.QueryRow(` err = models.DB.QueryRow(`
SELECT COALESCE(SUM(amount_donated), 0) SELECT COALESCE(SUM(amount_donated), 0)
FROM poll; FROM poll_responce;
`).Scan(&totalDonations) `).Scan(&totalDonations)
if err != nil { if err != nil {
log.Println("Donation query error:", err) log.Println("Donation query error:", err)

View File

@@ -8,17 +8,16 @@ import (
"github.com/patel-mann/poll-system/app/internal/utils" "github.com/patel-mann/poll-system/app/internal/utils"
) )
func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) { 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)
currentUserID := models.GetCurrentUserID(w,r) username, _ := models.GetCurrentUserName(r)
username,_ := models.GetCurrentUserName(r)
rows, err := models.DB.Query(` rows, err := models.DB.Query(`
SELECT SELECT
a.sched_id, a.sched_id,
a.user_id, a.user_id,
ad.address_id,
ad.address, ad.address,
ad.latitude, ad.latitude,
ad.longitude, ad.longitude,
@@ -38,20 +37,51 @@ func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
type AppointmentWithAddress struct { type AppointmentWithAddress struct {
SchedID int SchedID int
UserID int UserID int
AddressID int
Address string Address string
Latitude float64 Latitude float64
Longitude float64 Longitude float64
AppointmentDate time.Time AppointmentDate time.Time
AppointmentTime 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
} }
var appointments []AppointmentWithAddress var appointments []AppointmentWithAddress
for rows.Next() { for rows.Next() {
var a AppointmentWithAddress var a AppointmentWithAddress
if err := rows.Scan(&a.SchedID, &a.UserID, &a.Address, &a.Latitude, &a.Longitude, &a.AppointmentDate, &a.AppointmentTime); err != nil { 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) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) appointments = append(appointments, a)
} }
@@ -59,10 +89,10 @@ func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
adminnav := false adminnav := false
volunteernav := false volunteernav := false
if role == 1{ if role == 1 {
adminnav = true adminnav = true
volunteernav = false volunteernav = false
}else{ } else {
adminnav = false adminnav = false
volunteernav = true volunteernav = true
} }
@@ -71,10 +101,10 @@ func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
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, // your existing variable "ShowAdminNav": adminnav,
"ShowVolunteerNav": volunteernav, // your existing variable "ShowVolunteerNav": volunteernav,
"ActiveSection": "address", "ActiveSection": "address",
"UserName": username, "UserName": username,
"Appointments": appointments, // pass the fetched appointments "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) // Add this to your handlers package (create volunteer_posts.go or add to existing file)
package handlers package handlers
import ( import (
@@ -12,7 +11,18 @@ import (
"github.com/patel-mann/poll-system/app/internal/utils" "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) { func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
// Only allow GET requests for volunteers // Only allow GET requests for volunteers
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
@@ -23,7 +33,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
// Get user info from context // Get user info from context
role := r.Context().Value("user_role").(int) role := r.Context().Value("user_role").(int)
CurrentUserID := models.GetCurrentUserID(w, r) CurrentUserID := models.GetCurrentUserID(w, r)
username,_ := models.GetCurrentUserName(r) username, _ := models.GetCurrentUserName(r)
// Fetch posts from database // Fetch posts from database
rows, err := models.DB.Query(` 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 JOIN admin_volunteers x ON u.user_id = x.admin_id
WHERE x.volunteer_id = $1 WHERE x.volunteer_id = $1
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
`,CurrentUserID) `, CurrentUserID)
if err != nil { if err != nil {
fmt.Printf("Database query error: %v\n", err) fmt.Printf("Database query error: %v\n", err)
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError) 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 // Get navigation flags
showAdminNav, showVolunteerNav := getNavFlags(role) showAdminNav, showVolunteerNav := getNavFlags(role)
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, "dashboard/volunteer_dashboard.html", map[string]interface{}{
"Title": "Community Posts", "Title": "Volunteer Dashboard",
"IsAuthenticated": true, "IsAuthenticated": true,
"ShowAdminNav": showAdminNav, "ShowAdminNav": showAdminNav,
"ShowVolunteerNav": showVolunteerNav, "ShowVolunteerNav": showVolunteerNav,
"UserName": username, "UserName": username,
"Posts": posts, "Posts": posts,
"ActiveSection": "posts", "Statistics": stats,
"IsVolunteer": true, // Flag to indicate this is volunteer view "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 var currentUserName string
err := DB.QueryRow(` err := DB.QueryRow(`
SELECT first_name || ' ' || last_name SELECT first_name
FROM users FROM users
WHERE user_id = $1 WHERE user_id = $1
`, currentUserID).Scan(&currentUserName) `, 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">Validated</th>
<th class="px-6 py-3 whitespace-nowrap">Address</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">Assigned User</th>
<th class="px-6 py-3 whitespace-nowrap">Appointment</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">Assign</th>
@@ -158,7 +158,7 @@
{{ else }} {{ else }}
<button <button
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700" class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
onclick="openAssignModal({{ .AddressID }})" onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
> >
Assign Assign
</button> </button>
@@ -187,7 +187,7 @@
</tr> </tr>
{{ else }} {{ else }}
<tr> <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 No addresses found
</td> </td>
</tr> </tr>
@@ -201,17 +201,51 @@
id="assignModal" id="assignModal"
class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50" 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"> <div class="bg-white w-full max-w-lg mx-4 shadow-lg">
<h2 class="text-lg font-semibold mb-4">Assign Address</h2> <!-- Modal Header -->
<form id="assignForm" method="POST" action="/assign_address"> <div
<input type="hidden" name="address_id" id="modalAddressID" /> class="flex justify-between items-center px-6 py-4 border-b border-gray-200"
<label for="user_id" class="block text-sm font-medium mb-2"
>Select User</label
> >
<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 <select
name="user_id" name="user_id"
id="user_id" id="user_id"
class="w-full border border-gray-300 px-3 py-2 mb-4 rounded" 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 required
> >
<option value="">-- Select User --</option> <option value="">-- Select User --</option>
@@ -219,19 +253,73 @@
<option value="{{ .ID }}">{{ .Name }}</option> <option value="{{ .ID }}">{{ .Name }}</option>
{{ end }} {{ end }}
</select> </select>
<div class="flex justify-end gap-2"> </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 <button
type="button" type="button"
onclick="closeAssignModal()" 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 Cancel
</button> </button>
<button <button
type="submit" 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> </button>
</div> </div>
</form> </form>
@@ -239,26 +327,105 @@
</div> </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> <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("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.remove("hidden");
document.getElementById("assignModal").classList.add("flex"); document.getElementById("assignModal").classList.add("flex");
} }
function closeAssignModal() { function closeAssignModal() {
document.getElementById("assignModal").classList.remove("flex"); document.getElementById("assignModal").classList.remove("flex");
document.getElementById("assignModal").classList.add("hidden"); document.getElementById("assignModal").classList.add("hidden");
document.getElementById("assignForm").reset();
document.getElementById("selected-address").textContent = "None selected";
} }
function goToPage(page) { function goToPage(page) {
var urlParams = new URLSearchParams(window.location.search); var urlParams = new URLSearchParams(window.location.search);
urlParams.set("page", page); urlParams.set("page", page);
window.location.search = urlParams.toString(); window.location.search = urlParams.toString();
} }
function changePageSize(pageSize) { function changePageSize(pageSize) {
var urlParams = new URLSearchParams(window.location.search); var urlParams = new URLSearchParams(window.location.search);
urlParams.set("pageSize", pageSize); urlParams.set("pageSize", pageSize);
urlParams.set("page", 1); urlParams.set("page", 1);
window.location.search = urlParams.toString(); 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> </script>
{{ end }} {{ 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 <i
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-calendar-alt{{end}} text-green-600" class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-calendar-alt{{end}} text-green-600"
></i> ></i>
<span class="text-sm font-medium"> Appointments </span> <span class="text-sm font-medium">Appointments</span>
</div> </div>
</div> </div>
</div> </div>
@@ -42,7 +42,7 @@
class="text-left text-gray-700 font-medium border-b border-gray-200" 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">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 Date</th>
<th class="px-6 py-3 whitespace-nowrap">Appointment Time</th> <th class="px-6 py-3 whitespace-nowrap">Appointment Time</th>
<th class="px-6 py-3 whitespace-nowrap">Poll Question</th> <th class="px-6 py-3 whitespace-nowrap">Poll Question</th>
@@ -68,11 +68,16 @@
{{ .AppointmentTime.Format "15:04" }} {{ .AppointmentTime.Format "15:04" }}
</td> </td>
<td class="px-6 py-3 whitespace-nowrap"> <td class="px-6 py-3 whitespace-nowrap">
<button {{ if .HasPollResponse }}
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700" <span class="{{ .PollButtonClass }}"> {{ .PollButtonText }} </span>
{{ else }}
<a
href="/poll?address_id={{ .AddressID }}"
class="{{ .PollButtonClass }}"
> >
Ask Poll {{ .PollButtonText }}
</button> </a>
{{ end }}
</td> </td>
</tr> </tr>
{{ else }} {{ else }}

View File

@@ -1,20 +1,32 @@
{{ define "content" }} {{ define "content" }}
<div class="flex flex-col min-h-screen bg-gray-100"> <div class="flex-1 flex flex-col overflow-hidden">
<!-- Optional Header --> <!-- Top Navigation -->
<header class="bg-white shadow p-4"> <div class="bg-white border-b border-gray-200 px-4 sm:px-6 py-3">
<h1 class="text-xl font-bold">Community</h1> <div class="flex items-center justify-between">
</header> <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>
<div class="text-sm text-gray-600 hidden sm:block">
Welcome back, {{ .UserName }}!
</div>
</div>
</div>
<!-- Scrollable Posts --> <!-- Main Content -->
<main class="flex-1 overflow-y-auto px-2 py-4 max-w-2xl mx-auto space-y-4"> <div class="flex-1 overflow-hidden bg-gray-50">
<!-- Posts Feed --> <div class="h-full flex flex-col lg:flex-row gap-6 p-4 sm:p-6">
{{range .Posts}} <!-- 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"> <article class="bg-white border-b border-gray-200">
<!-- Post Header --> <!-- Post Header -->
<div class="flex items-center px-6 py-4"> <div class="flex items-center px-4 sm:px-6 py-4">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div <div
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold" class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold rounded-full"
> >
{{slice .AuthorName 0 1}} {{slice .AuthorName 0 1}}
</div> </div>
@@ -39,55 +51,9 @@
</div> </div>
{{end}} {{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 --> <!-- Post Content -->
{{if .Content}} {{if .Content}}
<div class="px-6 pb-4"> <div class="px-4 sm:px-6 pt-2 pb-4">
<p class="text-gray-900 leading-relaxed"> <p class="text-gray-900 leading-relaxed">
<span class="font-semibold">{{.AuthorName}}</span> {{.Content}} <span class="font-semibold">{{.AuthorName}}</span> {{.Content}}
</p> </p>
@@ -95,7 +61,7 @@
{{end}} {{end}}
</article> </article>
{{else}} {{else}}
<div class="bg-white p-12 text-center"> <div class="bg-white p-8 sm:p-12 text-center border-b border-gray-200">
<div class="max-w-sm mx-auto"> <div class="max-w-sm mx-auto">
<svg <svg
class="w-16 h-16 mx-auto text-gray-300 mb-4" class="w-16 h-16 mx-auto text-gray-300 mb-4"
@@ -116,7 +82,205 @@
</p> </p>
</div> </div>
</div> </div>
{{end}} {{ 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>
</div> </div>
</div> </div>
{{ end }} {{ end }}

View File

@@ -31,7 +31,7 @@
<i class="fas fa-bars text-gray-600"></i> <i class="fas fa-bars text-gray-600"></i>
</button> </button>
<span class="text-sm font-medium text-gray-600">{{.UserName}}</span> <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}} {{slice .UserName 0 1}}
</div> </div>
<a href="/logout" class="p-2 hover:bg-gray-200 rounded"> <a href="/logout" class="p-2 hover:bg-gray-200 rounded">
@@ -52,8 +52,8 @@
<!-- Right Side: User Info --> <!-- Right Side: User Info -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-600">Welcome, {{.UserName}}</span> <span class="text-sm font-medium text-gray-600">Hi, {{.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}} {{slice .UserName 0 1}}
</div> </div>
<a href="/logout" class="p-2 hover:bg-gray-200 rounded"> <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 package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@@ -20,30 +16,10 @@ import (
_ "github.com/lib/pq" // use PostgreSQL _ "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 // Helper function to determine navigation visibility based on role
func getNavFlags(role int) (bool, bool, bool) { func getNavFlags(role int) (bool, bool, bool) {
showAdminNav := role == 1 // Admin role showAdminNav := role == 1 // Admin role
showLeaderNav := role == 2 // Volunteer role showLeaderNav := role == 2 // Team Leader role
showVolunteerNav := role == 3 // Volunteer role showVolunteerNav := role == 3 // Volunteer role
return showAdminNav, showVolunteerNav, showLeaderNav return showAdminNav, showVolunteerNav, showLeaderNav
} }
@@ -70,18 +46,14 @@ func createTemplateData(title, activeSection string, role int, isAuthenticated b
} }
func authMiddleware(next http.HandlerFunc) http.HandlerFunc { func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
err := godotenv.Load()
err := godotenv.Load() // or specify path: godotenv.Load("/path/to/.env")
if err != nil { if err != nil {
log.Fatalf("Error loading .env file: %v", err) log.Fatalf("Error loading .env file: %v", err)
} }
// Get individual components from environment variables
jwtSecret := os.Getenv("JWT_SECRET") jwtSecret := os.Getenv("JWT_SECRET")
var jwtKey = []byte(jwtSecret) var jwtKey = []byte(jwtSecret)
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
if err != nil { if err != nil {
@@ -124,7 +96,6 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
return authMiddleware(func(w http.ResponseWriter, r *http.Request) { return authMiddleware(func(w http.ResponseWriter, r *http.Request) {
role, ok := r.Context().Value("user_role").(int) role, ok := r.Context().Value("user_role").(int)
if !ok || (role != 3 && role != 2) { if !ok || (role != 3 && role != 2) {
fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@@ -132,16 +103,10 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
}) })
} }
// Updated handler functions using the helper
func schedualHandler(w http.ResponseWriter, r *http.Request) { func schedualHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int) role := r.Context().Value("user_role").(int)
// currentUserID := r.Context().Value("user_id").(int)
data := createTemplateData("My Schedule", "schedual", role, true, nil) data := createTemplateData("My Schedule", "schedual", role, true, nil)
utils.Render(w, "Schedual/schedual.html", data) utils.Render(w, "Schedual/schedual.html", data)
} }
func HomeHandler(w http.ResponseWriter, r *http.Request) { func HomeHandler(w http.ResponseWriter, r *http.Request) {
@@ -152,16 +117,15 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
}) })
} }
func main() { func main() {
models.InitDB() models.InitDB()
// Static file servers with logging // Static file servers
fs := http.FileServer(http.Dir("static")) fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs)) http.Handle("/static/", http.StripPrefix("/static/", fs))
// Use logging file server for uploads uploadsFs := http.FileServer(http.Dir("uploads"))
http.Handle("/uploads/", http.StripPrefix("/uploads/", loggingFileServer("uploads"))) http.Handle("/uploads/", http.StripPrefix("/uploads/", uploadsFs))
// Public HTML Routes // Public HTML Routes
http.HandleFunc("/", HomeHandler) http.HandleFunc("/", HomeHandler)
@@ -187,16 +151,15 @@ func main() {
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler)) http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler)) http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler)) http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
//--- Volunteer-only routes //--- Volunteer-only routes
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler)) http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler)) http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler)) http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
// Poll routes (volunteer only)
http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler))
log.Println("Server started on localhost:8080") log.Println("Server started on localhost:8080")
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil)) 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.