262 lines
7.3 KiB
Go
262 lines
7.3 KiB
Go
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
|
|
} |