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(`
|
||||
|
||||
@@ -296,16 +296,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duration Display -->
|
||||
<div class="bg-blue-50 p-3 border border-blue-200">
|
||||
<div class="flex items-center space-x-2 text-blue-800">
|
||||
<i class="fas fa-stopwatch"></i>
|
||||
<span class="text-sm font-medium"
|
||||
>Duration: <span id="duration-display">0 minutes</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
|
||||
448
app/internal/templates/csv-upload.html
Normal file
448
app/internal/templates/csv-upload.html
Normal file
@@ -0,0 +1,448 @@
|
||||
{{ define "content" }}
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-upload text-2xl text-indigo-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
CSV Address Validation
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
Upload and process CSV files to validate addresses
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center"
|
||||
>
|
||||
<i class="fas fa-arrow-left mr-2"></i>Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
||||
<!-- Results Section (shown after processing) -->
|
||||
{{if .Result}}
|
||||
<div class="mb-8">
|
||||
<!-- Stats Overview -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600">Total Records</p>
|
||||
<p class="text-xl font-bold">{{ .Result.TotalRecords }}</p>
|
||||
</div>
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600">Validated</p>
|
||||
<p class="text-xl font-bold text-green-600">
|
||||
{{ .Result.ValidatedCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600">Not Found</p>
|
||||
<p class="text-xl font-bold text-yellow-600">
|
||||
{{ .Result.NotFoundCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600">Errors</p>
|
||||
<p class="text-xl font-bold text-red-600">{{ .Result.ErrorCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Results -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<!-- Validated Addresses -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-green-800 mb-4">
|
||||
Validated Addresses
|
||||
</h3>
|
||||
{{if .Result.ValidatedAddresses}}
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
<ul class="space-y-1">
|
||||
{{range .Result.ValidatedAddresses}}
|
||||
<li class="text-sm text-gray-700 p-2 bg-green-50 rounded">
|
||||
{{ . }}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-sm text-gray-500">No validated addresses found.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Not Found Addresses -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-yellow-800 mb-4">
|
||||
Not Found Addresses
|
||||
</h3>
|
||||
{{if .Result.NotFoundAddresses}}
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
<ul class="space-y-1">
|
||||
{{range .Result.NotFoundAddresses}}
|
||||
<li class="text-sm text-gray-700 p-2 bg-yellow-50 rounded">
|
||||
{{ . }}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-sm text-gray-500">No missing addresses.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Errors -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-red-800 mb-4">Errors</h3>
|
||||
{{if .Result.ErrorMessages}}
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
<ul class="space-y-1">
|
||||
{{range .Result.ErrorMessages}}
|
||||
<li class="text-sm text-red-600 p-2 bg-red-50 rounded">
|
||||
{{ . }}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-sm text-gray-500">No errors encountered.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Another File Button -->
|
||||
<div class="text-center">
|
||||
<button
|
||||
onclick="resetForm()"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-6 rounded"
|
||||
>
|
||||
<i class="fas fa-plus mr-2"></i>Process Another File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Upload Form -->
|
||||
<div id="uploadSection" class="{{if .Result}}hidden{{end}}">
|
||||
<!-- Instructions -->
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-8">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">How it works:</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li>Upload a CSV file with address data</li>
|
||||
<li>Preview and select the address column</li>
|
||||
<li>Addresses will be normalized (lowercase) and matched</li>
|
||||
<li>Matching addresses will be marked as validated</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-8">
|
||||
<form
|
||||
id="csvForm"
|
||||
action="/addresses/upload-csv"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- File Upload -->
|
||||
<div>
|
||||
<label
|
||||
for="csv_file"
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
Select CSV File
|
||||
</label>
|
||||
<div
|
||||
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-gray-400 transition-colors"
|
||||
>
|
||||
<div class="space-y-1 text-center">
|
||||
<i
|
||||
class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"
|
||||
></i>
|
||||
<div class="flex text-sm text-gray-600">
|
||||
<label
|
||||
for="csv_file"
|
||||
class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500"
|
||||
>
|
||||
<span>Upload a file</span>
|
||||
<input
|
||||
id="csv_file"
|
||||
name="csv_file"
|
||||
type="file"
|
||||
class="sr-only"
|
||||
accept=".csv"
|
||||
required
|
||||
onchange="handleFileSelect(this)"
|
||||
/>
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">CSV files up to 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="fileName"
|
||||
class="mt-2 text-sm text-gray-600 hidden"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Preview -->
|
||||
<div id="previewSection" class="hidden">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Data Preview
|
||||
</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table id="previewTable" class="min-w-full">
|
||||
<thead id="previewHeader"></thead>
|
||||
<tbody id="previewBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">
|
||||
Select Address Column:
|
||||
</label>
|
||||
<div
|
||||
id="columnOptions"
|
||||
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Column Preview -->
|
||||
<div
|
||||
id="selectedColumnPreview"
|
||||
class="hidden mt-4 p-4 bg-blue-50 rounded-lg"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-blue-900 mb-2">
|
||||
Preview of selected address column:
|
||||
</h4>
|
||||
<div
|
||||
id="selectedColumnContent"
|
||||
class="text-sm text-blue-800"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
id="submitBtn"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-6 rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled
|
||||
>
|
||||
<i class="fas fa-check mr-2"></i>Process CSV
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let csvData = [];
|
||||
let csvHeaders = [];
|
||||
|
||||
function handleFileSelect(input) {
|
||||
const fileName = document.getElementById("fileName");
|
||||
const previewSection = document.getElementById("previewSection");
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
fileName.textContent = `Selected: ${file.name} (${(
|
||||
file.size /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)} MB)`;
|
||||
fileName.classList.remove("hidden");
|
||||
|
||||
// Read and preview CSV
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const csv = e.target.result;
|
||||
parseCSV(csv);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
fileName.classList.add("hidden");
|
||||
previewSection.classList.add("hidden");
|
||||
document.getElementById("submitBtn").disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseCSV(csv) {
|
||||
const lines = csv.split("\n").filter((line) => line.trim() !== "");
|
||||
if (lines.length === 0) return;
|
||||
|
||||
// Parse CSV (simple parsing - assumes no commas in quoted fields)
|
||||
csvData = lines.map((line) => {
|
||||
return line.split(",").map((cell) => cell.trim().replace(/^"|"$/g, ""));
|
||||
});
|
||||
|
||||
if (csvData.length === 0) return;
|
||||
|
||||
csvHeaders = csvData[0];
|
||||
const sampleRows = csvData.slice(1, 6); // First 5 data rows
|
||||
|
||||
displayPreview(csvHeaders, sampleRows);
|
||||
}
|
||||
|
||||
function displayPreview(headers, sampleRows) {
|
||||
const previewSection = document.getElementById("previewSection");
|
||||
const previewHeader = document.getElementById("previewHeader");
|
||||
const previewBody = document.getElementById("previewBody");
|
||||
const columnOptions = document.getElementById("columnOptions");
|
||||
|
||||
// Clear previous content
|
||||
previewHeader.innerHTML = "";
|
||||
previewBody.innerHTML = "";
|
||||
columnOptions.innerHTML = "";
|
||||
|
||||
// Create table header
|
||||
const headerRow = document.createElement("tr");
|
||||
headerRow.className = "bg-gray-100";
|
||||
headers.forEach((header, index) => {
|
||||
const th = document.createElement("th");
|
||||
th.className =
|
||||
"px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase";
|
||||
th.textContent = `Col ${index}: ${header}`;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
previewHeader.appendChild(headerRow);
|
||||
|
||||
// Create table body
|
||||
sampleRows.forEach((row) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "border-t border-gray-200";
|
||||
row.forEach((cell) => {
|
||||
const td = document.createElement("td");
|
||||
td.className = "px-3 py-2 text-sm text-gray-900 max-w-xs truncate";
|
||||
td.textContent = cell || "";
|
||||
tr.appendChild(td);
|
||||
});
|
||||
previewBody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Create column selection options
|
||||
headers.forEach((header, index) => {
|
||||
const label = document.createElement("label");
|
||||
label.className =
|
||||
"relative flex items-center p-3 cursor-pointer bg-gray-50 hover:bg-gray-100 rounded-lg border border-gray-200 hover:border-indigo-300";
|
||||
|
||||
label.innerHTML = `
|
||||
<input type="radio" name="address_column" value="${index}" class="sr-only" onchange="updateColumnPreview(${index})">
|
||||
<div class="radio-custom w-4 h-4 border border-gray-300 rounded-full mr-3"></div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">Column ${index}</div>
|
||||
<div class="text-sm text-gray-500 truncate max-w-32">${header}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
columnOptions.appendChild(label);
|
||||
});
|
||||
|
||||
previewSection.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function updateColumnPreview(columnIndex) {
|
||||
const preview = document.getElementById("selectedColumnPreview");
|
||||
const content = document.getElementById("selectedColumnContent");
|
||||
|
||||
// Update radio button styling
|
||||
document
|
||||
.querySelectorAll('input[name="address_column"]')
|
||||
.forEach((radio, index) => {
|
||||
const label = radio.closest("label");
|
||||
const customRadio = label.querySelector(".radio-custom");
|
||||
|
||||
if (index === columnIndex) {
|
||||
customRadio.classList.add("bg-indigo-600", "border-indigo-600");
|
||||
label.classList.add("bg-indigo-50", "border-indigo-300");
|
||||
} else {
|
||||
customRadio.classList.remove("bg-indigo-600", "border-indigo-600");
|
||||
label.classList.remove("bg-indigo-50", "border-indigo-300");
|
||||
}
|
||||
});
|
||||
|
||||
// Show preview of selected column
|
||||
const sampleAddresses = csvData
|
||||
.slice(1, 4)
|
||||
.map((row) => row[columnIndex])
|
||||
.filter((addr) => addr && addr.trim());
|
||||
content.innerHTML = sampleAddresses
|
||||
.map((addr) => `<div class="mb-1">• ${addr}</div>`)
|
||||
.join("");
|
||||
preview.classList.remove("hidden");
|
||||
|
||||
// Enable submit button
|
||||
document.getElementById("submitBtn").disabled = false;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("uploadSection").classList.remove("hidden");
|
||||
document.getElementById("csvForm").reset();
|
||||
document.getElementById("fileName").classList.add("hidden");
|
||||
document.getElementById("previewSection").classList.add("hidden");
|
||||
document.getElementById("submitBtn").disabled = true;
|
||||
|
||||
// Hide results
|
||||
const results = document.querySelector(".mb-8");
|
||||
if (results && results.querySelector(".grid")) {
|
||||
results.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop functionality
|
||||
const dropArea = document.querySelector(".border-dashed");
|
||||
|
||||
dropArea?.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.add("border-indigo-500", "bg-indigo-50");
|
||||
});
|
||||
|
||||
dropArea?.addEventListener("dragleave", (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.remove("border-indigo-500", "bg-indigo-50");
|
||||
});
|
||||
|
||||
dropArea?.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.classList.remove("border-indigo-500", "bg-indigo-50");
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
document.getElementById("csv_file").files = files;
|
||||
handleFileSelect(document.getElementById("csv_file"));
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById("csvForm").addEventListener("submit", function (e) {
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin mr-2"></i>Processing...';
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -36,6 +36,12 @@
|
||||
>
|
||||
<i class="fas fa-download mr-2"></i>Export Data
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors"
|
||||
onclick="window.location.href='/addresses/upload-csv'"
|
||||
>
|
||||
<i class="fas fa-upload mr-2"></i>Import Data
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<span class="text-sm font-medium">Volunteer Dashboard</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 hidden sm:block">
|
||||
Welcome back, {{ .UserName }}!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,9 +74,6 @@
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
|
||||
<p class="text-gray-500">
|
||||
Be the first to share something with the community!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }} {{ else }}
|
||||
@@ -122,6 +116,22 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-calendar-week text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700"
|
||||
>Appointments Tomorrow</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsTomorrow }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
@@ -233,52 +243,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/volunteer/Addresses"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-calendar-alt text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">View Appointments</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/schedual"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-clock text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">My Schedule</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/profile"
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-gray-50 rounded transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<i class="fas fa-user text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Profile Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- Right Side: User Info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-600">Hi, {{.UserName}}</span>
|
||||
<span class="text-sm text-gray-600 hidden sm:block">Welcome back, {{ .UserName }}!</span>
|
||||
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
|
||||
{{slice .UserName 0 1}}
|
||||
</div>
|
||||
@@ -146,18 +146,18 @@
|
||||
<i class="fas fa-chart-pie text-gray-400 mr-2"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/volunteer/schedual"
|
||||
@click="sidebarOpen = false"
|
||||
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "schedual"}}bg-gray-100{{end}}">
|
||||
<i class="fas fa-calendar-alt text-gray-400 mr-2"></i>
|
||||
<span>My Schedule</span>
|
||||
</a>
|
||||
<a href="/volunteer/Addresses"
|
||||
@click="sidebarOpen = false"
|
||||
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "address"}}bg-gray-100{{end}}">
|
||||
<i class="fas fa-home text-gray-400 mr-2"></i>
|
||||
<span>Assigned Address</span>
|
||||
</a>
|
||||
<a href="/volunteer/schedual"
|
||||
@click="sidebarOpen = false"
|
||||
class="flex items-center text-sm text-gray-600 hover:bg-gray-100 rounded px-2 py-1 {{if eq .ActiveSection "schedual"}}bg-gray-100{{end}}">
|
||||
<i class="fas fa-calendar-alt text-gray-400 mr-2"></i>
|
||||
<span>My Schedule</span>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<a href="/profile"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div
|
||||
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold"
|
||||
>
|
||||
U
|
||||
You
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
|
||||
<!-- Phone -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Phone Number
|
||||
</label>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2"
|
||||
>Phone Number</label
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Phone Number
|
||||
</label>
|
||||
r
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
|
||||
Reference in New Issue
Block a user