feat: validate user availability
This commit is contained in:
@@ -18,10 +18,10 @@
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
|
||||
|
||||
|
||||
<button
|
||||
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors"
|
||||
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors rounded-lg"
|
||||
onclick="window.location.href='/addresses/upload-csv'"
|
||||
>
|
||||
<i class="fas fa-file-import mr-2"></i>Import Data
|
||||
@@ -39,7 +39,7 @@
|
||||
<option value="100" {{if eq .Pagination.PageSize 100}}selected{{end}}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick="goToPage({{.Pagination.PreviousPage}})"
|
||||
@@ -67,7 +67,7 @@
|
||||
<!-- Table Container -->
|
||||
<div class="flex-1 p-4 md:p-6 overflow-auto">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
|
||||
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden lg:block overflow-x-auto">
|
||||
<table class="w-full min-w-full">
|
||||
@@ -141,8 +141,9 @@
|
||||
<button
|
||||
class="px-3 py-1 bg-gray-100 text-gray-500 text-sm rounded-md cursor-not-allowed"
|
||||
disabled
|
||||
hidden
|
||||
>
|
||||
Assigned
|
||||
<i class="fa-solid fa-plus text-orange-400"></i>
|
||||
</button>
|
||||
<form action="/remove_assigned_address" method="POST" class="inline-block">
|
||||
<input type="hidden" name="address_id" value="{{ .AddressID }}" />
|
||||
@@ -151,16 +152,17 @@
|
||||
type="submit"
|
||||
class="text-red-400 hover:text-red-600 p-1"
|
||||
title="Remove assignment"
|
||||
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{{ else }}
|
||||
<button
|
||||
class="px-3 py-1 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition-colors"
|
||||
class="px-3 py-1 bg-blue-100 text-white text-sm rounded-md hover:bg-blue-600 transition-colors"
|
||||
onclick="openAssignModal({{ .AddressID }}, '{{ .Address }}')"
|
||||
>
|
||||
Assign
|
||||
<i class="fa-solid fa-plus text-blue-500"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#single-map {
|
||||
height: 50vh; /* Smaller height on mobile */
|
||||
}
|
||||
|
||||
body.sidebar-open .map-controls {
|
||||
display: none;
|
||||
}
|
||||
@@ -98,7 +97,6 @@
|
||||
.dashboard-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#single-map {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -134,12 +132,6 @@
|
||||
<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>
|
||||
|
||||
@@ -202,26 +194,17 @@
|
||||
</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...");
|
||||
if (initialized || !window.ol) return;
|
||||
|
||||
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: [
|
||||
@@ -235,7 +218,6 @@
|
||||
}),
|
||||
});
|
||||
|
||||
// Create popup
|
||||
popup = new ol.Overlay({
|
||||
element: document.getElementById("popup"),
|
||||
positioning: "bottom-center",
|
||||
@@ -244,13 +226,11 @@
|
||||
});
|
||||
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({
|
||||
@@ -264,26 +244,18 @@
|
||||
});
|
||||
theMap.addLayer(markerLayer);
|
||||
|
||||
// Click handler
|
||||
theMap.on("click", function (event) {
|
||||
const feature = theMap.forEachFeatureAtPixel(
|
||||
event.pixel,
|
||||
function (feature) {
|
||||
return feature;
|
||||
}
|
||||
);
|
||||
|
||||
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>
|
||||
`;
|
||||
<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);
|
||||
@@ -291,45 +263,32 @@
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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().fit(extent, { padding: [20, 20, 20, 20] });
|
||||
theMap.getView(extent, { padding: [20, 20, 20, 20] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading markers:", error);
|
||||
@@ -337,45 +296,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Control functions
|
||||
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();
|
||||
}
|
||||
|
||||
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 () {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
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');
|
||||
}
|
||||
document.body.classList.toggle('sidebar-open', sidebar && sidebar.classList.contains('active'));
|
||||
}
|
||||
|
||||
// Override the original toggleSidebar function to handle map controls
|
||||
if (typeof window.toggleSidebar === 'function') {
|
||||
const originalToggleSidebar = window.toggleSidebar;
|
||||
window.toggleSidebar = function() {
|
||||
@@ -384,4 +321,4 @@
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile sidebar overlay */
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
@@ -106,7 +106,7 @@
|
||||
{{ if .IsAuthenticated }}
|
||||
<!-- Authenticated User Interface -->
|
||||
<div class="min-h-screen">
|
||||
|
||||
|
||||
<!-- Mobile sidebar overlay -->
|
||||
<div id="sidebar-overlay" class="sidebar-overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
@@ -167,11 +167,6 @@
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
<a href="/profile" class="flex items-center px-3 py-2.5 text-sm {{if eq .ActiveSection "profile"}}bg-blue-light text-blue-primary border-r-4 border-blue-primary pl-2 rounded-none{{else}}text-text-secondary hover:bg-gray-50 rounded-md{{end}} group">
|
||||
<i class="fas fa-user-circle w-5 {{if eq .ActiveSection "profile"}}text-blue-primary{{else}}text-gray-400{{end}} mr-3"></i>
|
||||
<span {{if eq .ActiveSection "profile"}}class="font-medium"{{end}}>Profile</span>
|
||||
</a>
|
||||
|
||||
<a href="/logout" class="flex items-center px-3 py-2.5 text-sm text-text-secondary hover:bg-gray-50 rounded-md group">
|
||||
<i class="fas fa-sign-out-alt w-5 text-gray-400 mr-3"></i>
|
||||
<span>Logout</span>
|
||||
@@ -183,7 +178,7 @@
|
||||
<!-- Main Content Container -->
|
||||
<div class="main-content-container min-h-screen flex flex-col bg-custom-gray">
|
||||
<!-- Top Header -->
|
||||
<div class="bg-white border-b border-border-gray px-4 md:px-6 py-4">
|
||||
<div class="fixed top-0 left-0 right-0 z-20 bg-white border-b border-border-gray px-4 md:px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Hamburger (left aligned with consistent spacing) -->
|
||||
<div class="flex items-center">
|
||||
@@ -195,9 +190,9 @@
|
||||
<!-- Right side -->
|
||||
<div class="flex items-center space-x-2 md:space-x-4">
|
||||
<!-- Dark mode -->
|
||||
<button class="text-text-secondary hover:text-text-primary p-2">
|
||||
<i class="fas fa-moon text-lg"></i>
|
||||
</button>
|
||||
<a href="/logout" class="text-text-secondary hover:text-text-primary p-2">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket text-lg"></i>
|
||||
</a>
|
||||
|
||||
<!-- Profile (hover dropdown on desktop, click on mobile) -->
|
||||
<div class="relative group cursor-pointer">
|
||||
@@ -211,7 +206,7 @@
|
||||
<!-- Dropdown -->
|
||||
<div id="profile-menu" class="absolute right-0 mt-2 w-40 bg-white border border-border-gray rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
<a href="/profile" class="block px-4 py-2 text-sm text-text-primary hover:bg-gray-100">Profile</a>
|
||||
<a href="#" class="block px-4 py-2 text-sm text-text-primary hover:bg-gray-100">Settings</a>
|
||||
<a href="/profile" class="block px-4 py-2 text-sm text-text-primary hover:bg-gray-100">Settings</a>
|
||||
<a href="/logout" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +215,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 mt-20">
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,15 +224,15 @@
|
||||
{{else}}
|
||||
<!-- Split Screen Login/Register Page -->
|
||||
<div class="min-h-screen flex" x-data="{ isLogin: true }">
|
||||
|
||||
|
||||
<!-- Left Side - Image -->
|
||||
<div class="hidden lg:flex flex-1 relative overflow-hidden">
|
||||
<!-- Background overlay for better text readability -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-blue-700/40 z-10"></div>
|
||||
|
||||
|
||||
<!-- Background Image -->
|
||||
<img src="../../static/feature-mobile1.jpg" alt="Welcome to Poll System" class="object-cover w-full h-full"/>
|
||||
|
||||
|
||||
<!-- Logo and branding overlay -->
|
||||
<div class="absolute top-8 left-8 z-20 flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
|
||||
@@ -245,7 +240,7 @@
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-white">Linq</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Welcome text overlay -->
|
||||
<div class="absolute bottom-8 left-8 right-8 z-20 text-white">
|
||||
<h1 class="text-4xl font-bold mb-4">Welcome to Poll System</h1>
|
||||
@@ -256,7 +251,7 @@
|
||||
<!-- Right Side - Login/Register Forms -->
|
||||
<div class="flex-1 flex items-center justify-center p-6 lg:p-12 bg-white">
|
||||
<div class="w-full max-w-md">
|
||||
|
||||
|
||||
<!-- Mobile Logo (visible only on small screens) -->
|
||||
<div class="lg:hidden flex items-center justify-center gap-3 mb-8">
|
||||
<div class="w-10 h-10 bg-blue-primary rounded-full flex items-center justify-center">
|
||||
@@ -282,27 +277,27 @@
|
||||
<h2 class="text-3xl font-bold text-text-primary">Welcome back</h2>
|
||||
<p class="text-text-secondary mt-2">Please sign in to your account</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="login_email" class="block text-sm font-medium text-text-primary mb-2">Email Address</label>
|
||||
<input type="email"
|
||||
id="login_email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
<input type="email"
|
||||
id="login_email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label for="login_password" class="block text-sm font-medium text-text-primary mb-2">Password</label>
|
||||
<input type="password"
|
||||
id="login_password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
<input type="password"
|
||||
id="login_password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" class="h-4 w-4 text-blue-primary focus:ring-blue-primary border-gray-300 rounded">
|
||||
@@ -310,7 +305,7 @@
|
||||
</label>
|
||||
<a href="#" class="text-sm text-blue-primary hover:text-blue-600">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="w-full bg-blue-primary text-white py-3 rounded-lg hover:bg-blue-600 focus:ring-2 focus:ring-blue-primary focus:ring-offset-2 transition-colors font-medium">
|
||||
Sign In
|
||||
</button>
|
||||
@@ -324,49 +319,49 @@
|
||||
<h2 class="text-3xl font-bold text-text-primary">Create Account</h2>
|
||||
<p class="text-text-secondary mt-2">Join our polling platform today</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">First Name</label>
|
||||
<input type="text"
|
||||
name="first_name"
|
||||
required
|
||||
placeholder="First Name"
|
||||
<input type="text"
|
||||
name="first_name"
|
||||
required
|
||||
placeholder="First Name"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Last Name</label>
|
||||
<input type="text"
|
||||
name="last_name"
|
||||
required
|
||||
placeholder="Last Name"
|
||||
<input type="text"
|
||||
name="last_name"
|
||||
required
|
||||
placeholder="Last Name"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Email Address</label>
|
||||
<input type="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
<input type="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Phone</label>
|
||||
<input type="tel"
|
||||
<input type="tel"
|
||||
name="phone"
|
||||
required
|
||||
placeholder="Phone number"
|
||||
placeholder="Phone number"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Role</label>
|
||||
<select name="role"
|
||||
required
|
||||
onchange="toggleAdminCodeField()"
|
||||
<select name="role"
|
||||
required
|
||||
onchange="toggleAdminCodeField()"
|
||||
id="role"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors">
|
||||
<option value="">Select Role</option>
|
||||
@@ -374,26 +369,26 @@
|
||||
<option value="3">Volunteer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Admin/Team Leader Code Field (hidden by default) -->
|
||||
<div id="adminCodeField" class="hidden">
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Access Code</label>
|
||||
<input type="password"
|
||||
name="admin_code"
|
||||
placeholder="Enter access code"
|
||||
<input type="password"
|
||||
name="admin_code"
|
||||
placeholder="Enter access code"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
<p class="text-xs text-text-secondary mt-1">Required for Admin and Team Leader roles</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-primary mb-2">Password</label>
|
||||
<input type="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Create a password"
|
||||
<input type="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Create a password"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-primary focus:border-transparent transition-colors"/>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="w-full bg-blue-primary text-white py-3 rounded-lg hover:bg-blue-600 focus:ring-2 focus:ring-blue-primary focus:ring-offset-2 transition-colors font-medium">
|
||||
Create Account
|
||||
</button>
|
||||
@@ -410,21 +405,42 @@
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
|
||||
if (sidebar && sidebarOverlay) {
|
||||
sidebar.classList.toggle('active');
|
||||
sidebarOverlay.classList.toggle('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Profile menu toggle
|
||||
function toggleProfileMenu() {
|
||||
const menu = document.getElementById('profile-menu');
|
||||
if (menu) {
|
||||
menu.classList.toggle('opacity-0');
|
||||
menu.classList.toggle('invisible');
|
||||
menu.classList.toggle('opacity-100');
|
||||
menu.classList.toggle('visible');
|
||||
const isVisible = !menu.classList.contains('invisible');
|
||||
if (isVisible) {
|
||||
// Hide it
|
||||
menu.classList.add('opacity-0', 'invisible');
|
||||
menu.classList.remove('opacity-100', 'visible');
|
||||
document.removeEventListener('click', outsideClickListener);
|
||||
} else {
|
||||
// Show it
|
||||
menu.classList.remove('opacity-0', 'invisible');
|
||||
menu.classList.add('opacity-100', 'visible');
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', outsideClickListener);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function outsideClickListener(event) {
|
||||
const menu = document.getElementById('profile-menu');
|
||||
const trigger = event.target.closest('.group'); // The profile button wrapper
|
||||
|
||||
if (menu && !menu.contains(event.target) && !trigger) {
|
||||
// Hide menu
|
||||
menu.classList.add('opacity-0', 'invisible');
|
||||
menu.classList.remove('opacity-100', 'visible');
|
||||
document.removeEventListener('click', outsideClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +448,7 @@
|
||||
function toggleAdminCodeField() {
|
||||
const role = document.getElementById("role");
|
||||
const field = document.getElementById("adminCodeField");
|
||||
|
||||
|
||||
if (role && field) {
|
||||
const roleValue = role.value;
|
||||
if (roleValue === "1") { // Admin or Team Leader
|
||||
@@ -449,7 +465,7 @@
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const body = document.body;
|
||||
|
||||
|
||||
if (sidebar && sidebarOverlay && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
sidebarOverlay.classList.remove('active');
|
||||
@@ -464,7 +480,7 @@
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const body = document.body;
|
||||
|
||||
|
||||
if (sidebar && sidebarOverlay) {
|
||||
sidebar.classList.remove('active');
|
||||
sidebarOverlay.classList.remove('active');
|
||||
@@ -485,4 +501,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
>
|
||||
<option value="">Select Category</option>
|
||||
<option value="users" {{if eq .Category "users"}}selected{{end}}>Users & Teams</option>
|
||||
<option value="address" {{if eq .Category "address"}}selected{{end}}>Addresses</option>
|
||||
<option value="addresses" {{if eq .Category "addresses"}}selected{{end}}>Addresses</option>
|
||||
<option value="appointments" {{if eq .Category "appointments"}}selected{{end}}>Appointments</option>
|
||||
<option value="polls" {{if eq .Category "polls"}}selected{{end}}>Polls</option>
|
||||
<option value="availability" {{if eq .Category "availability"}}selected{{end}}>Availability</option>
|
||||
@@ -53,19 +53,19 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
{{if .Result}}
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div class="text-gray-600">
|
||||
<span>{{.Result.Count}} results</span>
|
||||
</div>
|
||||
<button onclick="exportResults()" class="px-3 py-1.5 bg-green-600 text-white hover:bg-green-700 transition-colors">
|
||||
<!-- <button onclick="exportResults()" class="px-3 py-1.5 bg-green-600 text-white hover:bg-green-700 transition-colors">
|
||||
<i class="fas fa-download mr-1"></i>Export CSV
|
||||
</button>
|
||||
<button onclick="printReport()" class="px-3 py-1.5 bg-blue-600 text-white hover:bg-blue-700 transition-colors">
|
||||
<i class="fas fa-print mr-1"></i>Print
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -146,34 +146,32 @@
|
||||
<script>
|
||||
const reportDefinitions = {
|
||||
users: [
|
||||
{ id: 'participation', name: 'Volunteer Participation Rate' },
|
||||
{ id: 'top_performers', name: 'Top Performing Volunteers' },
|
||||
{ id: 'efficiency', name: 'Response-to-Donation Ratio' },
|
||||
{ id: 'coverage', name: 'User Address Coverage' }
|
||||
{ id: 'volunteer_participation_rate', name: 'Volunteer Participation Rate' },
|
||||
{ id: 'top_performing_volunteers', name: 'Top-Performing Volunteers & Team Leads' },
|
||||
{ id: 'response_donation_ratio', name: 'Response-to-Donation Ratio per Volunteer' },
|
||||
{ id: 'user_address_coverage', name: 'User Address Coverage' }
|
||||
],
|
||||
address: [
|
||||
{ id: 'responses_by_address', name: 'Total Responses by Address' },
|
||||
addresses: [
|
||||
{ id: 'poll_responses_by_address', name: 'Total Poll Responses by Address' },
|
||||
{ id: 'donations_by_address', name: 'Total Donations by Address' },
|
||||
{ id: 'street_breakdown', name: 'Street-Level Breakdown' },
|
||||
{ id: 'quadrant_summary', name: 'Quadrant Summary' }
|
||||
{ id: 'street_level_breakdown', name: 'Street-Level Breakdown (Responses & Donations)' },
|
||||
{ id: 'quadrant_summary', name: 'Quadrant-Level Summary (NE, NW, SE, SW)' }
|
||||
],
|
||||
appointments: [
|
||||
{ id: 'upcoming', name: 'Upcoming Appointments' },
|
||||
{ id: 'completion', name: 'Appointments Completion Rate' },
|
||||
{ id: 'geo_distribution', name: 'Appointments by Quadrant' },
|
||||
{ id: 'lead_time', name: 'Average Lead Time' }
|
||||
{ id: 'upcoming_appointments', name: 'Upcoming Appointments per Volunteer/Team Lead' },
|
||||
{ id: 'missed_vs_completed', name: 'Missed vs Completed Appointments' },
|
||||
{ id: 'appointments_by_quadrant', name: 'Appointments by Quadrant/Region' },
|
||||
{ id: 'scheduling_lead_time', name: 'Average Lead Time (Scheduled vs Actual Date)' }
|
||||
],
|
||||
polls: [
|
||||
{ id: 'distribution', name: 'Response Distribution' },
|
||||
{ id: 'average', name: 'Average Poll Response' },
|
||||
{ id: 'response_distribution', name: 'Response Distribution (Yes/No/Neutral)' },
|
||||
{ id: 'average_poll_response', name: 'Average Poll Response (Yes/No %)' },
|
||||
{ id: 'donations_by_poll', name: 'Donations by Poll' },
|
||||
{ id: 'correlation', name: 'Response-to-Donation Correlation' }
|
||||
{ id: 'response_donation_correlation', name: 'Response-to-Donation Correlation' }
|
||||
],
|
||||
availability: [
|
||||
{ id: 'by_date', name: 'Volunteer Availability by Date' },
|
||||
{ id: 'gaps', name: 'Coverage Gaps' },
|
||||
{ id: 'overlaps', name: 'Volunteer Overlaps' },
|
||||
{ id: 'fulfillment', name: 'Volunteer Fulfillment' }
|
||||
{ id: 'volunteer_availability_schedule', name: 'Volunteer Availability by Date Range' },
|
||||
{ id: 'volunteer_fulfillment', name: 'Volunteer Fulfillment (Available vs Actually Worked)' }
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user