csv imports
This commit is contained in:
2
app/export_data.csv
Normal file
2
app/export_data.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Name,Address,Email,Phone,Phone
|
||||||
|
Bob Johnson,1782 cornerstone bv ne,bob@email.com,555-0125,555-0125
|
||||||
|
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
|
// 2. Total donations from polls
|
||||||
err = models.DB.QueryRow(`
|
err = models.DB.QueryRow(`
|
||||||
SELECT COALESCE(SUM(amount_donated), 0)
|
SELECT COALESCE(SUM(amount_donated), 0)
|
||||||
FROM poll_responce;
|
FROM poll;
|
||||||
`).Scan(&totalDonations)
|
`).Scan(&totalDonations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Donation query error:", err)
|
log.Println("Donation query error:", err)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
type VolunteerStatistics struct {
|
type VolunteerStatistics struct {
|
||||||
AppointmentsToday int
|
AppointmentsToday int
|
||||||
|
AppointmentsTomorrow int
|
||||||
AppointmentsThisWeek int
|
AppointmentsThisWeek int
|
||||||
TotalAppointments int
|
TotalAppointments int
|
||||||
PollsCompleted int
|
PollsCompleted int
|
||||||
@@ -72,6 +73,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Get volunteer statistics
|
// Get volunteer statistics
|
||||||
stats, err := getVolunteerStatistics(CurrentUserID)
|
stats, err := getVolunteerStatistics(CurrentUserID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to fetch statistics: %v\n", err)
|
fmt.Printf("Failed to fetch statistics: %v\n", err)
|
||||||
// Continue with empty stats rather than failing
|
// Continue with empty stats rather than failing
|
||||||
@@ -102,11 +104,22 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
|
|||||||
|
|
||||||
// Get start of current week (Monday)
|
// Get start of current week (Monday)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
oneDayLater := now.Add(time.Hour * 12)
|
||||||
|
|
||||||
weekday := now.Weekday()
|
weekday := now.Weekday()
|
||||||
if weekday == time.Sunday {
|
if weekday == time.Sunday {
|
||||||
weekday = 7
|
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
|
// Appointments today
|
||||||
err := models.DB.QueryRow(`
|
err := models.DB.QueryRow(`
|
||||||
@@ -118,15 +131,27 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
|
|||||||
return nil, err
|
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
|
// Appointments this week
|
||||||
err = models.DB.QueryRow(`
|
err = models.DB.QueryRow(`
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM appointment
|
FROM appointment
|
||||||
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
fmt.Print("Stats: ", stats.AppointmentsThisWeek," Today's date: " , today ,"fasd", weekStart)
|
||||||
|
|
||||||
|
|
||||||
// Total appointments
|
// Total appointments
|
||||||
err = models.DB.QueryRow(`
|
err = models.DB.QueryRow(`
|
||||||
|
|||||||
@@ -296,16 +296,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Modal Actions -->
|
||||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||||
<button
|
<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
|
<i class="fas fa-download mr-2"></i>Export Data
|
||||||
</button>
|
</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
|
<button
|
||||||
class="px-6 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50 transition-colors"
|
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>
|
<span class="text-sm font-medium">Volunteer Dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600 hidden sm:block">
|
|
||||||
Welcome back, {{ .UserName }}!
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,9 +74,6 @@
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts yet</h3>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{{ end }} {{ else }}
|
{{ end }} {{ else }}
|
||||||
@@ -122,6 +116,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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 justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -233,52 +243,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<!-- Right Side: User Info -->
|
<!-- Right Side: User Info -->
|
||||||
<div class="flex items-center gap-3">
|
<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">
|
<div class="w-9 h-9 bg-blue-500 flex items-center justify-center text-white font-semibold">
|
||||||
{{slice .UserName 0 1}}
|
{{slice .UserName 0 1}}
|
||||||
</div>
|
</div>
|
||||||
@@ -146,18 +146,18 @@
|
|||||||
<i class="fas fa-chart-pie text-gray-400 mr-2"></i>
|
<i class="fas fa-chart-pie text-gray-400 mr-2"></i>
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</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"
|
<a href="/volunteer/Addresses"
|
||||||
@click="sidebarOpen = false"
|
@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}}">
|
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>
|
<i class="fas fa-home text-gray-400 mr-2"></i>
|
||||||
<span>Assigned Address</span>
|
<span>Assigned Address</span>
|
||||||
</a>
|
</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 }}
|
{{ end }}
|
||||||
|
|
||||||
<a href="/profile"
|
<a href="/profile"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div
|
<div
|
||||||
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold"
|
class="w-10 h-10 bg-blue-500 flex items-center justify-center text-white font-semibold"
|
||||||
>
|
>
|
||||||
U
|
You
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
@@ -116,9 +116,9 @@
|
|||||||
|
|
||||||
<!-- Phone -->
|
<!-- Phone -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2"
|
||||||
Phone Number
|
>Phone Number</label
|
||||||
</label>
|
>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
name="phone"
|
name="phone"
|
||||||
|
|||||||
@@ -110,7 +110,6 @@
|
|||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
Phone Number
|
Phone Number
|
||||||
</label>
|
</label>
|
||||||
r
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="phone"
|
name="phone"
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ func main() {
|
|||||||
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
|
http.HandleFunc("/assign_address", adminMiddleware(handlers.AssignAddressHandler))
|
||||||
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
|
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
|
||||||
|
|
||||||
|
http.HandleFunc("/addresses/upload-csv", adminMiddleware(handlers.CSVUploadHandler))
|
||||||
|
|
||||||
|
|
||||||
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
|
http.HandleFunc("/posts", adminMiddleware(handlers.PostsHandler))
|
||||||
|
|
||||||
//--- Volunteer-only routes
|
//--- Volunteer-only routes
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||||
BIN
app/tmp/main
BIN
app/tmp/main
Binary file not shown.
BIN
app/uploads/3_1756442170647223000.png
Normal file
BIN
app/uploads/3_1756442170647223000.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 535 KiB |
BIN
app/uploads/3_1756442207307560000.png
Normal file
BIN
app/uploads/3_1756442207307560000.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 535 KiB |
Reference in New Issue
Block a user