CSV Import is now working

This commit is contained in:
Mann Patel
2025-09-03 14:35:47 -06:00
parent 7f2b7e481a
commit 86d733e80e
20 changed files with 2160 additions and 1626 deletions

View File

@@ -10,233 +10,365 @@
rel="stylesheet"
/>
<script src="https://cdn.tailwindcss.com"></script>
<script
type="text/javascript"
src="https://www.gstatic.com/charts/loader.js"
></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ol@7.5.2/ol.css"
/>
<script src="https://cdn.jsdelivr.net/npm/ol@7.5.2/dist/ol.js"></script>
<style>
/* CRITICAL: Prevent any duplicate maps */
.ol-viewport {
max-width: 100% !important;
max-height: 700px !important;
}
#single-map {
width: 100%;
height: 700px;
border: 1px solid #e5e7eb;
}
.map-controls {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.control-button {
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.ol-popup {
position: absolute;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 15px;
border-radius: 8px;
border: 1px solid #e5e7eb;
bottom: 12px;
left: -50px;
min-width: 200px;
max-width: 300px;
}
.ol-popup:after {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(255, 255, 255, 0);
border-top-color: #ffffff;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
</style>
</head>
<body class="bg-gray-50">
<!-- Full Width Container -->
<div class="min-h-screen w-full flex flex-col">
<!-- Main Dashboard Content -->
<div class="w-full">
<!-- Full Width Container -->
<div class="min-h-screen w-full flex flex-col">
<!-- Top Navigation Bar -->
<div class="bg-white border-b border-gray-200 w-full">
<div class="px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-600 flex items-center justify-center">
<i class="fas fa-chart-bar text-white text-sm"></i>
</div>
<span class="text-xl font-semibold text-gray-900">
Dashboard Overview
</span>
</div>
<div class="flex items-center gap-4">
<button
class="px-6 py-2.5 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
>
<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"
>
<i class="fas fa-filter mr-2"></i>Filter
</button> -->
<!-- Navigation -->
<div class="bg-white border-b border-gray-200 w-full">
<div class="px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-blue-600 flex items-center justify-center">
<i class="fas fa-chart-bar text-white text-sm"></i>
</div>
<span class="text-xl font-semibold text-gray-900"
>Dashboard Overview</span
>
</div>
</div>
</div>
<!-- Stats Grid - Full Width -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 bg-white border-b border-gray-200"
>
<!-- Active Volunteers -->
<div
class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer"
onclick="focusMap()"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-blue-50 flex items-center justify-center"
>
<i class="fas fa-users text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Active Volunteers
</p>
<p class="text-2xl font-bold text-gray-900">
{{.VolunteerCount}}
</p>
</div>
</div>
</div>
<!-- Addresses Visited -->
<div
class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer"
onclick="updateChart('visitors')"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-blue-50 flex items-center justify-center"
>
<i class="fas fa-map-marker-alt text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Addresses Visited
</p>
<p class="text-2xl font-bold text-gray-900">
{{.ValidatedCount}}
</p>
</div>
</div>
</div>
<!-- Total Donations -->
<div
class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer"
onclick="updateChart('revenue')"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-blue-50 flex items-center justify-center"
>
<i class="fas fa-dollar-sign text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Donation</p>
<p class="text-2xl font-bold text-gray-900">
${{.TotalDonations}}
</p>
</div>
</div>
</div>
<!-- Houses Left -->
<div
class="p-8 hover:bg-gray-50 transition-colors cursor-pointer"
onclick="updateChart('conversion')"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-blue-50 flex items-center justify-center"
>
<i class="fas fa-percentage text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Houses Left
</p>
<p class="text-2xl font-bold text-gray-900">
{{.HousesLeftPercent}}%
</p>
</div>
</div>
</div>
</div>
<!-- Map Section - Full Width -->
<div class="bg-white w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">
Location Analytics
</h3>
<div id="map" class="w-full h-[850px] border border-gray-200"></div>
<div class="flex items-center gap-4">
<button
class="px-6 py-2.5 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
onclick="refreshMap()"
>
<i class="fas fa-sync-alt mr-2"></i>Refresh Map
</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>
</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 bg-white border-b border-gray-200"
>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-50 flex items-center justify-center">
<i class="fas fa-users text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Active Volunteers
</p>
<p class="text-2xl font-bold text-gray-900">{{.VolunteerCount}}</p>
</div>
</div>
</div>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-green-50 flex items-center justify-center">
<i class="fas fa-map-marker-alt text-green-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">
Addresses Visited
</p>
<p class="text-2xl font-bold text-gray-900">{{.ValidatedCount}}</p>
<p id="marker-count" class="text-xs text-gray-500">Loading...</p>
</div>
</div>
</div>
<div class="border-r border-gray-200 p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-yellow-50 flex items-center justify-center">
<i class="fas fa-dollar-sign text-yellow-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Donation</p>
<p class="text-2xl font-bold text-gray-900">${{.TotalDonations}}</p>
</div>
</div>
</div>
<div class="p-8">
<div class="flex items-center">
<div class="w-12 h-12 bg-red-50 flex items-center justify-center">
<i class="fas fa-percentage text-red-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Houses Left</p>
<p class="text-2xl font-bold text-gray-900">
{{.HousesLeftPercent}}%
</p>
</div>
</div>
</div>
</div>
<!-- SINGLE MAP SECTION -->
<div class="bg-white w-full relative">
<div class="map-controls">
<button class="control-button" onclick="refreshMap()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button class="control-button" onclick="fitAllMarkers()">
<i class="fas fa-expand-arrows-alt"></i> Fit All
</button>
<button class="control-button" onclick="clearAllMarkers()">
<i class="fas fa-trash"></i> Clear
</button>
</div>
<!-- THIS IS THE ONLY MAP CONTAINER -->
<div id="single-map"></div>
<div id="popup" class="ol-popup">
<a
href="#"
id="popup-closer"
style="
position: absolute;
top: 8px;
right: 8px;
text-decoration: none;
"
>×</a
>
<div id="popup-content"></div>
</div>
</div>
<script>
let map;
// Global variables - only one set
let theMap = null;
let markerLayer = null;
let popup = null;
let initialized = false;
function focusMap() {
// Center map example
map.setCenter({ lat: 43.0896, lng: -79.0849 }); // Niagara Falls
map.setZoom(12);
// Clean initialization
function initializeMap() {
if (initialized || !window.ol) {
console.log("Map already initialized or OpenLayers not ready");
return;
}
console.log("Initializing single map...");
try {
// Calgary coordinates
const center = ol.proj.fromLonLat([-114.0719, 51.0447]);
// Create the ONE AND ONLY map
theMap = new ol.Map({
target: "single-map",
layers: [
new ol.layer.Tile({
source: new ol.source.OSM(),
}),
],
view: new ol.View({
center: center,
zoom: 11,
}),
});
// Create popup
popup = new ol.Overlay({
element: document.getElementById("popup"),
positioning: "bottom-center",
stopEvent: false,
offset: [0, -50],
});
theMap.addOverlay(popup);
// Close popup handler
document.getElementById("popup-closer").onclick = function () {
popup.setPosition(undefined);
return false;
};
// Create marker layer
markerLayer = new ol.layer.Vector({
source: new ol.source.Vector(),
style: new ol.style.Style({
text: new ol.style.Text({
text: "📍",
font: "24px sans-serif",
fill: new ol.style.Fill({ color: "#EF4444" }),
offsetY: -12, // Adjust vertical position so pin points to location
}),
}),
});
theMap.addLayer(markerLayer);
// Click handler
theMap.on("click", function (event) {
const feature = theMap.forEachFeatureAtPixel(
event.pixel,
function (feature) {
return feature;
}
);
if (feature && feature.get("address_data")) {
const data = feature.get("address_data");
document.getElementById("popup-content").innerHTML = `
<div class="text-sm">
<h4 class="font-semibold text-gray-900 mb-2">Address Details</h4>
<p><strong>Address:</strong> ${data.address}</p>
<p><strong>House #:</strong> ${data.house_number}</p>
<p><strong>Street:</strong> ${data.street_name} ${data.street_type}</p>
<p><strong>ID:</strong> ${data.address_id}</p>
</div>
`;
popup.setPosition(event.coordinate);
} else {
popup.setPosition(undefined);
}
});
initialized = true;
console.log("Map initialized successfully");
// Load markers
setTimeout(loadMarkers, 500);
} catch (error) {
console.error("Map initialization error:", error);
}
}
function initMap() {
const niagaraFalls = { lat: 43.0896, lng: -79.0849 };
// Load validated addresses
async function loadMarkers() {
try {
const response = await fetch("/api/validated-addresses");
const addresses = await response.json();
map = new google.maps.Map(document.getElementById("map"), {
zoom: 12,
center: niagaraFalls,
});
console.log(`Loading ${addresses.length} addresses`);
document.getElementById(
"marker-count"
).textContent = `${addresses.length} on map`;
new google.maps.Marker({
position: niagaraFalls,
map,
title: "Niagara Falls",
});
// Clear existing markers
markerLayer.getSource().clear();
// Add new markers
const features = [];
addresses.forEach((addr) => {
if (addr.longitude && addr.latitude) {
const coords = ol.proj.fromLonLat([
addr.longitude,
addr.latitude,
]);
const feature = new ol.Feature({
geometry: new ol.geom.Point(coords),
address_data: addr,
});
features.push(feature);
}
});
markerLayer.getSource().addFeatures(features);
if (features.length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
} catch (error) {
console.error("Error loading markers:", error);
document.getElementById("marker-count").textContent = "Error loading";
}
}
// Google Charts
google.charts.load("current", { packages: ["corechart", "line"] });
google.charts.setOnLoadCallback(drawAnalyticsChart);
function drawAnalyticsChart() {
var data = new google.visualization.DataTable();
data.addColumn("string", "Time");
data.addColumn("number", "Visitors");
data.addColumn("number", "Revenue");
data.addRows([
["Jan", 4200, 32000],
["Feb", 4800, 38000],
["Mar", 5200, 42000],
["Apr", 4900, 39000],
["May", 5800, 45000],
["Jun", 6200, 48000],
]);
var options = {
title: "Performance Over Time",
backgroundColor: "transparent",
hAxis: { title: "Month" },
vAxis: { title: "Value" },
colors: ["#3B82F6", "#10B981"],
chartArea: {
left: 60,
top: 40,
width: "90%",
height: "70%",
},
legend: { position: "top", alignment: "center" },
};
var chart = new google.visualization.LineChart(
document.getElementById("analytics_chart")
);
chart.draw(data, options);
// Control functions
function refreshMap() {
loadMarkers();
}
function updateChart(type) {
drawAnalyticsChart();
function fitAllMarkers() {
if (markerLayer && markerLayer.getSource().getFeatures().length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView().fit(extent, { padding: [20, 20, 20, 20] });
}
}
function clearAllMarkers() {
if (markerLayer) {
markerLayer.getSource().clear();
}
if (popup) {
popup.setPosition(undefined);
}
}
// Initialize when ready
document.addEventListener("DOMContentLoaded", function () {
setTimeout(initializeMap, 1000);
});
</script>
<script
async
defer
src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE&callback=initMap"
></script>
</body>
</html>
{{ end }}