csv imports
This commit is contained in:
262
app/internal/handlers/admin_csv_upload.go
Normal file
262
app/internal/handlers/admin_csv_upload.go
Normal 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, ¤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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(`
|
||||
|
||||
Reference in New Issue
Block a user