Update: Few issues to resolve see readme

this push will conclude the majority of pulls. this repos will now, not be actively be managed or any further code pushes will not be frequent.
This commit is contained in:
Mann Patel
2025-09-11 16:54:30 -06:00
parent 144436bbf3
commit b21e76eed0
19 changed files with 1953 additions and 1442 deletions

View File

@@ -5,7 +5,6 @@ import (
"net/http"
"strconv"
"time"
"fmt"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
@@ -36,19 +35,18 @@ type PageNumber struct {
// AddressWithDetails extends AddressDatabase with appointment and user info
type AddressWithDetails struct {
models.AddressDatabase
UserID *int
UserName string
UserEmail string
AppointmentDate string
AppointmentTime string
UserID *int
UserName string
UserEmail string
AppointmentDate string
AppointmentTime string
}
func AddressHandler(w http.ResponseWriter, r *http.Request) {
// Get pagination parameters from query string
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("pageSize")
username,_ := models.GetCurrentUserName(r)
username, _ := models.GetCurrentUserName(r)
page := 1
pageSize := 20
@@ -157,7 +155,7 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
}
// Get users associated with this admin
currentAdminID := r.Context().Value("user_id").(int)
currentAdminID := models.GetCurrentUserID(w, r)
userRows, err := models.DB.Query(`
SELECT u.user_id, u.first_name || ' ' || u.last_name AS name
FROM users u
@@ -216,7 +214,7 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
"ActiveSection": "address",
"Addresses": addresses,
"Users": users,
"UserName": username,
"UserName": username,
"Role": "admin",
"Pagination": pagination,
})
@@ -267,13 +265,9 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
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)
// Basic validation
if userIDStr == "" || addressIDStr == "" || appointmentDate == "" || startTime == "" {
http.Error(w, "All fields are required", http.StatusBadRequest)
return
}
@@ -289,54 +283,30 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Parse and validate the appointment date
// Parse 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
// Parse time
parsedTime, err := time.Parse("15:04", startTime)
is_valid := ValidatedFreeTime(parsedDate, parsedTime, userID)
if is_valid != true {
http.Error(w, "User is not availabile", http.StatusBadRequest)
return
}else{
fmt.Print("hello")
}
// Verify the user exists and is associated with the current admin
currentAdminID := r.Context().Value("user_id").(int)
var userExists int
err = models.DB.QueryRow(`
SELECT COUNT(*) FROM admin_volunteers av
JOIN users u ON av.volunteer_id = u.user_id
WHERE av.admin_id = $1 AND u.user_id = $2 AND av.is_active = true
`, currentAdminID, userID).Scan(&userExists)
if err != nil {
log.Println("User verification error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if userExists == 0 {
http.Error(w, "Invalid user selection", http.StatusBadRequest)
http.Error(w, "Invalid appointment time format", http.StatusBadRequest)
return
}
// Check if this address is already assigned to any user
// --- Availability Check (non-blocking) ---
isValid := ValidateAvailability(parsedDate, parsedTime, userID)
if !isValid {
// Instead of blocking, just log it
log.Printf("⚠️ User %d is not available on %s at %s", userID, appointmentDate, startTime)
}
// Check if this address is already assigned
var exists int
err = models.DB.QueryRow(`
SELECT COUNT(*) FROM appointment
WHERE address_id = $1
`, addressID).Scan(&exists)
err = models.DB.QueryRow(`SELECT COUNT(*) FROM appointment WHERE address_id = $1`, addressID).Scan(&exists)
if err != nil {
log.Println("Assignment check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
@@ -347,38 +317,39 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Check if the user already has an appointment at the same date and time
var timeConflict int
// Check for conflicting appointment for the user
var conflict 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)
WHERE user_id = $1 AND appointment_date = $2 AND appointment_time = $3`,
userID, appointmentDate, startTime).Scan(&conflict)
if err != nil {
log.Println("Time conflict check error:", err)
log.Println("Conflict check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if timeConflict > 0 {
if conflict > 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
// Insert the appointment anyway
_, err = models.DB.Exec(`
INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
`, userID, addressID, appointmentDate, startTime)
VALUES ($1, $2, $3, $4, NOW(), NOW())`,
userID, addressID, appointmentDate, startTime)
if err != nil {
log.Println("Assignment error:", err)
log.Println("Insert appointment error:", err)
http.Error(w, "Failed to assign address", http.StatusInternalServerError)
return
}
// Redirect back to addresses page with success
// ✅ Later: you can pass `UserNotAvailable: !isValid` to utils.Render instead of redirect
log.Printf("✅ Address %d assigned to user %d for %s at %s (Available: %v)",
addressID, userID, appointmentDate, startTime, isValid)
http.Redirect(w, r, "/addresses?success=assigned", http.StatusSeeOther)
}
func RemoveAssignedAddressHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/addresses", http.StatusSeeOther)

View File

@@ -12,22 +12,22 @@ import (
)
type VolunteerStatistics struct {
AppointmentsToday int
AppointmentsTomorrow int
AppointmentsThisWeek int
TotalAppointments int
PollsCompleted int
PollsRemaining int
LawnSignsRequested int
BannerSignsRequested int
AppointmentsToday int
AppointmentsTomorrow int
AppointmentsThisWeek int
TotalAppointments int
PollsCompleted int
PollsRemaining int
LawnSignsRequested int
BannerSignsRequested int
PollCompletionPercent int
}
type TeamMate struct {
UserID int
FullName string
Phone string
Role string
IsLead bool
UserID int
FullName string
Phone string
Role string
IsLead bool
}
// VolunteerPostsHandler - Dashboard view for volunteers with posts and statistics
@@ -79,7 +79,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
}
// Fetch teammates
teammatesRows, err := models.DB.Query(`
teammatesRows, err := models.DB.Query(`
SELECT u.user_id,
u.first_name || ' ' || u.last_name AS full_name,
COALESCE(u.phone, '') AS phone,
@@ -94,21 +94,20 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
)
ORDER BY CASE WHEN r.name = 'team_lead' THEN 0 ELSE 1 END, u.first_name;
`, CurrentUserID, CurrentUserID)
if err != nil {
fmt.Printf("Database query error (teammates): %v\n", err)
}
defer teammatesRows.Close()
var teammates []TeamMate
for teammatesRows.Next() {
var t TeamMate
if err := teammatesRows.Scan(&t.UserID, &t.FullName, &t.Phone, &t.Role); err != nil {
fmt.Printf("Row scan error (teammates): %v\n", err)
continue
}
teammates = append(teammates, t)
}
if err != nil {
fmt.Printf("Database query error (teammates): %v\n", err)
}
defer teammatesRows.Close()
var teammates []TeamMate
for teammatesRows.Next() {
var t TeamMate
if err := teammatesRows.Scan(&t.UserID, &t.FullName, &t.Phone, &t.Role); err != nil {
fmt.Printf("Row scan error (teammates): %v\n", err)
continue
}
teammates = append(teammates, t)
}
// Get volunteer statistics
stats, err := getVolunteerStatistics(CurrentUserID)
@@ -125,23 +124,23 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Volunteer viewing %d posts\n", len(posts))
utils.Render(w, "volunteer_dashboard.html", map[string]interface{}{
"Title": "Volunteer Dashboard",
"IsAuthenticated": true,
"ShowAdminNav": showAdminNav,
"ShowVolunteerNav": showVolunteerNav,
"UserName": username,
"Posts": posts,
"Statistics": stats,
"Teammates": teammates,
"ActiveSection": "dashboard",
"IsVolunteer": true,
"Title": "Volunteer Dashboard",
"IsAuthenticated": true,
"ShowAdminNav": showAdminNav,
"ShowVolunteerNav": showVolunteerNav,
"UserName": username,
"Posts": posts,
"Statistics": stats,
"Teammates": teammates,
"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()
oneDayLater := now.Add(time.Hour * 12)
@@ -160,11 +159,10 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
fmt.Println("Week Start:", weekStart.Format("2006-01-02"))
fmt.Println("Week End:", weekEnd.Format("2006-01-02"))
// Appointments today
err := models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, today).Scan(&stats.AppointmentsToday)
if err != nil {
@@ -173,8 +171,8 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Appointments tomorrow
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, oneDayLater).Scan(&stats.AppointmentsTomorrow)
if err != nil {
@@ -183,19 +181,18 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Appointments this week
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
`, userID, weekStart, weekEnd).Scan(&stats.AppointmentsThisWeek)
if err != nil {
return nil, err
}
// Total appointments
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1
`, userID).Scan(&stats.TotalAppointments)
if err != nil {
@@ -214,8 +211,12 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
}
// Polls remaining (appointments without poll responses)
stats.PollsRemaining = stats.TotalAppointments - stats.PollsCompleted
fmt.Print(stats.PollsRemaining)
// Calculate completion percentage
if stats.TotalAppointments > 0 {
stats.PollCompletionPercent = (stats.PollsCompleted * 100) / stats.TotalAppointments
@@ -225,7 +226,7 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Signs requested
err = models.DB.QueryRow(`
SELECT
SELECT
COALESCE(SUM(pr.question3_lawn_signs), 0),
COALESCE(SUM(pr.question4_banner_signs), 0)
FROM poll p
@@ -237,4 +238,4 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
}
return stats, nil
}
}

View File

@@ -14,7 +14,7 @@ import (
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 == "" {
@@ -27,7 +27,7 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
var userID int
fmt.Print(addressID, userID)
err := models.DB.QueryRow(`
SELECT a.address, ap.user_id
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
@@ -41,8 +41,8 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
// Check if poll already exists for this address
var pollID int
err = models.DB.QueryRow(`
SELECT poll_id
FROM poll
SELECT poll_id
FROM poll
WHERE address_id = $1 AND user_id = $2
`, addressID, userID).Scan(&pollID)
@@ -53,7 +53,7 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
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)
@@ -66,15 +66,15 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
}
utils.Render(w, "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",
"Title": "Poll Questions",
"IsAuthenticated": true,
"ShowVolunteerNav": true,
"ActiveSection": "schedule",
"UserName": username,
"PollID": pollID,
"AddressID": addressID,
"Address": address,
"PageIcon": "fas fa-poll",
})
return
}
@@ -123,35 +123,35 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
// Insert poll response
_, err = models.DB.Exec(`
INSERT INTO poll_response (
poll_id, respondent_postal_code, question1_voted_before,
poll_id, respondent_postal_code, question1_voted_before,
question2_vote_again, question3_lawn_signs, question4_banner_signs,
question5_thoughts, question6_donation_amount
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, pollID, postalCode, question1VotedBefore, question2VoteAgain,
question3LawnSigns, question4BannerSigns, question5Thoughts, question6donation)
`, pollID, postalCode, question1VotedBefore, question2VoteAgain,
question3LawnSigns, question4BannerSigns, question5Thoughts, question6donation)
if err != nil {
fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
return
}else{
} else {
_, err := models.DB.Exec(`
UPDATE address_database
SET visited_validated = true
UPDATE address_database
SET visited_validated = true
WHERE address_id IN (
SELECT address_id
FROM poll
SELECT address_id
FROM poll
WHERE poll_id = $1
)
`, pollID)
if err != nil {
fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
return
}
return
}
}
http.Redirect(w, r, "/volunteer/Addresses", http.StatusSeeOther)
}
}
}

View File

@@ -1,34 +1,132 @@
package handlers
import (
"fmt"
"database/sql"
"log"
"net/http"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
func ValidatedFreeTime(parsedDate time.Time, assignTime time.Time, userID int) (bool) {
var startTime, endTime time.Time
/////////////////////
// Core Validation //
/////////////////////
dateOnly := parsedDate.Format("2006-01-02")
// ValidateAvailability checks if a user is available at a specific time
func ValidateAvailability(checkDate time.Time, checkTime time.Time, userID int) bool {
var startTime, endTime time.Time
day := checkDate.Format("2006-01-02")
err := models.DB.QueryRow(
`SELECT start_time, end_time
FROM availability
WHERE user_id = $1 AND day = $2`,
userID, dateOnly,
).Scan(&startTime, &endTime)
err := models.DB.QueryRow(`
SELECT start_time, end_time
FROM availability
WHERE user_id = $1 AND day_of_week = $2`,
userID, day).Scan(&startTime, &endTime)
if err != nil {
fmt.Printf("Database query failed: %v\n", err)
return false
}
if assignTime.After(startTime) && assignTime.Before(endTime) {
return true
}else{
return false
if err != nil {
if err != sql.ErrNoRows {
log.Printf("DB error in ValidateAvailability: %v", err)
}
return false
}
return false
return checkTime.After(startTime) && checkTime.Before(endTime)
}
////////////////////
// Volunteer CRUD //
////////////////////
// View volunteer availability
func VolunteerGetAvailabilityHandler(w http.ResponseWriter, r *http.Request) {
userID := models.GetCurrentUserID(w, r)
username, _ := models.GetCurrentUserName(r)
role, _ := r.Context().Value("user_role").(int)
rows, err := models.DB.Query(`
SELECT availability_id, day_of_week, start_time, end_time, created_at
FROM availability
WHERE user_id = $1
ORDER BY day_of_week DESC`, userID)
if err != nil {
log.Println("Error fetching availability:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer rows.Close()
var availability []models.Availability
for rows.Next() {
var a models.Availability
if err := rows.Scan(&a.AvailabilityID, &a.DayOfWeek, &a.StartTime, &a.EndTime, &a.CreatedAt); err != nil {
log.Println("Row scan error:", err)
continue
}
availability = append(availability, a)
}
utils.Render(w, "volunteer_schedule.html", map[string]interface{}{
"Title": "My Schedule",
"ShowVolunteerNav": true,
"IsVolunteer": true,
"IsAuthenticated": true,
"Availability": availability,
"UserName": username,
"Role": role,
"ActiveSection": "schedule",
})
}
// Add or update schedule
func VolunteerPostScheduleHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
return
}
userID := models.GetCurrentUserID(w, r)
day := r.FormValue("day")
start := r.FormValue("start_time")
end := r.FormValue("end_time")
if day == "" || start == "" || end == "" {
http.Error(w, "All fields required", http.StatusBadRequest)
return
}
_, err := models.DB.Exec(`
INSERT INTO availability (user_id, day_of_week, start_time, end_time, created_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (user_id, day_of_week)
DO UPDATE SET start_time = $3, end_time = $4`,
userID, day, start, end)
if err != nil {
log.Println("Insert availability error:", err)
http.Error(w, "Could not save schedule", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
}
// Delete schedule
func VolunteerDeleteScheduleHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID := models.GetCurrentUserID(w, r)
idStr := r.FormValue("id")
_, err := models.DB.Exec(`DELETE FROM availability WHERE availability_id = $1 AND user_id = $2`, idStr, userID)
if err != nil {
log.Println("Delete availability error:", err)
http.Error(w, "Could not delete schedule", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
}