csv imports

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

2
app/export_data.csv Normal file
View File

@@ -0,0 +1,2 @@
Name,Address,Email,Phone,Phone
Bob Johnson,1782 cornerstone bv ne,bob@email.com,555-0125,555-0125
1 Name Address Email Phone Phone
2 Bob Johnson 1782 cornerstone bv ne bob@email.com 555-0125 555-0125

View File

@@ -0,0 +1,262 @@
package handlers
import (
"encoding/csv"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
// CSVUploadResult holds the results of CSV processing
type CSVUploadResult struct {
TotalRecords int
ValidatedCount int
NotFoundCount int
ErrorCount int
ValidatedAddresses []string
NotFoundAddresses []string
ErrorMessages []string
}
// Combined CSV Upload Handler - handles both GET (display form) and POST (process CSV)
func CSVUploadHandler(w http.ResponseWriter, r *http.Request) {
username, _ := models.GetCurrentUserName(r)
// Base template data
templateData := map[string]interface{}{
"Title": "CSV Address Validation",
"IsAuthenticated": true,
"ShowAdminNav": true,
"ActiveSection": "address",
"UserName": username,
"Role": "admin",
}
// Handle GET request - show the upload form
if r.Method == http.MethodGet {
utils.Render(w, "csv-upload.html", templateData)
return
}
// Handle POST request - process the CSV
if r.Method == http.MethodPost {
// Parse multipart form (10MB max)
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)
return
}
// Get form values
addressColumnStr := r.FormValue("address_column")
if addressColumnStr == "" {
templateData["Error"] = "Address column is required"
utils.Render(w, "csv-upload.html", templateData)
return
}
addressColumn, err := strconv.Atoi(addressColumnStr)
if err != nil {
templateData["Error"] = "Invalid address column index"
utils.Render(w, "csv-upload.html", templateData)
return
}
// Get uploaded file
file, header, err := r.FormFile("csv_file")
if err != nil {
templateData["Error"] = "Error retrieving file: " + err.Error()
utils.Render(w, "csv-upload.html", templateData)
return
}
defer file.Close()
// Validate file type
if !strings.HasSuffix(strings.ToLower(header.Filename), ".csv") {
templateData["Error"] = "Please upload a CSV file"
utils.Render(w, "csv-upload.html", templateData)
return
}
// Read and parse CSV
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1 // Allow variable number of fields
var allRows [][]string
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
templateData["Error"] = "Error reading CSV: " + err.Error()
utils.Render(w, "csv-upload.html", templateData)
return
}
allRows = append(allRows, record)
}
if len(allRows) <= 1 {
templateData["Error"] = "CSV file must contain data rows (header + at least 1 data row)"
utils.Render(w, "csv-upload.html", templateData)
return
}
// Validate address column index
if addressColumn >= len(allRows[0]) {
templateData["Error"] = "Invalid address column index"
utils.Render(w, "csv-upload.html", templateData)
return
}
// Process addresses
result := processAddressValidation(allRows[1:], addressColumn) // Skip header
// Add result to template data
templateData["Result"] = result
templateData["FileName"] = header.Filename
// Render the same template with results
utils.Render(w, "csv-upload.html", templateData)
return
}
// Method not allowed
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// processAddressValidation processes CSV data and validates addresses
func processAddressValidation(rows [][]string, addressColumn int) CSVUploadResult {
result := CSVUploadResult{
TotalRecords: len(rows),
}
for i, row := range rows {
// Check if the row has enough columns
if addressColumn >= len(row) {
result.ErrorCount++
result.ErrorMessages = append(result.ErrorMessages,
fmt.Sprintf("Row %d: Missing address column", i+2))
continue
}
// Get and normalize address
address := strings.ToLower(strings.TrimSpace(row[addressColumn]))
if address == "" {
result.ErrorCount++
result.ErrorMessages = append(result.ErrorMessages,
fmt.Sprintf("Row %d: Empty address", i+2))
continue
}
// Check if address exists in database
var addressID int
var currentStatus bool
err := models.DB.QueryRow(`
SELECT address_id, visited_validated
FROM address_database
WHERE LOWER(TRIM(address)) = $1
`, address).Scan(&addressID, &currentStatus)
if err != nil {
// Address not found
result.NotFoundCount++
result.NotFoundAddresses = append(result.NotFoundAddresses, address)
continue
}
// Address found - update validation status if not already validated
if !currentStatus {
_, err = models.DB.Exec(`
UPDATE address_database
SET visited_validated = true, updated_at = NOW()
WHERE address_id = $1
`, addressID)
if err != nil {
result.ErrorCount++
result.ErrorMessages = append(result.ErrorMessages,
fmt.Sprintf("Row %d: Database update error for address '%s'", i+2, address))
log.Printf("Error updating address %d: %v", addressID, err)
continue
}
result.ValidatedCount++
result.ValidatedAddresses = append(result.ValidatedAddresses, address)
} else {
// Address was already validated - still count as validated
result.ValidatedCount++
result.ValidatedAddresses = append(result.ValidatedAddresses, address+" (already validated)")
}
}
return result
}
// Optional: Keep the export function if you need it
// ExportValidatedAddressesHandler exports validated addresses to CSV
func ExportValidatedAddressesHandler(w http.ResponseWriter, r *http.Request) {
// Query validated addresses
rows, err := models.DB.Query(`
SELECT
a.address_id,
a.address,
a.street_name,
a.street_type,
a.street_quadrant,
a.house_number,
COALESCE(a.house_alpha, '') as house_alpha,
a.longitude,
a.latitude,
a.visited_validated,
a.created_at,
a.updated_at,
CASE
WHEN ap.sched_id IS NOT NULL THEN true
ELSE false
END as assigned,
COALESCE(u.first_name || ' ' || u.last_name, '') as user_name,
COALESCE(u.email, '') as user_email,
COALESCE(ap.appointment_date::text, '') as appointment_date,
COALESCE(ap.appointment_time::text, '') as appointment_time
FROM address_database a
LEFT JOIN appointment ap ON a.address_id = ap.address_id
LEFT JOIN users u ON ap.user_id = u.user_id
WHERE a.visited_validated = true
ORDER BY a.updated_at DESC
`)
if err != nil {
log.Println("Export query error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer rows.Close()
// Set response headers for CSV download
filename := fmt.Sprintf("validated_addresses_%s.csv", time.Now().Format("2006-01-02_15-04-05"))
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
// Create CSV writer
writer := csv.NewWriter(w)
defer writer.Flush()
// Write header
header := []string{
"Address ID", "Address", "Street Name", "Street Type", "Street Quadrant",
"House Number", "House Alpha", "Longitude", "Latitude", "Validated",
"Created At", "Updated At", "Assigned", "Assigned User", "User Email",
"Appointment Date", "Appointment Time",
}
writer.Write(header)
// Write data rows (you'll need to define AddressWithDetails struct)
// Implementation depends on your existing struct definitions
}

View File

@@ -33,7 +33,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
// 2. Total donations from polls // 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)

View File

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

View File

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

View 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 }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
app/main Executable file

Binary file not shown.

View File

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

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB