Files
Poll-system/app/internal/templates/dashboard.html
2025-09-05 15:39:06 -06:00

387 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ define "content" }}
<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>
#single-map {
width: 100%;
height: calc(100vh - 80px); /* Account for header height */
border: 1px solid #e5e7eb;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Hide map controls when sidebar is active on mobile */
@media (max-width: 768px) {
#single-map {
height: 50vh; /* Smaller height on mobile */
}
body.sidebar-open .map-controls {
display: none;
}
}
.control-button {
background: white;
border: 1px solid #ccc;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: all 0.2s;
}
.control-button:hover {
background: #f9fafb;
}
.ol-popup {
position: absolute;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
bottom: 12px;
left: -50px;
min-width: 180px;
max-width: 280px;
z-index: 200;
}
.ol-popup:after {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: #ffffff;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
/* Ensure OpenLayers controls stay below sidebar */
.ol-control {
z-index: 150 !important;
}
/* Hide popup when sidebar is open on mobile */
@media (max-width: 768px) {
body.sidebar-open .ol-popup {
display: none !important;
}
}
/* Dashboard layout */
.dashboard-container {
display: flex;
flex-direction: column;
height: calc(100vh - 80px); /* Account for header */
}
@media (min-width: 1024px) {
.dashboard-container {
flex-direction: row;
}
#single-map {
height: 100%;
}
}
.map-section {
flex: 1;
position: relative;
min-height: 50vh;
}
.stats-section {
width: 100%;
max-height: 50vh;
overflow-y: auto;
padding: 1rem;
background: #f9fafb;
}
@media (min-width: 1024px) {
.stats-section {
width: 20rem;
max-height: 100%;
}
}
</style>
<!-- Dashboard Layout -->
<div class="dashboard-container">
<!-- Left: Map -->
<div class="map-section bg-white">
<div class="map-controls">
<button class="control-button" onclick="refreshMap()" title="Refresh Map">
<i class="fas fa-sync-alt"></i>
</button>
<button class="control-button" onclick="fitAllMarkers()" title="Fit All Markers">
<i class="fas fa-expand-arrows-alt"></i>
</button>
<button class="control-button" onclick="clearAllMarkers()" title="Clear All Markers">
<i class="fas fa-trash"></i>
</button>
</div>
<div id="single-map"></div>
<div id="popup" class="ol-popup">
<a
href="#"
id="popup-closer"
class="absolute top-1 right-2 text-gray-500 hover:text-gray-800"
>×</a
>
<div id="popup-content"></div>
</div>
</div>
<!-- Right: Stats -->
<div class="stats-section">
<div class="flex flex-col gap-4">
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-blue-50 flex items-center justify-center rounded">
<i class="fas fa-users text-blue-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Active Volunteers</p>
<p class="text-xl font-bold text-gray-900">{{.VolunteerCount}}</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-green-50 flex items-center justify-center rounded">
<i class="fas fa-map-marker-alt text-green-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Addresses Visited</p>
<p class="text-xl font-bold text-gray-900">{{.ValidatedCount}}</p>
<p id="marker-count" class="text-xs text-gray-500">Loading...</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-yellow-50 flex items-center justify-center rounded">
<i class="fas fa-dollar-sign text-yellow-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Donation</p>
<p class="text-xl font-bold text-gray-900">${{.TotalDonations}}</p>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 flex items-center">
<div class="w-10 h-10 bg-red-50 flex items-center justify-center rounded">
<i class="fas fa-percentage text-red-600 text-lg"></i>
</div>
<div class="ml-3">
<p class="text-sm text-gray-600">Houses Left</p>
<p class="text-xl font-bold text-gray-900">{{.HousesLeftPercent}}%</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables - only one set
let theMap = null;
let markerLayer = null;
let popup = null;
let initialized = false;
// 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,
}),
}),
});
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);
}
}
// Load validated addresses
async function loadMarkers() {
try {
const response = await fetch("/api/validated-addresses");
const addresses = await response.json();
console.log(`Loading ${addresses.length} addresses`);
document.getElementById("marker-count").textContent = `${addresses.length} on map`;
// 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";
}
}
// Control functions
function refreshMap() {
loadMarkers();
}
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);
});
// Listen for sidebar state changes to manage map controls visibility
function handleSidebarToggle() {
const sidebar = document.getElementById('sidebar');
const body = document.body;
if (sidebar && sidebar.classList.contains('active')) {
body.classList.add('sidebar-open');
} else {
body.classList.remove('sidebar-open');
}
}
// Override the original toggleSidebar function to handle map controls
if (typeof window.toggleSidebar === 'function') {
const originalToggleSidebar = window.toggleSidebar;
window.toggleSidebar = function() {
originalToggleSidebar();
setTimeout(handleSidebarToggle, 50);
};
}
</script>
{{ end }}