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" }} -
@@ -12,7 +11,6 @@ Address Database
- {{if .Pagination}}
Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of @@ -25,33 +23,20 @@
-
-
-
- - -
-
-
- +
+ +
- - {{if .Pagination}}
-
- -
- - - - - {{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}} - - - + {{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}
- +
- - - - - - - + + + + + - - {{ range .Addresses }} - - - - - - + + + + + {{ else }} - @@ -170,27 +175,64 @@
IDAddressStreetHouse #LongitudeLatitude ValidatedAddressCordinatesAssigned UserAppointmentAssign
{{ .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
- - {{if .Pagination}} -
-
- -
- Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of - {{.Pagination.TotalRecords}} addresses -
+ + - {{end}}
-{{ end }} \ No newline at end of file +{{end}} \ No newline at end of file diff --git a/app/internal/templates/posts.html b/app/internal/templates/posts.html index 435a0b0..23e7976 100644 --- a/app/internal/templates/posts.html +++ b/app/internal/templates/posts.html @@ -1,9 +1,17 @@ {{ define "content" }}
-
-
-

Posts

+ +
+
+
+
+ + Volunteer Management +
+
diff --git a/app/internal/templates/profile/profile.html b/app/internal/templates/profile/profile.html index 0e44ca4..df189bc 100644 --- a/app/internal/templates/profile/profile.html +++ b/app/internal/templates/profile/profile.html @@ -1,199 +1,372 @@ {{ define "content" }}
- -
+ +
-
- -

User Profile

-
-
- - Secure Profile Management +
+
+ + Volunteer Management +
-
- -
-
-

- - Profile Overview -

-
-
-
- -
-
-
-

- {{ .User.FirstName }} {{ .User.LastName }} -

-

{{ .User.Email }}

-
- - - Active User - - ID: {{ .User.UserID }} -
-
-
-
- -
-

- Account Information -

-
-
- User ID: - {{ .User.UserID }} -
-
- Role: - {{ if eq .User.RoleID 1 }}Admin - {{ else if eq .User.RoleID 2 }}Team Leader - {{ else }}Volunteer - {{ end }} -
-
- Status: - Active -
- -
-
+ +
+
+
+

+ {{ .User.FirstName }} {{ .User.LastName }} +

+

{{ .User.Email }}

+
+ + + Active User + + Signup Code: + {{ .User.AdminCode }} + User ID: + {{ .User.UserID }} + Role: + + {{ if eq .User.RoleID 1 }}Admin {{ else if eq .User.RoleID 2 + }}Team Leader {{ else }}Volunteer {{ end }} +
- -
-
-

- - Edit Profile Information -

-
- -
-
-
- -
- - -
+ +
+

+ + Edit Profile Information +

- -
- - -
+ + +
+ +
+ + +
- -
- -
- -
- -
+ +
+ + +
+ + +
+ +
+ +
+
-

- Contact system administrator to change email -

-
- - -
- -
+

+ Contact system administrator to change email +

- -
-
- - Changes will be applied immediately after saving -
-
- - -
+ +
+ +
- -
+
+ + +
+
+ + Changes will be applied immediately after saving +
+
+ + +
+
+
- + +
+

+ + Configuration Settings +

+ +
+
+ +
+

+ Add New Setting +

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+

+ Current Settings +

+
+ +
+ No settings configured yet. Add your first setting above. +
+
+
+
+ + +
+
+ + Settings are applied immediately when added or removed +
+
+ + +
+
+
+ + +{{ end }} diff --git a/app/internal/templates/volunteer/team_builder.html b/app/internal/templates/volunteer/team_builder.html index 0d25e10..d4891f3 100644 --- a/app/internal/templates/volunteer/team_builder.html +++ b/app/internal/templates/volunteer/team_builder.html @@ -1,36 +1,114 @@ {{ define "content" }} -
-

Team Builder

- - {{range .TeamLeads}} -
-
- {{.Name}} -
- - - -
+
+ +
+
+
+ + Volunteer Management +
+
- - {{if .Volunteers}} -
    - {{range .Volunteers}} -
  • {{.Name}}
  • - {{end}} -
- {{else}} -

No volunteers assigned yet.

+ +
+ {{range .TeamLeads}} {{ $teamLeadID := .ID }} + + +
+ +
+
+ + {{.Name}} +
+
+ + + + +
+
+ + +
+ {{if .Volunteers}} +
    + {{range .Volunteers}} +
  • +
    + + {{.Name}} +
    +
    + + + +
    +
  • + {{end}} +
+ {{else}} +

No volunteers assigned yet.

+ {{end}} +
+
{{end}}
- {{end}}
+ + {{ end }} diff --git a/app/internal/templates/volunteer/volunteer.html b/app/internal/templates/volunteer/volunteer.html index 2b3cdc8..52af05d 100644 --- a/app/internal/templates/volunteer/volunteer.html +++ b/app/internal/templates/volunteer/volunteer.html @@ -9,7 +9,7 @@ - Volunteers + Volunteer Management
diff --git a/app/main.go b/app/main.go index e9bf409..acc8497 100644 --- a/app/main.go +++ b/app/main.go @@ -113,6 +113,7 @@ func adminMiddleware(next http.HandlerFunc) http.HandlerFunc { func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc { return authMiddleware(func(w http.ResponseWriter, r *http.Request) { role, ok := r.Context().Value("user_role").(int) + fmt.Print(role) if !ok || (role != 3 && role != 2) { fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log http.Redirect(w, r, "/", http.StatusSeeOther) @@ -127,9 +128,11 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc { // Updated handler functions using the helper func schedualHandler(w http.ResponseWriter, r *http.Request) { role := r.Context().Value("user_role").(int) + // currentUserID := r.Context().Value("user_id").(int) data := createTemplateData("My Schedule", "schedual", role, true, nil) utils.Render(w, "Schedual/schedual.html", data) + } func HomeHandler(w http.ResponseWriter, r *http.Request) { @@ -169,14 +172,21 @@ func main() { http.HandleFunc("/volunteer/edit", adminMiddleware(handlers.EditVolunteerHandler)) http.HandleFunc("/team_builder", adminMiddleware(handlers.TeamBuilderHandler)) + http.HandleFunc("/team_builder/remove_volunteer", adminMiddleware(handlers.RemoveVolunteerHandler)) http.HandleFunc("/addresses", adminMiddleware(handlers.AddressHandler)) + http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler)) + + http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler)) //--- Volunteer-only routes http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler)) + http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler)) + http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler)) + log.Println("Server started on localhost:8080") - log.Fatal(http.ListenAndServe(":8080", nil)) + log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil)) } diff --git a/app/tmp/build-errors.log b/app/tmp/build-errors.log index 4d4fa3f..e01cd56 100644 --- a/app/tmp/build-errors.log +++ b/app/tmp/build-errors.log @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/app/tmp/main b/app/tmp/main index cfc40c6..1590ef2 100755 Binary files a/app/tmp/main and b/app/tmp/main differ diff --git a/app/uploads/3_1756244688677386000.webp b/app/uploads/3_1756244688677386000.webp new file mode 100644 index 0000000..bee86cb Binary files /dev/null and b/app/uploads/3_1756244688677386000.webp differ diff --git a/app/uploads/3_1756268241641256000.webp b/app/uploads/3_1756268241641256000.webp new file mode 100644 index 0000000..88e316d Binary files /dev/null and b/app/uploads/3_1756268241641256000.webp differ