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 }