csv imports

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

View File

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

View File

@@ -33,7 +33,7 @@ func AdminDashboardHandler(w http.ResponseWriter, r *http.Request) {
// 2. Total donations from polls
err = models.DB.QueryRow(`
SELECT COALESCE(SUM(amount_donated), 0)
FROM poll_responce;
FROM poll;
`).Scan(&totalDonations)
if err != nil {
log.Println("Donation query error:", err)

View File

@@ -13,6 +13,7 @@ import (
type VolunteerStatistics struct {
AppointmentsToday int
AppointmentsTomorrow int
AppointmentsThisWeek int
TotalAppointments int
PollsCompleted int
@@ -72,6 +73,7 @@ func VolunteerPostsHandler(w http.ResponseWriter, r *http.Request) {
// Get volunteer statistics
stats, err := getVolunteerStatistics(CurrentUserID)
if err != nil {
fmt.Printf("Failed to fetch statistics: %v\n", err)
// Continue with empty stats rather than failing
@@ -102,11 +104,22 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
// Get start of current week (Monday)
now := time.Now()
oneDayLater := now.Add(time.Hour * 12)
weekday := now.Weekday()
if weekday == time.Sunday {
weekday = 7
}
weekStart := now.AddDate(0, 0, -int(weekday)+1).Format("2006-01-02")
// Get start of the week (Monday)
weekStart := now.AddDate(0, 0, -int(weekday)+1)
// Get end of the week (Sunday)
weekEnd := weekStart.AddDate(0, 0, 6)
fmt.Println("Week Start:", weekStart.Format("2006-01-02"))
fmt.Println("Week End:", weekEnd.Format("2006-01-02"))
// Appointments today
err := models.DB.QueryRow(`
@@ -118,15 +131,27 @@ func getVolunteerStatistics(userID int) (*VolunteerStatistics, error) {
return nil, err
}
// Appointments tomorrow
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) = $2
`, userID, oneDayLater).Scan(&stats.AppointmentsTomorrow)
if err != nil {
return nil, err
}
// Appointments this week
err = models.DB.QueryRow(`
SELECT COUNT(*)
FROM appointment
WHERE user_id = $1 AND DATE(appointment_date) >= $2 AND DATE(appointment_date) <= $3
`, userID, weekStart, today).Scan(&stats.AppointmentsThisWeek)
`, userID, weekStart, weekEnd).Scan(&stats.AppointmentsThisWeek)
if err != nil {
return nil, err
}
fmt.Print("Stats: ", stats.AppointmentsThisWeek," Today's date: " , today ,"fasd", weekStart)
// Total appointments
err = models.DB.QueryRow(`

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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