403 lines
10 KiB
Go
403 lines
10 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"database/sql"
|
||
|
|
"errors"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/patel-mann/poll-system/app/internal/models"
|
||
|
|
"github.com/patel-mann/poll-system/app/internal/utils"
|
||
|
|
)
|
||
|
|
|
||
|
|
// View model for listing/assigning schedules
|
||
|
|
type AssignmentVM struct {
|
||
|
|
ID int
|
||
|
|
VolunteerID int
|
||
|
|
VolunteerName string
|
||
|
|
AddressID int
|
||
|
|
Address string
|
||
|
|
Date string // YYYY-MM-DD (for input[type=date])
|
||
|
|
AppointmentTime string // HH:MM
|
||
|
|
VisitedValidated bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// GET + POST in one handler:
|
||
|
|
// - GET: show assignments + form to assign
|
||
|
|
// - POST: create a new assignment
|
||
|
|
func AdminAssignmentsHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
switch r.Method {
|
||
|
|
case http.MethodPost:
|
||
|
|
if err := createAssignmentFromForm(r); err != nil {
|
||
|
|
log.Println("create assignment error:", err)
|
||
|
|
volunteers, _ := fetchVolunteers()
|
||
|
|
addresses, _ := fetchAddresses()
|
||
|
|
assignments, _ := fetchAssignments()
|
||
|
|
|
||
|
|
utils.Render(w, "schedual/assignments.html", map[string]interface{}{
|
||
|
|
"Title": "Admin — Assign Addresses",
|
||
|
|
"IsAuthenticated": true,
|
||
|
|
"ActiveSection": "admin_assignments",
|
||
|
|
"Volunteers": volunteers,
|
||
|
|
"Addresses": addresses,
|
||
|
|
"Assignments": assignments,
|
||
|
|
"Error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// GET: fetch volunteers, addresses, and existing assignments
|
||
|
|
volunteers, err := fetchVolunteers()
|
||
|
|
if err != nil {
|
||
|
|
log.Println("fetch volunteers error:", err)
|
||
|
|
http.Error(w, "Failed to load volunteers", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
addresses, err := fetchAddresses()
|
||
|
|
if err != nil {
|
||
|
|
log.Println("fetch addresses error:", err)
|
||
|
|
http.Error(w, "Failed to load addresses", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
assignments, err := fetchAssignments()
|
||
|
|
if err != nil {
|
||
|
|
log.Println("fetch assignments error:", err)
|
||
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
utils.Render(w, "assignments.html", map[string]interface{}{
|
||
|
|
"Title": "Admin — Assign Addresses",
|
||
|
|
"IsAuthenticated": true,
|
||
|
|
"ActiveSection": "admin_assignments",
|
||
|
|
"Volunteers": volunteers,
|
||
|
|
"Addresses": addresses,
|
||
|
|
"Assignments": assignments,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// GET (edit form) + POST (update/delete)
|
||
|
|
func AdminAssignmentEditHandler(w http.ResponseWriter, r *http.Request) {
|
||
|
|
idStr := r.URL.Query().Get("id")
|
||
|
|
id, _ := strconv.Atoi(idStr)
|
||
|
|
if id <= 0 {
|
||
|
|
http.NotFound(w, r)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if r.Method == http.MethodPost {
|
||
|
|
action := r.FormValue("action")
|
||
|
|
switch action {
|
||
|
|
case "delete":
|
||
|
|
if err := deleteAssignment(id); err != nil {
|
||
|
|
log.Println("delete assignment error:", err)
|
||
|
|
http.Error(w, "Failed to delete assignment", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
case "update":
|
||
|
|
if err := updateAssignmentFromForm(id, r); err != nil {
|
||
|
|
log.Println("update assignment error:", err)
|
||
|
|
vm, _ := fetchAssignmentByID(id)
|
||
|
|
volunteers, _ := fetchVolunteers()
|
||
|
|
addresses, _ := fetchAddresses()
|
||
|
|
|
||
|
|
utils.Render(w, "assignment_edit.html", map[string]interface{}{
|
||
|
|
"Title": "Edit Assignment",
|
||
|
|
"Assignment": vm,
|
||
|
|
"Volunteers": volunteers,
|
||
|
|
"Addresses": addresses,
|
||
|
|
"Error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
http.Redirect(w, r, "/admin/assignments", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
default:
|
||
|
|
http.Error(w, "Unknown action", http.StatusBadRequest)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// GET edit
|
||
|
|
vm, err := fetchAssignmentByID(id)
|
||
|
|
if err != nil {
|
||
|
|
if err == sql.ErrNoRows {
|
||
|
|
http.NotFound(w, r)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
log.Println("fetch assignment by ID error:", err)
|
||
|
|
http.Error(w, "Failed to load assignment", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
volunteers, err := fetchVolunteers()
|
||
|
|
if err != nil {
|
||
|
|
log.Println("fetch volunteers error:", err)
|
||
|
|
http.Error(w, "Failed to load volunteers", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
addresses, err := fetchAddresses()
|
||
|
|
if err != nil {
|
||
|
|
log.Println("fetch addresses error:", err)
|
||
|
|
http.Error(w, "Failed to load addresses", http.StatusInternalServerError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
utils.Render(w, "assignment_edit.html", map[string]interface{}{
|
||
|
|
"Title": "Edit Assignment",
|
||
|
|
"Assignment": vm,
|
||
|
|
"Volunteers": volunteers,
|
||
|
|
"Addresses": addresses,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----- Helpers -----
|
||
|
|
|
||
|
|
func createAssignmentFromForm(r *http.Request) error {
|
||
|
|
volID, _ := strconv.Atoi(r.FormValue("volunteer_id"))
|
||
|
|
addrID, _ := strconv.Atoi(r.FormValue("address_id"))
|
||
|
|
dateStr := r.FormValue("date")
|
||
|
|
timeStr := r.FormValue("appointment_time")
|
||
|
|
|
||
|
|
if volID <= 0 || addrID <= 0 || dateStr == "" || timeStr == "" {
|
||
|
|
return errors.New("please fill all required fields")
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := time.Parse("2006-01-02", dateStr); err != nil {
|
||
|
|
return errors.New("invalid date format")
|
||
|
|
}
|
||
|
|
if _, err := time.Parse("15:04", timeStr); err != nil {
|
||
|
|
return errors.New("invalid time format")
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := models.DB.Exec(`
|
||
|
|
INSERT INTO schedual (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
|
||
|
|
VALUES ($1,$2,$3,$4,NOW(),NOW())
|
||
|
|
`, volID, addrID, dateStr, timeStr)
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
log.Println("database insert error:", err)
|
||
|
|
return errors.New("failed to create assignment")
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func updateAssignmentFromForm(id int, r *http.Request) error {
|
||
|
|
volID, _ := strconv.Atoi(r.FormValue("volunteer_id"))
|
||
|
|
addrID, _ := strconv.Atoi(r.FormValue("address_id"))
|
||
|
|
dateStr := r.FormValue("date")
|
||
|
|
timeStr := r.FormValue("appointment_time")
|
||
|
|
|
||
|
|
if volID <= 0 || addrID <= 0 || dateStr == "" || timeStr == "" {
|
||
|
|
return errors.New("please fill all required fields")
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := time.Parse("2006-01-02", dateStr); err != nil {
|
||
|
|
return errors.New("invalid date format")
|
||
|
|
}
|
||
|
|
if _, err := time.Parse("15:04", timeStr); err != nil {
|
||
|
|
return errors.New("invalid time format")
|
||
|
|
}
|
||
|
|
|
||
|
|
result, err := models.DB.Exec(`
|
||
|
|
UPDATE schedual
|
||
|
|
SET user_id=$1, address_id=$2, appointment_date=$3, appointment_time=$4, updated_at=NOW()
|
||
|
|
WHERE schedual_id=$5
|
||
|
|
`, volID, addrID, dateStr, timeStr, id)
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
log.Println("database update error:", err)
|
||
|
|
return errors.New("failed to update assignment")
|
||
|
|
}
|
||
|
|
|
||
|
|
rowsAffected, _ := result.RowsAffected()
|
||
|
|
if rowsAffected == 0 {
|
||
|
|
return errors.New("assignment not found")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func deleteAssignment(id int) error {
|
||
|
|
result, err := models.DB.Exec(`DELETE FROM schedual WHERE schedual_id=$1`, id)
|
||
|
|
if err != nil {
|
||
|
|
log.Println("database delete error:", err)
|
||
|
|
return errors.New("failed to delete assignment")
|
||
|
|
}
|
||
|
|
rowsAffected, _ := result.RowsAffected()
|
||
|
|
if rowsAffected == 0 {
|
||
|
|
return errors.New("assignment not found")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch volunteers
|
||
|
|
type VolunteerPick struct {
|
||
|
|
ID int
|
||
|
|
FirstName string
|
||
|
|
LastName string
|
||
|
|
Email string
|
||
|
|
}
|
||
|
|
|
||
|
|
func fetchVolunteers() ([]VolunteerPick, error) {
|
||
|
|
rows, err := models.DB.Query(`
|
||
|
|
SELECT users_id, first_name, last_name, email
|
||
|
|
FROM "user"
|
||
|
|
WHERE role='volunteer'
|
||
|
|
ORDER BY first_name, last_name
|
||
|
|
`)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
var out []VolunteerPick
|
||
|
|
for rows.Next() {
|
||
|
|
var v VolunteerPick
|
||
|
|
if err := rows.Scan(&v.ID, &v.FirstName, &v.LastName, &v.Email); err != nil {
|
||
|
|
log.Println("fetchVolunteers scan:", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
out = append(out, v)
|
||
|
|
}
|
||
|
|
return out, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fetch addresses
|
||
|
|
type AddressPick struct {
|
||
|
|
ID int
|
||
|
|
Label string
|
||
|
|
VisitedValidated bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func fetchAddresses() ([]AddressPick, error) {
|
||
|
|
rows, err := models.DB.Query(`
|
||
|
|
SELECT
|
||
|
|
address_id,
|
||
|
|
address,
|
||
|
|
street_name,
|
||
|
|
street_type,
|
||
|
|
street_quadrant,
|
||
|
|
house_number,
|
||
|
|
house_alpha,
|
||
|
|
longitude,
|
||
|
|
latitude,
|
||
|
|
visited_validated
|
||
|
|
FROM address_database
|
||
|
|
ORDER BY address_id DESC
|
||
|
|
`)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
var out []AddressPick
|
||
|
|
for rows.Next() {
|
||
|
|
var addr models.AddressDatabase
|
||
|
|
if err := rows.Scan(
|
||
|
|
&addr.AddressID,
|
||
|
|
&addr.Address,
|
||
|
|
&addr.StreetName,
|
||
|
|
&addr.StreetType,
|
||
|
|
&addr.StreetQuadrant,
|
||
|
|
&addr.HouseNumber,
|
||
|
|
&addr.HouseAlpha,
|
||
|
|
&addr.Longitude,
|
||
|
|
&addr.Latitude,
|
||
|
|
&addr.VisitedValidated,
|
||
|
|
); err != nil {
|
||
|
|
log.Println("fetchAddresses scan:", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
label := addr.Address
|
||
|
|
if label == "" {
|
||
|
|
label = addr.HouseNumber
|
||
|
|
if addr.StreetName != "" {
|
||
|
|
if label != "" {
|
||
|
|
label += " "
|
||
|
|
}
|
||
|
|
label += addr.StreetName
|
||
|
|
}
|
||
|
|
if addr.StreetType != "" {
|
||
|
|
label += " " + addr.StreetType
|
||
|
|
}
|
||
|
|
if addr.StreetQuadrant != "" {
|
||
|
|
label += " " + addr.StreetQuadrant
|
||
|
|
}
|
||
|
|
if addr.HouseAlpha != nil {
|
||
|
|
label += " " + *addr.HouseAlpha
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
out = append(out, AddressPick{
|
||
|
|
ID: addr.AddressID,
|
||
|
|
Label: label,
|
||
|
|
VisitedValidated: addr.VisitedValidated,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return out, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add this missing function
|
||
|
|
func fetchAssignments() ([]AssignmentVM, error) {
|
||
|
|
rows, err := models.DB.Query(`
|
||
|
|
SELECT
|
||
|
|
s.schedual_id,
|
||
|
|
u.users_id,
|
||
|
|
COALESCE(u.first_name,'') || ' ' || COALESCE(u.last_name,'') AS volunteer_name,
|
||
|
|
a.address_id,
|
||
|
|
COALESCE(a.address,'') AS address,
|
||
|
|
s.appointment_date,
|
||
|
|
s.appointment_time
|
||
|
|
FROM schedual s
|
||
|
|
JOIN "user" u ON u.users_id = s.user_id
|
||
|
|
JOIN address_database a ON a.address_id = s.address_id
|
||
|
|
ORDER BY s.appointment_date DESC, s.appointment_time DESC
|
||
|
|
`)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
var assignments []AssignmentVM
|
||
|
|
for rows.Next() {
|
||
|
|
var vm AssignmentVM
|
||
|
|
if err := rows.Scan(&vm.ID, &vm.VolunteerID, &vm.VolunteerName, &vm.AddressID, &vm.Address,
|
||
|
|
&vm.Date, &vm.AppointmentTime); err != nil {
|
||
|
|
log.Println("fetchAssignments scan:", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
assignments = append(assignments, vm)
|
||
|
|
}
|
||
|
|
return assignments, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func fetchAssignmentByID(id int) (AssignmentVM, error) {
|
||
|
|
var vm AssignmentVM
|
||
|
|
err := models.DB.QueryRow(`
|
||
|
|
SELECT
|
||
|
|
s.schedual_id,
|
||
|
|
u.users_id,
|
||
|
|
COALESCE(u.first_name,'') || ' ' || COALESCE(u.last_name,'') AS volunteer_name,
|
||
|
|
a.address_id,
|
||
|
|
COALESCE(a.address,'') AS address,
|
||
|
|
s.appointment_date,
|
||
|
|
s.appointment_time
|
||
|
|
FROM schedual s
|
||
|
|
JOIN "user" u ON u.users_id = s.user_id
|
||
|
|
JOIN address_database a ON a.address_id = s.address_id
|
||
|
|
WHERE s.schedual_id = $1
|
||
|
|
`, id).Scan(&vm.ID, &vm.VolunteerID, &vm.VolunteerName, &vm.AddressID, &vm.Address,
|
||
|
|
&vm.Date, &vm.AppointmentTime)
|
||
|
|
|
||
|
|
return vm, err
|
||
|
|
}
|