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.
413 lines
10 KiB
Go
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)
|
|
}
|