diff --git a/README.MD b/README.MD
index 49bfa09..1f93719 100644
--- a/README.MD
+++ b/README.MD
@@ -1,16 +1,7 @@
# Poll-system
-- TODO: volunteer Available
-- TODO: Update assign address func to take into account availability
-
-
-- 18'' Metal Ruler
-- Sketching Material (dollaram)(sketch)
-- Adhesive (staples/dollaram)
-
-Done:
-- Exacto
-- A Large Cutting Mat
-- Black Construction Paper
-- And a lock for your locker
-- White Foam Core or Cardstock Paper (dollaram)
+- TODO: Add Error popups
+- TODO: Add A email notification when the user gets assigned a address
+- TODO: Add a view when the volunteer signed schedule of the other meembers as a heat map
+- TODO: square API to retrive the amount and also to show admin
+- TODO: Square payment page qr code
diff --git a/app/database/schema.sql b/app/database/schema.sql
index 533da22..22c1a0e 100644
--- a/app/database/schema.sql
+++ b/app/database/schema.sql
@@ -13,12 +13,7 @@ CREATE TABLE users (
first_name VARCHAR(100),
last_name VARCHAR(100),
email VARCHAR(150) UNIQUE NOT NULL,
- phone VARCHAR(20),
- password TEXT NOT NULL,
- role_id INT REFERENCES role(role_id) ON DELETE SET NULL,
- admin_code CHAR(6) UNIQUE,
- -- Admin settings combined here
- ward_settings TEXT,
+ phone VARCHAR(20) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
@@ -100,7 +95,7 @@ CREATE TABLE post (
);
CREATE TABLE availability (
- availability_id SERIAL PRIMARY KEY,
+ sched_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id) ON DELETE CASCADE,
day_of_week VARCHAR(20),
start_time TIME,
@@ -139,3 +134,6 @@ INSERT INTO role (role_id, name) VALUES
(2, 'team_lead'),
(3, 'volunteer')
ON CONFLICT DO NOTHING;
+
+ALTER TABLE availability
+ADD CONSTRAINT availability_user_day_unique UNIQUE (user_id, day_of_week);
diff --git a/app/internal/handlers/admin_addresses.go b/app/internal/handlers/admin_addresses.go
index c3a3bfe..966dc63 100644
--- a/app/internal/handlers/admin_addresses.go
+++ b/app/internal/handlers/admin_addresses.go
@@ -5,7 +5,6 @@ import (
"net/http"
"strconv"
"time"
- "fmt"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
@@ -36,19 +35,18 @@ type PageNumber struct {
// AddressWithDetails extends AddressDatabase with appointment and user info
type AddressWithDetails struct {
models.AddressDatabase
- UserID *int
- UserName string
- UserEmail string
- AppointmentDate string
- AppointmentTime string
+ UserID *int
+ UserName string
+ UserEmail string
+ AppointmentDate string
+ AppointmentTime string
}
func AddressHandler(w http.ResponseWriter, r *http.Request) {
// Get pagination parameters from query string
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("pageSize")
- username,_ := models.GetCurrentUserName(r)
-
+ username, _ := models.GetCurrentUserName(r)
page := 1
pageSize := 20
@@ -157,7 +155,7 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
}
// Get users associated with this admin
- currentAdminID := r.Context().Value("user_id").(int)
+ currentAdminID := models.GetCurrentUserID(w, r)
userRows, err := models.DB.Query(`
SELECT u.user_id, u.first_name || ' ' || u.last_name AS name
FROM users u
@@ -216,7 +214,7 @@ func AddressHandler(w http.ResponseWriter, r *http.Request) {
"ActiveSection": "address",
"Addresses": addresses,
"Users": users,
- "UserName": username,
+ "UserName": username,
"Role": "admin",
"Pagination": pagination,
})
@@ -267,13 +265,9 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
appointmentDate := r.FormValue("appointment_date")
startTime := r.FormValue("time")
- if userIDStr == "" || addressIDStr == "" {
- http.Error(w, "User ID and Address ID are required", http.StatusBadRequest)
- return
- }
-
- if appointmentDate == "" || startTime == "" {
- http.Error(w, "Appointment date and start time are required", http.StatusBadRequest)
+ // Basic validation
+ if userIDStr == "" || addressIDStr == "" || appointmentDate == "" || startTime == "" {
+ http.Error(w, "All fields are required", http.StatusBadRequest)
return
}
@@ -289,54 +283,30 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // Parse and validate the appointment date
+ // Parse date
parsedDate, err := time.Parse("2006-01-02", appointmentDate)
if err != nil {
http.Error(w, "Invalid appointment date format", http.StatusBadRequest)
return
}
- // Validate that the appointment date is not in the past
- today := time.Now().Truncate(24 * time.Hour)
- if parsedDate.Before(today) {
- http.Error(w, "Appointment date cannot be in the past", http.StatusBadRequest)
- return
- }
-
- // Parse and validate the start time
+ // Parse time
parsedTime, err := time.Parse("15:04", startTime)
- is_valid := ValidatedFreeTime(parsedDate, parsedTime, userID)
- if is_valid != true {
- http.Error(w, "User is not availabile", http.StatusBadRequest)
- return
- }else{
- fmt.Print("hello")
- }
-
- // 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)
+ http.Error(w, "Invalid appointment time format", http.StatusBadRequest)
return
}
- // Check if this address is already assigned to any user
+ // --- Availability Check (non-blocking) ---
+ isValid := ValidateAvailability(parsedDate, parsedTime, userID)
+ if !isValid {
+ // Instead of blocking, just log it
+ log.Printf("⚠️ User %d is not available on %s at %s", userID, appointmentDate, startTime)
+ }
+
+ // Check if this address is already assigned
var exists int
- err = models.DB.QueryRow(`
- SELECT COUNT(*) FROM appointment
- WHERE address_id = $1
- `, addressID).Scan(&exists)
+ 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)
@@ -347,38 +317,39 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // Check if the user already has an appointment at the same date and time
- var timeConflict int
+ // Check for conflicting appointment for the user
+ var conflict int
err = models.DB.QueryRow(`
SELECT COUNT(*) FROM appointment
- WHERE user_id = $1 AND appointment_date = $2 AND appointment_time = $3
- `, userID, appointmentDate, startTime).Scan(&timeConflict)
+ WHERE user_id = $1 AND appointment_date = $2 AND appointment_time = $3`,
+ userID, appointmentDate, startTime).Scan(&conflict)
if err != nil {
- log.Println("Time conflict check error:", err)
+ log.Println("Conflict check error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
- if timeConflict > 0 {
+ if conflict > 0 {
http.Error(w, "User already has an appointment at this date and time", http.StatusBadRequest)
return
}
- // Assign the address - create appointment with specific date and time
+ // Insert the appointment anyway
_, err = models.DB.Exec(`
INSERT INTO appointment (user_id, address_id, appointment_date, appointment_time, created_at, updated_at)
- VALUES ($1, $2, $3, $4, NOW(), NOW())
- `, userID, addressID, appointmentDate, startTime)
+ VALUES ($1, $2, $3, $4, NOW(), NOW())`,
+ userID, addressID, appointmentDate, startTime)
if err != nil {
- log.Println("Assignment error:", err)
+ log.Println("Insert appointment error:", err)
http.Error(w, "Failed to assign address", http.StatusInternalServerError)
return
}
- // Redirect back to addresses page with success
+ // ✅ Later: you can pass `UserNotAvailable: !isValid` to utils.Render instead of redirect
+ log.Printf("✅ Address %d assigned to user %d for %s at %s (Available: %v)",
+ addressID, userID, appointmentDate, startTime, isValid)
+
http.Redirect(w, r, "/addresses?success=assigned", http.StatusSeeOther)
}
-
-
func RemoveAssignedAddressHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/addresses", http.StatusSeeOther)
diff --git a/app/internal/handlers/volunteer_dashboard.go b/app/internal/handlers/volunteer_dashboard.go
index eadf60a..667f4b7 100644
--- a/app/internal/handlers/volunteer_dashboard.go
+++ b/app/internal/handlers/volunteer_dashboard.go
@@ -12,22 +12,22 @@ import (
)
type VolunteerStatistics struct {
- AppointmentsToday int
- AppointmentsTomorrow int
- AppointmentsThisWeek int
- TotalAppointments int
- PollsCompleted int
- PollsRemaining int
- LawnSignsRequested int
- BannerSignsRequested int
+ AppointmentsToday int
+ AppointmentsTomorrow int
+ AppointmentsThisWeek int
+ TotalAppointments int
+ PollsCompleted int
+ PollsRemaining int
+ LawnSignsRequested int
+ BannerSignsRequested int
PollCompletionPercent int
}
type TeamMate struct {
- UserID int
- FullName string
- Phone string
- Role string
- IsLead bool
+ UserID int
+ FullName string
+ Phone string
+ Role string
+ IsLead bool
}
// VolunteerPostsHandler - Dashboard view for volunteers with posts and statistics
@@ -79,7 +79,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
}
// Fetch teammates
- teammatesRows, err := models.DB.Query(`
+ teammatesRows, err := models.DB.Query(`
SELECT u.user_id,
u.first_name || ' ' || u.last_name AS full_name,
COALESCE(u.phone, '') AS phone,
@@ -94,21 +94,20 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
)
ORDER BY CASE WHEN r.name = 'team_lead' THEN 0 ELSE 1 END, u.first_name;
`, CurrentUserID, CurrentUserID)
- if err != nil {
- fmt.Printf("Database query error (teammates): %v\n", err)
- }
- defer teammatesRows.Close()
-
- var teammates []TeamMate
- for teammatesRows.Next() {
- var t TeamMate
- if err := teammatesRows.Scan(&t.UserID, &t.FullName, &t.Phone, &t.Role); err != nil {
- fmt.Printf("Row scan error (teammates): %v\n", err)
- continue
- }
- teammates = append(teammates, t)
- }
+ if err != nil {
+ fmt.Printf("Database query error (teammates): %v\n", err)
+ }
+ defer teammatesRows.Close()
+ var teammates []TeamMate
+ for teammatesRows.Next() {
+ var t TeamMate
+ if err := teammatesRows.Scan(&t.UserID, &t.FullName, &t.Phone, &t.Role); err != nil {
+ fmt.Printf("Row scan error (teammates): %v\n", err)
+ continue
+ }
+ teammates = append(teammates, t)
+ }
// Get volunteer statistics
stats, err := getVolunteerStatistics(CurrentUserID)
@@ -125,23 +124,23 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Volunteer viewing %d posts\n", len(posts))
utils.Render(w, "volunteer_dashboard.html", map[string]interface{}{
- "Title": "Volunteer Dashboard",
- "IsAuthenticated": true,
- "ShowAdminNav": showAdminNav,
- "ShowVolunteerNav": showVolunteerNav,
- "UserName": username,
- "Posts": posts,
- "Statistics": stats,
- "Teammates": teammates,
- "ActiveSection": "dashboard",
- "IsVolunteer": true,
+ "Title": "Volunteer Dashboard",
+ "IsAuthenticated": true,
+ "ShowAdminNav": showAdminNav,
+ "ShowVolunteerNav": showVolunteerNav,
+ "UserName": username,
+ "Posts": posts,
+ "Statistics": stats,
+ "Teammates": teammates,
+ "ActiveSection": "dashboard",
+ "IsVolunteer": true,
})
}
func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
stats := &VolunteerStatistics{}
today := time.Now().Format("2006-01-02")
-
+
// Get start of current week (Monday)
now := time.Now()
oneDayLater := now.Add(time.Hour * 12)
@@ -160,11 +159,10 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
fmt.Println("Week Start:", weekStart.Format("2006-01-02"))
fmt.Println("Week End:", weekEnd.Format("2006-01-02"))
-
// Appointments today
err := models.DB.QueryRow(`
- SELECT COUNT(*)
- FROM appointment
+ SELECT COUNT(*)
+ FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, today).Scan(&stats.AppointmentsToday)
if err != nil {
@@ -173,8 +171,8 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Appointments tomorrow
err = models.DB.QueryRow(`
- SELECT COUNT(*)
- FROM appointment
+ SELECT COUNT(*)
+ FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, oneDayLater).Scan(&stats.AppointmentsTomorrow)
if err != nil {
@@ -183,19 +181,18 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Appointments this week
err = models.DB.QueryRow(`
- SELECT COUNT(*)
- FROM appointment
+ SELECT COUNT(*)
+ FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
`, userID, weekStart, weekEnd).Scan(&stats.AppointmentsThisWeek)
if err != nil {
return nil, err
}
-
// Total appointments
err = models.DB.QueryRow(`
- SELECT COUNT(*)
- FROM appointment
+ SELECT COUNT(*)
+ FROM appointment
WHERE user_id = $1
`, userID).Scan(&stats.TotalAppointments)
if err != nil {
@@ -214,8 +211,12 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
}
// Polls remaining (appointments without poll responses)
+
stats.PollsRemaining = stats.TotalAppointments - stats.PollsCompleted
+ fmt.Print(stats.PollsRemaining)
+
+
// Calculate completion percentage
if stats.TotalAppointments > 0 {
stats.PollCompletionPercent = (stats.PollsCompleted * 100) / stats.TotalAppointments
@@ -225,7 +226,7 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Signs requested
err = models.DB.QueryRow(`
- SELECT
+ SELECT
COALESCE(SUM(pr.question3_lawn_signs), 0),
COALESCE(SUM(pr.question4_banner_signs), 0)
FROM poll p
@@ -237,4 +238,4 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
}
return stats, nil
-}
\ No newline at end of file
+}
diff --git a/app/internal/handlers/volunteer_poll.go b/app/internal/handlers/volunteer_poll.go
index e14f8fd..0f20193 100644
--- a/app/internal/handlers/volunteer_poll.go
+++ b/app/internal/handlers/volunteer_poll.go
@@ -14,7 +14,7 @@ import (
func PollHandler(w http.ResponseWriter, r *http.Request) {
username, _ := models.GetCurrentUserName(r)
-
+
if r.Method == http.MethodGet {
addressID := r.URL.Query().Get("address_id")
if addressID == "" {
@@ -27,7 +27,7 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
var userID int
fmt.Print(addressID, userID)
err := models.DB.QueryRow(`
- SELECT a.address, ap.user_id
+ SELECT a.address, ap.user_id
FROM appointment AS ap
JOIN address_database a ON a.address_id = ap.address_id
WHERE ap.address_id = $1
@@ -41,8 +41,8 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
// Check if poll already exists for this address
var pollID int
err = models.DB.QueryRow(`
- SELECT poll_id
- FROM poll
+ SELECT poll_id
+ FROM poll
WHERE address_id = $1 AND user_id = $2
`, addressID, userID).Scan(&pollID)
@@ -53,7 +53,7 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
VALUES ($1, $2, 'Door-to-Door Poll', 'Campaign polling questions', true)
RETURNING poll_id
`, userID, addressID).Scan(&pollID)
-
+
if err != nil {
log.Printf("Failed to create poll: %v", err)
http.Error(w, "Failed to create poll", http.StatusInternalServerError)
@@ -66,15 +66,15 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
}
utils.Render(w, "poll_form.html", map[string]interface{}{
- "Title": "Poll Questions",
- "IsAuthenticated": true,
- "ShowAdminNav": true,
- "UserName": username,
- "ActiveSection": "appointments",
- "PollID": pollID,
- "AddressID": addressID,
- "Address": address,
- "PageIcon": "fas fa-poll",
+ "Title": "Poll Questions",
+ "IsAuthenticated": true,
+ "ShowVolunteerNav": true,
+ "ActiveSection": "schedule",
+ "UserName": username,
+ "PollID": pollID,
+ "AddressID": addressID,
+ "Address": address,
+ "PageIcon": "fas fa-poll",
})
return
}
@@ -123,35 +123,35 @@ func PollHandler(w http.ResponseWriter, r *http.Request) {
// Insert poll response
_, err = models.DB.Exec(`
INSERT INTO poll_response (
- poll_id, respondent_postal_code, question1_voted_before,
+ poll_id, respondent_postal_code, question1_voted_before,
question2_vote_again, question3_lawn_signs, question4_banner_signs,
question5_thoughts, question6_donation_amount
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
- `, pollID, postalCode, question1VotedBefore, question2VoteAgain,
- question3LawnSigns, question4BannerSigns, question5Thoughts, question6donation)
+ `, pollID, postalCode, question1VotedBefore, question2VoteAgain,
+ question3LawnSigns, question4BannerSigns, question5Thoughts, question6donation)
if err != nil {
fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
return
- }else{
+ } else {
_, err := models.DB.Exec(`
- UPDATE address_database
- SET visited_validated = true
+ UPDATE address_database
+ SET visited_validated = true
WHERE address_id IN (
- SELECT address_id
- FROM poll
+ SELECT address_id
+ FROM poll
WHERE poll_id = $1
)
`, pollID)
if err != nil {
fmt.Print(err)
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
- return
- }
+ return
+ }
}
http.Redirect(w, r, "/volunteer/Addresses", http.StatusSeeOther)
}
-}
\ No newline at end of file
+}
diff --git a/app/internal/handlers/volunteer_schedual.go b/app/internal/handlers/volunteer_schedual.go
index 6deb0b6..30bcc07 100644
--- a/app/internal/handlers/volunteer_schedual.go
+++ b/app/internal/handlers/volunteer_schedual.go
@@ -1,34 +1,132 @@
package handlers
import (
- "fmt"
+ "database/sql"
+ "log"
+ "net/http"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
+ "github.com/patel-mann/poll-system/app/internal/utils"
)
-func ValidatedFreeTime(parsedDate time.Time, assignTime time.Time, userID int) (bool) {
- var startTime, endTime time.Time
+/////////////////////
+// Core Validation //
+/////////////////////
- dateOnly := parsedDate.Format("2006-01-02")
+// ValidateAvailability checks if a user is available at a specific time
+func ValidateAvailability(checkDate time.Time, checkTime time.Time, userID int) bool {
+ var startTime, endTime time.Time
+ day := checkDate.Format("2006-01-02")
- err := models.DB.QueryRow(
- `SELECT start_time, end_time
- FROM availability
- WHERE user_id = $1 AND day = $2`,
- userID, dateOnly,
- ).Scan(&startTime, &endTime)
+ err := models.DB.QueryRow(`
+ SELECT start_time, end_time
+ FROM availability
+ WHERE user_id = $1 AND day_of_week = $2`,
+ userID, day).Scan(&startTime, &endTime)
- if err != nil {
- fmt.Printf("Database query failed: %v\n", err)
- return false
- }
-
- if assignTime.After(startTime) && assignTime.Before(endTime) {
- return true
- }else{
- return false
+ if err != nil {
+ if err != sql.ErrNoRows {
+ log.Printf("DB error in ValidateAvailability: %v", err)
}
+ return false
+ }
- return false
+ return checkTime.After(startTime) && checkTime.Before(endTime)
+}
+
+////////////////////
+// Volunteer CRUD //
+////////////////////
+
+// View volunteer availability
+func VolunteerGetAvailabilityHandler(w http.ResponseWriter, r *http.Request) {
+ userID := models.GetCurrentUserID(w, r)
+ username, _ := models.GetCurrentUserName(r)
+ role, _ := r.Context().Value("user_role").(int)
+
+ rows, err := models.DB.Query(`
+ SELECT availability_id, day_of_week, start_time, end_time, created_at
+ FROM availability
+ WHERE user_id = $1
+ ORDER BY day_of_week DESC`, userID)
+ if err != nil {
+ log.Println("Error fetching availability:", err)
+ http.Error(w, "Database error", http.StatusInternalServerError)
+ return
+ }
+ defer rows.Close()
+
+ var availability []models.Availability
+ for rows.Next() {
+ var a models.Availability
+ if err := rows.Scan(&a.AvailabilityID, &a.DayOfWeek, &a.StartTime, &a.EndTime, &a.CreatedAt); err != nil {
+ log.Println("Row scan error:", err)
+ continue
+ }
+ availability = append(availability, a)
+ }
+
+ utils.Render(w, "volunteer_schedule.html", map[string]interface{}{
+ "Title": "My Schedule",
+ "ShowVolunteerNav": true,
+ "IsVolunteer": true,
+ "IsAuthenticated": true,
+ "Availability": availability,
+ "UserName": username,
+ "Role": role,
+ "ActiveSection": "schedule",
+ })
+}
+
+// Add or update schedule
+func VolunteerPostScheduleHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
+ return
+ }
+
+ userID := models.GetCurrentUserID(w, r)
+ day := r.FormValue("day")
+ start := r.FormValue("start_time")
+ end := r.FormValue("end_time")
+
+ if day == "" || start == "" || end == "" {
+ http.Error(w, "All fields required", http.StatusBadRequest)
+ return
+ }
+
+ _, err := models.DB.Exec(`
+ INSERT INTO availability (user_id, day_of_week, start_time, end_time, created_at)
+ VALUES ($1, $2, $3, $4, NOW())
+ ON CONFLICT (user_id, day_of_week)
+ DO UPDATE SET start_time = $3, end_time = $4`,
+ userID, day, start, end)
+ if err != nil {
+ log.Println("Insert availability error:", err)
+ http.Error(w, "Could not save schedule", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
+}
+
+// Delete schedule
+func VolunteerDeleteScheduleHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ userID := models.GetCurrentUserID(w, r)
+ idStr := r.FormValue("id")
+
+ _, err := models.DB.Exec(`DELETE FROM availability WHERE availability_id = $1 AND user_id = $2`, idStr, userID)
+ if err != nil {
+ log.Println("Delete availability error:", err)
+ http.Error(w, "Could not delete schedule", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/volunteer/schedule", http.StatusSeeOther)
}
diff --git a/app/internal/models/token.go b/app/internal/models/data_storage.go
similarity index 100%
rename from app/internal/models/token.go
rename to app/internal/models/data_storage.go
diff --git a/app/internal/models/email_updates.go b/app/internal/models/email_updates.go
new file mode 100644
index 0000000..406d928
--- /dev/null
+++ b/app/internal/models/email_updates.go
@@ -0,0 +1,10 @@
+package models
+
+import (
+ "fmt"
+)
+
+func EmailMessage(msg string){
+ fmt.Print("Message is not sent (func not implmented) %s", msg)
+ return
+}
diff --git a/app/internal/models/structs.go b/app/internal/models/structs.go
index 4e1e701..b96a1fa 100644
--- a/app/internal/models/structs.go
+++ b/app/internal/models/structs.go
@@ -9,49 +9,49 @@ import (
type Claims struct {
- UserID int
+ UserID int
Role int
jwt.RegisteredClaims
}
type TokenResponse struct {
- Token string
- User User
+ Token string
+ User User
}
type ErrorResponse struct {
- Error string
- Details []string
+ Error string
+ Details []string
}
type Role struct {
- RoleID int
- Name string
- CreatedAt time.Time
- UpdatedAt time.Time
+ RoleID int
+ Name string
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
type User struct {
- UserID int
- FirstName string
- LastName string
- Email string
- Phone string
- Password string
+ UserID int
+ FirstName string
+ LastName string
+ Email string
+ Phone string
+ Password string
RoleID int
- AdminCode *string
- CreatedAt time.Time
- UpdatedAt time.Time
+ AdminCode *string
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
type UserAddress struct {
- UserID int
- AddressLine1 string
- AddressLine2 string
- City string
- Province string
- Country string
- PostalCode string
+ UserID int
+ AddressLine1 string
+ AddressLine2 string
+ City string
+ Province string
+ Country string
+ PostalCode string
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -61,18 +61,18 @@ type UserAddress struct {
// =====================
type AddressDatabase struct {
- AddressID int
- Address string
- StreetName string
- StreetType string
- StreetQuadrant string
- HouseNumber string
- HouseAlpha *string
- Longitude float64
- Latitude float64
- VisitedValidated bool
- CreatedAt time.Time
- UpdatedAt time.Time
+ AddressID int
+ Address string
+ StreetName string
+ StreetType string
+ StreetQuadrant string
+ HouseNumber string
+ HouseAlpha *string
+ Longitude float64
+ Latitude float64
+ VisitedValidated bool
+ CreatedAt time.Time
+ UpdatedAt time.Time
Assigned bool // <-- add this
}
@@ -83,29 +83,29 @@ type AddressDatabase struct {
type Team struct {
- TeamID int
- TeamLeadID int
- VolunteerID int
+ TeamID int
+ TeamLeadID int
+ VolunteerID int
CreatedAt time.Time
UpdatedAt time.Time
}
type AdminVolunteer struct {
- AdminID int
- VolunteerID int
- IsActive bool
- CreatedAt time.Time
- UpdatedAt time.Time
+ AdminID int
+ VolunteerID int
+ IsActive bool
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
type Appointment struct {
- SchedID int
- UserID int
- AddressID int
+ SchedID int
+ UserID int
+ AddressID int
AppointmentDate time.Time
AppointmentTime time.Time
- CreatedAt time.Time
- UpdatedAt time.Time
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
// =====================
@@ -113,22 +113,22 @@ type Appointment struct {
// =====================
type Poll struct {
- PollID int
- AddressID int
- UserID int
- ResponseURL string
- AmountDonated float64
- CreatedAt time.Time
- UpdatedAt time.Time
+ PollID int
+ AddressID int
+ UserID int
+ ResponseURL string
+ AmountDonated float64
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
type PollResponse struct {
- ResponseID int
- PollID int
- Signage bool
- VotingChoice string
- DonationAmount float64
- CreatedAt time.Time
+ ResponseID int
+ PollID int
+ Signage bool
+ VotingChoice string
+ DonationAmount float64
+ CreatedAt time.Time
}
// =====================
@@ -146,11 +146,11 @@ type Post struct {
type Reaction struct {
- ReactionID int
- PostID int
- UserID int
- ReactionType string
- CreatedAt time.Time
+ ReactionID int
+ PostID int
+ UserID int
+ ReactionType string
+ CreatedAt time.Time
}
// =====================
@@ -159,11 +159,11 @@ type Reaction struct {
type Availability struct {
AvailabilityID int
- UserID int
- DayOfWeek string
- StartTime time.Time
- EndTime time.Time
- CreatedAt time.Time
+ UserID int
+ DayOfWeek string
+ StartTime time.Time
+ EndTime time.Time
+ CreatedAt time.Time
}
// =====================
@@ -171,10 +171,10 @@ type Availability struct {
// =====================
type ChatLink struct {
- ChatID int
- Platform string
- URL string
- UserID *int
- TeamID *int
- CreatedAt time.Time
+ ChatID int
+ Platform string
+ URL string
+ UserID *int
+ TeamID *int
+ CreatedAt time.Time
}
diff --git a/app/internal/templates/appointment.html b/app/internal/templates/appointment.html
index 52d71c8..e6739c1 100644
--- a/app/internal/templates/appointment.html
+++ b/app/internal/templates/appointment.html
@@ -1,75 +1,185 @@
{{ define "content" }}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ if .Pagination }}
+
+
+
+
+
+
+
+
+
+ {{.Pagination.CurrentPage}} / {{.Pagination.TotalPages}}
+
+
+
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+ | Poll |
+ Address |
+ Appointment |
+
+
+
+ {{ range .Appointments }}
+
+
+ |
+ {{ if .HasPollResponse }}
+ {{ .PollButtonText }}
+ {{ else }}
+
+ {{ .PollButtonText }}
+
+ {{ end }}
+ |
+
+
+
+
+ {{ .Address }}
+
+ |
+
+
+
+ ({{ .AppointmentDate.Format "2006-01-02" }} @ {{ .AppointmentTime.Format "15:04" }})
+ |
+
+ {{ else }}
+
+ | No appointments found |
+
+ {{ end }}
+
+
+
+
+
+
+
+ {{ range .Appointments }}
+
+
+
+
+
+
+
+ {{ .Address }}
+
+
+
+ Date
+ {{ .AppointmentDate.Format "2006-01-02" }}
+
+
+
+ Time
+ {{ .AppointmentTime.Format "15:04" }}
+
+
+
+ {{ else }}
+
+
+
+
+
No appointments found
+
Try adjusting your search criteria.
+
+ {{ end }}
-
-
-
-
-
-
- | Poll |
- Address |
- Appointment |
-
-
-
- {{ range .Appointments }}
-
- |
- {{ if .HasPollResponse }}
- {{ .PollButtonText }}
- {{ else }}
-
- {{ .PollButtonText }}
-
- {{ end }}
- |
-
-
- {{ .Address }}
-
- |
-
- ({{ .AppointmentDate.Format "2006-01-02" }} @ {{
- .AppointmentTime.Format "15:04" }})
- |
-
- {{ else }}
-
- |
- No appointments found
- |
-
- {{ end }}
-
-
-
+
+
{{ end }}
diff --git a/app/internal/templates/layout.html b/app/internal/templates/layout.html
index 96659e4..ebf11be 100644
--- a/app/internal/templates/layout.html
+++ b/app/internal/templates/layout.html
@@ -161,10 +161,14 @@
Dashboard
-
+
Assigned Address
+
+
+ Schedule
+
{{ end }}
diff --git a/app/internal/templates/posts.html b/app/internal/templates/posts.html
index dae93d3..18fd1fe 100644
--- a/app/internal/templates/posts.html
+++ b/app/internal/templates/posts.html
@@ -1,338 +1,344 @@
{{ define "content" }}
-
-
-
-
{{ end }}
diff --git a/app/internal/templates/reports.html b/app/internal/templates/reports.html
index c8e0794..7cb73b2 100644
--- a/app/internal/templates/reports.html
+++ b/app/internal/templates/reports.html
@@ -1,149 +1,241 @@
{{ define "content" }}
-
-
-
-
-
-
-
- {{if .Result}}
-
-
-
{{.Result.Count}} results
+
+
+
+
+
+
+
+
+ {{if .Result}}
+
+
+ {{.Result.Count}} results
+
{{end}}
-
+
{{if .Result}}
{{if .Result.Error}}
-
-
-
Report Error
-
{{.Result.Error}}
+
+
+
+
+
+
+
Report Error
+
{{.Result.Error}}
+
+
{{else}}
-
-
-
-
{{.ReportTitle}}
-
{{.ReportDescription}}
+
+
+
+
+
+
{{.ReportTitle}}
+
{{.ReportDescription}}
+
+
+ Generated: {{.GeneratedAt}}
+
+
-
Generated: {{.GeneratedAt}}
-
-
- {{if gt .Result.Count 0}}
-
-
-
-
- {{range .Result.Columns}}
- | {{formatColumnName .}} |
- {{end}}
-
-
-
- {{range .Result.Rows}}
-
- {{range .}}
- | {{.}} |
- {{end}}
-
- {{end}}
-
-
-
+ {{if gt .Result.Count 0}}
+
+
+
+
+
+ {{range .Result.Columns}}
+ |
+ {{formatColumnName .}}
+ |
+ {{end}}
+
+
+
+ {{range .Result.Rows}}
+
+ {{range .}}
+ | {{.}} |
+ {{end}}
+
+ {{end}}
+
+
+
-
- {{if .SummaryStats}}
-
-
Summary Statistics
-
- {{range .SummaryStats}}
-
-
{{.Label}}
-
{{.Value}}
+
+
+
+ {{range $rowIndex, $row := .Result.Rows}}
+
+
+
+
+
+ Record {{add $rowIndex 1}}
+
+
+
+
+
+ {{range $colIndex, $column := $.Result.Columns}}
+
+ {{formatColumnName $column}}:
+ {{index $row $colIndex}}
+
+ {{end}}
+
+
+ {{end}}
+
+
+
+
+ {{if .SummaryStats}}
+
+
+ Summary Statistics
+
+
+ {{range .SummaryStats}}
+
+
{{.Label}}
+
{{.Value}}
+
+ {{end}}
+
{{end}}
-
-
- {{end}}
- {{else}}
-
-
No results match your selected criteria
+ {{else}}
+
+
+
+
+
+
No Results Found
+
No results match your selected criteria. Try adjusting your filters or date range.
+
+ {{end}}
- {{end}}
{{end}}
{{else}}
-
-
Select a category and report to generate results
+
+
+
+
+
+
+
Generate Report
+
+ Select a category and report type above to generate comprehensive analytics and insights.
+
+
+
+
+
User Analytics
+
Track volunteer performance and participation rates
+
+
+
+
Location Insights
+
Analyze address coverage and geographic data
+
+
+
+
Schedule Reports
+
Review appointments and availability patterns
+
+
+
{{end}}
+
+
+
+
+
{{ end }}
diff --git a/app/internal/templates/schedual.html b/app/internal/templates/schedual.html
deleted file mode 100644
index 8c1e1d9..0000000
--- a/app/internal/templates/schedual.html
+++ /dev/null
@@ -1,215 +0,0 @@
-{{ define "content" }}
-
-
-
-
-
-
Interactive Dashboard
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Active Locations
-
-
24
-
-
-
-
-
-
-
-
-
-
-
-
Total Visitors
-
12,847
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Conversion Rate
-
3.2%
-
-
-
-
-
-
-
-
- Location Analytics
-
-
-
-
-
-
-
-
- Performance Metrics
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{{ end }}
diff --git a/app/internal/templates/volunteer.html b/app/internal/templates/volunteer.html
index 0e539fa..d29dd19 100644
--- a/app/internal/templates/volunteer.html
+++ b/app/internal/templates/volunteer.html
@@ -1,285 +1,681 @@
{{ define "content" }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Showing of
+ volunteers
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Showing of
- volunteers
-
-
-
-
-
-
-
-
-
+
+
-
-
- ID
-
- |
-
-
- First Name
-
- |
-
-
- Last Name
-
- |
-
-
- Email
-
- |
-
-
- Phone
-
- |
-
-
- Role
-
- |
-
Actions |
-
-
+
+
+
+
+
+ |
+
+ ID
+
+
+ |
+
+
+ Name
+
+
+ |
+
+
+ Contact
+
+
+ |
+
+
+ Role
+
+
+ |
+
+ Actions
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
-
-
-
-
- |
- |
- |
- |
- |
-
-
- |
-
-
- Edit
+
-
-
- |
-
-
-
-
+ No volunteers found
+
+
-
-
-
-
No volunteers found
-
- Try adjusting your search or filter criteria
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Email
+
+
+
+
+
+ Phone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No volunteers found
+
+
+ Try adjusting your search or filter criteria.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ end }}
diff --git a/app/internal/templates/volunteer_dashboard.html b/app/internal/templates/volunteer_dashboard.html
index 9030ac8..3106c13 100644
--- a/app/internal/templates/volunteer_dashboard.html
+++ b/app/internal/templates/volunteer_dashboard.html
@@ -3,132 +3,88 @@
-
-
+
+
+
+
-
-
-
- Today's Overview
-
-
+
+
+
Today's Overview
+
-
+
-
-
{{ .Statistics.AppointmentsToday }}
+
{{ .Statistics.AppointmentsToday }}
-
+
-
Appointments Tomorrow
+
Appointments Tomorrow
-
{{ .Statistics.AppointmentsTomorrow }}
+
{{ .Statistics.AppointmentsTomorrow }}
-
-
{{ .Statistics.AppointmentsThisWeek }}
+
{{ .Statistics.AppointmentsThisWeek }}
-
-
-
- Polling Progress
-
-
+
+
+
Polling Progress
+
-
+
-
-
{{ .Statistics.PollsCompleted }}
+
{{ .Statistics.PollsCompleted }}
-
-
{{ .Statistics.PollsRemaining }}
+
{{ .Statistics.PollsRemaining }}
{{ if gt .Statistics.TotalAppointments 0 }}
-
+
Progress
- {{ .Statistics.PollsCompleted }}/{{
- .Statistics.TotalAppointments }}
+ {{ .Statistics.PollsCompleted }}/{{ .Statistics.TotalAppointments }}
{{ end }}
@@ -137,29 +93,22 @@
-
-
+
+
Team Members
-
+
-
+
{{ range .Teammates }}
- {{ .FullName }} {{ if .IsLead }}
- {{ .Role }}
+ {{ .FullName }}
+ {{ if .IsLead }}
+ {{ .Role }}
{{ else }}
- {{ .Role }}
+ {{ .Role }}
{{ end }}
@@ -174,85 +123,52 @@
-
-
- {{ if .Posts }}{{range .Posts}}
-
-
-
-
-
-
- {{slice .AuthorName 0 1}}
+
+
+
+ {{ if .Posts }}
+ {{ range .Posts }}
+
+
+
+
+
+ {{ slice .AuthorName 0 1 }}
+
+
+
+
{{ .AuthorName }}
+
{{ .CreatedAt.Format "Jan 2, 2006" }}
-
-
{{.AuthorName}}
-
- {{.CreatedAt.Format "Jan 2, 2006"}}
+
+
+ {{ if .ImageURL }}
+
+

+
+ {{ end }}
+
+
+ {{ if .Content }}
+
+
+ {{ .AuthorName }} {{ .Content }}
-
-
-
- {{if .ImageURL}}
-
-

-
- {{end}}
-
-
- {{if .Content}}
-
-
- {{.AuthorName}} {{.Content}}
-
-
- {{end}}
-
- {{else}}
-
-
-
-
No posts yet
-
- Be the first to share something with the community!
-
-
-
- {{ end }} {{ else }}
-
-
-
-
+ {{ end }}
+
+ {{ end }}
+ {{ else }}
+
+
+
+
+
+
No posts yet
+
Be the first to share something with the community!
-
No posts yet
-
- Check back later for updates from your team
-
-
{{ end }}
diff --git a/app/internal/templates/volunteer_schedule.html b/app/internal/templates/volunteer_schedule.html
new file mode 100644
index 0000000..ab9c65d
--- /dev/null
+++ b/app/internal/templates/volunteer_schedule.html
@@ -0,0 +1,134 @@
+{{ define "content" }}
+
+
+
+
My Schedule
+
+
+
+
+
+
+
+
+
+
+
+ | Day |
+ Start Time |
+ End Time |
+ Actions |
+
+
+
+ {{ range .Availability }}
+
+ | {{ .DayOfWeek }} |
+ {{ .StartTime }} |
+ {{ .EndTime }} |
+
+
+ |
+
+ {{ else }}
+
+ |
+ No availability set yet
+ |
+
+ {{ end }}
+
+
+
+
+
+
+ {{ range .Availability }}
+
+
+ {{ .DayOfWeek }}
+
+
+
+ Start
+ {{ .StartTime }}
+
+
+ End
+ {{ .EndTime }}
+
+
+ {{ else }}
+
No availability set yet
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+{{ end }}
diff --git a/app/main.go b/app/main.go
index add7e03..28c1700 100644
--- a/app/main.go
+++ b/app/main.go
@@ -16,35 +16,6 @@ import (
_ "github.com/lib/pq" // use PostgreSQL
)
-// Helper function to determine navigation visibility based on role
-func getNavFlags(role int) (bool, bool, bool) {
- showAdminNav := role == 1 // Admin role
- showLeaderNav := role == 2 // Team Leader role
- showVolunteerNav := role == 3 // Volunteer role
- return showAdminNav, showVolunteerNav, showLeaderNav
-}
-
-// Helper function to create template data with proper nav flags
-func createTemplateData(title, activeSection string, role int, isAuthenticated bool, additionalData map[string]interface{}) map[string]interface{} {
- showAdminNav, showVolunteerNav, _ := getNavFlags(role)
-
- data := map[string]interface{}{
- "Title": title,
- "IsAuthenticated": isAuthenticated,
- "Role": role,
- "ShowAdminNav": showAdminNav,
- "ShowVolunteerNav": showVolunteerNav,
- "ActiveSection": activeSection,
- }
-
- // Add any additional data
- for key, value := range additionalData {
- data[key] = value
- }
-
- return data
-}
-
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
err := godotenv.Load()
if err != nil {
@@ -103,12 +74,6 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
})
}
-func schedualHandler(w http.ResponseWriter, r *http.Request) {
- role := r.Context().Value("user_role").(int)
- data := createTemplateData("My Schedule", "schedual", role, true, nil)
- utils.Render(w, "Schedual/schedual.html", data)
-}
-
func HomeHandler(w http.ResponseWriter, r *http.Request) {
utils.Render(w, "dashboard.html", map[string]interface{}{
"Title": "Admin Dashboard",
@@ -116,14 +81,15 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
"ActiveSection": "dashboard",
})
}
-
func main() {
models.InitDB()
-
+
+ models.EmailMessage("hellow")
+
// Static file servers
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
-
+
uploadsFs := http.FileServer(http.Dir("uploads"))
http.Handle("/uploads/", http.StripPrefix("/uploads/", uploadsFs))
@@ -143,31 +109,32 @@ func main() {
http.HandleFunc("/dashboard", adminMiddleware(handlers.AdminDashboardHandler))
http.HandleFunc("/volunteers", adminMiddleware(handlers.VolunteerHandler))
http.HandleFunc("/volunteer/edit", adminMiddleware(handlers.EditVolunteerHandler))
-
http.HandleFunc("/team_builder", adminMiddleware(handlers.TeamBuilderHandler))
http.HandleFunc("/team_builder/remove_volunteer", adminMiddleware(handlers.RemoveVolunteerHandler))
-
http.HandleFunc("/addresses", adminMiddleware(handlers.AddressHandler))
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
-
http.HandleFunc("/addresses/upload-csv", adminMiddleware(handlers.CSVUploadHandler))
http.HandleFunc("/reports", adminMiddleware(handlers.ReportsHandler))
-
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
+ // Assignment management routes (Admin only)
+ // API Routes
http.HandleFunc("/api/validated-addresses", handlers.GetValidatedAddressesHandler)
- http.HandleFunc("/api/validated-addresses/stats", handlers.GetValidatedAddressesStatsHandler)
-
+ http.HandleFunc("/api/validated-addresses/stats", handlers.GetValidatedAddressesStatsHandler)
- //--- Volunteer-only routes
+ //--- Volunteer-only routes
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
- http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
- http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
+ http.HandleFunc("/volunteer/addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
+
+ // Schedule/Availability routes (Volunteer only)
+ http.HandleFunc("/volunteer/schedule", volunteerMiddleware(handlers.VolunteerGetAvailabilityHandler))
+ http.HandleFunc("/volunteer/schedule/post", volunteerMiddleware(handlers.VolunteerPostScheduleHandler))
+ http.HandleFunc("/volunteer/schedule/delete", volunteerMiddleware(handlers.VolunteerDeleteScheduleHandler))
// Poll routes (volunteer only)
http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler))
-
+
log.Println("Server started on http://localhost:8080")
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
-}
\ No newline at end of file
+}
diff --git a/app/static/feature-mobile4.jpg b/app/static/feature-mobile4.jpg
deleted file mode 100644
index e1f9ff5..0000000
Binary files a/app/static/feature-mobile4.jpg and /dev/null differ