CSV Import is now working
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -8,11 +8,20 @@
|
||||
class="w-full lg:w-1/2 flex flex-col gap-4 sm:gap-6 sticky top-0 self-start h-fit"
|
||||
>
|
||||
<!-- Today's Overview -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
<div class="bg-white border-b border-gray-200" x-data="{ open: true }">
|
||||
<div
|
||||
class="px-4 sm:px-6 py-4 flex justify-between items-center cursor-pointer"
|
||||
@click="open = !open"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-gray-900">
|
||||
Today's Overview
|
||||
</h3>
|
||||
<i
|
||||
class="fas"
|
||||
:class="open ? 'fa-chevron-up' : 'fa-chevron-down'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6 pb-4" x-show="open" x-collapse>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -23,9 +32,9 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Appointments Today</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsToday }}
|
||||
</span>
|
||||
<span class="text-lg font-semibold text-gray-900"
|
||||
>{{ .Statistics.AppointmentsToday }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -39,9 +48,9 @@
|
||||
>Appointments Tomorrow</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsTomorrow }}
|
||||
</span>
|
||||
<span class="text-lg font-semibold text-gray-900"
|
||||
>{{ .Statistics.AppointmentsTomorrow }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -53,20 +62,29 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">This Week</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ .Statistics.AppointmentsThisWeek }}
|
||||
</span>
|
||||
<span class="text-lg font-semibold text-gray-900"
|
||||
>{{ .Statistics.AppointmentsThisWeek }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Polling Progress -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="px-4 sm:px-6 py-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">
|
||||
<div class="bg-white border-b border-gray-200" x-data="{ open: true }">
|
||||
<div
|
||||
class="px-4 sm:px-6 py-4 flex justify-between items-center cursor-pointer"
|
||||
@click="open = !open"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-gray-900">
|
||||
Polling Progress
|
||||
</h3>
|
||||
<i
|
||||
class="fas"
|
||||
:class="open ? 'fa-chevron-up' : 'fa-chevron-down'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6 pb-4" x-show="open" x-collapse>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -77,9 +95,9 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Polls Completed</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-green-600">
|
||||
{{ .Statistics.PollsCompleted }}
|
||||
</span>
|
||||
<span class="text-lg font-semibold text-green-600"
|
||||
>{{ .Statistics.PollsCompleted }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -91,9 +109,9 @@
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">Polls Remaining</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-orange-600">
|
||||
{{ .Statistics.PollsRemaining }}
|
||||
</span>
|
||||
<span class="text-lg font-semibold text-orange-600"
|
||||
>{{ .Statistics.PollsRemaining }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
@@ -117,6 +135,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Members -->
|
||||
<div class="bg-white border-b border-gray-200" x-data="{ open: true }">
|
||||
<div
|
||||
class="px-4 sm:px-6 py-4 flex justify-between items-center cursor-pointer"
|
||||
@click="open = !open"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-gray-900">Team Members</h3>
|
||||
<i
|
||||
class="fas"
|
||||
:class="open ? 'fa-chevron-up' : 'fa-chevron-down'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6 pb-4" x-show="open" x-collapse>
|
||||
<div class="space-y-3">
|
||||
{{ range .Teammates }}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
{{ .FullName }} {{ if .IsLead }}
|
||||
<span class="ml-2 text-xs text-blue-600 font-semibold"
|
||||
>{{ .Role }}</span
|
||||
>
|
||||
{{ else }}
|
||||
<span class="ml-2 text-xs text-gray-500">{{ .Role }}</span>
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700">
|
||||
<i class="fas fa-phone mr-1 text-gray-500"></i>{{ .Phone }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<p class="text-gray-500 text-sm">No teammates found</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column - Statistics -->
|
||||
<div class="flex-1 lg:flex-none lg:w-1/2 overflow-y-auto pr-2">
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<title>{{if .Title}}{{.Title}}{{else}}Poll System{{end}}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||
@@ -52,7 +54,7 @@
|
||||
<a href="/posts" class="text-sm font-medium {{if eq .ActiveSection "post"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
Posts
|
||||
</a>
|
||||
<a href="/smart-reports" class="text-sm font-medium {{if eq .ActiveSection "report"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
<a href="/reports" class="text-sm font-medium {{if eq .ActiveSection "report"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
Reports
|
||||
</a>
|
||||
{{ end }}
|
||||
@@ -64,9 +66,9 @@
|
||||
<a href="/volunteer/Addresses" class="text-sm font-medium {{if eq .ActiveSection "address"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
Assigned Address
|
||||
</a>
|
||||
<a href="/volunteer/schedual" class="text-sm font-medium {{if eq .ActiveSection "schedual"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
<!-- <a href="/volunteer/schedual" class="text-sm font-medium {{if eq .ActiveSection "schedual"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
My Schedule
|
||||
</a>
|
||||
</a> -->
|
||||
{{ end }}
|
||||
|
||||
<a href="/profile" class="text-sm font-medium {{if eq .ActiveSection "profile"}}text-blue-300 border-b-2 border-blue-300{{else}}text-gray-300 hover:text-white{{end}} py-3 px-1">
|
||||
@@ -474,7 +476,6 @@
|
||||
onchange="toggleAdminCodeField()">
|
||||
<option value="">Select role</option>
|
||||
<option value="1">Admin</option>
|
||||
<option value="2">Team Leader</option>
|
||||
<option value="3">Volunteer</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -585,7 +586,7 @@
|
||||
function toggleAdminCodeField() {
|
||||
const role = document.getElementById("role").value;
|
||||
const field = document.getElementById("adminCodeField");
|
||||
field.classList.toggle("hidden", role !== "3"); // show only if Volunteer
|
||||
field.classList.toggle("hidden", role !== "3" && role !== "2"); // show only if Volunteer or Team Leader
|
||||
}
|
||||
|
||||
// Handle escape key
|
||||
|
||||
350
app/internal/templates/reports.html
Normal file
350
app/internal/templates/reports.html
Normal file
@@ -0,0 +1,350 @@
|
||||
{{ define "content" }}
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Toolbar with Report Selection -->
|
||||
<div class="bg-gray-50 border-b border-gray-200 px-6 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<form method="GET" action="/reports" class="flex items-center gap-3">
|
||||
<!-- Category Dropdown -->
|
||||
<div class="relative">
|
||||
<label for="category" class="text-gray-700 font-medium mr-2">Category:</label>
|
||||
<select
|
||||
name="category"
|
||||
id="category"
|
||||
onchange="updateReports()"
|
||||
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500 min-w-48"
|
||||
>
|
||||
<option value="">Select Category</option>
|
||||
<option value="users" {{if eq .Category "users"}}selected{{end}}>Users & Teams</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="responses" {{if eq .Category "responses"}}selected{{end}}>Poll Responses</option>
|
||||
<option value="availability" {{if eq .Category "availability"}}selected{{end}}>Availability</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Report Dropdown -->
|
||||
<div class="relative">
|
||||
<label for="report" class="text-gray-700 font-medium mr-2">Report:</label>
|
||||
<select
|
||||
name="report"
|
||||
id="report"
|
||||
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500 min-w-64"
|
||||
>
|
||||
<option value="">Select Report</option>
|
||||
{{if .Category}}
|
||||
{{range .AvailableReports}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.ReportID}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range (optional) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="date_from" class="text-gray-700 font-medium">From:</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date_from"
|
||||
id="date_from"
|
||||
value="{{.DateFrom}}"
|
||||
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
<label for="date_to" class="text-gray-700 font-medium">To:</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date_to"
|
||||
id="date_to"
|
||||
value="{{.DateTo}}"
|
||||
class="px-3 py-2 text-sm border border-gray-200 bg-white focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-purple-600 text-white font-medium hover:bg-purple-700 transition-all duration-200 text-sm"
|
||||
>
|
||||
<i class="fas fa-chart-bar mr-2"></i>Generate Report
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
{{if .Result}}
|
||||
{{if .Result.Error}}
|
||||
<!-- Error State -->
|
||||
<div class="p-6">
|
||||
<div class="bg-red-50 border border-red-200 p-6">
|
||||
<div class="flex items-start">
|
||||
<div class="w-10 h-10 bg-red-100 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-exclamation-triangle text-red-600"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-red-800 mb-2">Report Error</h3>
|
||||
<p class="text-red-700">{{.Result.Error}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<!-- Report Header -->
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-900">{{.ReportTitle}}</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">{{.ReportDescription}}</p>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Generated: {{.GeneratedAt}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Table -->
|
||||
{{if gt .Result.Count 0}}
|
||||
<div class="flex-1 overflow-x-auto overflow-y-auto bg-white">
|
||||
<table class="w-full divide-gray-200 text-sm table-auto">
|
||||
<thead class="bg-gray-50 divide-gray-200 sticky top-0">
|
||||
<tr class="text-left text-gray-700 font-medium border-b border-gray-200">
|
||||
{{range .Result.Columns}}
|
||||
<th class="px-6 py-3 whitespace-nowrap">{{formatColumnName .}}</th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{{range .Result.Rows}}
|
||||
<tr class="hover:bg-gray-50">
|
||||
{{range .}}
|
||||
<td class="px-6 py-3 text-sm text-gray-900">{{.}}</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats (if available) -->
|
||||
{{if .SummaryStats}}
|
||||
<div class="bg-gray-50 border-t border-gray-200 px-6 py-4">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">Summary Statistics</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{{range .SummaryStats}}
|
||||
<div class="bg-white border border-gray-200 px-3 py-2">
|
||||
<div class="text-xs text-gray-500">{{.Label}}</div>
|
||||
<div class="text-lg font-semibold text-gray-900">{{.Value}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<!-- No Results State -->
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="text-center py-12">
|
||||
<div class="w-16 h-16 bg-gray-100 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-chart-bar text-gray-400 text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No Data Found</h3>
|
||||
<p class="text-gray-500">No results match your selected criteria</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<!-- Welcome State -->
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="text-center py-12 max-w-4xl mx-auto px-6">
|
||||
<div class="mb-8">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-purple-600 to-purple-700 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-chart-line text-white text-2xl"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Campaign Reports</h1>
|
||||
<p class="text-gray-600 text-lg">Generate detailed reports across all your campaign data</p>
|
||||
</div>
|
||||
|
||||
<!-- Report Categories Overview -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-left">
|
||||
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
|
||||
<div class="font-medium text-gray-900 text-sm mb-1">Users & Teams</div>
|
||||
<div class="text-xs text-gray-500">Volunteer performance, team stats, role distribution</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
|
||||
<div class="font-medium text-gray-900 text-sm mb-1">Address Reports</div>
|
||||
<div class="text-xs text-gray-500">Coverage areas, visit status, geographical insights</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
|
||||
<div class="font-medium text-gray-900 text-sm mb-1">Appointments</div>
|
||||
<div class="text-xs text-gray-500">Schedule analysis, completion rates, time trends</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-white border border-gray-200 hover:border-purple-300 hover:shadow-md transition-all duration-200">
|
||||
<div class="font-medium text-gray-900 text-sm mb-1">Poll Analytics</div>
|
||||
<div class="text-xs text-gray-500">Response rates, donation tracking, engagement metrics</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-sm text-gray-500">
|
||||
Select a category above to see available reports
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Square corners across UI */
|
||||
* {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Print-specific styles */
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Report definitions for each category
|
||||
const reportDefinitions = {
|
||||
users: [
|
||||
{ id: 'users_by_role', name: 'Users by Role' },
|
||||
{ id: 'volunteer_activity', name: 'Volunteer Activity Summary' },
|
||||
{ id: 'team_performance', name: 'Team Performance Report' },
|
||||
{ id: 'admin_workload', name: 'Admin Workload Analysis' },
|
||||
{ id: 'inactive_users', name: 'Inactive Users Report' }
|
||||
],
|
||||
addresses: [
|
||||
{ id: 'coverage_by_area', name: 'Coverage by Area' },
|
||||
{ id: 'visits_by_postal', name: 'Visits by Postal Code' },
|
||||
{ id: 'unvisited_addresses', name: 'Unvisited Addresses' },
|
||||
{ id: 'donations_by_location', name: 'Donations by Location' },
|
||||
{ id: 'address_validation_status', name: 'Address Validation Status' }
|
||||
],
|
||||
appointments: [
|
||||
{ id: 'appointments_by_day', name: 'Appointments by Day' },
|
||||
{ id: 'completion_rates', name: 'Completion Rates' },
|
||||
{ id: 'volunteer_schedules', name: 'Volunteer Schedules' },
|
||||
{ id: 'missed_appointments', name: 'Missed Appointments' },
|
||||
{ id: 'peak_hours', name: 'Peak Activity Hours' }
|
||||
],
|
||||
polls: [
|
||||
{ id: 'poll_creation_stats', name: 'Poll Creation Statistics' },
|
||||
{ id: 'donation_analysis', name: 'Donation Analysis' },
|
||||
{ id: 'active_vs_inactive', name: 'Active vs Inactive Polls' },
|
||||
{ id: 'poll_trends', name: 'Poll Activity Trends' },
|
||||
{ id: 'creator_performance', name: 'Creator Performance' }
|
||||
],
|
||||
responses: [
|
||||
{ id: 'voter_status', name: 'Voter Status Report' },
|
||||
{ id: 'sign_requests', name: 'Sign Requests Summary' },
|
||||
{ id: 'feedback_analysis', name: 'Feedback Analysis' },
|
||||
{ id: 'response_trends', name: 'Response Trends' },
|
||||
{ id: 'repeat_voters', name: 'Repeat Voters Analysis' }
|
||||
],
|
||||
availability: [
|
||||
{ id: 'volunteer_availability', name: 'Volunteer Availability' },
|
||||
{ id: 'peak_availability', name: 'Peak Availability Times' },
|
||||
{ id: 'coverage_gaps', name: 'Coverage Gaps' },
|
||||
{ id: 'schedule_conflicts', name: 'Schedule Conflicts' }
|
||||
]
|
||||
};
|
||||
|
||||
// Update reports dropdown when category changes
|
||||
function updateReports() {
|
||||
const categorySelect = document.getElementById('category');
|
||||
const reportSelect = document.getElementById('report');
|
||||
const category = categorySelect.value;
|
||||
|
||||
// Clear existing options
|
||||
reportSelect.innerHTML = '<option value="">Select Report</option>';
|
||||
|
||||
if (category && reportDefinitions[category]) {
|
||||
reportDefinitions[category].forEach(report => {
|
||||
const option = document.createElement('option');
|
||||
option.value = report.id;
|
||||
option.textContent = report.name;
|
||||
reportSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export results
|
||||
function exportResults() {
|
||||
const form = document.querySelector('form');
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
params.set('export', 'csv');
|
||||
window.location.href = `/reports/export?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Print report
|
||||
function printReport() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
updateReports();
|
||||
|
||||
// Set default date range (last 30 days)
|
||||
const dateFrom = document.getElementById('date_from');
|
||||
const dateTo = document.getElementById('date_to');
|
||||
|
||||
if (!dateFrom.value) {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
dateFrom.value = thirtyDaysAgo.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
if (!dateTo.value) {
|
||||
dateTo.value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
@@ -1,752 +0,0 @@
|
||||
{{ define "content" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}}</title>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="min-h-screen w-full flex flex-col">
|
||||
<!-- Header -->
|
||||
<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-purple-600 flex items-center justify-center rounded"
|
||||
>
|
||||
<i class="fas fa-brain text-white text-sm"></i>
|
||||
</div>
|
||||
<span class="text-xl font-semibold text-gray-900"
|
||||
>Smart Reports & Analytics</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
{{if .SmartQuery}}
|
||||
<button
|
||||
onclick="exportResults()"
|
||||
class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium rounded hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-download mr-2"></i>Export Results
|
||||
</button>
|
||||
{{end}}
|
||||
<button
|
||||
onclick="clearQuery()"
|
||||
class="px-6 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<i class="fas fa-eraser mr-2"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Smart Search Interface -->
|
||||
<div class="bg-white w-full border-b border-gray-200">
|
||||
<div class="px-8 py-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">
|
||||
Ask About Your Data
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-8">
|
||||
Use natural language to query across users, polls, appointments,
|
||||
addresses, and teams
|
||||
</p>
|
||||
|
||||
<form method="GET" action="/smart-reports" class="mb-8">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"
|
||||
>
|
||||
<i class="fas fa-magic text-purple-400"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="smart_query"
|
||||
value="{{.SmartQuery}}"
|
||||
placeholder="e.g., 'volunteers who went to Main Street' or 'donations by team 5'"
|
||||
class="w-full pl-10 pr-4 py-4 text-lg border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="px-8 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-search mr-2"></i>Search
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick="toggleExamples()"
|
||||
class="text-purple-600 hover:text-purple-800 text-sm font-medium"
|
||||
>
|
||||
<i class="fas fa-lightbulb mr-1"></i>Show Examples
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Query Examples -->
|
||||
<div id="queryExamples" class="hidden">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Example Queries
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{{range .QueryExamples}}
|
||||
<div
|
||||
class="p-4 bg-gray-50 rounded-lg border border-gray-200 hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
onclick="useExample('{{.}}')"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{.}}</span>
|
||||
<i class="fas fa-arrow-right text-purple-500 text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section -->
|
||||
{{if .Result}} {{if .Result.Error}}
|
||||
<div class="bg-white w-full">
|
||||
<div class="px-8 py-6">
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle text-red-600 text-sm"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-semibold text-red-800">Query Error</h3>
|
||||
<p class="text-red-700 mt-2">{{.Result.Error}}</p>
|
||||
{{if .Result.Query}}
|
||||
<details class="mt-4">
|
||||
<summary class="text-sm text-red-600 cursor-pointer">
|
||||
Show Generated SQL
|
||||
</summary>
|
||||
<pre
|
||||
class="mt-2 p-3 bg-red-100 text-red-800 text-xs rounded overflow-x-auto"
|
||||
>
|
||||
{{.Result.Query}}</pre
|
||||
>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<!-- Successful Results -->
|
||||
<div class="bg-white w-full">
|
||||
<div class="px-8 py-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
Query Results ({{.Result.Count}} records found)
|
||||
</h3>
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
onclick="toggleQueryDetails()"
|
||||
class="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<i class="fas fa-code mr-1"></i>Show SQL
|
||||
</button>
|
||||
<button
|
||||
onclick="exportResults()"
|
||||
class="px-4 py-2 bg-green-600 text-white text-sm rounded hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-download mr-1"></i>Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Details (Hidden by default) -->
|
||||
<div
|
||||
id="queryDetails"
|
||||
class="hidden mb-6 p-4 bg-gray-50 rounded-lg border"
|
||||
>
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-2">
|
||||
Generated SQL Query:
|
||||
</h4>
|
||||
<pre class="text-xs text-gray-600 overflow-x-auto">
|
||||
{{.Result.Query}}</pre
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Results Table -->
|
||||
{{if gt .Result.Count 0}}
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{{range .Result.Columns}}
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200"
|
||||
>
|
||||
{{formatColumnName .}}
|
||||
</th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{{range .Result.Rows}}
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
{{range .}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{.}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-12">
|
||||
<div
|
||||
class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<i class="fas fa-search text-gray-400 text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||
No Results Found
|
||||
</h3>
|
||||
<p class="text-gray-500">
|
||||
Try adjusting your query or check the examples below
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{end}}
|
||||
|
||||
<!-- Query Builder Assistant -->
|
||||
<div class="bg-white w-full border-t border-gray-200">
|
||||
<div class="px-8 py-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">
|
||||
Smart Query Builder
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Common Queries -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium text-gray-900">
|
||||
User & Volunteer Queries
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
onclick="useSmartQuery('volunteers who went to')"
|
||||
class="w-full text-left p-3 bg-blue-50 text-blue-800 rounded border hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-user-friends mr-2"></i>Volunteers who went
|
||||
to...
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('users with role admin')"
|
||||
class="w-full text-left p-3 bg-blue-50 text-blue-800 rounded border hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-user-shield mr-2"></i>Users by role
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('volunteer activity by month')"
|
||||
class="w-full text-left p-3 bg-blue-50 text-blue-800 rounded border hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-chart-line mr-2"></i>Volunteer activity
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Poll & Donation Queries -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium text-gray-900">
|
||||
Poll & Donation Queries
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
onclick="useSmartQuery('poll responses for')"
|
||||
class="w-full text-left p-3 bg-green-50 text-green-800 rounded border hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-poll mr-2"></i>Poll responses for address
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('donations by volunteer')"
|
||||
class="w-full text-left p-3 bg-green-50 text-green-800 rounded border hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-donate mr-2"></i>Donations by volunteer
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('active polls created after 2024-01-01')"
|
||||
class="w-full text-left p-3 bg-green-50 text-green-800 rounded border hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-calendar-check mr-2"></i>Active polls by
|
||||
date
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team & Address Queries -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium text-gray-900">
|
||||
Team & Address Queries
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
onclick="useSmartQuery('team with most appointments')"
|
||||
class="w-full text-left p-3 bg-purple-50 text-purple-800 rounded border hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-users mr-2"></i>Top performing teams
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('visited addresses')"
|
||||
class="w-full text-left p-3 bg-purple-50 text-purple-800 rounded border hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-map-marked-alt mr-2"></i>Visited addresses
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('money made by team')"
|
||||
class="w-full text-left p-3 bg-purple-50 text-purple-800 rounded border hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
<i class="fas fa-dollar-sign mr-2"></i>Team earnings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Syntax Help -->
|
||||
<div class="mt-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<h4 class="font-semibold text-gray-900 mb-4">
|
||||
<i class="fas fa-info-circle text-blue-500 mr-2"></i>Smart Query
|
||||
Syntax Guide
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<h5 class="font-medium text-gray-800 mb-2">
|
||||
Entity Keywords:
|
||||
</h5>
|
||||
<ul class="space-y-1 text-gray-600">
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>volunteer, user, admin</code
|
||||
>
|
||||
- User data
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded">poll, polls</code>
|
||||
- Poll data
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>address, addresses</code
|
||||
>
|
||||
- Address data
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>appointment, appointments</code
|
||||
>
|
||||
- Appointment data
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded">team, teams</code>
|
||||
- Team data
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium text-gray-800 mb-2">
|
||||
Action Keywords:
|
||||
</h5>
|
||||
<ul class="space-y-1 text-gray-600">
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>went to, visited</code
|
||||
>
|
||||
- Filter by visits
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>donated, money, amount</code
|
||||
>
|
||||
- Financial data
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>active, inactive</code
|
||||
>
|
||||
- Status filters
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>most, highest, top</code
|
||||
>
|
||||
- Ranking queries
|
||||
</li>
|
||||
<li>
|
||||
<code class="bg-gray-200 px-1 rounded"
|
||||
>from DATE, after DATE</code
|
||||
>
|
||||
- Date filters
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Query Suggestions -->
|
||||
<div class="bg-white w-full border-t border-gray-200">
|
||||
<div class="px-8 py-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Popular Analysis Questions
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('which volunteer went to most addresses')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">
|
||||
Top Performing Volunteers
|
||||
</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
Find volunteers with most address visits
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('poll responses of visited addresses')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">
|
||||
Visited Address Polls
|
||||
</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
Polls from addresses that were visited
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('donations filtered by volunteer Sarah')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">
|
||||
Volunteer Donations
|
||||
</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
Total donations by specific volunteer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('team that did most appointments')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">Most Active Team</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
Team with highest appointment count
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('people in team 1')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">Team Members</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
View all members of a specific team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="p-4 border border-gray-200 rounded-lg hover:border-purple-300 transition-colors cursor-pointer"
|
||||
onclick="useSmartQuery('unvisited addresses with polls')"
|
||||
>
|
||||
<h5 class="font-medium text-gray-900 mb-2">
|
||||
Missed Opportunities
|
||||
</h5>
|
||||
<p class="text-sm text-gray-600">
|
||||
Addresses with polls but no visits
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Queries History -->
|
||||
<div class="bg-gray-50 w-full border-t border-gray-200">
|
||||
<div class="px-8 py-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
onclick="useSmartQuery('all users created today')"
|
||||
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Today's Users
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('donations over 50')"
|
||||
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
High Donations
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('appointments this week')"
|
||||
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
This Week's Appointments
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('inactive polls with donations')"
|
||||
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Inactive Paid Polls
|
||||
</button>
|
||||
<button
|
||||
onclick="useSmartQuery('teams created this month')"
|
||||
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
New Teams
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle query examples
|
||||
function toggleExamples() {
|
||||
const examples = document.getElementById("queryExamples");
|
||||
examples.classList.toggle("hidden");
|
||||
}
|
||||
|
||||
// Use example query
|
||||
function useExample(query) {
|
||||
document.querySelector('input[name="smart_query"]').value = query;
|
||||
}
|
||||
|
||||
// Use smart query (for buttons)
|
||||
function useSmartQuery(query) {
|
||||
document.querySelector('input[name="smart_query"]').value = query;
|
||||
document.querySelector("form").submit();
|
||||
}
|
||||
|
||||
// Toggle query details
|
||||
function toggleQueryDetails() {
|
||||
const details = document.getElementById("queryDetails");
|
||||
details.classList.toggle("hidden");
|
||||
}
|
||||
|
||||
// Export results
|
||||
function exportResults() {
|
||||
const query = encodeURIComponent(
|
||||
document.querySelector('input[name="smart_query"]').value
|
||||
);
|
||||
window.location.href = `/smart-reports/export?smart_query=${query}`;
|
||||
}
|
||||
|
||||
// Clear query
|
||||
function clearQuery() {
|
||||
window.location.href = "/smart-reports";
|
||||
}
|
||||
|
||||
// Format column names for display
|
||||
function formatColumnName(name) {
|
||||
return name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
}
|
||||
|
||||
// Auto-complete functionality
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const input = document.querySelector('input[name="smart_query"]');
|
||||
const suggestions = [
|
||||
"volunteers who went to",
|
||||
"poll responses for address",
|
||||
"donations by volunteer",
|
||||
"team with most appointments",
|
||||
"people in team",
|
||||
"money made by team",
|
||||
"visited addresses",
|
||||
"active polls",
|
||||
"appointments for",
|
||||
"users with role",
|
||||
"polls with donations over",
|
||||
"addresses visited by",
|
||||
"team leads with more than",
|
||||
"donations per address",
|
||||
"unvisited addresses with polls",
|
||||
];
|
||||
|
||||
// Simple autocomplete
|
||||
input.addEventListener("input", function () {
|
||||
const value = this.value.toLowerCase();
|
||||
// You could implement autocomplete dropdown here
|
||||
console.log("Typing:", value);
|
||||
});
|
||||
|
||||
// Submit on Enter
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Focus on load
|
||||
input.focus();
|
||||
});
|
||||
|
||||
// Real-time query validation
|
||||
function validateQuery(query) {
|
||||
const keywords = [
|
||||
"volunteer",
|
||||
"user",
|
||||
"poll",
|
||||
"address",
|
||||
"appointment",
|
||||
"team",
|
||||
"donation",
|
||||
];
|
||||
const hasKeyword = keywords.some((keyword) =>
|
||||
query.toLowerCase().includes(keyword)
|
||||
);
|
||||
|
||||
if (!hasKeyword) {
|
||||
return {
|
||||
valid: false,
|
||||
message:
|
||||
"Query should include at least one entity (volunteer, poll, address, etc.)",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Query suggestions based on context
|
||||
function getContextualSuggestions(partialQuery) {
|
||||
const suggestions = [];
|
||||
const query = partialQuery.toLowerCase();
|
||||
|
||||
if (query.includes("volunteer")) {
|
||||
suggestions.push("volunteers who went to Main Street");
|
||||
suggestions.push("volunteers with most donations");
|
||||
suggestions.push("volunteer activity by month");
|
||||
}
|
||||
|
||||
if (query.includes("team")) {
|
||||
suggestions.push("team with most appointments");
|
||||
suggestions.push("people in team 1");
|
||||
suggestions.push("money made by team 2");
|
||||
}
|
||||
|
||||
if (query.includes("address")) {
|
||||
suggestions.push("addresses visited by volunteer John");
|
||||
suggestions.push("poll responses for 123 Oak Street");
|
||||
suggestions.push("unvisited addresses with polls");
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Ctrl/Cmd + Enter to submit
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
document.querySelector("form").submit();
|
||||
}
|
||||
|
||||
// Escape to clear
|
||||
if (e.key === "Escape") {
|
||||
clearQuery();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Enhanced styling for smart interface */
|
||||
.smart-query-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(147, 51, 234, 0.1);
|
||||
border-color: #9333ea;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for code examples */
|
||||
code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* Hover effects for query buttons */
|
||||
.query-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Results table styling */
|
||||
.results-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
.border-gray-200 {
|
||||
border-color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.px-8 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.overflow-x-auto {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
@@ -196,16 +196,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Delivery Section (conditionally shown) -->
|
||||
<div id="delivery-section" class="hidden">
|
||||
<div id="delivery-section">
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
Delivery Address
|
||||
Donation Amount
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="delivery_address"
|
||||
placeholder="Enter delivery address"
|
||||
type="number"
|
||||
name="question6_amount"
|
||||
placeholder="Enter Donation Amount ($)"
|
||||
class="professional-input w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user