csv imports

This commit is contained in:
Mann Patel
2025-08-28 23:27:24 -06:00
parent 1955407d7c
commit f1b5cdc806
18 changed files with 777 additions and 78 deletions

View File

@@ -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, &currentStatus)
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
}

View File

@@ -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)

View File

@@ -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(`