Files
Poll-system/app/internal/templates/csv-upload.html
Mann Patel f1b5cdc806 csv imports
2025-08-28 23:27:24 -06:00

449 lines
15 KiB
HTML

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