449 lines
15 KiB
HTML
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 }}
|