admin core func done
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/uploads
|
/uploads
|
||||||
.env
|
.env
|
||||||
|
/Example_code
|
||||||
36
README.MD
36
README.MD
@@ -1,2 +1,38 @@
|
|||||||
# Poll-system
|
# 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)
|
||||||
|
);```
|
||||||
|
````
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -31,33 +31,37 @@ type PageNumber struct {
|
|||||||
IsCurrent bool
|
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) {
|
func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get pagination parameters from query string
|
// Get pagination parameters from query string
|
||||||
pageStr := r.URL.Query().Get("page")
|
pageStr := r.URL.Query().Get("page")
|
||||||
pageSizeStr := r.URL.Query().Get("pageSize")
|
pageSizeStr := r.URL.Query().Get("pageSize")
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
|
|
||||||
|
|
||||||
// Default values
|
|
||||||
page := 1
|
page := 1
|
||||||
pageSize := 20 // Default page size
|
pageSize := 20
|
||||||
|
|
||||||
// Parse page number
|
|
||||||
if pageStr != "" {
|
if pageStr != "" {
|
||||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||||
page = p
|
page = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse page size
|
|
||||||
if pageSizeStr != "" {
|
if pageSizeStr != "" {
|
||||||
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
|
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
|
||||||
pageSize = ps
|
pageSize = ps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate offset
|
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
// Get total count first
|
// Get total count
|
||||||
var totalRecords int
|
var totalRecords int
|
||||||
err := models.DB.QueryRow(`SELECT COUNT(*) FROM "address_database"`).Scan(&totalRecords)
|
err := models.DB.QueryRow(`SELECT COUNT(*) FROM "address_database"`).Scan(&totalRecords)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,27 +69,43 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate pagination info
|
|
||||||
totalPages := (totalRecords + pageSize - 1) / pageSize
|
totalPages := (totalRecords + pageSize - 1) / pageSize
|
||||||
if totalPages == 0 {
|
if totalPages == 0 {
|
||||||
totalPages = 1
|
totalPages = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure current page is within bounds
|
|
||||||
if page > totalPages {
|
if page > totalPages {
|
||||||
page = totalPages
|
page = totalPages
|
||||||
offset = (page - 1) * pageSize
|
offset = (page - 1) * pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get paginated results
|
// Query addresses with appointment + user info
|
||||||
rows, err := models.DB.Query(`
|
rows, err := models.DB.Query(`
|
||||||
SELECT address_id, address, street_name, street_type,
|
SELECT
|
||||||
street_quadrant, house_number, house_alpha, longitude,
|
a.address_id,
|
||||||
latitude, visited_validated
|
a.address,
|
||||||
FROM "address_database"
|
a.street_name,
|
||||||
WHERE street_quadrant = 'ne'
|
a.street_type,
|
||||||
ORDER BY address_id
|
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
|
LIMIT $1 OFFSET $2
|
||||||
`, pageSize, offset)
|
`, pageSize, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,9 +115,10 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var addresses []models.AddressDatabase
|
var addresses []AddressWithDetails
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var a models.AddressDatabase
|
var a AddressWithDetails
|
||||||
|
var houseAlpha string
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&a.AddressID,
|
&a.AddressID,
|
||||||
&a.Address,
|
&a.Address,
|
||||||
@@ -105,28 +126,68 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
&a.StreetType,
|
&a.StreetType,
|
||||||
&a.StreetQuadrant,
|
&a.StreetQuadrant,
|
||||||
&a.HouseNumber,
|
&a.HouseNumber,
|
||||||
&a.HouseAlpha,
|
&houseAlpha,
|
||||||
&a.Longitude,
|
&a.Longitude,
|
||||||
&a.Latitude,
|
&a.Latitude,
|
||||||
&a.VisitedValidated,
|
&a.VisitedValidated,
|
||||||
|
&a.CreatedAt,
|
||||||
|
&a.UpdatedAt,
|
||||||
|
&a.Assigned,
|
||||||
|
&a.UserName,
|
||||||
|
&a.UserEmail,
|
||||||
|
&a.AppointmentDate,
|
||||||
|
&a.AppointmentTime,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Scan error:", err)
|
log.Println("Scan error:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle nullable house_alpha
|
||||||
|
if houseAlpha != "" {
|
||||||
|
a.HouseAlpha = &houseAlpha
|
||||||
|
}
|
||||||
|
|
||||||
addresses = append(addresses, a)
|
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
|
startRecord := offset + 1
|
||||||
endRecord := offset + len(addresses)
|
endRecord := offset + len(addresses)
|
||||||
if totalRecords == 0 {
|
if totalRecords == 0 {
|
||||||
startRecord = 0
|
startRecord = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate page numbers for pagination controls
|
|
||||||
pageNumbers := generatePageNumbers(page, totalPages)
|
pageNumbers := generatePageNumbers(page, totalPages)
|
||||||
|
|
||||||
pagination := PaginationInfo{
|
pagination := PaginationInfo{
|
||||||
CurrentPage: page,
|
CurrentPage: page,
|
||||||
TotalPages: totalPages,
|
TotalPages: totalPages,
|
||||||
@@ -147,9 +208,11 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Title": "Addresses",
|
"Title": "Addresses",
|
||||||
"IsAuthenticated": true,
|
"IsAuthenticated": true,
|
||||||
"ShowAdminNav": true,
|
"ShowAdminNav": true,
|
||||||
"ActiveSection": "address", // Add this line
|
"ActiveSection": "address",
|
||||||
"Addresses": addresses,
|
"Addresses": addresses,
|
||||||
"Role": "admin",
|
"Users": users,
|
||||||
|
"UserName": username,
|
||||||
|
"Role": "admin",
|
||||||
"Pagination": pagination,
|
"Pagination": pagination,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -182,3 +245,83 @@ func generatePageNumbers(currentPage, totalPages int) []PageNumber {
|
|||||||
|
|
||||||
return pageNumbers
|
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)
|
||||||
|
}
|
||||||
83
app/internal/handlers/admin_apointment.go
Normal file
83
app/internal/handlers/admin_apointment.go
Normal file
@@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
currentAdminID := r.Context().Value("user_id").(int)
|
currentAdminID := r.Context().Value("user_id").(int)
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
role, _ := r.Context().Value("uesr_role").(int)
|
role, _ := r.Context().Value("uesr_role").(int)
|
||||||
|
|
||||||
var volunteerCount int
|
var volunteerCount int
|
||||||
@@ -75,6 +75,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"ValidatedCount": validatedCount,
|
"ValidatedCount": validatedCount,
|
||||||
"HousesLeftPercent": housesLeftPercent,
|
"HousesLeftPercent": housesLeftPercent,
|
||||||
"ShowAdminNav": true,
|
"ShowAdminNav": true,
|
||||||
|
"UserName": username,
|
||||||
"Role": role,
|
"Role": role,
|
||||||
"ActiveSection": "dashboard",
|
"ActiveSection": "dashboard",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import (
|
|||||||
func PostsHandler(w http.ResponseWriter, r *http.Request) {
|
func PostsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := r.Context().Value("user_id").(int)
|
userID := r.Context().Value("user_id").(int)
|
||||||
role := r.Context().Value("user_role").(int)
|
role := r.Context().Value("user_role").(int)
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
|
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
// Parse multipart form
|
// Parse multipart form
|
||||||
@@ -103,14 +105,18 @@ func PostsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CurrentUserID := models.GetCurrentUserID(w, r)
|
||||||
|
|
||||||
|
|
||||||
// GET request: fetch posts
|
// GET request: fetch posts
|
||||||
rows, err := models.DB.Query(`
|
rows, err := models.DB.Query(`
|
||||||
SELECT p.post_id, p.author_id, u.first_name || ' ' || u.last_name AS author_name,
|
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
|
p.content, COALESCE(p.image_url, '') as image_url, p.created_at
|
||||||
FROM post p
|
FROM post p
|
||||||
JOIN users u ON p.author_id = u.user_id
|
JOIN users u ON p.author_id = u.user_id
|
||||||
|
WHERE p.author_id = $1
|
||||||
ORDER BY p.created_at DESC
|
ORDER BY p.created_at DESC
|
||||||
`)
|
`, CurrentUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Database query error: %v\n", err)
|
fmt.Printf("Database query error: %v\n", err)
|
||||||
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
|
||||||
@@ -147,6 +153,7 @@ func PostsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"IsAuthenticated": true,
|
"IsAuthenticated": true,
|
||||||
"ShowAdminNav": showAdminNav,
|
"ShowAdminNav": showAdminNav,
|
||||||
"ShowVolunteerNav": showVolunteerNav,
|
"ShowVolunteerNav": showVolunteerNav,
|
||||||
|
"UserName": username,
|
||||||
"Posts": posts,
|
"Posts": posts,
|
||||||
"ActiveSection": "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)
|
// Helper function (add this to your main.go if not already there)
|
||||||
func getNavFlags(role int) (bool, bool) {
|
func getNavFlags(role int) (bool, bool) {
|
||||||
showAdminNav := role == 1 // Admin role
|
showAdminNav := role == 1 // Admin role
|
||||||
showVolunteerNav := role == 3 // Volunteer role
|
showVolunteerNav := role == 3 || role == 2
|
||||||
return showAdminNav, showVolunteerNav
|
return showAdminNav, showVolunteerNav
|
||||||
}
|
}
|
||||||
183
app/internal/handlers/admin_team_builder.go
Normal file
183
app/internal/handlers/admin_team_builder.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
func VolunteerHandler(w http.ResponseWriter, r *http.Request) {
|
func VolunteerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Replace this with actual session/jwt extraction
|
// TODO: Replace this with actual session/jwt extraction
|
||||||
currentAdminID := r.Context().Value("user_id").(int)
|
currentAdminID := r.Context().Value("user_id").(int)
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
|
|
||||||
rows, err := models.DB.Query(`
|
rows, err := models.DB.Query(`
|
||||||
SELECT u.user_id, u.email, u.role_id, u.first_name, u.last_name, u.phone
|
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",
|
"Title": "Assigned Volunteers",
|
||||||
"IsAuthenticated": true,
|
"IsAuthenticated": true,
|
||||||
"ShowAdminNav": true,
|
"ShowAdminNav": true,
|
||||||
|
"UserName": username,
|
||||||
"Users": user,
|
"Users": user,
|
||||||
"ActiveSection": "volunteer",
|
"ActiveSection": "volunteer",
|
||||||
})
|
})
|
||||||
@@ -92,6 +95,28 @@ func EditVolunteerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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(`
|
_, err = models.DB.Exec(`
|
||||||
UPDATE "users"
|
UPDATE "users"
|
||||||
SET first_name = $1, last_name = $2, email = $3, phone = $4, role_id = $5
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"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) {
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
@@ -107,7 +101,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
if email == "" || password == "" {
|
if email == "" || password == "" {
|
||||||
renderLoginError(w, "Email and password are required")
|
http.Redirect(w, r, "/?error=EmailAndPasswordRequired", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +118,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Login failed for email %s: %v", email, err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +126,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Password verification failed for user ID %d", userID)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +134,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
tokenString, expirationTime, err := createJWTToken(userID, role)
|
tokenString, expirationTime, err := createJWTToken(userID, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("JWT token creation failed for user ID %d: %v", userID, err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +147,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
utils.Render(w, "register.html", map[string]interface{}{
|
utils.Render(w, "register.html", map[string]interface{}{
|
||||||
@@ -168,6 +163,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
phone := r.FormValue("phone")
|
phone := r.FormValue("phone")
|
||||||
role := r.FormValue("role")
|
role := r.FormValue("role")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
adminCode := r.FormValue("admin_code") // for volunteers
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
if firstName == "" || lastName == "" || email == "" || password == "" || role == "" {
|
if firstName == "" || lastName == "" || email == "" || password == "" || role == "" {
|
||||||
@@ -183,185 +179,66 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert user into database
|
// Convert role to int
|
||||||
_, err = models.DB.Exec(`
|
roleID, err := strconv.Atoi(role)
|
||||||
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)
|
|
||||||
|
|
||||||
if err != nil {
|
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.")
|
renderRegisterError(w, "Could not create account. Email might already be in use.")
|
||||||
return
|
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)
|
log.Printf("User registered successfully: %s %s (%s)", firstName, lastName, email)
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
clearSessionCookie(w)
|
clearSessionCookie(w)
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,10 +11,11 @@ import (
|
|||||||
func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Extract current user ID from session/jwt
|
// Extract current user ID from session/jwt
|
||||||
currentUserID := r.Context().Value("user_id").(int)
|
currentUserID := r.Context().Value("user_id").(int)
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
|
|
||||||
var user models.User
|
var user models.User
|
||||||
err := models.DB.QueryRow(`
|
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"
|
FROM "users"
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
`, currentUserID).Scan(
|
`, currentUserID).Scan(
|
||||||
@@ -26,6 +27,7 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
&user.RoleID,
|
&user.RoleID,
|
||||||
&user.CreatedAt,
|
&user.CreatedAt,
|
||||||
&user.UpdatedAt,
|
&user.UpdatedAt,
|
||||||
|
&user.AdminCode,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Profile query error:", err)
|
log.Println("Profile query error:", err)
|
||||||
@@ -41,8 +43,8 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
adminnav = true
|
adminnav = true
|
||||||
volunteernav = false
|
volunteernav = false
|
||||||
}else{
|
}else{
|
||||||
volunteernav = true
|
|
||||||
adminnav = false
|
adminnav = false
|
||||||
|
volunteernav = true
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.Render(w, "profile/profile.html", map[string]interface{}{
|
utils.Render(w, "profile/profile.html", map[string]interface{}{
|
||||||
@@ -50,6 +52,7 @@ func ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"IsAuthenticated": true,
|
"IsAuthenticated": true,
|
||||||
"ShowAdminNav": adminnav,
|
"ShowAdminNav": adminnav,
|
||||||
"ShowVolunteerNav": volunteernav,
|
"ShowVolunteerNav": volunteernav,
|
||||||
|
"UserName": username,
|
||||||
"User": user,
|
"User": user,
|
||||||
"ActiveSection": "profile",
|
"ActiveSection": "profile",
|
||||||
})
|
})
|
||||||
|
|||||||
80
app/internal/handlers/volunteer_address.go
Normal file
80
app/internal/handlers/volunteer_address.go
Normal file
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Get user info from context
|
// Get user info from context
|
||||||
role := r.Context().Value("user_role").(int)
|
role := r.Context().Value("user_role").(int)
|
||||||
|
CurrentUserID := models.GetCurrentUserID(w, r)
|
||||||
|
username,_ := models.GetCurrentUserName(r)
|
||||||
|
|
||||||
// Fetch posts from database
|
// Fetch posts from database
|
||||||
rows, err := models.DB.Query(`
|
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
|
p.content, COALESCE(p.image_url, '') as image_url, p.created_at
|
||||||
FROM post p
|
FROM post p
|
||||||
JOIN users u ON p.author_id = u.user_id
|
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
|
ORDER BY p.created_at DESC
|
||||||
`)
|
`,CurrentUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Database query error: %v\n", err)
|
fmt.Printf("Database query error: %v\n", err)
|
||||||
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
|
||||||
@@ -66,6 +70,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"IsAuthenticated": true,
|
"IsAuthenticated": true,
|
||||||
"ShowAdminNav": showAdminNav,
|
"ShowAdminNav": showAdminNav,
|
||||||
"ShowVolunteerNav": showVolunteerNav,
|
"ShowVolunteerNav": showVolunteerNav,
|
||||||
|
"UserName": username,
|
||||||
"Posts": posts,
|
"Posts": posts,
|
||||||
"ActiveSection": "posts",
|
"ActiveSection": "posts",
|
||||||
"IsVolunteer": true, // Flag to indicate this is volunteer view
|
"IsVolunteer": true, // Flag to indicate this is volunteer view
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID int
|
UserID int
|
||||||
Role int
|
Role int
|
||||||
@@ -38,6 +39,7 @@ type User struct {
|
|||||||
Phone string
|
Phone string
|
||||||
Password string
|
Password string
|
||||||
RoleID int
|
RoleID int
|
||||||
|
AdminCode string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,8 @@ type AddressDatabase struct {
|
|||||||
VisitedValidated bool
|
VisitedValidated bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
Assigned bool // <-- add this
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
@@ -2,27 +2,32 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var jwtKey = []byte("your-secret-key") //TODO: Move to env/config
|
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) {
|
func GetCurrentUserName(r *http.Request) (string, error) {
|
||||||
claims := &Claims{}
|
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) {
|
var currentUserName string
|
||||||
return jwtKey, nil
|
err := DB.QueryRow(`
|
||||||
})
|
SELECT first_name || ' ' || last_name
|
||||||
|
FROM users
|
||||||
|
WHERE user_id = $1
|
||||||
|
`, currentUserID).Scan(¤tUserName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.Valid {
|
return currentUserName, nil
|
||||||
return nil, fmt.Errorf("invalid token")
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
<!-- Top Navigation -->
|
<!-- Top Navigation -->
|
||||||
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
@@ -12,7 +11,6 @@
|
|||||||
<span class="text-sm font-medium"> Address Database </span>
|
<span class="text-sm font-medium"> Address Database </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Records Info -->
|
|
||||||
{{if .Pagination}}
|
{{if .Pagination}}
|
||||||
<div class="text-sm text-gray-600">
|
<div class="text-sm text-gray-600">
|
||||||
Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of
|
Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of
|
||||||
@@ -25,33 +23,20 @@
|
|||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
|
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<!-- Search -->
|
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="relative">
|
||||||
<div class="relative">
|
<i
|
||||||
<i
|
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"
|
||||||
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"
|
></i>
|
||||||
></i>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
placeholder="Search Addresses"
|
||||||
placeholder="Search Addresses"
|
class="w-full pl-8 pr-3 py-2 text-sm border border-gray-200 rounded bg-white"
|
||||||
class="w-full pl-8 pr-3 py-2 text-sm border border-gray-200 rounded bg-white"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
class="px-6 py-2 border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50 transition-colors rounded"
|
|
||||||
>
|
|
||||||
<i class="fas fa-upload mr-2"></i>Import Data
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
{{if .Pagination}}
|
{{if .Pagination}}
|
||||||
<div class="flex items-center gap-4 text-sm">
|
<div class="flex items-center gap-4 text-sm">
|
||||||
<!-- Page Size Selector -->
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="pageSize" class="text-gray-600">Per page:</label>
|
<label for="pageSize" class="text-gray-600">Per page:</label>
|
||||||
<select
|
<select
|
||||||
@@ -76,10 +61,7 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page Navigation -->
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Previous Button -->
|
|
||||||
<button
|
<button
|
||||||
onclick="goToPage({{.Pagination.PreviousPage}})"
|
onclick="goToPage({{.Pagination.PreviousPage}})"
|
||||||
{{if
|
{{if
|
||||||
@@ -89,13 +71,9 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<span class="px-2 text-gray-600"
|
||||||
<!-- Page Info -->
|
>{{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}</span
|
||||||
<span class="px-2 text-gray-600">
|
>
|
||||||
{{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Next Button -->
|
|
||||||
<button
|
<button
|
||||||
onclick="goToPage({{.Pagination.NextPage}})"
|
onclick="goToPage({{.Pagination.NextPage}})"
|
||||||
{{if
|
{{if
|
||||||
@@ -111,38 +89,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Wrapper -->
|
<!-- Table -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 overflow-x-auto overflow-y-auto bg-white border border-gray-100"
|
class="flex-1 overflow-x-auto overflow-y-auto bg-white border border-gray-100"
|
||||||
>
|
>
|
||||||
<table class="w-full divide-gray-200 text-sm table-auto">
|
<table class="w-full divide-gray-200 text-sm table-auto">
|
||||||
<!-- Table Head -->
|
|
||||||
<thead class="bg-gray-50 divide-gray-200 sticky top-0">
|
<thead class="bg-gray-50 divide-gray-200 sticky top-0">
|
||||||
<tr
|
<tr
|
||||||
class="text-left text-gray-700 font-medium border-b border-gray-200"
|
class="text-left text-gray-700 font-medium border-b border-gray-200"
|
||||||
>
|
>
|
||||||
<th class="px-4 py-3 whitespace-nowrap">ID</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">Address</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">Street</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">House #</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">Longitude</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">Latitude</th>
|
|
||||||
<th class="px-6 py-3 whitespace-nowrap">Validated</th>
|
<th class="px-6 py-3 whitespace-nowrap">Validated</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Address</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Cordinates</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Assigned User</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Appointment</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Assign</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<!-- Table Body -->
|
|
||||||
<tbody class="divide-y divide-gray-200">
|
<tbody class="divide-y divide-gray-200">
|
||||||
{{ range .Addresses }}
|
{{ range .Addresses }}
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
<td class="px-6 py-3 whitespace-nowrap">{{ .AddressID }}</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">{{ .Address }}</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">
|
|
||||||
{{ .StreetName }} {{ .StreetType }} {{ .StreetQuadrant }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">{{ .HouseNumber }}</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">{{ .Longitude }}</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">{{ .Latitude }}</td>
|
|
||||||
<td class="px-6 py-3 whitespace-nowrap">
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
{{ if .VisitedValidated }}
|
{{ if .VisitedValidated }}
|
||||||
<span
|
<span
|
||||||
@@ -158,10 +124,49 @@
|
|||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">{{ .Address }}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
({{ .Latitude }}, {{ .Longitude }})
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
{{ if .UserName }}{{ .UserName }}<br /><span
|
||||||
|
class="text-xs text-gray-500"
|
||||||
|
>{{ .UserEmail }}</span
|
||||||
|
>{{ else }}<span class="text-gray-400">Unassigned</span>{{ end }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
{{ if .AppointmentDate }} {{ .AppointmentDate }} {{ .AppointmentTime
|
||||||
|
}} {{ else }}
|
||||||
|
<span class="text-gray-400">No appointment</span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
{{ if .Assigned }}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 bg-gray-400 text-white text-sm cursor-not-allowed"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Assigned
|
||||||
|
</button>
|
||||||
|
{{ else }}
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
|
||||||
|
onclick="openAssignModal({{ .AddressID }})"
|
||||||
|
>
|
||||||
|
Assign
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="px-6 py-8 text-center text-gray-500">
|
<td colspan="9" class="px-6 py-8 text-center text-gray-500">
|
||||||
No addresses found
|
No addresses found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -170,27 +175,64 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Assign Modal -->
|
||||||
{{if .Pagination}}
|
<div
|
||||||
<div class="bg-white border-t border-gray-200 px-6 py-3">
|
id="assignModal"
|
||||||
<div class="flex items-center justify-center">
|
class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"
|
||||||
<!-- Records Info -->
|
>
|
||||||
<div class="text-sm text-gray-600">
|
<div class="bg-white p-6 rounded shadow-lg w-96">
|
||||||
Showing {{.Pagination.StartRecord}}-{{.Pagination.EndRecord}} of
|
<h2 class="text-lg font-semibold mb-4">Assign Address</h2>
|
||||||
{{.Pagination.TotalRecords}} addresses
|
<form id="assignForm" method="POST" action="/assign_address">
|
||||||
</div>
|
<input type="hidden" name="address_id" id="modalAddressID" />
|
||||||
|
<label for="user_id" class="block text-sm font-medium mb-2"
|
||||||
|
>Select User</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="user_id"
|
||||||
|
id="user_id"
|
||||||
|
class="w-full border border-gray-300 px-3 py-2 mb-4 rounded"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">-- Select User --</option>
|
||||||
|
{{ range .Users }}
|
||||||
|
<option value="{{ .ID }}">{{ .Name }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="closeAssignModal()"
|
||||||
|
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Assign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function openAssignModal(addressID) {
|
||||||
|
document.getElementById("modalAddressID").value = addressID;
|
||||||
|
document.getElementById("assignModal").classList.remove("hidden");
|
||||||
|
document.getElementById("assignModal").classList.add("flex");
|
||||||
|
}
|
||||||
|
function closeAssignModal() {
|
||||||
|
document.getElementById("assignModal").classList.remove("flex");
|
||||||
|
document.getElementById("assignModal").classList.add("hidden");
|
||||||
|
}
|
||||||
function goToPage(page) {
|
function goToPage(page) {
|
||||||
var urlParams = new URLSearchParams(window.location.search);
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.set("page", page);
|
urlParams.set("page", page);
|
||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(pageSize) {
|
function changePageSize(pageSize) {
|
||||||
var urlParams = new URLSearchParams(window.location.search);
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.set("pageSize", pageSize);
|
urlParams.set("pageSize", pageSize);
|
||||||
|
|||||||
38
app/internal/templates/address_assigned.html
Normal file
38
app/internal/templates/address_assigned.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<div class="container mx-auto mt-6">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Assigned Addresses</h2>
|
||||||
|
|
||||||
|
<table class="min-w-full border border-gray-300 shadow-md">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-200">
|
||||||
|
<th class="px-4 py-2 border">ID</th>
|
||||||
|
<th class="px-4 py-2 border">Address</th>
|
||||||
|
<th class="px-4 py-2 border">Assigned</th>
|
||||||
|
<th class="px-4 py-2 border">Volunteer</th>
|
||||||
|
<th class="px-4 py-2 border">Email</th>
|
||||||
|
<th class="px-4 py-2 border">Phone</th>
|
||||||
|
<th class="px-4 py-2 border">Appointment Date</th>
|
||||||
|
<th class="px-4 py-2 border">Appointment Time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .AssignedList}}
|
||||||
|
<tr class="hover:bg-gray-100">
|
||||||
|
<td class="px-4 py-2 border">{{.AddressID}}</td>
|
||||||
|
<td class="px-4 py-2 border">
|
||||||
|
{{.Address}} {{.StreetName}} {{.StreetType}} {{.StreetQuadrant}}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 border">
|
||||||
|
{{if .Assigned}}✅ Yes{{else}}❌ No{{end}}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 border">{{.UserName}}</td>
|
||||||
|
<td class="px-4 py-2 border">{{.UserEmail}}</td>
|
||||||
|
<td class="px-4 py-2 border">{{.UserPhone}}</td>
|
||||||
|
<td class="px-4 py-2 border">{{.AppointmentDate}}</td>
|
||||||
|
<td class="px-4 py-2 border">{{.AppointmentTime}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
89
app/internal/templates/appointment.html
Normal file
89
app/internal/templates/appointment.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<!-- Top Navigation -->
|
||||||
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i
|
||||||
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-calendar-alt{{end}} text-green-600"
|
||||||
|
></i>
|
||||||
|
<span class="text-sm font-medium"> Appointments </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4 text-sm">
|
||||||
|
<div class="relative">
|
||||||
|
<i
|
||||||
|
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"
|
||||||
|
></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Appointments"
|
||||||
|
class="w-full pl-8 pr-3 py-2 text-sm border border-gray-200 rounded bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div
|
||||||
|
class="flex-1 overflow-x-auto overflow-y-auto bg-white border border-gray-100"
|
||||||
|
>
|
||||||
|
<table class="w-full divide-gray-200 text-sm table-auto">
|
||||||
|
<thead class="bg-gray-50 divide-gray-200 sticky top-0">
|
||||||
|
<tr
|
||||||
|
class="text-left text-gray-700 font-medium border-b border-gray-200"
|
||||||
|
>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Address</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Cordinated</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Appointment Date</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Appointment Time</th>
|
||||||
|
<th class="px-6 py-3 whitespace-nowrap">Poll Question</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{{ range .Appointments }}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">{{ .Address }}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/maps/search/?api=1&query={{ .Latitude }},{{ .Longitude }}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
({{ .Latitude }}, {{ .Longitude }})
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
{{ .AppointmentDate.Format "2006-01-02" }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
{{ .AppointmentTime.Format "15:04" }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Ask Poll
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ else }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-8 text-center text-gray-500">
|
||||||
|
No appointments found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
<body class="bg-white font-sans">
|
<body class="bg-white font-sans">
|
||||||
{{ if .IsAuthenticated }}
|
{{ if .IsAuthenticated }}
|
||||||
<!-- Authenticated User Interface -->
|
<!-- Authenticated User Interface -->
|
||||||
<div class="w-full h-screen bg-white overflow-hidden">
|
<div class="w-full h-screen bg-white overflow-hidden" x-data="{ sidebarOpen: false }">
|
||||||
<!-- Title Bar -->
|
<!-- Mobile Header -->
|
||||||
<div class="bg-gray-100 px-4 py-3 flex items-center justify-between border-b border-gray-200">
|
<div class="lg:hidden bg-gray-100 px-4 py-3 flex items-center justify-between border-b border-gray-200">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-5 h-5 bg-orange-500 rounded text-white text-xs flex items-center justify-center font-bold">
|
<div class="w-5 h-5 bg-orange-500 rounded text-white text-xs flex items-center justify-center font-bold">
|
||||||
L
|
L
|
||||||
@@ -25,6 +25,28 @@
|
|||||||
<span class="text-sm font-medium">Poll System</span>
|
<span class="text-sm font-medium">Poll System</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="sidebarOpen = !sidebarOpen" class="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i class="fas fa-bars text-gray-600"></i>
|
||||||
|
</button>
|
||||||
|
<span class="text-white font-semibold">Hi, {{.UserName}}</span>
|
||||||
|
|
||||||
|
<a href="/logout" class="p-2 hover:bg-gray-200 rounded">
|
||||||
|
<i class="fas fa-external-link-alt text-gray-500"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Title Bar -->
|
||||||
|
<div class="hidden lg:flex bg-gray-100 px-4 py-3 items-center justify-between border-b border-gray-200">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-5 h-5 bg-orange-500 rounded text-white text-xs flex items-center justify-center font-bold">
|
||||||
|
L
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">Poll System</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-white font-semibold">Hi, {{.UserName}}</span>
|
||||||
|
|
||||||
<a href="/logout" class="p-2 hover:bg-gray-100 rounded inline-block">
|
<a href="/logout" class="p-2 hover:bg-gray-100 rounded inline-block">
|
||||||
<i class="fas fa-external-link-alt text-gray-500"></i>
|
<i class="fas fa-external-link-alt text-gray-500"></i>
|
||||||
</a>
|
</a>
|
||||||
@@ -32,55 +54,108 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
|
<!-- Mobile Sidebar Overlay -->
|
||||||
|
<div x-show="sidebarOpen"
|
||||||
|
x-transition:enter="transition-opacity ease-linear duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition-opacity ease-linear duration-300"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 bg-gray-600 bg-opacity-75 z-20 lg:hidden"
|
||||||
|
@click="sidebarOpen = false">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="w-64 bg-gray-50 border-r border-gray-200 flex-shrink-0">
|
<div class="fixed inset-y-0 left-0 w-64 bg-gray-50 border-r border-gray-200 transform transition-transform duration-300 ease-in-out z-30 lg:relative lg:translate-x-0 lg:z-0"
|
||||||
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||||
|
x-show="sidebarOpen || window.innerWidth >= 1024"
|
||||||
|
x-transition:enter="transition ease-in-out duration-300 transform"
|
||||||
|
x-transition:enter-start="-translate-x-full"
|
||||||
|
x-transition:enter-end="translate-x-0"
|
||||||
|
x-transition:leave="transition ease-in-out duration-300 transform"
|
||||||
|
x-transition:leave-start="translate-x-0"
|
||||||
|
x-transition:leave-end="-translate-x-full">
|
||||||
|
|
||||||
|
<!-- Mobile Close Button -->
|
||||||
|
<div class="lg:hidden flex justify-between items-center p-4 border-b border-gray-200">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-5 h-5 bg-orange-500 rounded text-white text-xs flex items-center justify-center font-bold">
|
||||||
|
L
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium">Poll System</span>
|
||||||
|
</div>
|
||||||
|
<button @click="sidebarOpen = false" class="p-1 hover:bg-gray-200 rounded">
|
||||||
|
<i class="fas fa-times text-gray-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="p-3 space-y-4">
|
<div class="p-3 space-y-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{{ if .ShowAdminNav }}
|
{{ if .ShowAdminNav }}
|
||||||
<a href="/dashboard" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "dashboard"}}bg-gray-100{{end}}">
|
<a href="/dashboard"
|
||||||
<i class="fas fa-tachometer-alt text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "dashboard"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-chart-pie text-gray-400 mr-2"></i>
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/volunteers" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "volunteer"}}bg-gray-100{{end}}">
|
<a href="/volunteers"
|
||||||
<i class="fas fa-hands-helping text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "volunteer"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-users text-gray-400 mr-2"></i>
|
||||||
<span>Volunteers</span>
|
<span>Volunteers</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/team_builder" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "team_builder"}}bg-gray-100{{end}}">
|
<a href="/team_builder"
|
||||||
<i class="fas fa-hands-helping text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "team_builder"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-user-friends text-gray-400 mr-2"></i>
|
||||||
<span>Team Builder</span>
|
<span>Team Builder</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/addresses" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "address"}}bg-gray-100{{end}}">
|
<a href="/addresses"
|
||||||
<i class="fas fa-map-marker-alt text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "address"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-map-marked-alt text-gray-400 mr-2"></i>
|
||||||
<span>Addresses</span>
|
<span>Addresses</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/posts" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "post"}}bg-gray-100{{end}}">
|
<a href="/posts"
|
||||||
<i class="fas fa-chart-bar text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "post"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-blog text-gray-400 mr-2"></i>
|
||||||
<span>Posts</span>
|
<span>Posts</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/reports" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "report"}}bg-gray-100{{end}}">
|
<a href="/reports"
|
||||||
<i class="fas fa-chart-bar text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "report"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-table text-gray-400 mr-2"></i>
|
||||||
<span>Reports</span>
|
<span>Reports</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .ShowVolunteerNav }}
|
{{ if .ShowVolunteerNav }}
|
||||||
<a href="/volunteer/dashboard" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "dashboard"}}bg-gray-100{{end}}">
|
<a href="/volunteer/dashboard"
|
||||||
<i class="fas fa-tachometer-alt text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "dashboard"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-chart-pie text-gray-400 mr-2"></i>
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/volunteer/schedual" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "schedual"}}bg-gray-100{{end}}">
|
<a href="/volunteer/schedual"
|
||||||
<i class="fas fa-calendar text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "schedual"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-calendar-alt text-gray-400 mr-2"></i>
|
||||||
<span>My Schedule</span>
|
<span>My Schedule</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/volunteer/Addresses" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "schedual"}}bg-gray-100{{end}}">
|
<a href="/volunteer/Addresses"
|
||||||
<i class="fas fa-calendar text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "address"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-home text-gray-400 mr-2"></i>
|
||||||
<span>Assigned Address</span>
|
<span>Assigned Address</span>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<a href="/profile" class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "profile"}}bg-gray-100{{end}}">
|
<a href="/profile"
|
||||||
<i class="fas fa-user text-gray-400 mr-2"></i>
|
@click="sidebarOpen = false"
|
||||||
|
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "profile"}}bg-gray-100{{end}}">
|
||||||
|
<i class="fas fa-user-circle text-gray-400 mr-2"></i>
|
||||||
<span>Profile</span>
|
<span>Profile</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,15 +164,15 @@
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 flex flex-col overflow-hidden min-h-screen">
|
<div class="flex-1 flex flex-col overflow-hidden min-h-screen">
|
||||||
<div class="bg-white flex-1 overflow-auto pb-[60px]">
|
<div class="bg-white flex-1 overflow-auto pb-[60px]">
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<!-- Landing Page -->
|
<!-- Landing Page -->
|
||||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-gray-100">
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-gray-100" x-data="{ mobileMenuOpen: false }">
|
||||||
<!-- Fixed Navigation -->
|
<!-- Fixed Navigation -->
|
||||||
<nav class="fixed top-0 w-full bg-white/90 backdrop-blur-md shadow-sm border-b border-gray-200 z-40">
|
<nav class="fixed top-0 w-full bg-white/90 backdrop-blur-md shadow-sm border-b border-gray-200 z-40">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
@@ -108,37 +183,73 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="text-xl font-semibold text-gray-900">Poll System</span>
|
<span class="text-xl font-semibold text-gray-900">Poll System</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center gap-6">
|
<div class="hidden md:flex items-center gap-6">
|
||||||
<a href="#home" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">Home</a>
|
<a href="#home" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">Home</a>
|
||||||
<a href="#features" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">Features</a>
|
<a href="#features" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">Features</a>
|
||||||
<a href="#about" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">About</a>
|
<a href="#about" class="text-gray-600 hover:text-gray-900 font-medium transition-colors">About</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
|
<!-- Desktop Auth Buttons -->
|
||||||
|
<div class="hidden md:flex items-center gap-3">
|
||||||
<button onclick="openLoginModal()" class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium transition-colors">
|
<button onclick="openLoginModal()" class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium transition-colors">
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openRegisterModal()" class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 font-medium transition-colors">
|
<button onclick="openRegisterModal()" class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 font-medium transition-colors rounded">
|
||||||
Get Started
|
Get Started
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Button -->
|
||||||
|
<div class="md:hidden">
|
||||||
|
<button @click="mobileMenuOpen = !mobileMenuOpen" class="p-2 text-gray-600 hover:text-gray-900">
|
||||||
|
<i class="fas fa-bars text-xl" x-show="!mobileMenuOpen"></i>
|
||||||
|
<i class="fas fa-times text-xl" x-show="mobileMenuOpen"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu -->
|
||||||
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="md:hidden bg-white border-t border-gray-200">
|
||||||
|
<div class="px-4 py-4 space-y-3">
|
||||||
|
<a href="#home" @click="mobileMenuOpen = false" class="block text-gray-600 hover:text-gray-900 font-medium py-2">Home</a>
|
||||||
|
<a href="#features" @click="mobileMenuOpen = false" class="block text-gray-600 hover:text-gray-900 font-medium py-2">Features</a>
|
||||||
|
<a href="#about" @click="mobileMenuOpen = false" class="block text-gray-600 hover:text-gray-900 font-medium py-2">About</a>
|
||||||
|
<div class="border-t border-gray-200 pt-3 space-y-2">
|
||||||
|
<button onclick="openLoginModal(); document.querySelector('[x-data]').__x.$data.mobileMenuOpen = false" class="block w-full text-left px-4 py-2 text-gray-600 hover:bg-gray-100 rounded font-medium">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
<button onclick="openRegisterModal(); document.querySelector('[x-data]').__x.$data.mobileMenuOpen = false" class="block w-full px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 font-medium rounded">
|
||||||
|
Get Started
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section id="home" class="max-w-4xl mx-auto px-4 pt-32 pb-32 text-center">
|
<section id="home" class="max-w-4xl mx-auto px-4 pt-32 pb-32 text-center">
|
||||||
<h1 class="text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
||||||
Streamline Your<br>
|
Streamline Your<br>
|
||||||
<span class="text-blue-600">Polling Operations</span>
|
<span class="text-blue-600">Polling Operations</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
|
<p class="text-lg sm:text-xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||||
Manage volunteers, organize addresses, and track progress with our comprehensive polling system.
|
Manage volunteers, organize addresses, and track progress with our comprehensive polling system.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex flex-col sm:flex-row justify-center gap-4">
|
||||||
<button onclick="openRegisterModal()" class="px-8 py-3 bg-blue-600 text-white hover:bg-blue-700 font-semibold transition-colors">
|
<button onclick="openRegisterModal()" class="px-8 py-3 bg-blue-600 text-white hover:bg-blue-700 font-semibold transition-colors rounded">
|
||||||
Start Now
|
Start Now
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openLoginModal()" class="px-8 py-3 border border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold transition-colors">
|
<button onclick="openLoginModal()" class="px-8 py-3 border border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold transition-colors rounded">
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,26 +258,26 @@
|
|||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
<section id="features" class="max-w-6xl mx-auto px-4 py-20">
|
<section id="features" class="max-w-6xl mx-auto px-4 py-20">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h2 class="text-4xl font-bold text-gray-900 mb-4">Powerful Features</h2>
|
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-4">Powerful Features</h2>
|
||||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">Everything you need to manage your polling operations efficiently and effectively.</p>
|
<p class="text-lg sm:text-xl text-gray-600 max-w-3xl mx-auto">Everything you need to manage your polling operations efficiently and effectively.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-3 gap-8">
|
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
|
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow rounded-lg">
|
||||||
<div class="w-12 h-12 bg-blue-100 flex items-center justify-center mb-4">
|
<div class="w-12 h-12 bg-blue-100 rounded flex items-center justify-center mb-4">
|
||||||
<i class="fas fa-users text-blue-600 text-xl"></i>
|
<i class="fas fa-users text-blue-600 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Volunteer Management</h3>
|
<h3 class="text-xl font-semibold mb-3">Volunteer Management</h3>
|
||||||
<p class="text-gray-600">Organize and coordinate your volunteer teams efficiently with role-based access and scheduling.</p>
|
<p class="text-gray-600">Organize and coordinate your volunteer teams efficiently with role-based access and scheduling.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
|
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow rounded-lg">
|
||||||
<div class="w-12 h-12 bg-green-100 flex items-center justify-center mb-4">
|
<div class="w-12 h-12 bg-green-100 rounded flex items-center justify-center mb-4">
|
||||||
<i class="fas fa-map-marker-alt text-green-600 text-xl"></i>
|
<i class="fas fa-map-marker-alt text-green-600 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Address Tracking</h3>
|
<h3 class="text-xl font-semibold mb-3">Address Tracking</h3>
|
||||||
<p class="text-gray-600">Keep track of all polling locations and assignments with real-time updates and mapping.</p>
|
<p class="text-gray-600">Keep track of all polling locations and assignments with real-time updates and mapping.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
|
<div class="bg-white p-8 shadow-sm border border-gray-200 hover:shadow-md transition-shadow rounded-lg sm:col-span-2 lg:col-span-1">
|
||||||
<div class="w-12 h-12 bg-purple-100 flex items-center justify-center mb-4">
|
<div class="w-12 h-12 bg-purple-100 rounded flex items-center justify-center mb-4">
|
||||||
<i class="fas fa-chart-bar text-purple-600 text-xl"></i>
|
<i class="fas fa-chart-bar text-purple-600 text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-semibold mb-3">Real-time Reports</h3>
|
<h3 class="text-xl font-semibold mb-3">Real-time Reports</h3>
|
||||||
@@ -178,32 +289,32 @@
|
|||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<section id="about" class="bg-white py-20">
|
<section id="about" class="bg-white py-20">
|
||||||
<div class="max-w-6xl mx-auto px-4">
|
<div class="max-w-6xl mx-auto px-4">
|
||||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
<div class="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-4xl font-bold text-gray-900 mb-6">About Poll System</h2>
|
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-6">About Poll System</h2>
|
||||||
<p class="text-lg text-gray-600 mb-6">
|
<p class="text-base sm:text-lg text-gray-600 mb-6">
|
||||||
Poll System was created to simplify and streamline the complex process of managing polling operations.
|
Poll System was created to simplify and streamline the complex process of managing polling operations.
|
||||||
Our platform brings together volunteers, administrators, and team leaders in one unified system.
|
Our platform brings together volunteers, administrators, and team leaders in one unified system.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-lg text-gray-600 mb-8">
|
<p class="text-base sm:text-lg text-gray-600 mb-8">
|
||||||
With years of experience in civic technology, we understand the challenges faced by polling organizations.
|
With years of experience in civic technology, we understand the challenges faced by polling organizations.
|
||||||
Our solution provides the tools needed to coordinate effectively and ensure smooth operations.
|
Our solution provides the tools needed to coordinate effectively and ensure smooth operations.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-6 h-6 bg-green-100 flex items-center justify-center flex-shrink-0">
|
<div class="w-6 h-6 bg-green-100 rounded flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-gray-700">Streamlined volunteer coordination</span>
|
<span class="text-gray-700">Streamlined volunteer coordination</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-6 h-6 bg-green-100 flex items-center justify-center flex-shrink-0">
|
<div class="w-6 h-6 bg-green-100 rounded flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-gray-700">Real-time progress tracking</span>
|
<span class="text-gray-700">Real-time progress tracking</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-6 h-6 bg-green-100 flex items-center justify-center flex-shrink-0">
|
<div class="w-6 h-6 bg-green-100 rounded flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-gray-700">Comprehensive reporting tools</span>
|
<span class="text-gray-700">Comprehensive reporting tools</span>
|
||||||
@@ -211,7 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="bg-gradient-to-br from-blue-500 to-blue-700 p-8 text-white">
|
<div class="bg-gradient-to-br from-blue-500 to-blue-700 p-8 text-white rounded-lg">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i class="fas fa-users text-6xl mb-6 opacity-20"></i>
|
<i class="fas fa-users text-6xl mb-6 opacity-20"></i>
|
||||||
<h3 class="text-2xl font-bold mb-4">Trusted by Organizations</h3>
|
<h3 class="text-2xl font-bold mb-4">Trusted by Organizations</h3>
|
||||||
@@ -242,10 +353,10 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-gray-900 text-white py-12">
|
<footer class="bg-gray-900 text-white py-12">
|
||||||
<div class="max-w-6xl mx-auto px-4">
|
<div class="max-w-6xl mx-auto px-4">
|
||||||
<div class="grid md:grid-cols-4 gap-8">
|
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
<div class="md:col-span-2">
|
<div class="sm:col-span-2 lg:col-span-2">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<div class="w-8 h-8 bg-blue-600 text-white text-sm flex items-center justify-center font-bold">
|
<div class="w-8 h-8 bg-blue-600 text-white text-sm flex items-center justify-center font-bold rounded">
|
||||||
L
|
L
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xl font-semibold">Poll System</span>
|
<span class="text-xl font-semibold">Poll System</span>
|
||||||
@@ -255,13 +366,13 @@
|
|||||||
address tracking, and real-time reporting capabilities.
|
address tracking, and real-time reporting capabilities.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a href="#" class="w-10 h-10 bg-gray-800 flex items-center justify-center hover:bg-blue-600 transition-colors">
|
<a href="#" class="w-10 h-10 bg-gray-800 rounded flex items-center justify-center hover:bg-blue-600 transition-colors">
|
||||||
<i class="fab fa-twitter"></i>
|
<i class="fab fa-twitter"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="w-10 h-10 bg-gray-800 flex items-center justify-center hover:bg-blue-600 transition-colors">
|
<a href="#" class="w-10 h-10 bg-gray-800 rounded flex items-center justify-center hover:bg-blue-600 transition-colors">
|
||||||
<i class="fab fa-linkedin"></i>
|
<i class="fab fa-linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="w-10 h-10 bg-gray-800 flex items-center justify-center hover:bg-blue-600 transition-colors">
|
<a href="#" class="w-10 h-10 bg-gray-800 rounded flex items-center justify-center hover:bg-blue-600 transition-colors">
|
||||||
<i class="fab fa-github"></i>
|
<i class="fab fa-github"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,21 +404,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Login Modal -->
|
<!-- Login Modal -->
|
||||||
<div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4">
|
||||||
<div class="bg-white shadow-2xl max-w-4xl w-full mx-4 overflow-hidden">
|
<div class="bg-white shadow-2xl max-w-4xl w-full overflow-hidden rounded-lg">
|
||||||
<div class="flex min-h-[500px]">
|
<div class="flex flex-col lg:flex-row min-h-[500px]">
|
||||||
<!-- Left Side - Image -->
|
<!-- Left Side - Image -->
|
||||||
<div class="flex-1 bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center p-8">
|
<div class="flex-1 bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center p-8">
|
||||||
<div class="text-center text-white">
|
<div class="text-center text-white">
|
||||||
<i class="fas fa-chart-line text-6xl mb-6"></i>
|
<i class="fas fa-chart-line text-4xl sm:text-6xl mb-6"></i>
|
||||||
<h2 class="text-3xl font-bold mb-4">Welcome Back</h2>
|
<h2 class="text-2xl sm:text-3xl font-bold mb-4">Welcome Back</h2>
|
||||||
<p class="text-lg opacity-90">Continue managing your polling operations</p>
|
<p class="text-base sm:text-lg opacity-90">Continue managing your polling operations</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Side - Form -->
|
<!-- Right Side - Form -->
|
||||||
<div class="flex-1 p-8">
|
<div class="flex-1 p-6 sm:p-8">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h3 class="text-2xl font-bold text-gray-900">Sign In</h3>
|
<h3 class="text-xl sm:text-2xl font-bold text-gray-900">Sign In</h3>
|
||||||
<button onclick="closeLoginModal()" class="text-gray-400 hover:text-gray-600">
|
<button onclick="closeLoginModal()" class="text-gray-400 hover:text-gray-600">
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -316,14 +427,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<label for="login_email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
<label for="login_email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
||||||
<input type="email" name="email" id="login_email" required
|
<input type="email" name="email" id="login_email" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-4 py-3 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="login_password" class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
<label for="login_password" class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||||
<input type="password" name="password" id="login_password" required
|
<input type="password" name="password" id="login_password" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-4 py-3 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors">
|
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors rounded">
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -337,64 +448,74 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Register Modal -->
|
<!-- Register Modal -->
|
||||||
<div id="registerModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="registerModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 p-4">
|
||||||
<div class="bg-white shadow-2xl max-w-4xl w-full mx-4 overflow-hidden">
|
<div class="bg-white shadow-2xl max-w-4xl w-full overflow-hidden rounded-lg">
|
||||||
<div class="flex min-h-[600px]">
|
<div class="flex flex-col lg:flex-row min-h-[600px]">
|
||||||
<!-- Left Side - Image -->
|
<!-- Left Side - Image -->
|
||||||
<div class="flex-1 bg-gradient-to-br from-blue-600 to-blue-800 flex items-center justify-center p-8">
|
<div class="flex-1 bg-gradient-to-br from-blue-600 to-blue-800 flex items-center justify-center p-8">
|
||||||
<div class="text-center text-white">
|
<div class="text-center text-white">
|
||||||
<i class="fas fa-rocket text-6xl mb-6"></i>
|
<i class="fas fa-rocket text-4xl sm:text-6xl mb-6"></i>
|
||||||
<h2 class="text-3xl font-bold mb-4">Get Started</h2>
|
<h2 class="text-2xl sm:text-3xl font-bold mb-4">Get Started</h2>
|
||||||
<p class="text-lg opacity-90">Join our platform and streamline your operations</p>
|
<p class="text-base sm:text-lg opacity-90">Join our platform and streamline your operations</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Side - Form -->
|
<!-- Right Side - Form -->
|
||||||
<div class="flex-1 p-8 overflow-y-auto">
|
<div class="flex-1 p-6 sm:p-8 overflow-y-auto">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h3 class="text-2xl font-bold text-gray-900">Create Account</h3>
|
<h3 class="text-xl sm:text-2xl font-bold text-gray-900">Create Account</h3>
|
||||||
<button onclick="closeRegisterModal()" class="text-gray-400 hover:text-gray-600">
|
<button onclick="closeRegisterModal()" class="text-gray-400 hover:text-gray-600">
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/register" class="space-y-4">
|
<form method="POST" action="/register" class="space-y-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="first_name" class="block text-sm font-medium text-gray-700 mb-1">First Name</label>
|
<label for="first_name" class="block text-sm font-medium text-gray-700 mb-1">First Name</label>
|
||||||
<input type="text" name="first_name" id="first_name" required
|
<input type="text" name="first_name" id="first_name" required
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="last_name" class="block text-sm font-medium text-gray-700 mb-1">Last Name</label>
|
<label for="last_name" class="block text-sm font-medium text-gray-700 mb-1">Last Name</label>
|
||||||
<input type="text" name="last_name" id="last_name" required
|
<input type="text" name="last_name" id="last_name" required
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="register_email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
<label for="register_email" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||||
<input type="email" name="email" id="register_email" required
|
<input type="email" name="email" id="register_email" required
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">Phone</label>
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">Phone</label>
|
||||||
<input type="tel" name="phone" id="phone"
|
<input type="tel" name="phone" id="phone"
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
<label for="role" class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
||||||
<select name="role" id="role" required
|
<select name="role" id="role" required
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
onchange="toggleAdminCodeField()">
|
||||||
<option value="">Select role</option>
|
<option value="">Select role</option>
|
||||||
<option value="1">Admin</option>
|
<option value="1">Admin</option>
|
||||||
<option value="2">Team Leader</option>
|
<option value="2">Team Leader</option>
|
||||||
<option value="3">Volunteer</option>
|
<option value="3">Volunteer</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Code field (hidden by default) -->
|
||||||
|
<div id="adminCodeField" class="hidden">
|
||||||
|
<label for="admin_code" class="block text-sm font-medium text-gray-700 mb-1">Admin Code</label>
|
||||||
|
<input type="text" name="admin_code" id="admin_code"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="Enter your admin's code">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="register_password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
<label for="register_password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||||
<input type="password" name="password" id="register_password" required
|
<input type="password" name="password" id="register_password" required
|
||||||
class="w-full px-3 py-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-colors">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors mt-6">
|
<button type="submit" class="w-full bg-blue-600 text-white py-3 hover:bg-blue-700 font-medium transition-colors rounded mt-6">
|
||||||
Create Account
|
Create Account
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -406,9 +527,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Initialize Alpine.js data for mobile menu
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('sidebar', () => ({
|
||||||
|
open: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
// Smooth scrolling for navigation links
|
// Smooth scrolling for navigation links
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const links = document.querySelectorAll('a[href^="#"]');
|
const links = document.querySelectorAll('a[href^="#"]');
|
||||||
@@ -477,6 +606,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAdminCodeField() {
|
||||||
|
const role = document.getElementById("role").value;
|
||||||
|
const field = document.getElementById("adminCodeField");
|
||||||
|
field.classList.toggle("hidden", role !== "3"); // show only if Volunteer
|
||||||
|
}
|
||||||
|
|
||||||
// Handle escape key
|
// Handle escape key
|
||||||
document.addEventListener('keydown', function(event) {
|
document.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
@@ -484,7 +619,29 @@
|
|||||||
closeRegisterModal();
|
closeRegisterModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close mobile menu when clicking outside (for landing page)
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const mobileMenuButton = event.target.closest('[\\@click="mobileMenuOpen = !mobileMenuOpen"]');
|
||||||
|
const mobileMenu = event.target.closest('.md\\:hidden .bg-white');
|
||||||
|
|
||||||
|
if (!mobileMenuButton && !mobileMenu) {
|
||||||
|
// This will be handled by Alpine.js automatically
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window resize to ensure proper mobile behavior
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
// Close mobile menus on desktop
|
||||||
|
const sidebarComponent = document.querySelector('[x-data]');
|
||||||
|
if (sidebarComponent && sidebarComponent.__x) {
|
||||||
|
sidebarComponent.__x.$data.sidebarOpen = false;
|
||||||
|
sidebarComponent.__x.$data.mobileMenuOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{ end }}
|
{{end}}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="min-h-screen bg-gray-100">
|
<div class="min-h-screen bg-gray-100">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<!-- Top Navigation -->
|
||||||
<div class="max-w-2xl mx-auto px-4 py-4">
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Posts</h1>
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i
|
||||||
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||||||
|
></i>
|
||||||
|
<span class="text-sm font-medium">Volunteer Management</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,199 +1,372 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<!-- Header Bar -->
|
<!-- Top Navigation -->
|
||||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center gap-4">
|
||||||
<i class="fas fa-user-circle text-blue-600 text-xl"></i>
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-xl font-semibold text-gray-900">User Profile</h1>
|
<i
|
||||||
</div>
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||||||
<div class="flex items-center space-x-2 text-sm text-gray-600">
|
></i>
|
||||||
<i class="fas fa-shield-check text-blue-500"></i>
|
<span class="text-sm font-medium">Volunteer Management</span>
|
||||||
<span>Secure Profile Management</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Profile Overview Tile -->
|
<!-- Profile Info Section -->
|
||||||
<div class="bg-white border border-gray-200 mb-6">
|
<div class="mb-8">
|
||||||
<div class="bg-blue-50 border-b border-gray-200 px-6 py-4">
|
<div class="flex items-start space-x-4">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
<div class="flex-1">
|
||||||
<i class="fas fa-id-card text-blue-600 mr-3"></i>
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
Profile Overview
|
{{ .User.FirstName }} {{ .User.LastName }}
|
||||||
</h2>
|
</h3>
|
||||||
</div>
|
<p class="text-gray-600">{{ .User.Email }}</p>
|
||||||
<div class="p-6">
|
<div class="flex items-center mt-2 space-x-4">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<span
|
||||||
<!-- User Info -->
|
class="inline-flex items-center px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200"
|
||||||
<div class="lg:col-span-2">
|
>
|
||||||
<div class="flex items-start space-x-4">
|
<i class="fas fa-user-check mr-1"></i>
|
||||||
<div class="flex-1">
|
Active User
|
||||||
<h3 class="text-xl font-semibold text-gray-900">
|
</span>
|
||||||
{{ .User.FirstName }} {{ .User.LastName }}
|
<span class="text-gray-600">Signup Code:</span>
|
||||||
</h3>
|
<span class="font-mono text-gray-900">{{ .User.AdminCode }}</span>
|
||||||
<p class="text-gray-600">{{ .User.Email }}</p>
|
<span class="text-gray-600">User ID:</span>
|
||||||
<div class="flex items-center mt-2 space-x-4">
|
<span class="font-mono text-gray-900">{{ .User.UserID }}</span>
|
||||||
<span
|
<span class="text-gray-600">Role:</span>
|
||||||
class="inline-flex items-center px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200"
|
<span class="text-gray-900">
|
||||||
>
|
{{ if eq .User.RoleID 1 }}Admin {{ else if eq .User.RoleID 2
|
||||||
<i class="fas fa-user-check mr-1"></i>
|
}}Team Leader {{ else }}Volunteer {{ end }}
|
||||||
Active User
|
</span>
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-500"
|
|
||||||
>ID: {{ .User.UserID }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Quick Stats -->
|
|
||||||
<div class="bg-gray-50 border border-gray-200 p-4">
|
|
||||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">
|
|
||||||
Account Information
|
|
||||||
</h4>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-gray-600">User ID:</span>
|
|
||||||
<span class="font-mono text-gray-900">{{ .User.UserID }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-gray-600">Role:</span>
|
|
||||||
<span class="text-gray-900">{{ if eq .User.RoleID 1 }}Admin
|
|
||||||
{{ else if eq .User.RoleID 2 }}Team Leader
|
|
||||||
{{ else }}Volunteer
|
|
||||||
{{ end }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-gray-600">Status:</span>
|
|
||||||
<span class="text-green-600 font-medium">Active</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit Profile Form Tile -->
|
<!-- Edit Profile Section -->
|
||||||
<div class="bg-white border border-gray-200 mt-0 m-6">
|
<div class="border-t border-gray-200 pt-8">
|
||||||
<div class="bg-blue-50 border-b border-gray-200 px-6 py-4">
|
<h3 class="text-lg font-semibold text-gray-900 mb-6 flex items-center">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
<i class="fas fa-edit text-blue-600 mr-3"></i>
|
||||||
<i class="fas fa-edit text-blue-600 mr-3"></i>
|
Edit Profile Information
|
||||||
Edit Profile Information
|
</h3>
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
<!-- Edit Profile Form -->
|
||||||
<form method="post" action="/profile/update">
|
<form method="post" action="/profile/update">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- First Name -->
|
<!-- First Name -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
First Name <span class="text-red-500">*</span>
|
First Name <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first_name"
|
||||||
|
value="{{ .User.FirstName }}"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter first name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Name -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Last Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last_name"
|
||||||
|
value="{{ .User.LastName }}"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter last name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email (Read-only) -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Email Address
|
||||||
|
<span class="ml-2 text-xs bg-gray-200 text-gray-600 px-2 py-1"
|
||||||
|
>Read Only</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="email"
|
||||||
name="first_name"
|
name="email"
|
||||||
value="{{ .User.FirstName }}"
|
value="{{ .User.Email }}"
|
||||||
required
|
disabled
|
||||||
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
class="w-full px-4 py-3 border border-gray-300 bg-gray-100 text-gray-600 cursor-not-allowed"
|
||||||
placeholder="Enter first name"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||||
|
<i class="fas fa-lock text-gray-400"></i>
|
||||||
<!-- Last Name -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
|
||||||
Last Name <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="last_name"
|
|
||||||
value="{{ .User.LastName }}"
|
|
||||||
required
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
|
||||||
placeholder="Enter last name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Email (Read-only) -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
|
||||||
Email Address
|
|
||||||
<span class="ml-2 text-xs bg-gray-200 text-gray-600 px-2 py-1"
|
|
||||||
>Read Only</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value="{{ .User.Email }}"
|
|
||||||
disabled
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 bg-gray-100 text-gray-600 cursor-not-allowed"
|
|
||||||
/>
|
|
||||||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
||||||
<i class="fas fa-lock text-gray-400"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
|
||||||
Contact system administrator to change email
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Phone -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
|
||||||
Phone Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
name="phone"
|
|
||||||
value="{{ .User.Phone }}"
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
|
||||||
placeholder="Enter phone number"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
Contact system administrator to change email
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Phone -->
|
||||||
<div
|
<div>
|
||||||
class="mt-8 pt-6 border-t border-gray-200 flex justify-between items-center"
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
>
|
Phone Number
|
||||||
<div class="flex items-center text-sm text-gray-500">
|
</label>
|
||||||
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
|
<input
|
||||||
Changes will be applied immediately after saving
|
type="tel"
|
||||||
</div>
|
name="phone"
|
||||||
<div class="flex space-x-3">
|
value="{{ .User.Phone }}"
|
||||||
<button
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
type="button"
|
placeholder="Enter phone number"
|
||||||
onclick="window.history.back()"
|
/>
|
||||||
class="px-6 py-2 border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times mr-2"></i>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-save mr-2"></i>
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Profile Form Actions -->
|
||||||
|
<div
|
||||||
|
class="mt-8 pt-6 border-t border-gray-200 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
|
||||||
|
Changes will be applied immediately after saving
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="window.history.back()"
|
||||||
|
class="px-6 py-2 border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times mr-2"></i>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Settings Section -->
|
||||||
|
<div class="border-t border-gray-200 pt-8 mt-8">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-6 flex items-center">
|
||||||
|
<i class="fas fa-cog text-blue-600 mr-3"></i>
|
||||||
|
Configuration Settings
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form method="post" action="/profile/settings" id="settingsForm">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Add New Setting -->
|
||||||
|
<div class="bg-white border border-gray-200 p-6">
|
||||||
|
<h4 class="text-md font-semibold text-gray-800 mb-4">
|
||||||
|
Add New Setting
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Setting Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="newSettingName"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter setting name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Setting Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="newSettingValue"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter setting value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="addSetting()"
|
||||||
|
class="w-full px-6 py-3 bg-green-600 text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus mr-2"></i>
|
||||||
|
Add Setting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Settings -->
|
||||||
|
<div class="bg-white border border-gray-200 p-6">
|
||||||
|
<h4 class="text-md font-semibold text-gray-800 mb-4">
|
||||||
|
Current Settings
|
||||||
|
</h4>
|
||||||
|
<div id="settingsList" class="space-y-3">
|
||||||
|
<!-- Settings will be dynamically added here -->
|
||||||
|
<div class="text-gray-500 text-sm" id="noSettingsMessage">
|
||||||
|
No settings configured yet. Add your first setting above.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Form Actions -->
|
||||||
|
<div
|
||||||
|
class="mt-8 pt-6 border-t border-gray-200 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
|
||||||
|
Settings are applied immediately when added or removed
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="clearAllSettings()"
|
||||||
|
class="px-6 py-2 border border-red-300 text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash mr-2"></i>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let settings = [];
|
||||||
|
|
||||||
|
function addSetting() {
|
||||||
|
const nameInput = document.getElementById("newSettingName");
|
||||||
|
const valueInput = document.getElementById("newSettingValue");
|
||||||
|
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const value = valueInput.value.trim();
|
||||||
|
|
||||||
|
if (!name || !value) {
|
||||||
|
alert("Please enter both setting name and value");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if setting already exists
|
||||||
|
const existingIndex = settings.findIndex(
|
||||||
|
(s) => s.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Update existing setting
|
||||||
|
settings[existingIndex].value = value;
|
||||||
|
} else {
|
||||||
|
// Add new setting
|
||||||
|
settings.push({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear inputs
|
||||||
|
nameInput.value = "";
|
||||||
|
valueInput.value = "";
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
displaySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSetting(index) {
|
||||||
|
settings.splice(index, 1);
|
||||||
|
displaySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySettings() {
|
||||||
|
const settingsList = document.getElementById("settingsList");
|
||||||
|
const noSettingsMessage = document.getElementById("noSettingsMessage");
|
||||||
|
|
||||||
|
if (settings.length === 0) {
|
||||||
|
noSettingsMessage.style.display = "block";
|
||||||
|
settingsList.innerHTML =
|
||||||
|
'<div class="text-gray-500 text-sm" id="noSettingsMessage">No settings configured yet. Add your first setting above.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
noSettingsMessage.style.display = "none";
|
||||||
|
|
||||||
|
settingsList.innerHTML = settings
|
||||||
|
.map(
|
||||||
|
(setting, index) => `
|
||||||
|
<div class="flex items-center justify-between p-4 border border-gray-200 bg-gray-50">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="font-semibold text-gray-900">${setting.name}:</span>
|
||||||
|
<span class="text-gray-700">${setting.value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="removeSetting(${index})"
|
||||||
|
class="px-3 py-1 text-red-600 hover:text-red-800 focus:outline-none"
|
||||||
|
title="Remove setting"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllSettings() {
|
||||||
|
if (settings.length === 0) return;
|
||||||
|
|
||||||
|
if (confirm("Are you sure you want to clear all settings?")) {
|
||||||
|
settings = [];
|
||||||
|
displaySettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to add setting
|
||||||
|
document
|
||||||
|
.getElementById("newSettingName")
|
||||||
|
.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById("newSettingValue").focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("newSettingValue")
|
||||||
|
.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addSetting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
document
|
||||||
|
.getElementById("settingsForm")
|
||||||
|
.addEventListener("submit", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Here you would typically send the settings to your server
|
||||||
|
console.log("Saving settings:", settings);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert("Settings saved successfully!");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Professional square corner design */
|
/* Professional square corner design */
|
||||||
* {
|
* {
|
||||||
@@ -216,23 +389,21 @@
|
|||||||
box-shadow: 0 0 0 2px #3b82f6;
|
box-shadow: 0 0 0 2px #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects for tiles */
|
/* Hover effects */
|
||||||
.hover\:bg-blue-50:hover {
|
.hover\:bg-gray-50:hover {
|
||||||
background-color: #eff6ff;
|
background-color: #f9fafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:border-blue-500:hover {
|
.hover\:bg-blue-700:hover {
|
||||||
border-color: #3b82f6;
|
background-color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Professional table-like layout */
|
.hover\:bg-green-700:hover {
|
||||||
.grid {
|
background-color: #15803d;
|
||||||
display: grid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure full width usage */
|
.hover\:bg-red-50:hover {
|
||||||
.min-h-screen {
|
background-color: #fef2f2;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Professional button styling */
|
/* Professional button styling */
|
||||||
@@ -260,6 +431,10 @@
|
|||||||
background-color: #2563eb;
|
background-color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-green-600 {
|
||||||
|
background-color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.lg\:grid-cols-2 {
|
.lg\:grid-cols-2 {
|
||||||
|
|||||||
@@ -1,45 +1,512 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h2>Edit Volunteer</h2>
|
<div class="min-h-screen bg-gray-50">
|
||||||
<form method="POST" action="/volunteer/edit">
|
<!-- Header Bar -->
|
||||||
<input type="hidden" name="user_id" value="{{.Volunteer.UserID}}" />
|
|
||||||
|
|
||||||
<label>First Name:</label>
|
<!-- Top Navigation -->
|
||||||
<input type="text" name="first_name" value="{{.Volunteer.FirstName}}" /><br />
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i
|
||||||
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||||||
|
></i>
|
||||||
|
<span class="text-sm font-medium">Volunteer Management</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>Last Name:</label>
|
<!-- Main Content -->
|
||||||
<input type="text" name="last_name" value="{{.Volunteer.LastName}}" /><br />
|
<div class="p-6">
|
||||||
|
<!-- Volunteer Info Section -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-start space-x-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">
|
||||||
|
{{ .Volunteer.FirstName }} {{ .Volunteer.LastName }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600">{{ .Volunteer.Email }}</p>
|
||||||
|
<div class="flex items-center mt-2 space-x-4">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-check mr-1"></i>
|
||||||
|
Volunteer
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-600">User ID:</span>
|
||||||
|
<span class="font-mono text-gray-900">{{ .Volunteer.UserID }}</span>
|
||||||
|
<span class="text-gray-600">Current Role:</span>
|
||||||
|
<span class="text-gray-900">
|
||||||
|
{{ if eq .Volunteer.RoleID 2 }}Team Leader{{ else }}Volunteer{{
|
||||||
|
end }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>Email:</label>
|
<!-- Edit Volunteer Section -->
|
||||||
<input type="email" name="email" value="{{.Volunteer.Email}}" /><br />
|
<div class="border-t border-gray-200 pt-8">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-6 flex items-center">
|
||||||
|
<i class="fas fa-edit text-blue-600 mr-3"></i>
|
||||||
|
Edit Volunteer Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
<label>Phone:</label>
|
<!-- Edit Volunteer Form -->
|
||||||
<input type="text" name="phone" value="{{.Volunteer.Phone}}" /><br />
|
<form method="POST" action="/volunteer/edit">
|
||||||
|
<input type="hidden" name="user_id" value="{{.Volunteer.UserID}}" />
|
||||||
|
|
||||||
<label for="role_id">Role</label><br />
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<select name="role_id" id="role_id" required>
|
<!-- First Name -->
|
||||||
<option value="">--Select Role--</option>
|
<div>
|
||||||
<option
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
type="number"
|
First Name <span class="text-red-500">*</span>
|
||||||
value="3"
|
</label>
|
||||||
{{if
|
<input
|
||||||
eq
|
type="text"
|
||||||
.Volunteer.RoleID
|
name="first_name"
|
||||||
3}}selected{{end}}
|
value="{{.Volunteer.FirstName}}"
|
||||||
>
|
required
|
||||||
Volunteer
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
</option>
|
placeholder="Enter first name"
|
||||||
<option
|
/>
|
||||||
type="number"
|
</div>
|
||||||
value="2"
|
|
||||||
{{if
|
|
||||||
eq
|
|
||||||
.Volunteer.RoleID
|
|
||||||
2}}selected{{end}}
|
|
||||||
>
|
|
||||||
Team Leader
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit">Save</button>
|
<!-- Last Name -->
|
||||||
</form>
|
<div>
|
||||||
{{end}}
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Last Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last_name"
|
||||||
|
value="{{.Volunteer.LastName}}"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter last name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Email Address <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value="{{.Volunteer.Email}}"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phone -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
value="{{.Volunteer.Phone}}"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Selection -->
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Role Assignment <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="role_id"
|
||||||
|
id="role_id"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
>
|
||||||
|
<option value="">--Select Role--</option>
|
||||||
|
<option value="3" {{if eq .Volunteer.RoleID 3}}selected{{end}}>
|
||||||
|
Volunteer
|
||||||
|
</option>
|
||||||
|
<option value="2" {{if eq .Volunteer.RoleID 2}}selected{{end}}>
|
||||||
|
Team Leader
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
Team Leaders can manage volunteers and access additional features
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div
|
||||||
|
class="mt-8 pt-6 border-t border-gray-200 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
|
||||||
|
Changes will be applied immediately after saving
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="window.history.back()"
|
||||||
|
class="px-6 py-2 border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times mr-2"></i>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Settings Section -->
|
||||||
|
<div class="border-t border-gray-200 pt-8 mt-8">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-6 flex items-center">
|
||||||
|
<i class="fas fa-cog text-blue-600 mr-3"></i>
|
||||||
|
Volunteer Settings
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="/volunteer/settings"
|
||||||
|
id="volunteerSettingsForm"
|
||||||
|
>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Add New Setting -->
|
||||||
|
<div class="bg-white border border-gray-200 p-6">
|
||||||
|
<h4 class="text-md font-semibold text-gray-800 mb-4">
|
||||||
|
Add New Setting
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Setting Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="newVolunteerSettingName"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter setting name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Setting Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="newVolunteerSettingValue"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
placeholder="Enter setting value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="addVolunteerSetting()"
|
||||||
|
class="w-full px-6 py-3 bg-green-600 text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus mr-2"></i>
|
||||||
|
Add Setting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Settings -->
|
||||||
|
<div class="bg-white border border-gray-200 p-6">
|
||||||
|
<h4 class="text-md font-semibold text-gray-800 mb-4">
|
||||||
|
Current Settings
|
||||||
|
</h4>
|
||||||
|
<div id="volunteerSettingsList" class="space-y-3">
|
||||||
|
<!-- Settings will be dynamically added here -->
|
||||||
|
<div
|
||||||
|
class="text-gray-500 text-sm"
|
||||||
|
id="noVolunteerSettingsMessage"
|
||||||
|
>
|
||||||
|
No settings configured for this volunteer yet. Add settings
|
||||||
|
above.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Form Actions -->
|
||||||
|
<div
|
||||||
|
class="mt-8 pt-6 border-t border-gray-200 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center text-sm text-gray-500">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
|
||||||
|
Settings are specific to this volunteer and applied immediately
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="clearAllVolunteerSettings()"
|
||||||
|
class="px-6 py-2 border border-red-300 text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash mr-2"></i>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save mr-2"></i>
|
||||||
|
Save Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let volunteerSettings = [];
|
||||||
|
|
||||||
|
function addVolunteerSetting() {
|
||||||
|
const nameInput = document.getElementById("newVolunteerSettingName");
|
||||||
|
const valueInput = document.getElementById("newVolunteerSettingValue");
|
||||||
|
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const value = valueInput.value.trim();
|
||||||
|
|
||||||
|
if (!name || !value) {
|
||||||
|
alert("Please enter both setting name and value");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if setting already exists
|
||||||
|
const existingIndex = volunteerSettings.findIndex(
|
||||||
|
(s) => s.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Update existing setting
|
||||||
|
volunteerSettings[existingIndex].value = value;
|
||||||
|
} else {
|
||||||
|
// Add new setting
|
||||||
|
volunteerSettings.push({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear inputs
|
||||||
|
nameInput.value = "";
|
||||||
|
valueInput.value = "";
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
displayVolunteerSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeVolunteerSetting(index) {
|
||||||
|
volunteerSettings.splice(index, 1);
|
||||||
|
displayVolunteerSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayVolunteerSettings() {
|
||||||
|
const settingsList = document.getElementById("volunteerSettingsList");
|
||||||
|
const noSettingsMessage = document.getElementById(
|
||||||
|
"noVolunteerSettingsMessage"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (volunteerSettings.length === 0) {
|
||||||
|
settingsList.innerHTML =
|
||||||
|
'<div class="text-gray-500 text-sm" id="noVolunteerSettingsMessage">No settings configured for this volunteer yet. Add settings above.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsList.innerHTML = volunteerSettings
|
||||||
|
.map(
|
||||||
|
(setting, index) => `
|
||||||
|
<div class="flex items-center justify-between p-4 border border-gray-200 bg-gray-50">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="font-semibold text-gray-900">${setting.name}:</span>
|
||||||
|
<span class="text-gray-700">${setting.value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="removeVolunteerSetting(${index})"
|
||||||
|
class="px-3 py-1 text-red-600 hover:text-red-800 focus:outline-none"
|
||||||
|
title="Remove setting"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllVolunteerSettings() {
|
||||||
|
if (volunteerSettings.length === 0) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
confirm("Are you sure you want to clear all settings for this volunteer?")
|
||||||
|
) {
|
||||||
|
volunteerSettings = [];
|
||||||
|
displayVolunteerSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Enter key to add setting
|
||||||
|
document
|
||||||
|
.getElementById("newVolunteerSettingName")
|
||||||
|
.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById("newVolunteerSettingValue").focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById("newVolunteerSettingValue")
|
||||||
|
.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addVolunteerSetting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Volunteer Settings form submission handler
|
||||||
|
document
|
||||||
|
.getElementById("volunteerSettingsForm")
|
||||||
|
.addEventListener("submit", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Here you would typically send the settings to your server
|
||||||
|
console.log("Saving volunteer settings:", volunteerSettings);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert("Volunteer settings saved successfully!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form validation for main volunteer form
|
||||||
|
document
|
||||||
|
.querySelector('form[action="/volunteer/edit"]')
|
||||||
|
.addEventListener("submit", function (e) {
|
||||||
|
const firstName = document
|
||||||
|
.querySelector('input[name="first_name"]')
|
||||||
|
.value.trim();
|
||||||
|
const lastName = document
|
||||||
|
.querySelector('input[name="last_name"]')
|
||||||
|
.value.trim();
|
||||||
|
const email = document.querySelector('input[name="email"]').value.trim();
|
||||||
|
const roleId = document.querySelector('select[name="role_id"]').value;
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email || !roleId) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert("Please fill in all required fields.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert("Please enter a valid email address.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Professional square corner design */
|
||||||
|
* {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean transitions */
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
.transition-colors {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus states with blue accent */
|
||||||
|
input:focus,
|
||||||
|
select:focus {
|
||||||
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
box-shadow: 0 0 0 2px #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.hover\:bg-gray-50:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-blue-50:hover {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-blue-700:hover {
|
||||||
|
background-color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-orange-50:hover {
|
||||||
|
background-color: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-red-50:hover {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-green-700:hover {
|
||||||
|
background-color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Professional button styling */
|
||||||
|
button {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicators */
|
||||||
|
.bg-blue-100 {
|
||||||
|
background-color: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-800 {
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select styling */
|
||||||
|
select {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1.5em 1.5em;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.lg\:grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg\:grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg\:col-span-2 {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ end }}
|
||||||
|
|||||||
@@ -1,36 +1,114 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="p-6 space-y-6">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<h1 class="text-2xl font-bold mb-4">Team Builder</h1>
|
<!-- Top Navigation -->
|
||||||
|
<div class="bg-white border-b border-gray-200 px-6 py-3">
|
||||||
{{range .TeamLeads}}
|
<div class="flex items-center justify-between">
|
||||||
<div class="mb-4 p-4 bg-white rounded shadow">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex justify-between items-center">
|
<i
|
||||||
<span class="font-bold">{{.Name}}</span>
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||||||
<form action="/team_builderx" method="POST" class="flex space-x-2">
|
></i>
|
||||||
<input type="hidden" name="team_lead_id" value="{{.ID}}" />
|
<span class="text-sm font-medium">Volunteer Management</span>
|
||||||
<select name="volunteer_id" class="border px-2 py-1 rounded">
|
</div>
|
||||||
<option value="">--Select Volunteer--</option>
|
|
||||||
{{range $.UnassignedVolunteers}}
|
|
||||||
<option value="{{.ID}}">{{.Name}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="bg-blue-500 text-white px-3 py-1 rounded">
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- List of already assigned volunteers -->
|
<!-- Main Content -->
|
||||||
{{if .Volunteers}}
|
<div class="p-6 space-y-6">
|
||||||
<ul class="mt-2 list-disc list-inside">
|
{{range .TeamLeads}} {{ $teamLeadID := .ID }}
|
||||||
{{range .Volunteers}}
|
<!-- store team lead ID -->
|
||||||
<li>{{.Name}}</li>
|
|
||||||
{{end}}
|
<div class="bg-white border border-gray-200 shadow-sm">
|
||||||
</ul>
|
<!-- Team Lead Header -->
|
||||||
{{else}}
|
<div
|
||||||
<p class="text-gray-500 mt-1">No volunteers assigned yet.</p>
|
class="flex justify-between items-center px-4 py-3 border-b border-gray-200"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<i class="fas fa-user-tie text-blue-600"></i>
|
||||||
|
<span class="font-semibold text-gray-900">{{.Name}}</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
action="/team_builder"
|
||||||
|
method="POST"
|
||||||
|
class="flex items-center space-x-3"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="team_lead_id" value="{{.ID}}" />
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="volunteer_id"
|
||||||
|
class="px-3 py-2 border border-gray-300 bg-white text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">--Select Volunteer--</option>
|
||||||
|
{{range $.UnassignedVolunteers}}
|
||||||
|
<option value="{{.ID}}">{{.Name}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus mr-2"></i> Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assigned Volunteers -->
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
{{if .Volunteers}}
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{{range .Volunteers}}
|
||||||
|
<li
|
||||||
|
class="flex items-center justify-between text-gray-800 border-b border-gray-200 py-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fas fa-user text-gray-500"></i>
|
||||||
|
<span>{{.Name}}</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
action="/team_builder/remove_volunteer"
|
||||||
|
method="POST"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="team_lead_id"
|
||||||
|
value="{{ $teamLeadID }}"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="volunteer_id" value="{{.ID}}" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
aria-label="Remove {{.Name}}"
|
||||||
|
class="px-3 py-1 bg-red-600 text-white hover:bg-red-700 focus:outline-none focus:ring-1 focus:ring-red-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i> Remove
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<p class="text-gray-500 italic">No volunteers assigned yet.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Square corners across UI */
|
||||||
|
* {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<i
|
<i
|
||||||
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-users{{end}} text-blue-600"
|
||||||
></i>
|
></i>
|
||||||
<span class="text-sm font-medium">Volunteers</span>
|
<span class="text-sm font-medium">Volunteer Management</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
12
app/main.go
12
app/main.go
@@ -113,6 +113,7 @@ func adminMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return authMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
return authMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||||
role, ok := r.Context().Value("user_role").(int)
|
role, ok := r.Context().Value("user_role").(int)
|
||||||
|
fmt.Print(role)
|
||||||
if !ok || (role != 3 && role != 2) {
|
if !ok || (role != 3 && role != 2) {
|
||||||
fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log
|
fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
@@ -127,9 +128,11 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
// Updated handler functions using the helper
|
// Updated handler functions using the helper
|
||||||
func schedualHandler(w http.ResponseWriter, r *http.Request) {
|
func schedualHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
role := r.Context().Value("user_role").(int)
|
role := r.Context().Value("user_role").(int)
|
||||||
|
// currentUserID := r.Context().Value("user_id").(int)
|
||||||
|
|
||||||
data := createTemplateData("My Schedule", "schedual", role, true, nil)
|
data := createTemplateData("My Schedule", "schedual", role, true, nil)
|
||||||
utils.Render(w, "Schedual/schedual.html", data)
|
utils.Render(w, "Schedual/schedual.html", data)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -169,14 +172,21 @@ func main() {
|
|||||||
http.HandleFunc("/volunteer/edit", adminMiddleware(handlers.EditVolunteerHandler))
|
http.HandleFunc("/volunteer/edit", adminMiddleware(handlers.EditVolunteerHandler))
|
||||||
|
|
||||||
http.HandleFunc("/team_builder", adminMiddleware(handlers.TeamBuilderHandler))
|
http.HandleFunc("/team_builder", adminMiddleware(handlers.TeamBuilderHandler))
|
||||||
|
http.HandleFunc("/team_builder/remove_volunteer", adminMiddleware(handlers.RemoveVolunteerHandler))
|
||||||
|
|
||||||
http.HandleFunc("/addresses", adminMiddleware(handlers.AddressHandler))
|
http.HandleFunc("/addresses", adminMiddleware(handlers.AddressHandler))
|
||||||
|
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
|
||||||
|
|
||||||
|
|
||||||
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
|
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
|
||||||
|
|
||||||
//--- Volunteer-only routes
|
//--- Volunteer-only routes
|
||||||
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
|
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
|
||||||
|
http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
|
||||||
|
|
||||||
http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
|
http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
|
||||||
|
|
||||||
|
|
||||||
log.Println("Server started on localhost:8080")
|
log.Println("Server started on localhost:8080")
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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
|
||||||
BIN
app/tmp/main
BIN
app/tmp/main
Binary file not shown.
BIN
app/uploads/3_1756244688677386000.webp
Normal file
BIN
app/uploads/3_1756244688677386000.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
app/uploads/3_1756268241641256000.webp
Normal file
BIN
app/uploads/3_1756268241641256000.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Reference in New Issue
Block a user