Files
Poll-system/app/internal/handlers/admin_addresses.go
Mann Patel b21e76eed0 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.
2025-09-11 16:54:30 -06:00

413 lines
10 KiB
Go

package handlers
import (
"log"
"net/http"
"strconv"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
// PaginationInfo holds pagination metadata
type PaginationInfo struct {
CurrentPage int
TotalPages int
TotalRecords int
PageSize int
HasPrevious bool
HasNext bool
StartRecord int
EndRecord int
PreviousPage int
NextPage int
FirstPage int
LastPage int
PageNumbers []PageNumber
}
type PageNumber struct {
Number int
IsCurrent bool
}
// AddressWithDetails extends AddressDatabase with appointment and user info
type AddressWithDetails struct {
models.AddressDatabase
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)
page := 1
pageSize := 20
if pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
if pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
pageSize = ps
}
}
offset := (page - 1) * pageSize
// Get total count
var totalRecords int
err := models.DB.QueryRow(`SELECT COUNT(*) FROM "address_database"`).Scan(&totalRecords)
if err != nil {
log.Println("Count query error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
totalPages := (totalRecords + pageSize - 1) / pageSize
if totalPages == 0 {
totalPages = 1
}
if page > totalPages {
page = totalPages
offset = (page - 1) * pageSize
}
// Query addresses with appointment + user info
rows, err := models.DB.Query(`
SELECT
a.address_id,
a.address,
a.street_name,
a.street_type,
a.street_quadrant,
a.house_number,
COALESCE(a.house_alpha, '') as house_alpha,
a.longitude,
a.latitude,
a.visited_validated,
a.created_at,
a.updated_at,
CASE
WHEN ap.sched_id IS NOT NULL THEN true
ELSE false
END as assigned,
ap.user_id,
COALESCE(u.first_name || ' ' || u.last_name, '') as user_name,
COALESCE(u.email, '') as user_email,
COALESCE(ap.appointment_date::text, '') as appointment_date,
COALESCE(ap.appointment_time::text, '') as appointment_time
FROM address_database a
LEFT JOIN appointment ap ON a.address_id = ap.address_id
LEFT JOIN users u ON ap.user_id = u.user_id
WHERE a.street_quadrant = 'NE'
ORDER BY a.address_id
LIMIT $1 OFFSET $2
`, pageSize, offset)
if err != nil {
log.Println("Query error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer rows.Close()
var addresses []AddressWithDetails
for rows.Next() {
var a AddressWithDetails
var houseAlpha string
err := rows.Scan(
&a.AddressID,
&a.Address,
&a.StreetName,
&a.StreetType,
&a.StreetQuadrant,
&a.HouseNumber,
&houseAlpha,
&a.Longitude,
&a.Latitude,
&a.VisitedValidated,
&a.CreatedAt,
&a.UpdatedAt,
&a.Assigned,
&a.UserID,
&a.UserName,
&a.UserEmail,
&a.AppointmentDate,
&a.AppointmentTime,
)
if err != nil {
log.Println("Scan error:", err)
continue
}
// Handle nullable house_alpha
if houseAlpha != "" {
a.HouseAlpha = &houseAlpha
}
addresses = append(addresses, a)
}
// Get users associated with this admin
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
JOIN admin_volunteers av ON u.user_id = av.volunteer_id
WHERE av.admin_id = $1 AND av.is_active = true
`, currentAdminID)
if err != nil {
log.Println("Failed to fetch users:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer userRows.Close()
type UserOption struct {
ID int
Name string
}
var users []UserOption
for userRows.Next() {
var u UserOption
if err := userRows.Scan(&u.ID, &u.Name); err != nil {
log.Println("User scan error:", err)
continue
}
users = append(users, u)
}
// Pagination info
startRecord := offset + 1
endRecord := offset + len(addresses)
if totalRecords == 0 {
startRecord = 0
}
pageNumbers := generatePageNumbers(page, totalPages)
pagination := PaginationInfo{
CurrentPage: page,
TotalPages: totalPages,
TotalRecords: totalRecords,
PageSize: pageSize,
HasPrevious: page > 1,
HasNext: page < totalPages,
StartRecord: startRecord,
EndRecord: endRecord,
PreviousPage: page - 1,
NextPage: page + 1,
FirstPage: 1,
LastPage: totalPages,
PageNumbers: pageNumbers,
}
utils.Render(w, "address.html", map[string]interface{}{
"Title": "Addresses",
"IsAuthenticated": true,
"ShowAdminNav": true,
"ActiveSection": "address",
"Addresses": addresses,
"Users": users,
"UserName": username,
"Role": "admin",
"Pagination": pagination,
})
}
func generatePageNumbers(currentPage, totalPages int) []PageNumber {
var pageNumbers []PageNumber
// Generate page numbers to show (max 7 pages)
start := currentPage - 3
end := currentPage + 3
if start < 1 {
end += 1 - start
start = 1
}
if end > totalPages {
start -= end - totalPages
end = totalPages
}
if start < 1 {
start = 1
}
for i := start; i <= end; i++ {
pageNumbers = append(pageNumbers, PageNumber{
Number: i,
IsCurrent: i == currentPage,
})
}
return pageNumbers
}
func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/addresses", http.StatusSeeOther)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
userIDStr := r.FormValue("user_id")
addressIDStr := r.FormValue("address_id")
appointmentDate := r.FormValue("appointment_date")
startTime := r.FormValue("time")
// Basic validation
if userIDStr == "" || addressIDStr == "" || appointmentDate == "" || startTime == "" {
http.Error(w, "All fields are required", http.StatusBadRequest)
return
}
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
addressID, err := strconv.Atoi(addressIDStr)
if err != nil {
http.Error(w, "Invalid address ID", http.StatusBadRequest)
return
}
// Parse date
parsedDate, err := time.Parse("2006-01-02", appointmentDate)
if err != nil {
http.Error(w, "Invalid appointment date format", http.StatusBadRequest)
return
}
// Parse time
parsedTime, err := time.Parse("15:04", startTime)
if err != nil {
http.Error(w, "Invalid appointment time format", http.StatusBadRequest)
return
}
// --- 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)
if err != nil {
log.Println("Assignment check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if exists > 0 {
http.Error(w, "This address is already assigned to another user", http.StatusBadRequest)
return
}
// 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(&conflict)
if err != nil {
log.Println("Conflict check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if conflict > 0 {
http.Error(w, "User already has an appointment at this date and time", http.StatusBadRequest)
return
}
// 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)
if err != nil {
log.Println("Insert appointment error:", err)
http.Error(w, "Failed to assign address", http.StatusInternalServerError)
return
}
// ✅ 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)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
userIDStr := r.FormValue("user_id")
addressIDStr := r.FormValue("address_id")
if userIDStr == "" || addressIDStr == "" {
http.Error(w, "User ID and Address ID are required", http.StatusBadRequest)
return
}
userID, err := strconv.Atoi(userIDStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
addressID, err := strconv.Atoi(addressIDStr)
if err != nil {
http.Error(w, "Invalid address ID", http.StatusBadRequest)
return
}
// Verify the user is managed by current admin
currentAdminID := r.Context().Value("user_id").(int)
var userExists int
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM admin_volunteers av
JOIN appointment ap ON av.volunteer_id = ap.user_id
WHERE av.admin_id = $1 AND ap.user_id = $2 AND ap.address_id = $3
`, currentAdminID, userID, addressID).Scan(&userExists)
if err != nil {
log.Println("Verification error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if userExists == 0 {
http.Error(w, "Unauthorized removal", http.StatusForbidden)
return
}
// Remove volunteer assignment
_, err = models.DB.Exec(`DELETE FROM appointment WHERE user_id = $1 AND address_id = $2`, userID, addressID)
if err != nil {
log.Println("Remove assignment error:", err)
http.Error(w, "Failed to remove assignment", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/addresses?success=removed", http.StatusSeeOther)
}