update to volunteer
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/uploads
|
||||
.env
|
||||
/tmp
|
||||
/Example_code
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/patel-mann/poll-system/app/internal/models"
|
||||
"github.com/patel-mann/poll-system/app/internal/utils"
|
||||
@@ -262,12 +263,19 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userIDStr := r.FormValue("user_id")
|
||||
addressIDStr := r.FormValue("address_id")
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(userIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
@@ -280,6 +288,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and validate the appointment 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
|
||||
_, err = time.Parse("15:04", startTime)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid start time format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the user exists and is associated with the current admin
|
||||
currentAdminID := r.Context().Value("user_id").(int)
|
||||
var userExists int
|
||||
@@ -314,11 +343,27 @@ func AssignAddressHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign the address - create appointment
|
||||
// Check if the user already has an appointment at the same date and time
|
||||
var timeConflict 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)
|
||||
if err != nil {
|
||||
log.Println("Time conflict check error:", err)
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if timeConflict > 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
|
||||
_, 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)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
`, userID, addressID, appointmentDate, startTime)
|
||||
if err != nil {
|
||||
log.Println("Assignment error:", err)
|
||||
http.Error(w, "Failed to assign address", http.StatusInternalServerError)
|
||||
|
||||
@@ -33,7 +33,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 2. Total donations from polls
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT COALESCE(SUM(amount_donated), 0)
|
||||
FROM poll;
|
||||
FROM poll_responce;
|
||||
`).Scan(&totalDonations)
|
||||
if err != nil {
|
||||
log.Println("Donation query error:", err)
|
||||
|
||||
@@ -8,73 +8,103 @@ import (
|
||||
"github.com/patel-mann/poll-system/app/internal/utils"
|
||||
)
|
||||
|
||||
|
||||
func VolunteerAppointmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Fetch appointments joined with address info
|
||||
// Fetch appointments joined with address info
|
||||
currentUserID := models.GetCurrentUserID(w, r)
|
||||
username, _ := models.GetCurrentUserName(r)
|
||||
|
||||
currentUserID := models.GetCurrentUserID(w,r)
|
||||
username,_ := models.GetCurrentUserName(r)
|
||||
rows, err := models.DB.Query(`
|
||||
SELECT
|
||||
a.sched_id,
|
||||
a.user_id,
|
||||
ad.address_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()
|
||||
|
||||
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
|
||||
AddressID int
|
||||
Address string
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
AppointmentDate time.Time
|
||||
AppointmentTime time.Time
|
||||
HasPollResponse bool // New field to track poll status
|
||||
PollButtonText string // New field for button text
|
||||
PollButtonClass string // New field for button styling
|
||||
}
|
||||
|
||||
// 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.AddressID, &a.Address, &a.Latitude, &a.Longitude, &a.AppointmentDate, &a.AppointmentTime); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Check if poll response exists for this address
|
||||
var pollResponseExists bool
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM poll p
|
||||
JOIN poll_response pr ON p.poll_id = pr.poll_id
|
||||
WHERE p.address_id = $1 AND p.user_id = $2
|
||||
)
|
||||
`, a.AddressID, currentUserID).Scan(&pollResponseExists)
|
||||
|
||||
if err != nil {
|
||||
// If there's an error checking, default to no response
|
||||
pollResponseExists = false
|
||||
}
|
||||
|
||||
// Set button properties based on poll response status
|
||||
a.HasPollResponse = pollResponseExists
|
||||
if pollResponseExists {
|
||||
a.PollButtonText = "Poll Taken"
|
||||
a.PollButtonClass = "px-3 py-1 bg-green-600 text-white text-sm rounded cursor-not-allowed"
|
||||
} else {
|
||||
a.PollButtonText = "Ask Poll"
|
||||
a.PollButtonClass = "px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700 rounded"
|
||||
}
|
||||
|
||||
appointments = append(appointments, a)
|
||||
}
|
||||
|
||||
role := r.Context().Value("user_role").(int)
|
||||
adminnav := false
|
||||
volunteernav := false
|
||||
|
||||
if role == 1{
|
||||
if role == 1 {
|
||||
adminnav = true
|
||||
volunteernav = false
|
||||
}else{
|
||||
} 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
|
||||
})
|
||||
// Render template
|
||||
utils.Render(w, "/appointment.html", map[string]interface{}{
|
||||
"Title": "My Profile",
|
||||
"IsAuthenticated": true,
|
||||
"ShowAdminNav": adminnav,
|
||||
"ShowVolunteerNav": volunteernav,
|
||||
"ActiveSection": "address",
|
||||
"UserName": username,
|
||||
"Appointments": appointments,
|
||||
})
|
||||
}
|
||||
138
app/internal/handlers/volunteer_poll.go
Normal file
138
app/internal/handlers/volunteer_poll.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/patel-mann/poll-system/app/internal/models"
|
||||
"github.com/patel-mann/poll-system/app/internal/utils"
|
||||
)
|
||||
|
||||
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 == "" {
|
||||
http.Error(w, "Address ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get address details
|
||||
var address string
|
||||
var userID int
|
||||
fmt.Print(addressID, userID)
|
||||
err := models.DB.QueryRow(`
|
||||
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
|
||||
`, addressID).Scan(&address, &userID)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Address not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if poll already exists for this address
|
||||
var pollID int
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT poll_id
|
||||
FROM poll
|
||||
WHERE address_id = $1 AND user_id = $2
|
||||
`, addressID, userID).Scan(&pollID)
|
||||
|
||||
// If no poll exists, create one
|
||||
if err == sql.ErrNoRows {
|
||||
err = models.DB.QueryRow(`
|
||||
INSERT INTO poll (user_id, address_id, poll_title, poll_description, is_active)
|
||||
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)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Printf("Database error: %v", err)
|
||||
http.Error(w, "Database error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Render(w, "volunteer/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",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pollID := r.FormValue("poll_id")
|
||||
postalCode := r.FormValue("postal_code")
|
||||
question5Thoughts := r.FormValue("question5_thoughts")
|
||||
|
||||
// Parse boolean values
|
||||
var question1VotedBefore *bool
|
||||
if val := r.FormValue("question1_voted_before"); val != "" {
|
||||
if val == "true" {
|
||||
b := true
|
||||
question1VotedBefore = &b
|
||||
} else if val == "false" {
|
||||
b := false
|
||||
question1VotedBefore = &b
|
||||
}
|
||||
}
|
||||
|
||||
var question2VoteAgain *bool
|
||||
if val := r.FormValue("question2_vote_again"); val != "" {
|
||||
if val == "true" {
|
||||
b := true
|
||||
question2VoteAgain = &b
|
||||
} else if val == "false" {
|
||||
b := false
|
||||
question2VoteAgain = &b
|
||||
}
|
||||
}
|
||||
|
||||
// Parse integer values
|
||||
question3LawnSigns, _ := strconv.Atoi(r.FormValue("question3_lawn_signs"))
|
||||
question4BannerSigns, _ := strconv.Atoi(r.FormValue("question4_banner_signs"))
|
||||
|
||||
// Insert poll response
|
||||
_, err = models.DB.Exec(`
|
||||
INSERT INTO poll_response (
|
||||
poll_id, respondent_postal_code, question1_voted_before,
|
||||
question2_vote_again, question3_lawn_signs, question4_banner_signs,
|
||||
question5_thoughts
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, pollID, postalCode, question1VotedBefore, question2VoteAgain,
|
||||
question3LawnSigns, question4BannerSigns, question5Thoughts)
|
||||
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
http.Error(w, "Failed to save poll response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/volunteer/Addresses", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Add this to your handlers package (create volunteer_posts.go or add to existing file)
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -12,7 +11,18 @@ import (
|
||||
"github.com/patel-mann/poll-system/app/internal/utils"
|
||||
)
|
||||
|
||||
// VolunteerPostsHandler - Read-only posts view for volunteers
|
||||
type VolunteerStatistics struct {
|
||||
AppointmentsToday int
|
||||
AppointmentsThisWeek int
|
||||
TotalAppointments int
|
||||
PollsCompleted int
|
||||
PollsRemaining int
|
||||
LawnSignsRequested int
|
||||
BannerSignsRequested int
|
||||
PollCompletionPercent int
|
||||
}
|
||||
|
||||
// VolunteerPostsHandler - Dashboard view for volunteers with posts and statistics
|
||||
func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET requests for volunteers
|
||||
if r.Method != http.MethodGet {
|
||||
@@ -23,7 +33,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get user info from context
|
||||
role := r.Context().Value("user_role").(int)
|
||||
CurrentUserID := models.GetCurrentUserID(w, r)
|
||||
username,_ := models.GetCurrentUserName(r)
|
||||
username, _ := models.GetCurrentUserName(r)
|
||||
|
||||
// Fetch posts from database
|
||||
rows, err := models.DB.Query(`
|
||||
@@ -34,7 +44,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
JOIN admin_volunteers x ON u.user_id = x.admin_id
|
||||
WHERE x.volunteer_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
`,CurrentUserID)
|
||||
`, CurrentUserID)
|
||||
if err != nil {
|
||||
fmt.Printf("Database query error: %v\n", err)
|
||||
http.Error(w, "Failed to fetch posts", http.StatusInternalServerError)
|
||||
@@ -60,20 +70,107 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get volunteer statistics
|
||||
stats, err := getVolunteerStatistics(CurrentUserID)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to fetch statistics: %v\n", err)
|
||||
// Continue with empty stats rather than failing
|
||||
stats = &VolunteerStatistics{}
|
||||
}
|
||||
|
||||
// Get navigation flags
|
||||
showAdminNav, showVolunteerNav := getNavFlags(role)
|
||||
|
||||
fmt.Printf("Volunteer viewing %d posts\n", len(posts))
|
||||
|
||||
utils.Render(w, "dashboard/volunteer_dashboard.html", map[string]interface{}{
|
||||
"Title": "Community Posts",
|
||||
"Title": "Volunteer Dashboard",
|
||||
"IsAuthenticated": true,
|
||||
"ShowAdminNav": showAdminNav,
|
||||
"ShowVolunteerNav": showVolunteerNav,
|
||||
"UserName": username,
|
||||
"UserName": username,
|
||||
"Posts": posts,
|
||||
"ActiveSection": "posts",
|
||||
"IsVolunteer": true, // Flag to indicate this is volunteer view
|
||||
"Statistics": stats,
|
||||
"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()
|
||||
weekday := now.Weekday()
|
||||
if weekday == time.Sunday {
|
||||
weekday = 7
|
||||
}
|
||||
weekStart := now.AddDate(0, 0, -int(weekday)+1).Format("2006-01-02")
|
||||
|
||||
// Appointments today
|
||||
err := models.DB.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM appointment
|
||||
WHERE user_id = $1 AND DATE(appointment_date) = $2
|
||||
`, userID, today).Scan(&stats.AppointmentsToday)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Appointments this week
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM appointment
|
||||
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
|
||||
`, userID, weekStart, today).Scan(&stats.AppointmentsThisWeek)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Total appointments
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM appointment
|
||||
WHERE user_id = $1
|
||||
`, userID).Scan(&stats.TotalAppointments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Polls completed
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT COUNT(DISTINCT pr.poll_response_id)
|
||||
FROM poll p
|
||||
JOIN poll_response pr ON p.poll_id = pr.poll_id
|
||||
WHERE p.user_id = $1
|
||||
`, userID).Scan(&stats.PollsCompleted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Polls remaining (appointments without poll responses)
|
||||
stats.PollsRemaining = stats.TotalAppointments - stats.PollsCompleted
|
||||
|
||||
// Calculate completion percentage
|
||||
if stats.TotalAppointments > 0 {
|
||||
stats.PollCompletionPercent = (stats.PollsCompleted * 100) / stats.TotalAppointments
|
||||
} else {
|
||||
stats.PollCompletionPercent = 0
|
||||
}
|
||||
|
||||
// Signs requested
|
||||
err = models.DB.QueryRow(`
|
||||
SELECT
|
||||
COALESCE(SUM(pr.question3_lawn_signs), 0),
|
||||
COALESCE(SUM(pr.question4_banner_signs), 0)
|
||||
FROM poll p
|
||||
JOIN poll_response pr ON p.poll_id = pr.poll_id
|
||||
WHERE p.user_id = $1
|
||||
`, userID).Scan(&stats.LawnSignsRequested, &stats.BannerSignsRequested)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func GetCurrentUserName(r *http.Request) (string, error) {
|
||||
|
||||
var currentUserName string
|
||||
err := DB.QueryRow(`
|
||||
SELECT first_name || ' ' || last_name
|
||||
SELECT first_name
|
||||
FROM users
|
||||
WHERE user_id = $1
|
||||
`, currentUserID).Scan(¤tUserName)
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
>
|
||||
<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">Coordinates</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>
|
||||
@@ -158,7 +158,7 @@
|
||||
{{ else }}
|
||||
<button
|
||||
class="px-3 py-1 bg-blue-600 text-white text-sm hover:bg-blue-700"
|
||||
onclick="openAssignModal({{ .AddressID }})"
|
||||
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
|
||||
>
|
||||
Assign
|
||||
</button>
|
||||
@@ -187,7 +187,7 @@
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-8 text-center text-gray-500">
|
||||
<td colspan="7" class="px-6 py-8 text-center text-gray-500">
|
||||
No addresses found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -201,37 +201,125 @@
|
||||
id="assignModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-white p-6 rounded shadow-lg w-96">
|
||||
<h2 class="text-lg font-semibold mb-4">Assign Address</h2>
|
||||
<form id="assignForm" method="POST" action="/assign_address">
|
||||
<div class="bg-white w-full max-w-lg mx-4 shadow-lg">
|
||||
<!-- Modal Header -->
|
||||
<div
|
||||
class="flex justify-between items-center px-6 py-4 border-b border-gray-200"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Assign Address</h2>
|
||||
<button
|
||||
onclick="closeAssignModal()"
|
||||
class="text-gray-400 hover:text-gray-600 focus:outline-none"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<form
|
||||
id="assignForm"
|
||||
method="POST"
|
||||
action="/assign_address"
|
||||
class="p-6 space-y-4"
|
||||
>
|
||||
<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">
|
||||
|
||||
<!-- Selected Address Display -->
|
||||
<div class="bg-gray-50 p-3 border border-gray-200">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<i class="fas fa-map-marker-alt text-gray-500"></i>
|
||||
<span class="font-medium text-gray-900">Selected Address:</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 mb-4" id="selected-address">
|
||||
None selected
|
||||
</div>
|
||||
|
||||
<!-- User Selection -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="user_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
<i class="fas fa-user mr-2"></i>Select User
|
||||
</label>
|
||||
<select
|
||||
name="user_id"
|
||||
id="user_id"
|
||||
class="w-full 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"
|
||||
required
|
||||
>
|
||||
<option value="">-- Select User --</option>
|
||||
{{ range .Users }}
|
||||
<option value="{{ .ID }}">{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Selection -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="appointment-date"
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
<i class="fas fa-calendar mr-2"></i>Appointment Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="appointment-date"
|
||||
name="appointment_date"
|
||||
required
|
||||
class="w-full 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"
|
||||
min=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Time Selection -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Start Time -->
|
||||
<div>
|
||||
<label
|
||||
for="time"
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
<i class="fas fa-clock mr-2"></i>Time
|
||||
</label>
|
||||
<select
|
||||
id="time"
|
||||
name="time"
|
||||
required
|
||||
onchange="updateEndTime()"
|
||||
class="w-full 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 Time</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duration Display -->
|
||||
<div class="bg-blue-50 p-3 border border-blue-200">
|
||||
<div class="flex items-center space-x-2 text-blue-800">
|
||||
<i class="fas fa-stopwatch"></i>
|
||||
<span class="text-sm font-medium"
|
||||
>Duration: <span id="duration-display">0 minutes</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onclick="closeAssignModal()"
|
||||
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
class="px-4 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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
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"
|
||||
>
|
||||
Assign
|
||||
<i class="fas fa-check mr-2"></i> Assign
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -239,26 +327,105 @@
|
||||
</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>
|
||||
|
||||
<script>
|
||||
function openAssignModal(addressID) {
|
||||
// Generate time options in 10-minute increments
|
||||
function generateTimeOptions() {
|
||||
const times = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 20) {
|
||||
const timeString =
|
||||
String(hour).padStart(2, "0") + ":" + String(minute).padStart(2, "0");
|
||||
const displayTime = formatTime12Hour(hour, minute);
|
||||
times.push({ value: timeString, display: displayTime });
|
||||
}
|
||||
}
|
||||
return times;
|
||||
}
|
||||
|
||||
// Format time to 12-hour format
|
||||
function formatTime12Hour(hour, minute) {
|
||||
const ampm = hour >= 12 ? "PM" : "AM";
|
||||
const displayHour = hour % 12 || 12;
|
||||
return displayHour + ":" + String(minute).padStart(2, "0") + " " + ampm;
|
||||
}
|
||||
|
||||
// Populate time dropdown
|
||||
function populateTimeSelect() {
|
||||
const timeSelect = document.getElementById("time");
|
||||
const times = generateTimeOptions();
|
||||
|
||||
timeSelect.innerHTML = '<option value="">Select Time</option>';
|
||||
times.forEach((time) => {
|
||||
const option = new Option(time.display, time.value);
|
||||
timeSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function openAssignModal(addressID, address) {
|
||||
document.getElementById("modalAddressID").value = addressID;
|
||||
document.getElementById("selected-address").textContent =
|
||||
address || "Address ID: " + addressID;
|
||||
|
||||
// Set minimum date to today
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
document.getElementById("appointment-date").min = today;
|
||||
document.getElementById("appointment-date").value = today;
|
||||
|
||||
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");
|
||||
document.getElementById("assignForm").reset();
|
||||
document.getElementById("selected-address").textContent = "None selected";
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set("page", page);
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
function changePageSize(pageSize) {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set("pageSize", pageSize);
|
||||
urlParams.set("page", 1);
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
populateTimeSelect();
|
||||
|
||||
// Close modal when clicking outside
|
||||
document
|
||||
.getElementById("assignModal")
|
||||
.addEventListener("click", function (e) {
|
||||
if (e.target === this) {
|
||||
closeAssignModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{{ 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 }}
|
||||
@@ -8,7 +8,7 @@
|
||||
<i
|
||||
class="{{if .PageIcon}}{{.PageIcon}}{{else}}fas fa-calendar-alt{{end}} text-green-600"
|
||||
></i>
|
||||
<span class="text-sm font-medium"> Appointments </span>
|
||||
<span class="text-sm font-medium">Appointments</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
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">Coordinates</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>
|
||||
@@ -68,11 +68,16 @@
|
||||
{{ .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"
|
||||
{{ if .HasPollResponse }}
|
||||
<span class="{{ .PollButtonClass }}"> {{ .PollButtonText }} </span>
|
||||
{{ else }}
|
||||
<a
|
||||
href="/poll?address_id={{ .AddressID }}"
|
||||
class="{{ .PollButtonClass }}"
|
||||
>
|
||||
Ask Poll
|
||||
</button>
|
||||
{{ .PollButtonText }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
|
||||
@@ -1,122 +1,286 @@
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col min-h-screen bg-gray-100">
|
||||
<!-- Optional Header -->
|
||||
<header class="bg-white shadow p-4">
|
||||
<h1 class="text-xl font-bold">Community</h1>
|
||||
</header>
|
||||
|
||||
<!-- Scrollable Posts -->
|
||||
<main class="flex-1 overflow-y-auto px-2 py-4 max-w-2xl mx-auto space-y-4">
|
||||
<!-- Posts Feed -->
|
||||
{{range .Posts}}
|
||||
<article class="bg-white border-b border-gray-200">
|
||||
<!-- Post Header -->
|
||||
<div class="flex items-center px-6 py-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold"
|
||||
>
|
||||
{{slice .AuthorName 0 1}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-semibold text-gray-900">{{.AuthorName}}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{.CreatedAt.Format "Jan 2, 2006"}}
|
||||
</p>
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Top Navigation -->
|
||||
<div class="bg-white border-b border-gray-200 px-4 sm: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="fas fa-tachometer-alt text-green-600"></i>
|
||||
<span class="text-sm font-medium">Volunteer Dashboard</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Image -->
|
||||
{{if .ImageURL}}
|
||||
<div class="w-full">
|
||||
<img
|
||||
src="{{.ImageURL}}"
|
||||
alt="Post image"
|
||||
class="w-full max-h-96 object-cover"
|
||||
onerror="this.parentElement.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Post Actions -->
|
||||
<div class="px-6 py-3">
|
||||
<div class="flex items-center space-x-6">
|
||||
<button
|
||||
class="reaction-btn flex items-center space-x-2 text-gray-600 hover:text-blue-500 transition-colors"
|
||||
data-post-id="{{.PostID}}"
|
||||
data-reaction="like"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V18m-7-8a2 2 0 01-2-2V7a2 2 0 012-2h3.764a2 2 0 011.789 1.106L14 8v2m-7-8V5a2 2 0 012-2h1m-5 10h3m4 3H8"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium like-count">0</span>
|
||||
</button>
|
||||
<button
|
||||
class="reaction-btn flex items-center space-x-2 text-gray-600 hover:text-red-500 transition-colors"
|
||||
data-post-id="{{.PostID}}"
|
||||
data-reaction="dislike"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018c.163 0 .326.02.485.06L17 4m-7 10v-8m7 8a2 2 0 002 2v1a2 2 0 01-2 2h-3.764a2 2 0 01-1.789-1.106L10 16v-2m7 8V19a2 2 0 00-2-2h-1m5-10H12m-4-3h4"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium dislike-count">0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Content -->
|
||||
{{if .Content}}
|
||||
<div class="px-6 pb-4">
|
||||
<p class="text-gray-900 leading-relaxed">
|
||||
<span class="font-semibold">{{.AuthorName}}</span> {{.Content}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</article>
|
||||
{{else}}
|
||||
<div class="bg-white p-12 text-center">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<svg
|
||||
class="w-16 h-16 mx-auto text-gray-300 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
|
||||
<p class="text-gray-500">
|
||||
Be the first to share something with the community!
|
||||
</p>
|
||||
<div class="text-sm text-gray-600 hidden sm:block">
|
||||
Welcome back, {{ .UserName }}!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 overflow-hidden bg-gray-50">
|
||||
<div class="h-full flex flex-col lg:flex-row gap-6 p-4 sm:p-6">
|
||||
<!-- Left Column - Posts -->
|
||||
<div class="flex-1 lg:flex-none lg:w-2/3 space-y-0">
|
||||
{{ if .Posts }}{{range .Posts}}
|
||||
<article class="bg-white border-b border-gray-200">
|
||||
<!-- Post Header -->
|
||||
<div class="flex items-center px-4 sm:px-6 py-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold rounded-full"
|
||||
>
|
||||
{{slice .AuthorName 0 1}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-semibold text-gray-900">{{.AuthorName}}</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{.CreatedAt.Format "Jan 2, 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Image -->
|
||||
{{if .ImageURL}}
|
||||
<div class="w-full">
|
||||
<img
|
||||
src="{{.ImageURL}}"
|
||||
alt="Post image"
|
||||
class="w-full max-h-96 object-cover"
|
||||
onerror="this.parentElement.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Post Content -->
|
||||
{{if .Content}}
|
||||
<div class="px-4 sm:px-6 pt-2 pb-4">
|
||||
<p class="text-gray-900 leading-relaxed">
|
||||
<span class="font-semibold">{{.AuthorName}}</span> {{.Content}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</article>
|
||||
{{else}}
|
||||
<div class="bg-white p-8 sm:p-12 text-center border-b border-gray-200">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<svg
|
||||
class="w-16 h-16 mx-auto text-gray-300 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
|
||||
<p class="text-gray-500">
|
||||
Be the first to share something with the community!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }} {{ else }}
|
||||
<div class="bg-white border-b border-gray-200 p-8 sm:p-12 text-center">
|
||||
<div class="max-w-sm mx-auto">
|
||||
<div
|
||||
class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<i class="fas fa-inbox text-2xl text-gray-400"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 font-medium mb-2">No posts yet</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Check back later for updates from your team
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Statistics -->
|
||||
<div class="w-full lg:w-1/3 flex flex-col gap-4 sm:gap-6">
|
||||
<!-- Today's Overview -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
Today's Overview
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-calendar-day text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Appointments Today</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsToday }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-calendar-week text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">This Week</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsThisWeek }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Polling Progress -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
Polling Progress
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-check-circle text-green-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Polls Completed</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-green-600">
|
||||
{{ .Statistics.PollsCompleted }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-clock text-orange-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Polls Remaining</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-orange-600">
|
||||
{{ .Statistics.PollsRemaining }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
{{ if gt .Statistics.TotalAppointments 0 }}
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between text-xs text-gray-600 mb-2">
|
||||
<span>Progress</span>
|
||||
<span
|
||||
>{{ .Statistics.PollsCompleted }}/{{
|
||||
.Statistics.TotalAppointments }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-gray-600 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {{ .Statistics.PollCompletionPercent }}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signs Summary -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
Signs Requested
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-sign text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Lawn Signs</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.LawnSignsRequested }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-flag text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Banner Signs</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.BannerSignsRequested }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/volunteer/Addresses"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-calendar-alt text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">View Appointments</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/schedual"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-clock text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">My Schedule</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/profile"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-user text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Profile Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<i class="fas fa-bars text-gray-600"></i>
|
||||
</button>
|
||||
<span class="text-sm font-medium text-gray-600">{{.UserName}}</span>
|
||||
<div class="w-9 h-9 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
|
||||
{{slice .UserName 0 1}}
|
||||
</div>
|
||||
<a href="/logout" class="p-2 hover:bg-gray-200 rounded">
|
||||
@@ -52,8 +52,8 @@
|
||||
|
||||
<!-- Right Side: User Info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-600">Welcome, {{.UserName}}</span>
|
||||
<div class="w-9 h-9 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
<span class="text-sm font-medium text-gray-600">Hi, {{.UserName}}</span>
|
||||
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
|
||||
{{slice .UserName 0 1}}
|
||||
</div>
|
||||
<a href="/logout" class="p-2 hover:bg-gray-200 rounded">
|
||||
|
||||
197
app/internal/templates/volunteer/poll_form.html
Normal file
197
app/internal/templates/volunteer/poll_form.html
Normal file
@@ -0,0 +1,197 @@
|
||||
{{ 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-poll{{end}} text-green-600"
|
||||
></i>
|
||||
<span class="text-sm font-medium">Poll Questions</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/appointments"
|
||||
class="px-3 py-1 bg-gray-500 text-white text-sm hover:bg-gray-600 rounded"
|
||||
>
|
||||
Back to Appointments
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 p-6">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
Campaign Poll Questions
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">Address: {{ .Address }}</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="space-y-6">
|
||||
<input type="hidden" name="poll_id" value="{{ .PollID }}" />
|
||||
|
||||
<!-- Postal Code -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Respondent's Postal Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="postal_code"
|
||||
placeholder="Enter postal code (e.g., T2P 1J9)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Question 1 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">
|
||||
1. Have you voted before?
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="question1_voted_before"
|
||||
value="true"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-sm">Yes</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="question1_voted_before"
|
||||
value="false"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-sm">No</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Question 2 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">
|
||||
2. Will you vote again for this candidate?
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="question2_vote_again"
|
||||
value="true"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-sm">Yes</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="question2_vote_again"
|
||||
value="false"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-sm">No</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Question 3 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
3. How many lawn signs do you need?
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="question3_lawn_signs"
|
||||
min="0"
|
||||
max="10"
|
||||
value="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Question 4 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
4. How many banner signs do you need?
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="question4_banner_signs"
|
||||
min="0"
|
||||
max="5"
|
||||
value="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Question 5 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
5. Write your thoughts (optional)
|
||||
</label>
|
||||
<textarea
|
||||
name="question5_thoughts"
|
||||
rows="4"
|
||||
placeholder="Any additional comments or feedback..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex justify-end gap-3 pt-6">
|
||||
<a
|
||||
href="/appointments"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Submit Poll Response
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Show delivery address section if user needs signs
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const lawnSignsInput = document.querySelector(
|
||||
'input[name="question3_lawn_signs"]'
|
||||
);
|
||||
const bannerSignsInput = document.querySelector(
|
||||
'input[name="question4_banner_signs"]'
|
||||
);
|
||||
const deliverySection = document.getElementById("delivery-section");
|
||||
|
||||
function toggleDeliverySection() {
|
||||
const lawnSigns = parseInt(lawnSignsInput.value) || 0;
|
||||
const bannerSigns = parseInt(bannerSignsInput.value) || 0;
|
||||
|
||||
if (lawnSigns > 0 || bannerSigns > 0) {
|
||||
deliverySection.style.display = "block";
|
||||
} else {
|
||||
deliverySection.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
lawnSignsInput.addEventListener("input", toggleDeliverySection);
|
||||
bannerSignsInput.addEventListener("input", toggleDeliverySection);
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
59
app/main.go
59
app/main.go
@@ -1,14 +1,10 @@
|
||||
// Add this debugging code to your main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/joho/godotenv"
|
||||
@@ -20,30 +16,10 @@ import (
|
||||
_ "github.com/lib/pq" // use PostgreSQL
|
||||
)
|
||||
|
||||
// Custom file server with logging
|
||||
func loggingFileServer(dir string) http.Handler {
|
||||
fs := http.FileServer(http.Dir(dir))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Log the request
|
||||
log.Printf("File request: %s", r.URL.Path)
|
||||
|
||||
// Check if file exists
|
||||
filePath := filepath.Join(dir, r.URL.Path)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
log.Printf("File not found: %s", filePath)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Serving file: %s", filePath)
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to determine navigation visibility based on role
|
||||
func getNavFlags(role int) (bool, bool, bool) {
|
||||
showAdminNav := role == 1 // Admin role
|
||||
showLeaderNav := role == 2 // Volunteer role
|
||||
showLeaderNav := role == 2 // Team Leader role
|
||||
showVolunteerNav := role == 3 // Volunteer role
|
||||
return showAdminNav, showVolunteerNav, showLeaderNav
|
||||
}
|
||||
@@ -70,18 +46,14 @@ func createTemplateData(title, activeSection string, role int, isAuthenticated b
|
||||
}
|
||||
|
||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
err := godotenv.Load() // or specify path: godotenv.Load("/path/to/.env")
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
// Get individual components from environment variables
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
var jwtKey = []byte(jwtSecret)
|
||||
|
||||
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
@@ -124,7 +96,6 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
role, ok := r.Context().Value("user_role").(int)
|
||||
if !ok || (role != 3 && role != 2) {
|
||||
fmt.Printf("Access denied: role %d not allowed\n", role) // Debug log
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
@@ -132,36 +103,29 @@ func volunteerMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Updated handler functions using the helper
|
||||
func schedualHandler(w http.ResponseWriter, r *http.Request) {
|
||||
role := r.Context().Value("user_role").(int)
|
||||
// currentUserID := r.Context().Value("user_id").(int)
|
||||
|
||||
data := createTemplateData("My Schedule", "schedual", role, true, nil)
|
||||
utils.Render(w, "Schedual/schedual.html", data)
|
||||
|
||||
}
|
||||
|
||||
func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Render(w, "dashboard/dashboard.html", map[string]interface{}{
|
||||
"Title": "Admin Dashboard",
|
||||
"IsAuthenticated": false,
|
||||
"ActiveSection": "dashboard",
|
||||
})
|
||||
"Title": "Admin Dashboard",
|
||||
"IsAuthenticated": false,
|
||||
"ActiveSection": "dashboard",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
models.InitDB()
|
||||
|
||||
// Static file servers with logging
|
||||
// Static file servers
|
||||
fs := http.FileServer(http.Dir("static"))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||
|
||||
// Use logging file server for uploads
|
||||
http.Handle("/uploads/", http.StripPrefix("/uploads/", loggingFileServer("uploads")))
|
||||
uploadsFs := http.FileServer(http.Dir("uploads"))
|
||||
http.Handle("/uploads/", http.StripPrefix("/uploads/", uploadsFs))
|
||||
|
||||
// Public HTML Routes
|
||||
http.HandleFunc("/", HomeHandler)
|
||||
@@ -187,16 +151,15 @@ func main() {
|
||||
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
|
||||
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
|
||||
|
||||
|
||||
|
||||
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
|
||||
|
||||
//--- Volunteer-only routes
|
||||
http.HandleFunc("/volunteer/dashboard", volunteerMiddleware(handlers.VolunteerPostsHandler))
|
||||
http.HandleFunc("/volunteer/Addresses", volunteerMiddleware(handlers.VolunteerAppointmentHandler))
|
||||
|
||||
http.HandleFunc("/schedual", volunteerMiddleware(schedualHandler))
|
||||
|
||||
// Poll routes (volunteer only)
|
||||
http.HandleFunc("/poll", volunteerMiddleware(handlers.PollHandler))
|
||||
|
||||
log.Println("Server started on localhost:8080")
|
||||
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 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 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 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 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.
Reference in New Issue
Block a user