feat: added a side bar
This commit is contained in:
@@ -16,10 +16,9 @@
|
||||
>
|
||||
<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="address" {{if eq .Category "address"}}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>
|
||||
@@ -41,30 +40,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range (optional) -->
|
||||
<!-- Date Range -->
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -76,16 +60,10 @@
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -93,43 +71,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<!-- Main Content -->
|
||||
<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>
|
||||
<h3 class="text-lg font-semibold text-red-800 mb-2">Report Error</h3>
|
||||
<p class="text-red-700">{{.Result.Error}}</p>
|
||||
</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 class="bg-white border-b border-gray-200 px-6 py-4 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>
|
||||
|
||||
<!-- 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">
|
||||
<thead class="bg-gray-50 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>
|
||||
@@ -148,7 +114,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats (if available) -->
|
||||
<!-- Summary Stats -->
|
||||
{{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>
|
||||
@@ -164,187 +130,75 @@
|
||||
{{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>
|
||||
<p class="text-gray-500">No results match your selected criteria</p>
|
||||
</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>
|
||||
<p class="text-gray-600">Select a category and report to generate results</p>
|
||||
</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' }
|
||||
{ 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' }
|
||||
],
|
||||
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' }
|
||||
address: [
|
||||
{ id: 'responses_by_address', name: 'Total 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' }
|
||||
],
|
||||
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' }
|
||||
{ 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' }
|
||||
],
|
||||
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' }
|
||||
{ id: 'distribution', name: 'Response Distribution' },
|
||||
{ id: 'average', name: 'Average Poll Response' },
|
||||
{ id: 'donations_by_poll', name: 'Donations by Poll' },
|
||||
{ id: 'correlation', name: 'Response-to-Donation Correlation' }
|
||||
],
|
||||
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' }
|
||||
{ id: 'by_date', name: 'Volunteer Availability by Date' },
|
||||
{ id: 'gaps', name: 'Coverage Gaps' },
|
||||
{ id: 'overlaps', name: 'Volunteer Overlaps' },
|
||||
{ id: 'fulfillment', name: 'Volunteer Fulfillment' }
|
||||
]
|
||||
};
|
||||
|
||||
// Update reports dropdown when category changes
|
||||
function updateReports() {
|
||||
const categorySelect = document.getElementById('category');
|
||||
const category = document.getElementById('category').value;
|
||||
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);
|
||||
if (reportDefinitions[category]) {
|
||||
reportDefinitions[category].forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.name;
|
||||
reportSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export results
|
||||
function exportResults() {
|
||||
const form = document.querySelector('form');
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams(formData);
|
||||
const params = new URLSearchParams(new FormData(document.querySelector('form')));
|
||||
params.set('export', 'csv');
|
||||
window.location.href = `/reports/export?${params.toString()}`;
|
||||
window.location.href = `/reports?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Print report
|
||||
function printReport() {
|
||||
window.print();
|
||||
}
|
||||
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];
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", updateReports);
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user