From 6edd4ee030a0621609e155bc134d133b13a929ae Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+Patel-Mann@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:21:11 -0600 Subject: [PATCH] admin core func done --- .gitignore | 3 +- README.MD | 36 ++ app/internal/handlers/admin.go | 402 ------------- app/internal/handlers/admin_addresses.go | 199 ++++++- app/internal/handlers/admin_apointment.go | 83 +++ app/internal/handlers/admin_dashboard.go | 3 +- app/internal/handlers/admin_post.go | 11 +- app/internal/handlers/admin_team_builder.go | 183 ++++++ app/internal/handlers/admin_voluteers.go | 127 +--- app/internal/handlers/login.go | 239 ++------ app/internal/handlers/profile.go | 7 +- app/internal/handlers/volunteer_address.go | 80 +++ app/internal/handlers/volunteer_posts.go | 7 +- app/internal/models/structs.go | 6 +- app/internal/models/token.go | 33 +- app/internal/templates/address/address.html | 170 +++--- app/internal/templates/address_assigned.html | 38 ++ app/internal/templates/appointment.html | 89 +++ app/internal/templates/layout.html | 329 ++++++++--- app/internal/templates/posts.html | 14 +- app/internal/templates/profile/profile.html | 537 +++++++++++------ .../templates/volunteer/edit_volunteer.html | 543 ++++++++++++++++-- .../templates/volunteer/team_builder.html | 136 ++++- .../templates/volunteer/volunteer.html | 2 +- app/main.go | 12 +- app/tmp/build-errors.log | 2 +- app/tmp/main | Bin 13196912 -> 13232480 bytes app/uploads/3_1756244688677386000.webp | Bin 0 -> 41902 bytes app/uploads/3_1756268241641256000.webp | Bin 0 -> 76320 bytes 29 files changed, 2152 insertions(+), 1139 deletions(-) delete mode 100644 app/internal/handlers/admin.go create mode 100644 app/internal/handlers/admin_apointment.go create mode 100644 app/internal/handlers/admin_team_builder.go create mode 100644 app/internal/handlers/volunteer_address.go create mode 100644 app/internal/templates/address_assigned.html create mode 100644 app/internal/templates/appointment.html create mode 100644 app/uploads/3_1756244688677386000.webp create mode 100644 app/uploads/3_1756268241641256000.webp diff --git a/.gitignore b/.gitignore index 759d391..d399d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /uploads -.env \ No newline at end of file +.env +/Example_code \ No newline at end of file diff --git a/README.MD b/README.MD index 5db9ae3..4eb08d0 100644 --- a/README.MD +++ b/README.MD @@ -1,2 +1,38 @@ # Poll-system +# ADDRESSES: + +- A giant dataset of all the addresses and their log,lat location (not interactive) +- A user able to see his ward addresses +- Assing the address to a user whose role is leader or volunteer + - mass assign addresses to the user, multiple houses can be assined ith tiem left blank + - we can assing only after checking id the volunter is free on that day and time +- volunteer schedualing their time and date +- view the volunteers schedualling preference + +# TODO + +## VOLUNTEER + +- Volunteer Schdual +- Appointment + +## APPOINTMENT + +````sql +create table user_addresses +( + user_id integer not null + references users + on delete cascade, + address_line1 varchar(200) not null, + address_line2 varchar(200), + city varchar(100), + province varchar(100), + country varchar(100), + postal_code varchar(20) not null, + created_at timestamp default now(), + updated_at timestamp default now(), + primary key (user_id, address_line1, postal_code) +);``` +```` diff --git a/app/internal/handlers/admin.go b/app/internal/handlers/admin.go deleted file mode 100644 index 8f05e3e..0000000 --- a/app/internal/handlers/admin.go +++ /dev/null @@ -1,402 +0,0 @@ -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 -} diff --git a/app/internal/handlers/admin_addresses.go b/app/internal/handlers/admin_addresses.go index c53c708..342b6c8 100644 --- a/app/internal/handlers/admin_addresses.go +++ b/app/internal/handlers/admin_addresses.go @@ -31,33 +31,37 @@ type PageNumber struct { IsCurrent bool } +// AddressWithDetails extends AddressDatabase with appointment and user info +type AddressWithDetails struct { + models.AddressDatabase + 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) + - // Default values page := 1 - pageSize := 20 // Default page size - - // Parse page number + pageSize := 20 if pageStr != "" { if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { page = p } } - - // Parse page size if pageSizeStr != "" { if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 { pageSize = ps } } - - // Calculate offset offset := (page - 1) * pageSize - // Get total count first + // Get total count var totalRecords int err := models.DB.QueryRow(`SELECT COUNT(*) FROM "address_database"`).Scan(&totalRecords) if err != nil { @@ -65,27 +69,43 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Database error", http.StatusInternalServerError) return } - - // Calculate pagination info totalPages := (totalRecords + pageSize - 1) / pageSize if totalPages == 0 { totalPages = 1 } - - // Ensure current page is within bounds if page > totalPages { page = totalPages offset = (page - 1) * pageSize } - // Get paginated results + // Query addresses with appointment + user info 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" - WHERE street_quadrant = 'ne' - ORDER BY address_id + 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, + 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 { @@ -95,9 +115,10 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { } defer rows.Close() - var addresses []models.AddressDatabase + var addresses []AddressWithDetails for rows.Next() { - var a models.AddressDatabase + var a AddressWithDetails + var houseAlpha string err := rows.Scan( &a.AddressID, &a.Address, @@ -105,28 +126,68 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { &a.StreetType, &a.StreetQuadrant, &a.HouseNumber, - &a.HouseAlpha, + &houseAlpha, &a.Longitude, &a.Latitude, &a.VisitedValidated, + &a.CreatedAt, + &a.UpdatedAt, + &a.Assigned, + &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) } - // Calculate start and end record numbers for display + // Get users associated with this admin + currentAdminID := r.Context().Value("user_id").(int) + 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 } - // Generate page numbers for pagination controls pageNumbers := generatePageNumbers(page, totalPages) - pagination := PaginationInfo{ CurrentPage: page, TotalPages: totalPages, @@ -147,9 +208,11 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) { "Title": "Addresses", "IsAuthenticated": true, "ShowAdminNav": true, - "ActiveSection": "address", // Add this line + "ActiveSection": "address", "Addresses": addresses, - "Role": "admin", + "Users": users, + "UserName": username, + "Role": "admin", "Pagination": pagination, }) } @@ -182,3 +245,83 @@ func generatePageNumbers(currentPage, totalPages int) []PageNumber { 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") + + 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 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) + return + } + + // Check if this address is already assigned to any user + 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 + } + + // Assign the address - create appointment + _, err = models.DB.Exec(` + INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at) + VALUES ($1, $2, CURRENT_DATE, CURRENT_TIME, NOW(), NOW()) + `, userID, addressID) + if err != nil { + log.Println("Assignment error:", err) + http.Error(w, "Failed to assign address", http.StatusInternalServerError) + return + } + + // Redirect back to addresses page with success + http.Redirect(w, r, "/addresses?success=assigned", http.StatusSeeOther) +} \ No newline at end of file diff --git a/app/internal/handlers/admin_apointment.go b/app/internal/handlers/admin_apointment.go new file mode 100644 index 0000000..33b18f9 --- /dev/null +++ b/app/internal/handlers/admin_apointment.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +type AssignedAddress struct { + AddressID int + Address string + StreetName string + StreetType string + StreetQuadrant string + HouseNumber string + HouseAlpha *string + Longitude float64 + Latitude float64 + VisitedValidated bool + CreatedAt string + UpdatedAt string + Assigned bool + UserName string + UserEmail string + UserPhone string + AppointmentDate *string + AppointmentTime *string +} + +func AssignedAddressesHandler(w http.ResponseWriter, r *http.Request) { + username,_ := models.GetCurrentUserName(r) + + rows, err := models.DB.Query(` + SELECT + a.address_id, a.address, a.street_name, a.street_type, a.street_quadrant, + a.house_number, a.house_alpha, a.longitude, a.latitude, a.visited_validated, + a.created_at, a.updated_at, + CASE WHEN ap.user_id IS NOT NULL THEN true ELSE false END as assigned, + COALESCE(u.first_name || ' ' || u.last_name, '') as user_name, + COALESCE(u.email, '') as user_email, + COALESCE(u.phone, '') as user_phone, + TO_CHAR(ap.appointment_date, 'YYYY-MM-DD') as appointment_date, + TO_CHAR(ap.appointment_time, 'HH24:MI') 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 + ORDER BY a.address_id; + `) + if err != nil { + log.Printf("query error: %v", err) + http.Error(w, "query error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var assignedAddresses []AssignedAddress + for rows.Next() { + var addr AssignedAddress + err := rows.Scan( + &addr.AddressID, &addr.Address, &addr.StreetName, &addr.StreetType, &addr.StreetQuadrant, + &addr.HouseNumber, &addr.HouseAlpha, &addr.Longitude, &addr.Latitude, &addr.VisitedValidated, + &addr.CreatedAt, &addr.UpdatedAt, &addr.Assigned, &addr.UserName, &addr.UserEmail, + &addr.UserPhone, &addr.AppointmentDate, &addr.AppointmentTime, + ) + if err != nil { + log.Printf("scan error: %v", err) + continue + } + assignedAddresses = append(assignedAddresses, addr) + } + + utils.Render(w, "address_assigned.html", map[string]interface{}{ + "Title": "Assigned Addresses", + "IsAuthenticated": true, + "AssignedList": assignedAddresses, + "ShowAdminNav": true, + "Role": "admin", + "UserName": username, + "ActiveSection": "assigned", + }) +} diff --git a/app/internal/handlers/admin_dashboard.go b/app/internal/handlers/admin_dashboard.go index fd10f5e..2aea753 100644 --- a/app/internal/handlers/admin_dashboard.go +++ b/app/internal/handlers/admin_dashboard.go @@ -11,7 +11,7 @@ import ( func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { currentAdminID := r.Context().Value("user_id").(int) - + username,_ := models.GetCurrentUserName(r) role, _ := r.Context().Value("uesr_role").(int) var volunteerCount int @@ -75,6 +75,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { "ValidatedCount": validatedCount, "HousesLeftPercent": housesLeftPercent, "ShowAdminNav": true, + "UserName": username, "Role": role, "ActiveSection": "dashboard", }) diff --git a/app/internal/handlers/admin_post.go b/app/internal/handlers/admin_post.go index 9cbd632..cc6b0a8 100644 --- a/app/internal/handlers/admin_post.go +++ b/app/internal/handlers/admin_post.go @@ -19,6 +19,8 @@ import ( func PostsHandler(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value("user_id").(int) role := r.Context().Value("user_role").(int) + username,_ := models.GetCurrentUserName(r) + if r.Method == http.MethodPost { // Parse multipart form @@ -103,14 +105,18 @@ func PostsHandler(w http.ResponseWriter, r *http.Request) { return } + CurrentUserID := models.GetCurrentUserID(w, r) + + // GET request: fetch posts rows, err := models.DB.Query(` SELECT p.post_id, p.author_id, u.first_name || ' ' || u.last_name AS author_name, p.content, COALESCE(p.image_url, '') as image_url, p.created_at FROM post p JOIN users u ON p.author_id = u.user_id + WHERE p.author_id = $1 ORDER BY p.created_at DESC - `) + `, CurrentUserID) if err != nil { fmt.Printf("Database query error: %v\n", err) http.Error(w, "Failed to fetch posts", http.StatusInternalServerError) @@ -147,6 +153,7 @@ func PostsHandler(w http.ResponseWriter, r *http.Request) { "IsAuthenticated": true, "ShowAdminNav": showAdminNav, "ShowVolunteerNav": showVolunteerNav, + "UserName": username, "Posts": posts, "ActiveSection": "posts", }) @@ -155,6 +162,6 @@ func PostsHandler(w http.ResponseWriter, r *http.Request) { // Helper function (add this to your main.go if not already there) func getNavFlags(role int) (bool, bool) { showAdminNav := role == 1 // Admin role - showVolunteerNav := role == 3 // Volunteer role + showVolunteerNav := role == 3 || role == 2 return showAdminNav, showVolunteerNav } \ No newline at end of file diff --git a/app/internal/handlers/admin_team_builder.go b/app/internal/handlers/admin_team_builder.go new file mode 100644 index 0000000..3bc2d65 --- /dev/null +++ b/app/internal/handlers/admin_team_builder.go @@ -0,0 +1,183 @@ +package handlers + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + + +type User struct { + ID int + Name string +} + +type TeamLead struct { + ID int + Name string + Volunteers []User +} + +type TeamBuilderData struct { + TeamLeads []TeamLead + UnassignedVolunteers []User +} + + + +func TeamBuilderHandler(w http.ResponseWriter, r *http.Request) { + // GET request: show team leads and unassigned volunteers + if r.Method == http.MethodGet { + var teamLeads []TeamLead + var unassignedVolunteers []User + + CurrentUserID := models.GetCurrentUserID(w, r) + username,_ := models.GetCurrentUserName(r) + + + + // Get all team leads (role_id = 2) + tlRows, err := models.DB.Query(`SELECT u.user_id, u.first_name || ' ' || u.last_name AS name + FROM users u + JOIN admin_volunteers x ON x.volunteer_id = u.user_id + WHERE u.role_id = 2 AND x.admin_id = $1`, CurrentUserID) + if err != nil { + http.Error(w, "Error fetching team leads", http.StatusInternalServerError) + return + } + defer tlRows.Close() + for tlRows.Next() { + var tl TeamLead + tlRows.Scan(&tl.ID, &tl.Name) + + // Get assigned volunteers for this team lead + vRows, _ := models.DB.Query(`SELECT u.user_id, u.first_name || ' ' || u.last_name AS name + FROM users u + JOIN team t ON u.user_id = t.volunteer_id + WHERE t.team_lead_id = $1`, tl.ID) + + for vRows.Next() { + var vol User + vRows.Scan(&vol.ID, &vol.Name) + tl.Volunteers = append(tl.Volunteers, vol) + } + + teamLeads = append(teamLeads, tl) + } + + // Get unassigned volunteers (role_id = 3) + vRows, _ := models.DB.Query(`SELECT u.user_id, u.first_name || ' ' || u.last_name AS name + FROM users u + LEFT JOIN team t ON u.user_id = t.volunteer_id + JOIN admin_volunteers x ON x.volunteer_id = u.user_id + WHERE u.role_id = 3 AND x.admin_id = $1 + AND t.volunteer_id IS NULL`, CurrentUserID ) + for vRows.Next() { + var vol User + vRows.Scan(&vol.ID, &vol.Name) + unassignedVolunteers = append(unassignedVolunteers, vol) + } + + utils.Render(w, "volunteer/team_builder.html", map[string]interface{}{ + "Title": "Team Builder", + "IsAuthenticated": true, + "ShowAdminNav": true, + "TeamLeads": teamLeads, + "UserName": username, + "UnassignedVolunteers": unassignedVolunteers, + "ActiveSection": "team_builder", + }) + return + } + + // POST request: assign volunteer to a team lead + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + + volunteerIDStr := r.FormValue("volunteer_id") + teamLeadIDStr := r.FormValue("team_lead_id") + + if volunteerIDStr == "" || teamLeadIDStr == "" { + http.Error(w, "Volunteer ID and Team Lead ID are required", http.StatusBadRequest) + return + } + + volunteerID, err := strconv.Atoi(volunteerIDStr) + if err != nil { + http.Error(w, "Invalid volunteer ID", http.StatusBadRequest) + return + } + + teamLeadID, err := strconv.Atoi(teamLeadIDStr) + if err != nil { + http.Error(w, "Invalid team lead ID", http.StatusBadRequest) + return + } + + // Optional: check if volunteer is already assigned + var exists int + err = models.DB.QueryRow(`SELECT COUNT(*) FROM team WHERE volunteer_id = $1`, volunteerID).Scan(&exists) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + if exists > 0 { + http.Error(w, "Volunteer is already assigned to a team", http.StatusBadRequest) + return + } + + // Assign volunteer to team lead + _, err = models.DB.Exec(`INSERT INTO team (volunteer_id, team_lead_id) VALUES ($1, $2)`, volunteerID, teamLeadID) + if err != nil { + fmt.Println(err) + http.Error(w, "Failed to assign volunteer", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/team_builder", http.StatusSeeOther) + } +} + + +func RemoveVolunteerHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/team_builder", http.StatusSeeOther) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + + volunteerID, err := strconv.Atoi(r.FormValue("volunteer_id")) + if err != nil { + http.Error(w, "Invalid volunteer ID", http.StatusBadRequest) + return + } + + teamLeadID, err := strconv.Atoi(r.FormValue("team_lead_id")) + fmt.Print(teamLeadID) + if err != nil { + http.Error(w, "Invalid team lead ID", http.StatusBadRequest) + return + } + + // Remove volunteer from the team + _, err = models.DB.Exec(`DELETE FROM team WHERE team_lead_id = $1 AND volunteer_id = $2`, teamLeadID, volunteerID) + if err != nil { + fmt.Println(err) + http.Error(w, "Failed to remove volunteer from team", http.StatusInternalServerError) + return + } + + + http.Redirect(w, r, "/team_builder", http.StatusSeeOther) +} + diff --git a/app/internal/handlers/admin_voluteers.go b/app/internal/handlers/admin_voluteers.go index 8e370f1..fd6038e 100644 --- a/app/internal/handlers/admin_voluteers.go +++ b/app/internal/handlers/admin_voluteers.go @@ -1,6 +1,7 @@ package handlers import ( + "database/sql" "fmt" "log" "net/http" @@ -14,6 +15,7 @@ import ( func VolunteerHandler(w http.ResponseWriter, r *http.Request) { // TODO: Replace this with actual session/jwt extraction currentAdminID := r.Context().Value("user_id").(int) + username,_ := models.GetCurrentUserName(r) rows, err := models.DB.Query(` SELECT u.user_id, u.email, u.role_id, u.first_name, u.last_name, u.phone @@ -42,6 +44,7 @@ func VolunteerHandler(w http.ResponseWriter, r *http.Request) { "Title": "Assigned Volunteers", "IsAuthenticated": true, "ShowAdminNav": true, + "UserName": username, "Users": user, "ActiveSection": "volunteer", }) @@ -92,6 +95,28 @@ func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) { return } + // If role is being updated to Team Leader + if rid == 2 { + // Check if the volunteer is in any team + var teamID int + err := models.DB.QueryRow(`SELECT team_id FROM team WHERE volunteer_id = $1`, volunteerID).Scan(&teamID) + if err != nil && err != sql.ErrNoRows { + log.Printf("DB error checking team for user %s: %v", volunteerID, err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // If found, remove from the team + if err == nil { + _, err := models.DB.Exec(`UPDATE team SET volunteer_id = NULL WHERE team_id = $1`, teamID) + if err != nil { + log.Printf("Failed to remove volunteer %s from team %d: %v", volunteerID, teamID, err) + http.Error(w, "Failed to update team assignment", http.StatusInternalServerError) + return + } + } + } + _, err = models.DB.Exec(` UPDATE "users" SET first_name = $1, last_name = $2, email = $3, phone = $4, role_id = $5 @@ -108,108 +133,6 @@ func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) { } } -type User struct { - ID int - Name string -} - -type TeamLead struct { - ID int - Name string - Volunteers []User -} - -type TeamBuilderData struct { - TeamLeads []TeamLead - UnassignedVolunteers []User -} - - - -func TeamBuilderHandler(w http.ResponseWriter, r *http.Request) { - // GET request: show team leads and unassigned volunteers - if r.Method == http.MethodGet { - var teamLeads []TeamLead - var unassignedVolunteers []User - - // Get all team leads (role_id = 2) - tlRows, err := models.DB.Query(`SELECT user_id, first_name || ' ' || last_name AS name FROM users WHERE role_id = 2`) - if err != nil { - http.Error(w, "Error fetching team leads", http.StatusInternalServerError) - return - } - defer tlRows.Close() - for tlRows.Next() { - var tl TeamLead - tlRows.Scan(&tl.ID, &tl.Name) - - // Get assigned volunteers for this team lead - vRows, _ := models.DB.Query(`SELECT u.user_id, u.first_name || ' ' || u.last_name AS name - FROM users u - JOIN team t ON u.user_id = t.volunteer_id - WHERE t.team_lead_id = $1`, tl.ID) - - for vRows.Next() { - var vol User - vRows.Scan(&vol.ID, &vol.Name) - tl.Volunteers = append(tl.Volunteers, vol) - } - - teamLeads = append(teamLeads, tl) - } - - // Get unassigned volunteers (role_id = 3) - vRows, _ := models.DB.Query(`SELECT user_id, first_name || ' ' || last_name AS name - FROM users - WHERE role_id = 3 - AND user_id NOT IN (SELECT volunteer_id FROM team)`) - for vRows.Next() { - var vol User - vRows.Scan(&vol.ID, &vol.Name) - unassignedVolunteers = append(unassignedVolunteers, vol) - } - - utils.Render(w, "volunteer/team_builder.html", map[string]interface{}{ - "Title": "Team Builder", - "IsAuthenticated": true, - "ShowAdminNav": true, - "TeamLeads": teamLeads, - "UnassignedVolunteers": unassignedVolunteers, - "ActiveSection": "team_builder", - }) - return - } - - // POST request: assign volunteer to a team lead - if r.Method == http.MethodPost { - if err := r.ParseForm(); err != nil { - http.Error(w, "Invalid form", http.StatusBadRequest) - return - } - - volunteerID, err := strconv.Atoi(r.FormValue("volunteer_id")) - if err != nil { - http.Error(w, "Invalid volunteer ID", http.StatusBadRequest) - return - } - teamLeadID, err := strconv.Atoi(r.FormValue("team_lead_id")) - if err != nil { - http.Error(w, "Invalid team lead ID", http.StatusBadRequest) - return - } - - _, err = models.DB.Exec(`INSERT INTO team (volunteer_id, team_lead_id) VALUES ($1, $2)`, volunteerID, teamLeadID) - if err != nil { - fmt.Println(err) - http.Error(w, "Failed to assign volunteer", http.StatusInternalServerError) - return - } - - http.Redirect(w, r, "/team_builder", http.StatusSeeOther) - } -} - - diff --git a/app/internal/handlers/login.go b/app/internal/handlers/login.go index 5fcef08..e8de9e4 100644 --- a/app/internal/handlers/login.go +++ b/app/internal/handlers/login.go @@ -1,9 +1,10 @@ package handlers import ( - "context" + "database/sql" "log" "net/http" + "strconv" "time" "github.com/golang-jwt/jwt/v5" @@ -89,13 +90,6 @@ func clearSessionCookie(w http.ResponseWriter) { }) } -// func LoginPage(w http.ResponseWriter, r *http.Request) { -// utils.Render(w, "login.html", map[string]interface{}{ -// "Title": "Login", -// "IsAuthenticated": false, -// }) -// } - func LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Redirect(w, r, "/", http.StatusSeeOther) @@ -107,7 +101,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // Input validation if email == "" || password == "" { - renderLoginError(w, "Email and password are required") + http.Redirect(w, r, "/?error=EmailAndPasswordRequired", http.StatusSeeOther) return } @@ -124,7 +118,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Login failed for email %s: %v", email, err) - renderLoginError(w, "Invalid email or password") + http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther) return } @@ -132,7 +126,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) if err != nil { log.Printf("Password verification failed for user ID %d", userID) - renderLoginError(w, "Invalid email or password") + http.Redirect(w, r, "/?error=InvalidCredentials", http.StatusSeeOther) return } @@ -140,7 +134,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { tokenString, expirationTime, err := createJWTToken(userID, role) if err != nil { log.Printf("JWT token creation failed for user ID %d: %v", userID, err) - http.Error(w, "Could not log in", http.StatusInternalServerError) + http.Redirect(w, r, "/?error=InternalError", http.StatusSeeOther) return } @@ -153,6 +147,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, redirectURL, http.StatusSeeOther) } + func RegisterHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { utils.Render(w, "register.html", map[string]interface{}{ @@ -168,6 +163,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { phone := r.FormValue("phone") role := r.FormValue("role") password := r.FormValue("password") + adminCode := r.FormValue("admin_code") // for volunteers // Input validation if firstName == "" || lastName == "" || email == "" || password == "" || role == "" { @@ -183,185 +179,66 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } - // Insert user into database - _, err = models.DB.Exec(` - INSERT INTO "users" (first_name, last_name, email, phone, password, role_id) - VALUES ($1, $2, $3, $4, $5, $6) - `, firstName, lastName, email, phone, string(hashedPassword), role) - + // Convert role to int + roleID, err := strconv.Atoi(role) if err != nil { - log.Printf("User registration failed for email %s: %v", email, err) + renderRegisterError(w, "Invalid role") + return + } + + var adminID int + if roleID == 3 { // volunteer + if adminCode == "" { + renderRegisterError(w, "Admin code is required for volunteers") + return + } + + // Check if admin exists + err = models.DB.QueryRow(`SELECT user_id FROM users WHERE role_id = 1 AND admin_code = $1`, adminCode).Scan(&adminID) + if err != nil { + if err == sql.ErrNoRows { + renderRegisterError(w, "Invalid admin code") + return + } + log.Printf("DB error checking admin code: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } + + // Insert user and get ID + var userID int + err = models.DB.QueryRow(` + INSERT INTO users (first_name, last_name, email, phone, password, role_id) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING user_id + `, firstName, lastName, email, phone, string(hashedPassword), roleID).Scan(&userID) + if err != nil { + log.Printf("User registration failed: %v", err) renderRegisterError(w, "Could not create account. Email might already be in use.") return } + // Link volunteer to admin if role is volunteer + if roleID == 3 { + _, err = models.DB.Exec(` + INSERT INTO admin_volunteers (admin_id, volunteer_id) + VALUES ($1, $2) + `, adminID, userID) + if err != nil { + log.Printf("Failed to link volunteer to admin: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } + log.Printf("User registered successfully: %s %s (%s)", firstName, lastName, email) http.Redirect(w, r, "/", http.StatusSeeOther) } + + func LogoutHandler(w http.ResponseWriter, r *http.Request) { clearSessionCookie(w) http.Redirect(w, r, "/", http.StatusSeeOther) } - -// // Admin Dashboard Handler -// func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) -// userID := r.Context().Value("user_id").(int) - -// // TODO: Fetch real data from database -// dashboardData := map[string]interface{}{ -// "UserID": userID, -// "TotalUsers": 100, // Example: get from database -// "TotalVolunteers": 50, // Example: get from database -// "TotalAddresses": 200, // Example: get from database -// "RecentActivity": []string{"User logged in", "New volunteer registered"}, // Example -// } - -// data := createTemplateData("Admin Dashboard", "dashboard", role, true, dashboardData) -// utils.Render(w, "dashboard/dashboard.html", data) -// } - -// // Volunteer Management Handler -// func VolunteerHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) - -// // TODO: Fetch real volunteer data from database -// volunteerData := map[string]interface{}{ -// "Volunteers": []map[string]interface{}{ -// {"ID": 1, "Name": "John Doe", "Email": "john@example.com", "Status": "Active"}, -// {"ID": 2, "Name": "Jane Smith", "Email": "jane@example.com", "Status": "Active"}, -// }, // Example: get from database -// } - -// data := createTemplateData("Volunteers", "volunteer", role, true, volunteerData) -// utils.Render(w, "volunteers/volunteers.html", data) -// } - -// // Address Management Handler -// func AddressHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) - -// // TODO: Fetch real address data from database -// addressData := map[string]interface{}{ -// "Addresses": []map[string]interface{}{ -// {"ID": 1, "Street": "123 Main St", "City": "Calgary", "Status": "Validated"}, -// {"ID": 2, "Street": "456 Oak Ave", "City": "Calgary", "Status": "Pending"}, -// }, // Example: get from database -// } - -// data := createTemplateData("Addresses", "address", role, true, addressData) -// utils.Render(w, "addresses/addresses.html", data) -// } - -// // Reports Handler -// func ReportHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) - -// // TODO: Fetch real report data from database -// reportData := map[string]interface{}{ -// "Reports": []map[string]interface{}{ -// {"ID": 1, "Name": "Weekly Summary", "Date": "2025-08-25", "Status": "Complete"}, -// {"ID": 2, "Name": "Monthly Analytics", "Date": "2025-08-01", "Status": "Pending"}, -// }, // Example: get from database -// } - -// data := createTemplateData("Reports", "report", role, true, reportData) -// utils.Render(w, "reports/reports.html", data) -// } - -// // Profile Handler (works for both admin and volunteer) -// func ProfileHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) -// userID := r.Context().Value("user_id").(int) - -// // Fetch real user data from database -// var firstName, lastName, email, phone string -// err := models.DB.QueryRow(` -// SELECT first_name, last_name, email, phone -// FROM "users" -// WHERE user_id = $1 -// `, userID).Scan(&firstName, &lastName, &email, &phone) - -// profileData := map[string]interface{}{ -// "UserID": userID, -// } - -// if err != nil { -// log.Printf("Error fetching user profile for ID %d: %v", userID, err) -// profileData["Error"] = "Could not load profile data" -// } else { -// profileData["FirstName"] = firstName -// profileData["LastName"] = lastName -// profileData["Email"] = email -// profileData["Phone"] = phone -// } - -// data := createTemplateData("Profile", "profile", role, true, profileData) -// utils.Render(w, "profile/profile.html", data) -// } - -// // Volunteer Dashboard Handler -// func VolunteerDashboardHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) -// userID := r.Context().Value("user_id").(int) - -// // TODO: Fetch volunteer-specific data from database -// dashboardData := map[string]interface{}{ -// "UserID": userID, -// "AssignedTasks": 5, // Example: get from database -// "CompletedTasks": 12, // Example: get from database -// "UpcomingEvents": []string{"Community Meeting - Aug 30", "Training Session - Sep 5"}, // Example -// } - -// data := createTemplateData("Volunteer Dashboard", "dashboard", role, true, dashboardData) -// utils.Render(w, "volunteer/dashboard.html", data) -// } - -// // Schedule Handler for Volunteers -// func ScheduleHandler(w http.ResponseWriter, r *http.Request) { -// role := r.Context().Value("user_role").(int) -// userID := r.Context().Value("user_id").(int) - -// // TODO: Fetch schedule data from database -// scheduleData := map[string]interface{}{ -// "UserID": userID, -// "Schedule": []map[string]interface{}{ -// {"Date": "2025-08-26", "Time": "10:00 AM", "Task": "Door-to-door survey", "Location": "Downtown"}, -// {"Date": "2025-08-28", "Time": "2:00 PM", "Task": "Data entry", "Location": "Office"}, -// }, // Example: get from database -// } - -// data := createTemplateData("My Schedule", "schedual", role, true, scheduleData) -// utils.Render(w, "volunteer/schedule.html", data) -// } - -// Enhanced middleware to check JWT auth and add user context -func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("session") - if err != nil { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - - claims := &models.Claims{} - token, err := jwt.ParseWithClaims(cookie.Value, claims, func(token *jwt.Token) (interface{}, error) { - return jwtKey, nil - }) - - if err != nil || !token.Valid { - log.Printf("Invalid token: %v", err) - clearSessionCookie(w) // Clear invalid cookie - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - - // Add user info to context - ctx := context.WithValue(r.Context(), "user_id", claims.UserID) - ctx = context.WithValue(ctx, "user_role", claims.Role) - r = r.WithContext(ctx) - - next.ServeHTTP(w, r) - } -} \ No newline at end of file diff --git a/app/internal/handlers/profile.go b/app/internal/handlers/profile.go index 29c8418..226e40b 100644 --- a/app/internal/handlers/profile.go +++ b/app/internal/handlers/profile.go @@ -11,10 +11,11 @@ import ( func ProfileHandler(w http.ResponseWriter, r *http.Request) { // Extract current user ID from session/jwt currentUserID := r.Context().Value("user_id").(int) + username,_ := models.GetCurrentUserName(r) var user models.User err := models.DB.QueryRow(` - SELECT user_id, first_name, last_name, email, phone, role_id, created_at, updated_at + SELECT user_id, first_name, last_name, email, phone, role_id, created_at, updated_at, admin_code FROM "users" WHERE user_id = $1 `, currentUserID).Scan( @@ -26,6 +27,7 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) { &user.RoleID, &user.CreatedAt, &user.UpdatedAt, + &user.AdminCode, ) if err != nil { log.Println("Profile query error:", err) @@ -41,8 +43,8 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) { adminnav = true volunteernav = false }else{ - volunteernav = true adminnav = false + volunteernav = true } utils.Render(w, "profile/profile.html", map[string]interface{}{ @@ -50,6 +52,7 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) { "IsAuthenticated": true, "ShowAdminNav": adminnav, "ShowVolunteerNav": volunteernav, + "UserName": username, "User": user, "ActiveSection": "profile", }) diff --git a/app/internal/handlers/volunteer_address.go b/app/internal/handlers/volunteer_address.go new file mode 100644 index 0000000..3bded6c --- /dev/null +++ b/app/internal/handlers/volunteer_address.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + + +func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) { + // Fetch appointments joined with address info + + currentUserID := models.GetCurrentUserID(w,r) + username,_ := models.GetCurrentUserName(r) + + rows, err := models.DB.Query(` + SELECT + a.sched_id, + a.user_id, + ad.address, + ad.latitude, + ad.longitude, + a.appointment_date, + a.appointment_time + FROM appointment a + JOIN address_database ad ON a.address_id = ad.address_id + WHERE a.user_id = $1 + `, currentUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + // Struct to hold appointment + address info + type AppointmentWithAddress struct { + SchedID int + UserID int + Address string + Latitude float64 + Longitude float64 + AppointmentDate time.Time + AppointmentTime time.Time + } + + var appointments []AppointmentWithAddress + for rows.Next() { + var a AppointmentWithAddress + if err := rows.Scan(&a.SchedID, &a.UserID, &a.Address, &a.Latitude, &a.Longitude, &a.AppointmentDate, &a.AppointmentTime); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + appointments = append(appointments, a) + } + + role := r.Context().Value("user_role").(int) + adminnav := false + volunteernav := false + + if role == 1{ + adminnav = true + volunteernav = false + }else{ + adminnav = false + volunteernav = true + } + + // Render template + utils.Render(w, "/appointment.html", map[string]interface{}{ + "Title": "My Profile", + "IsAuthenticated": true, + "ShowAdminNav": adminnav, // your existing variable + "ShowVolunteerNav": volunteernav, // your existing variable + "ActiveSection": "address", + "UserName": username, + "Appointments": appointments, // pass the fetched appointments + }) +} diff --git a/app/internal/handlers/volunteer_posts.go b/app/internal/handlers/volunteer_posts.go index 3472859..d9ee1e9 100644 --- a/app/internal/handlers/volunteer_posts.go +++ b/app/internal/handlers/volunteer_posts.go @@ -22,6 +22,8 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) { // Get user info from context role := r.Context().Value("user_role").(int) + CurrentUserID := models.GetCurrentUserID(w, r) + username,_ := models.GetCurrentUserName(r) // Fetch posts from database rows, err := models.DB.Query(` @@ -29,8 +31,10 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) { p.content, COALESCE(p.image_url, '') as image_url, p.created_at FROM post p JOIN users u ON p.author_id = u.user_id + JOIN admin_volunteers x ON u.user_id = x.admin_id + WHERE x.volunteer_id = $1 ORDER BY p.created_at DESC - `) + `,CurrentUserID) if err != nil { fmt.Printf("Database query error: %v\n", err) http.Error(w, "Failed to fetch posts", http.StatusInternalServerError) @@ -66,6 +70,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) { "IsAuthenticated": true, "ShowAdminNav": showAdminNav, "ShowVolunteerNav": showVolunteerNav, + "UserName": username, "Posts": posts, "ActiveSection": "posts", "IsVolunteer": true, // Flag to indicate this is volunteer view diff --git a/app/internal/models/structs.go b/app/internal/models/structs.go index 6be0847..a4262e9 100644 --- a/app/internal/models/structs.go +++ b/app/internal/models/structs.go @@ -7,6 +7,7 @@ import ( ) + type Claims struct { UserID int Role int @@ -37,7 +38,8 @@ type User struct { Email string Phone string Password string - RoleID int + RoleID int + AdminCode string CreatedAt time.Time UpdatedAt time.Time } @@ -71,6 +73,8 @@ type AddressDatabase struct { VisitedValidated bool CreatedAt time.Time UpdatedAt time.Time + Assigned bool // <-- add this + } // ===================== diff --git a/app/internal/models/token.go b/app/internal/models/token.go index d398833..6a66b13 100644 --- a/app/internal/models/token.go +++ b/app/internal/models/token.go @@ -2,27 +2,32 @@ package models import ( "fmt" - - "github.com/golang-jwt/jwt/v5" + "net/http" ) var jwtKey = []byte("your-secret-key") //TODO: Move to env/config +func GetCurrentUserID(w http.ResponseWriter, r *http.Request)(int){ + currentUserID := r.Context().Value("user_id").(int) + return currentUserID +} -func ExtractClaims(tokenStr string) (*Claims, error) { - claims := &Claims{} +func GetCurrentUserName(r *http.Request) (string, error) { + currentUserID, ok := r.Context().Value("user_id").(int) + if !ok { + return "", fmt.Errorf("user_id not found in context") + } - token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { - return jwtKey, nil - }) + var currentUserName string + err := DB.QueryRow(` + SELECT first_name || ' ' || last_name + FROM users + WHERE user_id = $1 + `, currentUserID).Scan(¤tUserName) if err != nil { - return nil, err + return "", err } - if !token.Valid { - return nil, fmt.Errorf("invalid token") - } - - return claims, nil -} \ No newline at end of file + return currentUserName, nil +} diff --git a/app/internal/templates/address/address.html b/app/internal/templates/address/address.html index b0fe18e..d8a8441 100644 --- a/app/internal/templates/address/address.html +++ b/app/internal/templates/address/address.html @@ -1,5 +1,4 @@ {{ define "content" }} -
| ID | -Address | -Street | -House # | -Longitude | -Latitude | Validated | +Address | +Cordinates | +Assigned User | +Appointment | +Assign | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ .AddressID }} | -{{ .Address }} | -- {{ .StreetName }} {{ .StreetType }} {{ .StreetQuadrant }} - | -{{ .HouseNumber }} | -{{ .Longitude }} | -{{ .Latitude }} | {{ if .VisitedValidated }} {{ end }} | +{{ .Address }} | ++ + ({{ .Latitude }}, {{ .Longitude }}) + + | +
+ {{ if .UserName }}{{ .UserName }} {{ .UserEmail }}{{ else }}Unassigned{{ end }} + |
+ + {{ if .AppointmentDate }} {{ .AppointmentDate }} {{ .AppointmentTime + }} {{ else }} + No appointment + {{ end }} + | ++ {{ if .Assigned }} + + {{ else }} + + {{ end }} + | ||||
| + | No addresses found | ||||||||||||||