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

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