diff --git a/app/export_data.csv b/app/export_data.csv new file mode 100644 index 0000000..f24c956 --- /dev/null +++ b/app/export_data.csv @@ -0,0 +1,2 @@ +Name,Address,Email,Phone,Phone +Bob Johnson,1782 cornerstone bv ne,bob@email.com,555-0125,555-0125 diff --git a/app/internal/handlers/admin_csv_upload.go b/app/internal/handlers/admin_csv_upload.go new file mode 100644 index 0000000..04c1eaa --- /dev/null +++ b/app/internal/handlers/admin_csv_upload.go @@ -0,0 +1,262 @@ +package handlers + +import ( + "encoding/csv" + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/patel-mann/poll-system/app/internal/models" + "github.com/patel-mann/poll-system/app/internal/utils" +) + +// CSVUploadResult holds the results of CSV processing +type CSVUploadResult struct { + TotalRecords int + ValidatedCount int + NotFoundCount int + ErrorCount int + ValidatedAddresses []string + NotFoundAddresses []string + ErrorMessages []string +} + +// Combined CSV Upload Handler - handles both GET (display form) and POST (process CSV) +func CSVUploadHandler(w http.ResponseWriter, r *http.Request) { + username, _ := models.GetCurrentUserName(r) + + // Base template data + templateData := map[string]interface{}{ + "Title": "CSV Address Validation", + "IsAuthenticated": true, + "ShowAdminNav": true, + "ActiveSection": "address", + "UserName": username, + "Role": "admin", + } + + // Handle GET request - show the upload form + if r.Method == http.MethodGet { + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Handle POST request - process the CSV + if r.Method == http.MethodPost { + // Parse multipart form (10MB max) + err := r.ParseMultipartForm(10 << 20) + if err != nil { + http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest) + return + } + + // Get form values + addressColumnStr := r.FormValue("address_column") + if addressColumnStr == "" { + templateData["Error"] = "Address column is required" + utils.Render(w, "csv-upload.html", templateData) + return + } + + addressColumn, err := strconv.Atoi(addressColumnStr) + if err != nil { + templateData["Error"] = "Invalid address column index" + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Get uploaded file + file, header, err := r.FormFile("csv_file") + if err != nil { + templateData["Error"] = "Error retrieving file: " + err.Error() + utils.Render(w, "csv-upload.html", templateData) + return + } + defer file.Close() + + // Validate file type + if !strings.HasSuffix(strings.ToLower(header.Filename), ".csv") { + templateData["Error"] = "Please upload a CSV file" + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Read and parse CSV + reader := csv.NewReader(file) + reader.FieldsPerRecord = -1 // Allow variable number of fields + + var allRows [][]string + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + templateData["Error"] = "Error reading CSV: " + err.Error() + utils.Render(w, "csv-upload.html", templateData) + return + } + allRows = append(allRows, record) + } + + if len(allRows) <= 1 { + templateData["Error"] = "CSV file must contain data rows (header + at least 1 data row)" + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Validate address column index + if addressColumn >= len(allRows[0]) { + templateData["Error"] = "Invalid address column index" + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Process addresses + result := processAddressValidation(allRows[1:], addressColumn) // Skip header + + // Add result to template data + templateData["Result"] = result + templateData["FileName"] = header.Filename + + // Render the same template with results + utils.Render(w, "csv-upload.html", templateData) + return + } + + // Method not allowed + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// processAddressValidation processes CSV data and validates addresses +func processAddressValidation(rows [][]string, addressColumn int) CSVUploadResult { + result := CSVUploadResult{ + TotalRecords: len(rows), + } + + for i, row := range rows { + // Check if the row has enough columns + if addressColumn >= len(row) { + result.ErrorCount++ + result.ErrorMessages = append(result.ErrorMessages, + fmt.Sprintf("Row %d: Missing address column", i+2)) + continue + } + + // Get and normalize address + address := strings.ToLower(strings.TrimSpace(row[addressColumn])) + if address == "" { + result.ErrorCount++ + result.ErrorMessages = append(result.ErrorMessages, + fmt.Sprintf("Row %d: Empty address", i+2)) + continue + } + + // Check if address exists in database + var addressID int + var currentStatus bool + err := models.DB.QueryRow(` + SELECT address_id, visited_validated + FROM address_database + WHERE LOWER(TRIM(address)) = $1 + `, address).Scan(&addressID, ¤tStatus) + + if err != nil { + // Address not found + result.NotFoundCount++ + result.NotFoundAddresses = append(result.NotFoundAddresses, address) + continue + } + + // Address found - update validation status if not already validated + if !currentStatus { + _, err = models.DB.Exec(` + UPDATE address_database + SET visited_validated = true, updated_at = NOW() + WHERE address_id = $1 + `, addressID) + + if err != nil { + result.ErrorCount++ + result.ErrorMessages = append(result.ErrorMessages, + fmt.Sprintf("Row %d: Database update error for address '%s'", i+2, address)) + log.Printf("Error updating address %d: %v", addressID, err) + continue + } + + result.ValidatedCount++ + result.ValidatedAddresses = append(result.ValidatedAddresses, address) + } else { + // Address was already validated - still count as validated + result.ValidatedCount++ + result.ValidatedAddresses = append(result.ValidatedAddresses, address+" (already validated)") + } + } + + return result +} + +// Optional: Keep the export function if you need it +// ExportValidatedAddressesHandler exports validated addresses to CSV +func ExportValidatedAddressesHandler(w http.ResponseWriter, r *http.Request) { + // Query validated addresses + rows, err := models.DB.Query(` + SELECT + a.address_id, + a.address, + a.street_name, + a.street_type, + a.street_quadrant, + a.house_number, + COALESCE(a.house_alpha, '') as house_alpha, + a.longitude, + a.latitude, + a.visited_validated, + a.created_at, + a.updated_at, + CASE + WHEN ap.sched_id IS NOT NULL THEN true + ELSE false + END as assigned, + COALESCE(u.first_name || ' ' || u.last_name, '') as user_name, + COALESCE(u.email, '') as user_email, + COALESCE(ap.appointment_date::text, '') as appointment_date, + COALESCE(ap.appointment_time::text, '') as appointment_time + FROM address_database a + LEFT JOIN appointment ap ON a.address_id = ap.address_id + LEFT JOIN users u ON ap.user_id = u.user_id + WHERE a.visited_validated = true + ORDER BY a.updated_at DESC + `) + if err != nil { + log.Println("Export query error:", err) + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + defer rows.Close() + + // Set response headers for CSV download + filename := fmt.Sprintf("validated_addresses_%s.csv", time.Now().Format("2006-01-02_15-04-05")) + w.Header().Set("Content-Type", "text/csv") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + + // Create CSV writer + writer := csv.NewWriter(w) + defer writer.Flush() + + // Write header + header := []string{ + "Address ID", "Address", "Street Name", "Street Type", "Street Quadrant", + "House Number", "House Alpha", "Longitude", "Latitude", "Validated", + "Created At", "Updated At", "Assigned", "Assigned User", "User Email", + "Appointment Date", "Appointment Time", + } + writer.Write(header) + + // Write data rows (you'll need to define AddressWithDetails struct) + // Implementation depends on your existing struct definitions +} \ No newline at end of file diff --git a/app/internal/handlers/admin_dashboard.go b/app/internal/handlers/admin_dashboard.go index eaca1b6..1670258 100644 --- a/app/internal/handlers/admin_dashboard.go +++ b/app/internal/handlers/admin_dashboard.go @@ -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_responce; + FROM poll; `).Scan(&totalDonations) if err != nil { log.Println("Donation query error:", err) diff --git a/app/internal/handlers/volunteer_posts.go b/app/internal/handlers/volunteer_posts.go index 0cfa9a9..17c9233 100644 --- a/app/internal/handlers/volunteer_posts.go +++ b/app/internal/handlers/volunteer_posts.go @@ -13,6 +13,7 @@ import ( type VolunteerStatistics struct { AppointmentsToday int + AppointmentsTomorrow int AppointmentsThisWeek int TotalAppointments int PollsCompleted int @@ -72,6 +73,7 @@ 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 @@ -102,11 +104,22 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) { // Get start of current week (Monday) now := time.Now() + oneDayLater := now.Add(time.Hour * 12) + weekday := now.Weekday() if weekday == time.Sunday { weekday = 7 } - weekStart := now.AddDate(0, 0, -int(weekday)+1).Format("2006-01-02") + + // Get start of the week (Monday) + weekStart := now.AddDate(0, 0, -int(weekday)+1) + + // Get end of the week (Sunday) + weekEnd := weekStart.AddDate(0, 0, 6) + + fmt.Println("Week Start:", weekStart.Format("2006-01-02")) + fmt.Println("Week End:", weekEnd.Format("2006-01-02")) + // Appointments today err := models.DB.QueryRow(` @@ -118,15 +131,27 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) { return nil, err } + // Appointments tomorrow + err = models.DB.QueryRow(` + SELECT COUNT(*) + FROM appointment + WHERE user_id = $1 AND DATE(appointment_date) = $2 + `, userID, oneDayLater).Scan(&stats.AppointmentsTomorrow) + 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) + `, userID, weekStart, weekEnd).Scan(&stats.AppointmentsThisWeek) if err != nil { return nil, err } + fmt.Print("Stats: ", stats.AppointmentsThisWeek," Today's date: " , today ,"fasd", weekStart) + // Total appointments err = models.DB.QueryRow(` diff --git a/app/internal/templates/address/address.html b/app/internal/templates/address/address.html index 4d2dd49..2bc760a 100644 --- a/app/internal/templates/address/address.html +++ b/app/internal/templates/address/address.html @@ -296,16 +296,6 @@ - -
-
- - Duration: 0 minutes -
-
-
+
+ + {{end}} + + +
+ +
+
+
+ +
+
+

How it works:

+
+
    +
  • Upload a CSV file with address data
  • +
  • Preview and select the address column
  • +
  • Addresses will be normalized (lowercase) and matched
  • +
  • Matching addresses will be marked as validated
  • +
+
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+ +

or drag and drop

+
+

CSV files up to 10MB

+
+
+ +
+ + + + + +
+ +
+
+
+
+
+ + + + +{{ end }} diff --git a/app/internal/templates/dashboard/dashboard.html b/app/internal/templates/dashboard/dashboard.html index a34882a..9de96ed 100644 --- a/app/internal/templates/dashboard/dashboard.html +++ b/app/internal/templates/dashboard/dashboard.html @@ -36,6 +36,12 @@ > Export Data +