2025-09-03 14:35:47 -06:00
|
|
|
{{ define "content" }}
|
2025-09-11 16:54:30 -06:00
|
|
|
<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>
|
2025-09-03 14:35:47 -06:00
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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>
|
2025-09-03 14:35:47 -06:00
|
|
|
{{end}}
|
2025-09-11 16:54:30 -06:00
|
|
|
{{end}}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2025-09-03 14:35:47 -06:00
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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>
|
2025-09-03 14:35:47 -06:00
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<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>
|
2025-09-09 10:42:24 -06:00
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- Actions & Results Count -->
|
2025-09-03 14:35:47 -06:00
|
|
|
{{if .Result}}
|
2025-09-11 16:54:30 -06:00
|
|
|
<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
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-05 15:39:06 -06:00
|
|
|
<!-- Main Content -->
|
2025-09-11 16:54:30 -06:00
|
|
|
<div class="flex-1 p-4 md:p-6 overflow-auto">
|
2025-09-03 14:35:47 -06:00
|
|
|
{{if .Result}}
|
|
|
|
|
{{if .Result.Error}}
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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>
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{{else}}
|
2025-09-11 16:54:30 -06:00
|
|
|
<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>
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
{{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>
|
2025-09-03 14:35:47 -06:00
|
|
|
{{end}}
|
2025-09-11 16:54:30 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-09-03 14:35:47 -06:00
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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>
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
{{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}}
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
{{else}}
|
2025-09-11 16:54:30 -06:00
|
|
|
<!-- 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>
|
2025-09-03 14:35:47 -06:00
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-09-11 16:54:30 -06:00
|
|
|
function reportsData() {
|
|
|
|
|
return {
|
|
|
|
|
// Any Alpine.js data can go here if needed
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 14:35:47 -06:00
|
|
|
const reportDefinitions = {
|
|
|
|
|
users: [
|
2025-09-09 10:42:24 -06:00
|
|
|
{ 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' }
|
2025-09-03 14:35:47 -06:00
|
|
|
],
|
2025-09-09 10:42:24 -06:00
|
|
|
addresses: [
|
|
|
|
|
{ id: 'poll_responses_by_address', name: 'Total Poll Responses by Address' },
|
2025-09-05 15:39:06 -06:00
|
|
|
{ id: 'donations_by_address', name: 'Total Donations by Address' },
|
2025-09-09 10:42:24 -06:00
|
|
|
{ id: 'street_level_breakdown', name: 'Street-Level Breakdown (Responses & Donations)' },
|
|
|
|
|
{ id: 'quadrant_summary', name: 'Quadrant-Level Summary (NE, NW, SE, SW)' }
|
2025-09-03 14:35:47 -06:00
|
|
|
],
|
|
|
|
|
appointments: [
|
2025-09-09 10:42:24 -06:00
|
|
|
{ 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)' }
|
2025-09-03 14:35:47 -06:00
|
|
|
],
|
|
|
|
|
polls: [
|
2025-09-09 10:42:24 -06:00
|
|
|
{ id: 'response_distribution', name: 'Response Distribution (Yes/No/Neutral)' },
|
|
|
|
|
{ id: 'average_poll_response', name: 'Average Poll Response (Yes/No %)' },
|
2025-09-05 15:39:06 -06:00
|
|
|
{ id: 'donations_by_poll', name: 'Donations by Poll' },
|
2025-09-09 10:42:24 -06:00
|
|
|
{ id: 'response_donation_correlation', name: 'Response-to-Donation Correlation' }
|
2025-09-03 14:35:47 -06:00
|
|
|
],
|
|
|
|
|
availability: [
|
2025-09-09 10:42:24 -06:00
|
|
|
{ id: 'volunteer_availability_schedule', name: 'Volunteer Availability by Date Range' },
|
|
|
|
|
{ id: 'volunteer_fulfillment', name: 'Volunteer Fulfillment (Available vs Actually Worked)' }
|
2025-09-03 14:35:47 -06:00
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function updateReports() {
|
2025-09-05 15:39:06 -06:00
|
|
|
const category = document.getElementById('category').value;
|
2025-09-03 14:35:47 -06:00
|
|
|
const reportSelect = document.getElementById('report');
|
|
|
|
|
reportSelect.innerHTML = '<option value="">Select Report</option>';
|
2025-09-05 15:39:06 -06:00
|
|
|
if (reportDefinitions[category]) {
|
|
|
|
|
reportDefinitions[category].forEach(r => {
|
|
|
|
|
const opt = document.createElement('option');
|
|
|
|
|
opt.value = r.id;
|
|
|
|
|
opt.textContent = r.name;
|
|
|
|
|
reportSelect.appendChild(opt);
|
2025-09-03 14:35:47 -06:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-11 16:54:30 -06:00
|
|
|
// Initialize reports on page load
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
updateReports();
|
|
|
|
|
});
|
2025-09-03 14:35:47 -06:00
|
|
|
</script>
|
2025-09-11 16:54:30 -06:00
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
2025-09-05 15:39:06 -06:00
|
|
|
{{ end }}
|