Files
Poll-system/app/internal/templates/dashboard.html
2025-09-09 10:42:24 -06:00

325 lines
8.8 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>
</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>
let theMap = null;
let markerLayer = null;
let popup = null;
let initialized = false;
function initializeMap() {
if (initialized || !window.ol) return;
try {
const center = ol.proj.fromLonLat([-114.0719, 51.0447]);
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,
}),
});
popup = new ol.Overlay({
element: document.getElementById("popup"),
positioning: "bottom-center",
stopEvent: false,
offset: [0, -50],
});
theMap.addOverlay(popup);
document.getElementById("popup-closer").onclick = function () {
popup.setPosition(undefined);
return false;
};
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);
theMap.on("click", function (event) {
const feature = theMap.forEachFeatureAtPixel(event.pixel, f => f);
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;
setTimeout(loadMarkers, 500);
} catch (error) {
console.error("Map initialization error:", error);
}
}
async function loadMarkers() {
try {
const response = await fetch("/api/validated-addresses");
const addresses = await response.json();
document.getElementById("marker-count").textContent = `${addresses.length} on map`;
markerLayer.getSource().clear();
const features = addresses
.filter(addr => addr.longitude && addr.latitude)
.map(addr => new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([addr.longitude, addr.latitude])),
address_data: addr,
}));
markerLayer.getSource().addFeatures(features);
if (features.length > 0) {
const extent = markerLayer.getSource().getExtent();
theMap.getView(extent, { padding: [20, 20, 20, 20] });
}
} catch (error) {
console.error("Error loading markers:", error);
document.getElementById("marker-count").textContent = "Error loading";
}
}
function refreshMap() {
const view = theMap.getView();
const calgaryCenter = ol.proj.fromLonLat([-114.0719, 51.0447]); // Downtown Calgary
view.setCenter(calgaryCenter);
view.setZoom(11); // your default zoom leve
loadMarkers();
}
document.addEventListener("DOMContentLoaded", () => {
setTimeout(initializeMap, 1000);
});
function handleSidebarToggle() {
const sidebar = document.getElementById('sidebar');
document.body.classList.toggle('sidebar-open', sidebar && sidebar.classList.contains('active'));
}
if (typeof window.toggleSidebar === 'function') {
const originalToggleSidebar = window.toggleSidebar;
window.toggleSidebar = function() {
originalToggleSidebar();
setTimeout(handleSidebarToggle, 50);
};
}
</script>
{{ end }}