Files
Poll-system/app/internal/templates/reports.html
Mann Patel b21e76eed0 Update: Few issues to resolve see readme
this push will conclude the majority of pulls. this repos will now, not be actively be managed or any further code pushes will not be frequent.
2025-09-11 16:54:30 -06:00

327 lines
13 KiB
HTML

{{ define "content" }}
<div class="flex-1 flex flex-col overflow-hidden" x-data="reportsData()">
<!-- Toolbar -->
<div class="bg-white border-b border-gray-200 px-4 md:px-6 py-4">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<!-- Report Selection Form -->
<form method="GET" action="/reports" class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full lg:w-auto">
<!-- Category Selection -->
<div class="flex items-center gap-2">
<label for="category" class="text-sm text-gray-600 whitespace-nowrap font-medium">Category:</label>
<select
name="category"
id="category"
onchange="updateReports()"
class="px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-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="availability" {{if eq .Category "availability"}}selected{{end}}>Availability</option>
</select>
</div>
<!-- Report Selection -->
<div class="flex items-center gap-2">
<label for="report" class="text-sm text-gray-600 whitespace-nowrap font-medium">Report:</label>
<select
name="report"
id="report"
class="px-3 py-2 text-sm border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-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 -->
<div class="flex items-center gap-2">
<label for="date_from" class="text-sm text-gray-600 whitespace-nowrap 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 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div class="flex items-center gap-2">
<label for="date_to" class="text-sm text-gray-600 whitespace-nowrap 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 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Generate Button -->
<button
type="submit"
class="px-6 py-2.5 bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors rounded-lg"
>
<i class="fas fa-chart-bar mr-2"></i>Generate Report
</button>
</form>
<!-- Actions & Results Count -->
{{if .Result}}
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div class="text-sm text-gray-600">
<span class="font-medium">{{.Result.Count}}</span> results
</div>
</div>
{{end}}
</div>
</div>
<!-- Main Content -->
<div class="flex-1 p-4 md:p-6 overflow-auto">
{{if .Result}}
{{if .Result.Error}}
<!-- Error State -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
<div class="flex items-center space-x-3">
<i class="fas fa-exclamation-triangle text-red-500 text-xl"></i>
<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>
</div>
</div>
{{else}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<!-- Report Header -->
<div class="bg-gray-50 border-b border-gray-200 px-6 py-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<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">
<i class="fas fa-clock mr-1"></i>Generated: {{.GeneratedAt}}
</div>
</div>
</div>
{{if gt .Result.Count 0}}
<!-- Desktop Table -->
<div class="hidden lg:block overflow-x-auto">
<table class="w-full min-w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
{{range .Result.Columns}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{formatColumnName .}}
</th>
{{end}}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{{range .Result.Rows}}
<tr class="hover:bg-gray-50">
{{range .}}
<td class="px-6 py-4 text-sm text-gray-900">{{.}}</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
</div>
<!-- Mobile Cards -->
<div class="lg:hidden">
<div class="space-y-4 p-4">
{{range $rowIndex, $row := .Result.Rows}}
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
<!-- Card Header -->
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200">
<div class="flex items-center space-x-2">
<i class="fas fa-chart-line text-gray-400"></i>
<span class="text-sm font-semibold text-gray-900">Record {{add $rowIndex 1}}</span>
</div>
</div>
<!-- Card Content -->
<div class="p-4 space-y-3">
{{range $colIndex, $column := $.Result.Columns}}
<div class="flex justify-between items-start">
<span class="text-sm text-gray-500 font-medium">{{formatColumnName $column}}:</span>
<span class="text-sm text-gray-900 text-right ml-4">{{index $row $colIndex}}</span>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
<!-- 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-4">
<i class="fas fa-chart-pie mr-2 text-blue-500"></i>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 rounded-lg p-4">
<div class="text-xs text-gray-500 uppercase tracking-wide font-medium">{{.Label}}</div>
<div class="text-2xl font-bold text-gray-900 mt-1">{{.Value}}</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{else}}
<!-- No Results -->
<div class="text-center py-16">
<div class="text-gray-400 mb-4">
<i class="fas fa-chart-bar text-4xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No Results Found</h3>
<p class="text-gray-500">No results match your selected criteria. Try adjusting your filters or date range.</p>
</div>
{{end}}
</div>
{{end}}
{{else}}
<!-- Initial State -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="text-center py-16">
<div class="text-gray-400 mb-6">
<i class="fas fa-chart-line text-5xl"></i>
</div>
<h3 class="text-xl font-medium text-gray-900 mb-4">Generate Report</h3>
<p class="text-gray-500 mb-6 max-w-md mx-auto">
Select a category and report type above to generate comprehensive analytics and insights.
</p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<i class="fas fa-users text-blue-500 text-2xl mb-2"></i>
<h4 class="font-medium text-gray-900 mb-1">User Analytics</h4>
<p class="text-sm text-gray-600">Track volunteer performance and participation rates</p>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<i class="fas fa-map-marker-alt text-green-500 text-2xl mb-2"></i>
<h4 class="font-medium text-gray-900 mb-1">Location Insights</h4>
<p class="text-sm text-gray-600">Analyze address coverage and geographic data</p>
</div>
<div class="bg-purple-50 border border-purple-200 rounded-lg p-4">
<i class="fas fa-calendar-check text-purple-500 text-2xl mb-2"></i>
<h4 class="font-medium text-gray-900 mb-1">Schedule Reports</h4>
<p class="text-sm text-gray-600">Review appointments and availability patterns</p>
</div>
</div>
</div>
</div>
{{end}}
</div>
</div>
<script>
function reportsData() {
return {
// Any Alpine.js data can go here if needed
};
}
const reportDefinitions = {
users: [
{ 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' }
],
addresses: [
{ id: 'poll_responses_by_address', name: 'Total Poll Responses by Address' },
{ id: 'donations_by_address', name: 'Total Donations by Address' },
{ id: 'street_level_breakdown', name: 'Street-Level Breakdown (Responses & Donations)' },
{ id: 'quadrant_summary', name: 'Quadrant-Level Summary (NE, NW, SE, SW)' }
],
appointments: [
{ 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: '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: 'response_donation_correlation', name: 'Response-to-Donation Correlation' }
],
availability: [
{ id: 'volunteer_availability_schedule', name: 'Volunteer Availability by Date Range' },
{ id: 'volunteer_fulfillment', name: 'Volunteer Fulfillment (Available vs Actually Worked)' }
]
};
function updateReports() {
const category = document.getElementById('category').value;
const reportSelect = document.getElementById('report');
reportSelect.innerHTML = '<option value="">Select Report</option>';
if (reportDefinitions[category]) {
reportDefinitions[category].forEach(r => {
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = r.name;
reportSelect.appendChild(opt);
});
}
}
// Initialize reports on page load
document.addEventListener("DOMContentLoaded", function() {
updateReports();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<style>
/* Consistent styling */
input, select, button {
transition: all 0.2s ease;
}
button {
font-weight: 500;
letter-spacing: 0.025em;
}
/* Print styles */
@media print {
.no-print {
display: none !important;
}
.bg-gray-50 {
background-color: white !important;
}
}
/* Mobile responsive adjustments */
@media (max-width: 640px) {
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
}
</style>
{{ end }}