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 }} +
+
+ + +
+
+ + + + + +
+
+ {{ range .Appointments }} +
+ +
+ Appointment + {{ if .HasPollResponse }} + + {{ .PollButtonText }} + + {{ else }} + + {{ .PollButtonText }} + + {{ end }} +
+ + +
+
+ {{ .Address }} +
+ +
+ Date + {{ .AppointmentDate.Format "2006-01-02" }} +
+ +
+ Time + {{ .AppointmentTime.Format "15:04" }} +
+
+
+ {{ else }} +
+
+ +
+

No appointments found

+

Try adjusting your search criteria.

+
+ {{ end }}
- - -
- - - - - - - - - - {{ range .Appointments }} - - - - - - {{ else }} - - - - {{ end }} - -
PollAddressAppointment
- {{ if .HasPollResponse }} - {{ .PollButtonText }} - {{ else }} - - {{ .PollButtonText }} - - {{ end }} - - - {{ .Address }} - - - ({{ .AppointmentDate.Format "2006-01-02" }} @ {{ - .AppointmentTime.Format "15:04" }}) -
- No appointments found -
-
+ + {{ 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" }}
- -
- -
-
-
-
-
+ +
+ - You +
+
+
+ You +
+
+
+ +
+
+ +
+
+ + +
+ +
+ + + + +
+ + +
+ {{range .Posts}} +
+ +
+
+
+ {{slice .AuthorName 0 1}} +
+
+
+

+ {{.AuthorName}} +

+

+ {{.CreatedAt.Format "Jan 2, 2006"}} +

+
+
+ + + {{if .ImageURL}} +
+ Post image +
+ {{end}} + + + {{if .Content}} +
+

+ {{.AuthorName}} + {{.Content}} +

+
+ {{end}} +
+ {{else}} +
+
+ + + +

+ No posts yet +

+

+ Be the first to share something with the community! +

+
-
-
- -
+ {{end}}
- -
-
- - -
- -
- - - -
- - -
- {{range .Posts}} -
- -
-
-
- {{slice .AuthorName 0 1}} -
-
-
-

{{.AuthorName}}

-

- {{.CreatedAt.Format "Jan 2, 2006"}} -

-
-
- - - {{if .ImageURL}} -
- Post image -
- {{end}} - - - {{if .Content}} -
-

- {{.AuthorName}} {{.Content}} -

-
- {{end}} -
- {{else}} -
-
- - - -

No posts yet

-

- Be the first to share something with the community! -

-
-
- {{end}} -
-
{{ 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}} - - {{end}} - - - - {{range .Result.Rows}} - - {{range .}} - - {{end}} - - {{end}} - -
{{formatColumnName .}}
{{.}}
-
+ {{if gt .Result.Count 0}} + + - - {{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

-
-
-
- - -
-
-
- -
-
-

Revenue

-

$47,392

-
-
-
- - -
-
-
- -
-
-

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 +
- - - - - + No volunteers found +
+
- -
- -

No volunteers found

-

- Try adjusting your search or filter criteria -

+ +
+
+ + + +
+
+ +
+

+ No volunteers found +

+

+ Try adjusting your search or filter criteria. +

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

+ Add Volunteer +

+
+ +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ +
-
+ + {{ 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

+
-
+
-
+
Appointments Today
- {{ .Statistics.AppointmentsToday }} + {{ .Statistics.AppointmentsToday }}
-
+
- Appointments Tomorrow + Appointments Tomorrow
- {{ .Statistics.AppointmentsTomorrow }} + {{ .Statistics.AppointmentsTomorrow }}
-
- +
+
This Week
- {{ .Statistics.AppointmentsThisWeek }} + {{ .Statistics.AppointmentsThisWeek }}
-
-
-

- Polling Progress -

- +
+
+

Polling Progress

+
-
+
-
+
Polls Completed
- {{ .Statistics.PollsCompleted }} + {{ .Statistics.PollsCompleted }}
-
+
Polls Remaining
- {{ .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 }} +

+ Post image +
+ {{ end }} + + + {{ if .Content }} +
+

+ {{ .AuthorName }} {{ .Content }}

-
- - - {{if .ImageURL}} -
- Post image -
- {{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

+ +
+ + +
+
+ + + + +
+ {{ range .Availability }} +
+
+ {{ .DayOfWeek }} +
+ + +
+
+
+ Start + {{ .StartTime }} +
+
+ End + {{ .EndTime }} +
+
+ {{ else }} +
No availability set yet
+ {{ end }} +
+
+
+
+ + + +
+ +
+

Add Availability

+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +{{ 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