changing some things for reports section

This commit is contained in:
Mann Patel
2025-09-01 17:32:00 -06:00
parent e071ced77f
commit 76744e81cb
16 changed files with 411461 additions and 1587 deletions

9
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/app/uploads
/app/internal/.DS_Store
/app/misc-code/*
app/tmp/*.log
/app/uploads/*
/app/misc-code/*git
app/tmp/*
*.env
*.DS_Store

View File

@@ -1,5 +1,18 @@
# Poll-system
- TODO: Volunteer scehdual database
- TODO: Update the Database On the linode server
- TODO: Map View
- TODO: CSV smart address upload
- TODO: Reports
- TODO: Print Reports properly with print button
- TODO: Natural Language Filtering e.i. (From _DB1_,_DB2_,_DB3_, ... Where Date = _date_ And Time = _Time_)
- which volunteer whent to what address
- what are the poll respoce of a address
- what are the poll respoces of a vouneered address
- what are the admutn donat filtered by volunteer, address,
- which team did most appointments
- what is are the poeple in that team
- how much money the tam made
- what are the csv imported data
- what are the poll taken data
- what is the simple the

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,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="/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="/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">
Reports
</a>
{{ end }}

View File

@@ -1,831 +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>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
</head>
<body class="bg-gray-50">
<!-- Full Width Container -->
<div class="min-h-screen w-full flex flex-col">
<!-- Main Reports 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-line text-white text-sm"></i>
</div>
<span class="text-xl font-semibold text-gray-900">
Reports & Analytics
</span>
</div>
<div class="flex items-center gap-4">
{{if .SearchType}}
<button onclick="exportData()" class="px-6 py-2.5 bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors">
<i class="fas fa-download mr-2"></i>Export CSV
</button>
{{end}}
<button onclick="toggleAdvancedSearch()" class="px-6 py-2.5 bg-purple-600 text-white text-sm font-medium hover:bg-purple-700 transition-colors">
<i class="fas fa-search-plus mr-2"></i>Advanced Search
</button>
<button onclick="printReport()" 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-print mr-2"></i>Print Report
</button>
</div>
</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">
<!-- Total Users -->
<div class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer" onclick="applyQuickFilter('users', {})">
<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">Total Users</p>
<p class="text-2xl font-bold text-gray-900">{{.ReportData.TotalUsers}}</p>
</div>
</div>
</div>
<!-- Total Polls -->
<div class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer" onclick="applyQuickFilter('polls', {})">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-50 flex items-center justify-center">
<i class="fas fa-poll text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Total Polls</p>
<p class="text-2xl font-bold text-gray-900">{{.ReportData.TotalPolls}}</p>
</div>
</div>
</div>
<!-- Total Addresses -->
<div class="border-r border-gray-200 p-8 hover:bg-gray-50 transition-colors cursor-pointer" onclick="applyQuickFilter('addresses', {})">
<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">Total Addresses</p>
<p class="text-2xl font-bold text-gray-900">{{.ReportData.TotalAddresses}}</p>
</div>
</div>
</div>
<!-- Total Appointments -->
<div class="p-8 hover:bg-gray-50 transition-colors cursor-pointer" onclick="applyQuickFilter('appointments', {})">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-50 flex items-center justify-center">
<i class="fas fa-calendar text-blue-600 text-lg"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 mb-1">Appointments</p>
<p class="text-2xl font-bold text-gray-900">{{len .ReportData.Appointments}}</p>
</div>
</div>
</div>
</div>
<!-- Search and Filter 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">Search & Filter Data</h3>
<form method="GET" action="/reports" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Search Type -->
<div>
<label for="search_type" class="block text-sm font-medium text-gray-700 mb-2">Search In</label>
<select name="search_type" id="search_type" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="" {{if eq .SearchType ""}}selected{{end}}>All Data</option>
<option value="users" {{if eq .SearchType "users"}}selected{{end}}>Users</option>
<option value="polls" {{if eq .SearchType "polls"}}selected{{end}}>Polls</option>
<option value="appointments" {{if eq .SearchType "appointments"}}selected{{end}}>Appointments</option>
<option value="addresses" {{if eq .SearchType "addresses"}}selected{{end}}>Addresses</option>
<option value="teams" {{if eq .SearchType "teams"}}selected{{end}}>Teams</option>
</select>
</div>
<!-- Search Query -->
<div>
<label for="search_query" class="block text-sm font-medium text-gray-700 mb-2">Search Term</label>
<input type="text" name="search_query" id="search_query" value="{{.SearchQuery}}"
placeholder="Enter search term..."
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Role Filter -->
<div>
<label for="role_filter" class="block text-sm font-medium text-gray-700 mb-2">Role Filter</label>
<select name="role_filter" id="role_filter" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">All Roles</option>
<option value="1" {{if eq .RoleFilter "1"}}selected{{end}}>Admin</option>
<option value="2" {{if eq .RoleFilter "2"}}selected{{end}}>Volunteer</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<!-- Date Range -->
<div>
<label for="date_from" class="block text-sm font-medium text-gray-700 mb-2">Date From</label>
<input type="date" name="date_from" id="date_from" value="{{.DateFrom}}"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="date_to" class="block text-sm font-medium text-gray-700 mb-2">Date To</label>
<input type="date" name="date_to" id="date_to" value="{{.DateTo}}"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Status Filter -->
<div>
<label for="status_filter" class="block text-sm font-medium text-gray-700 mb-2">Status Filter</label>
<select name="status_filter" id="status_filter" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">All Status</option>
<option value="active" {{if eq .StatusFilter "active"}}selected{{end}}>Active</option>
<option value="inactive" {{if eq .StatusFilter "inactive"}}selected{{end}}>Inactive</option>
</select>
</div>
<!-- Sort Options -->
<div>
<label for="sort_by" class="block text-sm font-medium text-gray-700 mb-2">Sort By</label>
<select name="sort_by" id="sort_by" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="created_at" {{if eq .SortBy "created_at"}}selected{{end}}>Created Date</option>
<option value="updated_at" {{if eq .SortBy "updated_at"}}selected{{end}}>Updated Date</option>
<option value="first_name" {{if eq .SortBy "first_name"}}selected{{end}}>First Name</option>
<option value="last_name" {{if eq .SortBy "last_name"}}selected{{end}}>Last Name</option>
<option value="email" {{if eq .SortBy "email"}}selected{{end}}>Email</option>
<option value="poll_title" {{if eq .SortBy "poll_title"}}selected{{end}}>Poll Title</option>
<option value="amount_donated" {{if eq .SortBy "amount_donated"}}selected{{end}}>Amount Donated</option>
</select>
</div>
</div>
<div class="flex gap-4">
<button type="submit" 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-search mr-2"></i>Search
</button>
<a href="/reports" class="px-6 py-2.5 bg-gray-600 text-white text-sm font-medium hover:bg-gray-700 transition-colors">
<i class="fas fa-times mr-2"></i>Clear Filters
</a>
</div>
</form>
</div>
</div>
<!-- Results Section - Full Width -->
{{if eq .SearchType "users"}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Users Results ({{.ReportData.TotalUsers}} total)</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 text-sm font-medium text-gray-600">User</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Email</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Phone</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Role</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Admin Code</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .ReportData.Users}}
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-4">
<div class="text-sm font-medium text-gray-900">{{.FirstName}} {{.LastName}}</div>
<div class="text-sm text-gray-500">ID: {{.UserID}}</div>
</td>
<td class="py-4 text-sm text-gray-900">{{.Email}}</td>
<td class="py-4 text-sm text-gray-900">{{.Phone}}</td>
<td class="py-4">
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if eq .RoleID 1}}bg-red-100 text-red-800{{else}}bg-green-100 text-green-800{{end}}">
{{if eq .RoleID 1}}Admin{{else}}Volunteer{{end}}
</span>
</td>
<td class="py-4 text-sm text-gray-900">{{.AdminCode}}</td>
<td class="py-4 text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{else if eq .SearchType "polls"}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Polls Results ({{.ReportData.TotalPolls}} total)</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 text-sm font-medium text-gray-600">Poll</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Author</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Address</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Status</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Donated</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .ReportData.Polls}}
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-4">
<div class="text-sm font-medium text-gray-900">{{.PollTitle}}</div>
<div class="text-sm text-gray-500">{{.PollDescription}}</div>
</td>
<td class="py-4 text-sm text-gray-900">{{.AuthorName}}</td>
<td class="py-4 text-sm text-gray-900">{{.Address}}</td>
<td class="py-4">
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if .IsActive}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{if .IsActive}}Active{{else}}Inactive{{end}}
</span>
</td>
<td class="py-4 text-sm text-gray-900">${{printf "%.2f" .AmountDonated}}</td>
<td class="py-4 text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{else if eq .SearchType "appointments"}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Appointments Results ({{len .ReportData.Appointments}} total)</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 text-sm font-medium text-gray-600">User</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Address</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Date</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Time</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .ReportData.Appointments}}
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-4 text-sm font-medium text-gray-900">{{.UserName}}</td>
<td class="py-4 text-sm text-gray-900">{{.Address}}</td>
<td class="py-4 text-sm text-gray-900">{{.AppointmentDate.Format "2006-01-02"}}</td>
<td class="py-4 text-sm text-gray-900">{{.AppointmentTime.Format "15:04"}}</td>
<td class="py-4 text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{else if eq .SearchType "addresses"}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Addresses Results ({{.ReportData.TotalAddresses}} total)</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 text-sm font-medium text-gray-600">Address</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Street Details</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Coordinates</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Visited</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .ReportData.Addresses}}
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-4">
<div class="text-sm font-medium text-gray-900">{{.Address}}</div>
<div class="text-sm text-gray-500">{{.HouseNumber}}{{.HouseAlpha}}</div>
</td>
<td class="py-4">
<div class="text-sm text-gray-900">{{.StreetName}} {{.StreetType}}</div>
<div class="text-sm text-gray-500">{{.StreetQuadrant}}</div>
</td>
<td class="py-4 text-sm text-gray-900">{{printf "%.6f" .Latitude}}, {{printf "%.6f" .Longitude}}</td>
<td class="py-4">
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if .VisitedValidated}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{if .VisitedValidated}}Visited{{else}}Not Visited{{end}}
</span>
</td>
<td class="py-4 text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{else if eq .SearchType "teams"}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Teams Results ({{len .ReportData.Teams}} total)</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 text-sm font-medium text-gray-600">Team ID</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Team Lead</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Volunteer</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Created</th>
<th class="text-left py-3 text-sm font-medium text-gray-600">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .ReportData.Teams}}
<tr class="hover:bg-gray-50 transition-colors">
<td class="py-4 text-sm font-medium text-gray-900">{{.TeamID}}</td>
<td class="py-4 text-sm text-gray-900">{{.TeamLeadName}}</td>
<td class="py-4 text-sm text-gray-900">{{.VolunteerName}}</td>
<td class="py-4 text-sm text-gray-500">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
<td class="py-4 text-sm font-medium">
<a href="/teams/{{.TeamID}}" class="text-blue-600 hover:text-blue-800">View Details</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
<!-- Summary Dashboard View -->
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Data Overview</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Users -->
<div class="border border-gray-200">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<h4 class="text-base font-medium text-gray-900">Recent Users</h4>
<a href="/reports?search_type=users" class="text-blue-600 hover:text-blue-800 text-sm">View All</a>
</div>
</div>
<div class="p-6">
{{if .ReportData.Users}}
<div class="space-y-4">
{{range .ReportData.Users}}
<div class="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<div>
<div class="text-sm font-medium text-gray-900">{{.FirstName}} {{.LastName}}</div>
<div class="text-xs text-gray-500">{{.Email}}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">{{.CreatedAt.Format "Jan 02, 2006"}}</div>
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if eq .RoleID 1}}bg-red-100 text-red-800{{else}}bg-green-100 text-green-800{{end}}">
{{if eq .RoleID 1}}Admin{{else}}Volunteer{{end}}
</span>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="text-gray-500 text-center py-8">No users found</p>
{{end}}
</div>
</div>
<!-- Recent Polls -->
<div class="border border-gray-200">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<h4 class="text-base font-medium text-gray-900">Recent Polls</h4>
<a href="/reports?search_type=polls" class="text-blue-600 hover:text-blue-800 text-sm">View All</a>
</div>
</div>
<div class="p-6">
{{if .ReportData.Polls}}
<div class="space-y-4">
{{range .ReportData.Polls}}
<div class="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<div>
<div class="text-sm font-medium text-gray-900">{{.PollTitle}}</div>
<div class="text-xs text-gray-500">by {{.AuthorName}}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">{{.CreatedAt.Format "Jan 02, 2006"}}</div>
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if .IsActive}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{if .IsActive}}Active{{else}}Inactive{{end}}
</span>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="text-gray-500 text-center py-8">No polls found</p>
{{end}}
</div>
</div>
<!-- Recent Appointments -->
<div class="border border-gray-200">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<h4 class="text-base font-medium text-gray-900">Recent Appointments</h4>
<a href="/reports?search_type=appointments" class="text-blue-600 hover:text-blue-800 text-sm">View All</a>
</div>
</div>
<div class="p-6">
{{if .ReportData.Appointments}}
<div class="space-y-4">
{{range .ReportData.Appointments}}
<div class="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<div>
<div class="text-sm font-medium text-gray-900">{{.UserName}}</div>
<div class="text-xs text-gray-500">{{.Address}}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-900">{{.AppointmentDate.Format "Jan 02, 2006"}}</div>
<div class="text-xs text-gray-500">{{.AppointmentTime.Format "15:04"}}</div>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="text-gray-500 text-center py-8">No appointments found</p>
{{end}}
</div>
</div>
<!-- Address Summary -->
<div class="border border-gray-200">
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center">
<h4 class="text-base font-medium text-gray-900">Address Summary</h4>
<a href="/reports?search_type=addresses" class="text-blue-600 hover:text-blue-800 text-sm">View All</a>
</div>
</div>
<div class="p-6">
{{if .ReportData.Addresses}}
<div class="space-y-4">
{{range .ReportData.Addresses}}
<div class="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors">
<div>
<div class="text-sm font-medium text-gray-900">{{.Address}}</div>
<div class="text-xs text-gray-500">{{.StreetName}} {{.StreetType}}, {{.StreetQuadrant}}</div>
</div>
<div class="text-right">
<span class="px-2 py-1 text-xs font-semibold rounded-full {{if .VisitedValidated}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{if .VisitedValidated}}Visited{{else}}Not Visited{{end}}
</span>
</div>
</div>
{{end}}
</div>
{{else}}
<p class="text-gray-500 text-center py-8">No addresses found</p>
{{end}}
</div>
</div>
</div>
</div>
</div>
<!-- Analytics Chart Section - Full Width -->
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-6">
<h3 class="text-lg font-semibold text-gray-900 mb-6">Analytics Overview</h3>
<div id="analytics_chart" class="w-full h-[400px]"></div>
</div>
</div>
{{end}}
<!-- Pagination -->
{{if and .SearchType (or (gt .ReportData.TotalUsers 0) (gt .ReportData.TotalPolls 0) (gt .ReportData.TotalAddresses 0))}}
<div class="bg-white border-t border-gray-200 w-full">
<div class="px-8 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span class="text-sm text-gray-700">
Page {{.CurrentPage}} | {{.Limit}} per page
</span>
</div>
<div class="flex items-center gap-2">
{{if gt .CurrentPage 1}}
<a href="?{{.Request.URL.RawQuery}}&page={{sub .CurrentPage 1}}" class="px-3 py-2 border border-gray-300 text-gray-700 text-sm hover:bg-gray-50 transition-colors">
<i class="fas fa-chevron-left"></i>
</a>
{{end}}
<span class="px-3 py-2 bg-blue-50 text-blue-600 text-sm font-medium border border-blue-200">
{{.CurrentPage}}
</span>
<a href="?{{.Request.URL.RawQuery}}&page={{add .CurrentPage 1}}" class="px-3 py-2 border border-gray-300 text-gray-700 text-sm hover:bg-gray-50 transition-colors">
<i class="fas fa-chevron-right"></i>
</a>
</div>
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
<!-- Advanced Search Modal -->
<div id="advancedSearchModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg bg-white">
<div class="mt-3">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-semibold text-gray-900">Advanced Search</h3>
<button onclick="toggleAdvancedSearch()" class="text-gray-400 hover:text-gray-600 text-xl">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/reports/advanced-search" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Entity Type</label>
<select name="entity" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="users">Users</option>
<option value="polls">Polls</option>
<option value="appointments">Appointments</option>
<option value="addresses">Addresses</option>
<option value="teams">Teams</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Text Search</label>
<input type="text" name="text_search" placeholder="Search in names, titles, descriptions..."
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date From</label>
<input type="date" name="date_from"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date To</label>
<input type="date" name="date_to"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Role Filter</label>
<select name="role_filter" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">All Roles</option>
<option value="1">Admin</option>
<option value="2">Volunteer</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Status Filter</label>
<select name="status_filter" class="w-full px-3 py-2 border border-gray-300 bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Min Amount (Polls)</label>
<input type="number" name="amount_min" step="0.01" placeholder="0.00"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Max Amount (Polls)</label>
<input type="number" name="amount_max" step="0.01" placeholder="1000.00"
class="w-full px-3 py-2 border border-gray-300 text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div class="flex justify-end gap-3 pt-6">
<button type="button" onclick="toggleAdvancedSearch()" class="px-6 py-2.5 bg-gray-300 text-gray-800 text-sm font-medium hover:bg-gray-400 transition-colors">
Cancel
</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">
<i class="fas fa-search mr-2"></i>Search
</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Advanced search modal toggle
function toggleAdvancedSearch() {
const modal = document.getElementById('advancedSearchModal');
modal.classList.toggle('hidden');
}
// Print report functionality
function printReport() {
window.print();
}
// Quick filter application
function applyQuickFilter(type, filter) {
const url = new URL(window.location);
url.searchParams.set('search_type', type);
if (filter) {
Object.keys(filter).forEach(key => {
url.searchParams.set(key, filter[key]);
});
}
window.location.href = url.toString();
}
// Export data with current filters
function exportData() {
const url = new URL('/reports/export', window.location.origin);
const currentParams = new URLSearchParams(window.location.search);
currentParams.forEach((value, key) => {
url.searchParams.set(key, value);
});
window.location.href = url.toString();
}
// Auto-submit form when search type changes
document.addEventListener('DOMContentLoaded', function() {
const searchTypeSelect = document.getElementById('search_type');
const searchInput = document.getElementById('search_query');
// Update placeholder based on search type
searchTypeSelect.addEventListener('change', function() {
const placeholders = {
'users': 'Search by name, email...',
'polls': 'Search by title, description...',
'appointments': 'Search by user name, address...',
'addresses': 'Search by address, street name...',
'teams': 'Search by team lead, volunteer name...',
'': 'Enter search term...'
};
searchInput.placeholder = placeholders[this.value] || 'Enter search term...';
});
// Close modal when clicking outside
document.getElementById('advancedSearchModal').addEventListener('click', function(e) {
if (e.target === this) {
toggleAdvancedSearch();
}
});
});
// Google Charts for analytics
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", "Users");
data.addColumn("number", "Polls");
data.addColumn("number", "Addresses");
// Sample data - you can replace this with real data from your backend
data.addRows([
["Jan", {{if .ReportData.TotalUsers}}{{.ReportData.TotalUsers}}{{else}}120{{end}}, {{if .ReportData.TotalPolls}}{{.ReportData.TotalPolls}}{{else}}45{{end}}, {{if .ReportData.TotalAddresses}}{{.ReportData.TotalAddresses}}{{else}}380{{end}}],
["Feb", {{if .ReportData.TotalUsers}}{{add .ReportData.TotalUsers 50}}{{else}}180{{end}}, {{if .ReportData.TotalPolls}}{{add .ReportData.TotalPolls 15}}{{else}}62{{end}}, {{if .ReportData.TotalAddresses}}{{add .ReportData.TotalAddresses 120}}{{else}}420{{end}}],
["Mar", {{if .ReportData.TotalUsers}}{{add .ReportData.TotalUsers 80}}{{else}}220{{end}}, {{if .ReportData.TotalPolls}}{{add .ReportData.TotalPolls 25}}{{else}}78{{end}}, {{if .ReportData.TotalAddresses}}{{add .ReportData.TotalAddresses 200}}{{else}}510{{end}}],
["Apr", {{if .ReportData.TotalUsers}}{{add .ReportData.TotalUsers 120}}{{else}}280{{end}}, {{if .ReportData.TotalPolls}}{{add .ReportData.TotalPolls 35}}{{else}}95{{end}}, {{if .ReportData.TotalAddresses}}{{add .ReportData.TotalAddresses 280}}{{else}}650{{end}}],
["May", {{if .ReportData.TotalUsers}}{{add .ReportData.TotalUsers 150}}{{else}}320{{end}}, {{if .ReportData.TotalPolls}}{{add .ReportData.TotalPolls 42}}{{else}}108{{end}}, {{if .ReportData.TotalAddresses}}{{add .ReportData.TotalAddresses 350}}{{else}}720{{end}}],
["Jun", {{if .ReportData.TotalUsers}}{{add .ReportData.TotalUsers 200}}{{else}}380{{end}}, {{if .ReportData.TotalPolls}}{{add .ReportData.TotalPolls 58}}{{else}}125{{end}}, {{if .ReportData.TotalAddresses}}{{add .ReportData.TotalAddresses 420}}{{else}}800{{end}}],
]);
var options = {
title: "System Growth Over Time",
backgroundColor: "transparent",
hAxis: { title: "Month" },
vAxis: { title: "Count" },
colors: ["#3B82F6", "#10B981", "#F59E0B"],
chartArea: {
left: 60,
top: 40,
width: "90%",
height: "70%",
},
legend: { position: "top", alignment: "center" },
lineWidth: 3,
pointSize: 5,
};
var chart = new google.visualization.LineChart(
document.getElementById("analytics_chart")
);
chart.draw(data, options);
}
function updateChart(type) {
drawAnalyticsChart();
}
// Real-time search functionality
let searchTimeout;
function handleSearchInput() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
// You can implement real-time search here
console.log('Performing search...');
}, 500);
}
// Quick stats click handlers
function focusUsers() {
applyQuickFilter('users', {});
}
function focusPolls() {
applyQuickFilter('polls', {});
}
function focusAddresses() {
applyQuickFilter('addresses', {});
}
function focusAppointments() {
applyQuickFilter('appointments', {});
}
</script>
<style>
@media print {
.no-print {
display: none !important;
}
.print-only {
display: block !important;
}
body {
background: white !important;
}
.border-gray-200 {
border-color: #000 !important;
}
}
/* Enhanced hover effects matching dashboard style */
.hover-highlight:hover {
background-color: #f7fafc;
transform: translateY(-1px);
transition: all 0.2s ease;
}
/* Custom scrollbar for tables */
.overflow-x-auto::-webkit-scrollbar {
height: 6px;
}
.overflow-x-auto::-webkit-scrollbar-track {
background: #f1f5f9;
}
.overflow-x-auto::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</body>
</html>
{{ end }}

View File

@@ -0,0 +1,752 @@
{{ 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 }}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
)
@@ -45,6 +46,17 @@ var templateFuncs = template.FuncMap{
}
return pages
},
"formatColumnName": func(name string) string {
// Replace underscores with spaces and title case each word
formatted := strings.ReplaceAll(name, "_", " ")
words := strings.Fields(formatted)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
}
}
return strings.Join(words, " ")
},
}
func Render(w http.ResponseWriter, tmpl string, data interface{}) {

BIN
app/main

Binary file not shown.

View File

@@ -152,7 +152,7 @@ func main() {
http.HandleFunc("/remove_assigned_address", adminMiddleware(handlers.RemoveAssignedAddressHandler))
http.HandleFunc("/addresses/upload-csv", adminMiddleware(handlers.CSVUploadHandler))
http.HandleFunc("/reports", adminMiddleware(handlers.ReportHandler))
http.HandleFunc("/smart-reports", adminMiddleware(handlers.SmartFilterHandler))

408996
misc-code/Address.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,824 @@
package handlers
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
// ReportData represents the combined data for reports
type ReportData struct {
Users []models.User
Polls []PollWithDetails
Appointments []AppointmentWithDetails
Addresses []models.AddressDatabase
Teams []TeamWithDetails
TotalUsers int
TotalPolls int
TotalAddresses int
}
type PollWithDetails struct {
PollID int `json:"poll_id"`
UserID int `json:"user_id"`
AuthorName string `json:"author_name"`
AddressID int `json:"address_id"`
Address string `json:"address"`
PollTitle string `json:"poll_title"`
PollDescription string `json:"poll_description"`
IsActive bool `json:"is_active"`
AmountDonated float64 `json:"amount_donated"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AppointmentWithDetails struct {
SchedID int `json:"sched_id"`
UserID int `json:"user_id"`
UserName string `json:"user_name"`
AddressID int `json:"address_id"`
Address string `json:"address"`
AppointmentDate time.Time `json:"appointment_date"`
AppointmentTime time.Time `json:"appointment_time"`
CreatedAt time.Time `json:"created_at"`
}
type TeamWithDetails struct {
TeamID int `json:"team_id"`
TeamLeadID int `json:"team_lead_id"`
TeamLeadName string `json:"team_lead_name"`
VolunteerID int `json:"volunteer_id"`
VolunteerName string `json:"volunteer_name"`
CreatedAt time.Time `json:"created_at"`
}
// ReportHandler handles the report page with search and filter functionality
func ReportHandler(w http.ResponseWriter, r *http.Request) {
// currentUserID := r.Context().Value("user_id").(int)
username, _ := models.GetCurrentUserName(r)
role := r.Context().Value("user_role").(int)
// Check if user has permission to view reports
if role != 1 { // Assuming role 1 is admin
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
// Parse query parameters for filtering
searchType := r.URL.Query().Get("search_type") // users, polls, appointments, addresses, teams
searchQuery := r.URL.Query().Get("search_query") // general search term
dateFrom := r.URL.Query().Get("date_from")
dateTo := r.URL.Query().Get("date_to")
roleFilter := r.URL.Query().Get("role_filter")
statusFilter := r.URL.Query().Get("status_filter") // active, inactive for polls
sortBy := r.URL.Query().Get("sort_by") // created_at, name, email, etc.
sortOrder := r.URL.Query().Get("sort_order") // asc, desc
page := r.URL.Query().Get("page")
limit := r.URL.Query().Get("limit")
// Set defaults
if sortBy == "" {
sortBy = "created_at"
}
if sortOrder == "" {
sortOrder = "desc"
}
if page == "" {
page = "1"
}
if limit == "" {
limit = "50"
}
pageInt, _ := strconv.Atoi(page)
limitInt, _ := strconv.Atoi(limit)
offset := (pageInt - 1) * limitInt
reportData := ReportData{}
// Build queries based on search type and filters
switch searchType {
case "users":
reportData.Users = searchUsers(searchQuery, roleFilter, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset)
reportData.TotalUsers = countUsers(searchQuery, roleFilter, dateFrom, dateTo)
case "polls":
reportData.Polls = searchPolls(searchQuery, statusFilter, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset)
reportData.TotalPolls = countPolls(searchQuery, statusFilter, dateFrom, dateTo)
case "appointments":
reportData.Appointments = searchAppointments(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset)
case "addresses":
reportData.Addresses = searchAddresses(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset)
reportData.TotalAddresses = countAddresses(searchQuery, dateFrom, dateTo)
case "teams":
reportData.Teams = searchTeams(searchQuery, dateFrom, dateTo, sortBy, sortOrder, limitInt, offset)
default:
// Load summary data for all types
reportData.Users = searchUsers("", "", "", "", "created_at", "desc", 10, 0)
reportData.Polls = searchPolls("", "", "", "", "created_at", "desc", 10, 0)
reportData.Appointments = searchAppointments("", "", "", "created_at", "desc", 10, 0)
reportData.Addresses = searchAddresses("", "", "", "created_at", "desc", 10, 0)
reportData.Teams = searchTeams("", "", "", "created_at", "desc", 10, 0)
reportData.TotalUsers = countUsers("", "", "", "")
reportData.TotalPolls = countPolls("", "", "", "")
reportData.TotalAddresses = countAddresses("", "", "")
}
adminnav := role == 1
volunteernav := role != 1
utils.Render(w, "reports.html", map[string]interface{}{
"Title": "Reports & Analytics",
"IsAuthenticated": true,
"ShowAdminNav": adminnav,
"ShowVolunteerNav": volunteernav,
"UserName": username,
"ActiveSection": "reports",
"ReportData": reportData,
"SearchType": searchType,
"SearchQuery": searchQuery,
"DateFrom": dateFrom,
"DateTo": dateTo,
"RoleFilter": roleFilter,
"StatusFilter": statusFilter,
"SortBy": sortBy,
"SortOrder": sortOrder,
"CurrentPage": pageInt,
"Limit": limitInt,
})
}
// searchUsers searches users with filters
func searchUsers(searchQuery, roleFilter, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []models.User {
var users []models.User
query := `
SELECT u.user_id, u.first_name, u.last_name, u.email, u.phone, u.role_id, u.created_at, u.updated_at, u.admin_code
FROM users u
LEFT JOIN role r ON u.role_id = r.role_id
WHERE 1=1`
var args []interface{}
argCount := 0
// Add search conditions
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(u.email) LIKE LOWER($%d))`, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if roleFilter != "" {
argCount++
query += fmt.Sprintf(` AND u.role_id = $%d`, argCount)
roleID, _ := strconv.Atoi(roleFilter)
args = append(args, roleID)
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND u.created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND u.created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
// Add sorting
validSortColumns := map[string]bool{"created_at": true, "first_name": true, "last_name": true, "email": true}
if !validSortColumns[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
query += fmt.Sprintf(` ORDER BY u.%s %s`, sortBy, strings.ToUpper(sortOrder))
// Add pagination
argCount++
query += fmt.Sprintf(` LIMIT $%d`, argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(` OFFSET $%d`, argCount)
args = append(args, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
log.Println("Error searching users:", err)
return users
}
defer rows.Close()
for rows.Next() {
var user models.User
err := rows.Scan(&user.UserID, &user.FirstName, &user.LastName, &user.Email, &user.Phone, &user.RoleID, &user.CreatedAt, &user.UpdatedAt, &user.AdminCode)
if err != nil {
log.Println("Error scanning user:", err)
continue
}
users = append(users, user)
}
return users
}
// searchPolls searches polls with filters
func searchPolls(searchQuery, statusFilter, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []PollWithDetails {
var polls []PollWithDetails
query := `
SELECT p.poll_id, p.user_id, COALESCE(u.first_name || ' ' || u.last_name, 'Unknown') as author_name,
p.address_id, COALESCE(a.address, 'No Address') as address,
p.poll_title, p.poll_description, p.is_active, p.amount_donated, p.created_at, p.updated_at
FROM poll p
LEFT JOIN users u ON p.user_id = u.user_id
LEFT JOIN address_database a ON p.address_id = a.address_id
WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(p.poll_title) LIKE LOWER($%d) OR LOWER(p.poll_description) LIKE LOWER($%d))`, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if statusFilter == "active" {
query += ` AND p.is_active = true`
} else if statusFilter == "inactive" {
query += ` AND p.is_active = false`
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND p.created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND p.created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
validSortColumns := map[string]bool{"created_at": true, "poll_title": true, "amount_donated": true, "is_active": true}
if !validSortColumns[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
query += fmt.Sprintf(` ORDER BY p.%s %s`, sortBy, strings.ToUpper(sortOrder))
argCount++
query += fmt.Sprintf(` LIMIT $%d`, argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(` OFFSET $%d`, argCount)
args = append(args, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
log.Println("Error searching polls:", err)
return polls
}
defer rows.Close()
for rows.Next() {
var poll PollWithDetails
err := rows.Scan(&poll.PollID, &poll.UserID, &poll.AuthorName, &poll.AddressID, &poll.Address,
&poll.PollTitle, &poll.PollDescription, &poll.IsActive, &poll.AmountDonated, &poll.CreatedAt, &poll.UpdatedAt)
if err != nil {
log.Println("Error scanning poll:", err)
continue
}
polls = append(polls, poll)
}
return polls
}
// searchAppointments searches appointments with filters
func searchAppointments(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []AppointmentWithDetails {
var appointments []AppointmentWithDetails
query := `
SELECT a.sched_id, a.user_id, COALESCE(u.first_name || ' ' || u.last_name, 'Unknown') as user_name,
a.address_id, COALESCE(ad.address, 'No Address') as address,
a.appointment_date, a.appointment_time, a.created_at
FROM appointment a
LEFT JOIN users u ON a.user_id = u.user_id
LEFT JOIN address_database ad ON a.address_id = ad.address_id
WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(ad.address) LIKE LOWER($%d))`, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND a.appointment_date >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND a.appointment_date <= $%d`, argCount)
args = append(args, dateTo)
}
validSortColumns := map[string]bool{"created_at": true, "appointment_date": true, "appointment_time": true}
if !validSortColumns[sortBy] {
sortBy = "appointment_date"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
query += fmt.Sprintf(` ORDER BY a.%s %s`, sortBy, strings.ToUpper(sortOrder))
argCount++
query += fmt.Sprintf(` LIMIT $%d`, argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(` OFFSET $%d`, argCount)
args = append(args, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
log.Println("Error searching appointments:", err)
return appointments
}
defer rows.Close()
for rows.Next() {
var apt AppointmentWithDetails
var appointmentTime sql.NullTime
err := rows.Scan(&apt.SchedID, &apt.UserID, &apt.UserName, &apt.AddressID, &apt.Address,
&apt.AppointmentDate, &appointmentTime, &apt.CreatedAt)
if err != nil {
log.Println("Error scanning appointment:", err)
continue
}
if appointmentTime.Valid {
apt.AppointmentTime = appointmentTime.Time
}
appointments = append(appointments, apt)
}
return appointments
}
// searchAddresses searches addresses with filters
func searchAddresses(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []models.AddressDatabase {
var addresses []models.AddressDatabase
query := `
SELECT address_id, address, street_name, street_type, street_quadrant,
house_number, house_alpha, longitude, latitude, visited_validated, created_at, updated_at
FROM address_database
WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(address) LIKE LOWER($%d) OR LOWER(street_name) LIKE LOWER($%d) OR house_number LIKE $%d)`, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
validSortColumns := map[string]bool{"created_at": true, "address": true, "street_name": true, "visited_validated": true}
if !validSortColumns[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
query += fmt.Sprintf(` ORDER BY %s %s`, sortBy, strings.ToUpper(sortOrder))
argCount++
query += fmt.Sprintf(` LIMIT $%d`, argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(` OFFSET $%d`, argCount)
args = append(args, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
log.Println("Error searching addresses:", err)
return addresses
}
defer rows.Close()
for rows.Next() {
var addr models.AddressDatabase
err := rows.Scan(&addr.AddressID, &addr.Address, &addr.StreetName, &addr.StreetType, &addr.StreetQuadrant,
&addr.HouseNumber, &addr.HouseAlpha, &addr.Longitude, &addr.Latitude, &addr.VisitedValidated, &addr.CreatedAt, &addr.UpdatedAt)
if err != nil {
log.Println("Error scanning address:", err)
continue
}
addresses = append(addresses, addr)
}
return addresses
}
// searchTeams searches teams with filters
func searchTeams(searchQuery, dateFrom, dateTo, sortBy, sortOrder string, limit, offset int) []TeamWithDetails {
var teams []TeamWithDetails
query := `
SELECT t.team_id, t.team_lead_id,
COALESCE(lead.first_name || ' ' || lead.last_name, 'No Lead') as team_lead_name,
t.volunteer_id,
COALESCE(vol.first_name || ' ' || vol.last_name, 'No Volunteer') as volunteer_name,
t.created_at
FROM team t
LEFT JOIN users lead ON t.team_lead_id = lead.user_id
LEFT JOIN users vol ON t.volunteer_id = vol.user_id
WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(lead.first_name) LIKE LOWER($%d) OR LOWER(lead.last_name) LIKE LOWER($%d) OR LOWER(vol.first_name) LIKE LOWER($%d) OR LOWER(vol.last_name) LIKE LOWER($%d))`, argCount, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND t.created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND t.created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
validSortColumns := map[string]bool{"created_at": true, "team_id": true}
if !validSortColumns[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
query += fmt.Sprintf(` ORDER BY t.%s %s`, sortBy, strings.ToUpper(sortOrder))
argCount++
query += fmt.Sprintf(` LIMIT $%d`, argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(` OFFSET $%d`, argCount)
args = append(args, offset)
rows, err := models.DB.Query(query, args...)
if err != nil {
log.Println("Error searching teams:", err)
return teams
}
defer rows.Close()
for rows.Next() {
var team TeamWithDetails
err := rows.Scan(&team.TeamID, &team.TeamLeadID, &team.TeamLeadName, &team.VolunteerID, &team.VolunteerName, &team.CreatedAt)
if err != nil {
log.Println("Error scanning team:", err)
continue
}
teams = append(teams, team)
}
return teams
}
// Helper functions for counting total records
func countUsers(searchQuery, roleFilter, dateFrom, dateTo string) int {
query := `SELECT COUNT(*) FROM users u WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(u.first_name) LIKE LOWER($%d) OR LOWER(u.last_name) LIKE LOWER($%d) OR LOWER(u.email) LIKE LOWER($%d))`, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if roleFilter != "" {
argCount++
query += fmt.Sprintf(` AND u.role_id = $%d`, argCount)
roleID, _ := strconv.Atoi(roleFilter)
args = append(args, roleID)
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND u.created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND u.created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
var count int
err := models.DB.QueryRow(query, args...).Scan(&count)
if err != nil {
log.Println("Error counting users:", err)
return 0
}
return count
}
func countPolls(searchQuery, statusFilter, dateFrom, dateTo string) int {
query := `SELECT COUNT(*) FROM poll p WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(p.poll_title) LIKE LOWER($%d) OR LOWER(p.poll_description) LIKE LOWER($%d))`, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if statusFilter == "active" {
query += ` AND p.is_active = true`
} else if statusFilter == "inactive" {
query += ` AND p.is_active = false`
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND p.created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND p.created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
var count int
err := models.DB.QueryRow(query, args...).Scan(&count)
if err != nil {
log.Println("Error counting polls:", err)
return 0
}
return count
}
func countAddresses(searchQuery, dateFrom, dateTo string) int {
query := `SELECT COUNT(*) FROM address_database WHERE 1=1`
var args []interface{}
argCount := 0
if searchQuery != "" {
argCount++
query += fmt.Sprintf(` AND (LOWER(address) LIKE LOWER($%d) OR LOWER(street_name) LIKE LOWER($%d) OR house_number LIKE $%d)`, argCount, argCount, argCount)
args = append(args, "%"+searchQuery+"%")
}
if dateFrom != "" {
argCount++
query += fmt.Sprintf(` AND created_at >= $%d`, argCount)
args = append(args, dateFrom)
}
if dateTo != "" {
argCount++
query += fmt.Sprintf(` AND created_at <= $%d`, argCount)
args = append(args, dateTo+" 23:59:59")
}
var count int
err := models.DB.QueryRow(query, args...).Scan(&count)
if err != nil {
log.Println("Error counting addresses:", err)
return 0
}
return count
}
// ExportReportHandler handles CSV export of filtered data
func ExportReportHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
if role != 1 { // Admin only
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
searchType := r.URL.Query().Get("search_type")
// Get all the same filter parameters
searchQuery := r.URL.Query().Get("search_query")
dateFrom := r.URL.Query().Get("date_from")
dateTo := r.URL.Query().Get("date_to")
roleFilter := r.URL.Query().Get("role_filter")
statusFilter := r.URL.Query().Get("status_filter")
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s_report_%s.csv\"", searchType, time.Now().Format("2006-01-02")))
switch searchType {
case "users":
users := searchUsers(searchQuery, roleFilter, dateFrom, dateTo, "created_at", "desc", 10000, 0) // Get all for export
w.Write([]byte("User ID,First Name,Last Name,Email,Phone,Role ID,Admin Code,Created At\n"))
for _, user := range users {
line := fmt.Sprintf("%d,%s,%s,%s,%s,%d,%s,%s\n",
user.UserID, user.FirstName, user.LastName, user.Email, user.Phone, user.RoleID, user.AdminCode, user.CreatedAt.Format("2006-01-02 15:04:05"))
w.Write([]byte(line))
}
case "polls":
polls := searchPolls(searchQuery, statusFilter, dateFrom, dateTo, "created_at", "desc", 10000, 0)
w.Write([]byte("Poll ID,Author,Address,Title,Description,Active,Amount Donated,Created At\n"))
for _, poll := range polls {
line := fmt.Sprintf("%d,%s,%s,%s,%s,%t,%.2f,%s\n",
poll.PollID, poll.AuthorName, poll.Address, poll.PollTitle, poll.PollDescription, poll.IsActive, poll.AmountDonated, poll.CreatedAt.Format("2006-01-02 15:04:05"))
w.Write([]byte(line))
}
case "appointments":
appointments := searchAppointments(searchQuery, dateFrom, dateTo, "appointment_date", "desc", 10000, 0)
w.Write([]byte("Schedule ID,User,Address,Date,Time,Created At\n"))
for _, apt := range appointments {
timeStr := ""
if !apt.AppointmentTime.IsZero() {
timeStr = apt.AppointmentTime.Format("15:04:05")
}
line := fmt.Sprintf("%d,%s,%s,%s,%s,%s\n",
apt.SchedID, apt.UserName, apt.Address, apt.AppointmentDate.Format("2006-01-02"), timeStr, apt.CreatedAt.Format("2006-01-02 15:04:05"))
w.Write([]byte(line))
}
case "addresses":
addresses := searchAddresses(searchQuery, dateFrom, dateTo, "created_at", "desc", 10000, 0)
w.Write([]byte("Address ID,Address,Street Name,Street Type,Quadrant,House Number,Alpha,Longitude,Latitude,Visited,Created At\n"))
for _, addr := range addresses {
line := fmt.Sprintf("%d,%s,%s,%s,%s,%s,%s,%.6f,%.6f,%t,%s\n",
addr.AddressID, addr.Address, addr.StreetName, addr.StreetType, addr.StreetQuadrant, addr.HouseNumber, addr.HouseAlpha,
addr.Longitude, addr.Latitude, addr.VisitedValidated, addr.CreatedAt.Format("2006-01-02 15:04:05"))
w.Write([]byte(line))
}
case "teams":
teams := searchTeams(searchQuery, dateFrom, dateTo, "created_at", "desc", 10000, 0)
w.Write([]byte("Team ID,Team Lead,Volunteer,Created At\n"))
for _, team := range teams {
line := fmt.Sprintf("%d,%s,%s,%s\n",
team.TeamID, team.TeamLeadName, team.VolunteerName, team.CreatedAt.Format("2006-01-02 15:04:05"))
w.Write([]byte(line))
}
default:
http.Error(w, "Invalid export type", http.StatusBadRequest)
return
}
}
// ReportStatsHandler provides JSON API for dashboard statistics
func ReportStatsHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
if role != 1 { // Admin only
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
stats := make(map[string]interface{})
// Get total counts
var totalUsers, totalPolls, totalAddresses, totalAppointments, totalTeams int
var activePolls, inactivePolls int
var visitedAddresses, unvisitedAddresses int
// Total users
models.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&totalUsers)
// Total and active/inactive polls
models.DB.QueryRow("SELECT COUNT(*) FROM poll").Scan(&totalPolls)
models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE is_active = true").Scan(&activePolls)
models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE is_active = false").Scan(&inactivePolls)
// Total and visited/unvisited addresses
models.DB.QueryRow("SELECT COUNT(*) FROM address_database").Scan(&totalAddresses)
models.DB.QueryRow("SELECT COUNT(*) FROM address_database WHERE visited_validated = true").Scan(&visitedAddresses)
models.DB.QueryRow("SELECT COUNT(*) FROM address_database WHERE visited_validated = false").Scan(&unvisitedAddresses)
// Total appointments and teams
models.DB.QueryRow("SELECT COUNT(*) FROM appointment").Scan(&totalAppointments)
models.DB.QueryRow("SELECT COUNT(*) FROM team").Scan(&totalTeams)
// Recent activity (last 30 days)
var recentUsers, recentPolls, recentAppointments int
models.DB.QueryRow("SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentUsers)
models.DB.QueryRow("SELECT COUNT(*) FROM poll WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentPolls)
models.DB.QueryRow("SELECT COUNT(*) FROM appointment WHERE created_at >= NOW() - INTERVAL '30 days'").Scan(&recentAppointments)
// Total donations
var totalDonations float64
models.DB.QueryRow("SELECT COALESCE(SUM(amount_donated), 0) FROM poll").Scan(&totalDonations)
stats["totals"] = map[string]int{
"users": totalUsers,
"polls": totalPolls,
"addresses": totalAddresses,
"appointments": totalAppointments,
"teams": totalTeams,
}
stats["poll_breakdown"] = map[string]int{
"active": activePolls,
"inactive": inactivePolls,
}
stats["address_breakdown"] = map[string]int{
"visited": visitedAddresses,
"unvisited": unvisitedAddresses,
}
stats["recent_activity"] = map[string]int{
"users": recentUsers,
"polls": recentPolls,
"appointments": recentAppointments,
}
stats["total_donations"] = totalDonations
w.Header().Set("Content-Type", "application/json")
// utils.WriteJSON(w, stats)
}
// Advanced search handler for complex queries
func AdvancedSearchHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
if role != 1 { // Admin only
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, "Invalid form data", http.StatusBadRequest)
return
}
// Build complex query based on multiple criteria
searchCriteria := map[string]string{
"entity": r.FormValue("entity"), // users, polls, appointments, etc.
"text_search": r.FormValue("text_search"),
"date_from": r.FormValue("date_from"),
"date_to": r.FormValue("date_to"),
"role_filter": r.FormValue("role_filter"),
"status_filter": r.FormValue("status_filter"),
"location_filter": r.FormValue("location_filter"), // address-based filtering
"amount_min": r.FormValue("amount_min"), // for polls with donations
"amount_max": r.FormValue("amount_max"),
"sort_by": r.FormValue("sort_by"),
"sort_order": r.FormValue("sort_order"),
}
// Redirect with query parameters
redirectURL := "/reports?"
params := []string{}
for key, value := range searchCriteria {
if value != "" {
params = append(params, fmt.Sprintf("%s=%s", key, value))
}
}
redirectURL += strings.Join(params, "&")
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}

319
misc-code/postal_get.py Normal file
View File

@@ -0,0 +1,319 @@
import pandas as pd
import requests
import time
import json
from typing import Optional, Dict, Any
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PostalCodeFetcher:
"""Fetch postal codes for Canadian addresses using various geocoding services"""
def __init__(self, api_key: Optional[str] = None, service: str = 'nominatim'):
"""
Initialize the postal code fetcher
Args:
api_key: API key for paid services (Google, MapBox, etc.)
service: Geocoding service to use ('nominatim', 'google', 'mapbox')
"""
self.api_key = api_key
self.service = service.lower()
self.session = requests.Session()
self.rate_limit_delay = 1.0 # seconds between requests
def format_address(self, row: Dict[str, Any]) -> str:
"""
Format address from CSV row data
Args:
row: Dictionary containing address components
Returns:
Formatted address string
"""
# Extract components
house_num = str(row.get('HOUSE_NUMBER', '')).strip()
house_alpha = str(row.get('HOUSE_ALPHA', '')).strip()
street_name = str(row.get('STREET_NAME', '')).strip()
street_type = str(row.get('STREET_TYPE', '')).strip()
street_quad = str(row.get('STREET_QUAD', '')).strip()
# Build address
address_parts = []
# House number and alpha
house_part = house_num
if house_alpha and house_alpha != 'nan':
house_part += house_alpha
if house_part:
address_parts.append(house_part)
# Street name and type
street_part = street_name
if street_type and street_type != 'nan' and street_type.upper() not in street_name.upper():
street_part += f" {street_type}"
if street_part:
address_parts.append(street_part)
# Quadrant
if street_quad and street_quad != 'nan':
address_parts.append(street_quad)
# Add city and province
address_parts.extend(["Calgary", "AB", "Canada"])
return ", ".join(address_parts)
def get_postal_code_nominatim(self, address: str, lat: float = None, lon: float = None) -> Optional[str]:
"""
Get postal code using OpenStreetMap Nominatim (free)
Args:
address: Full address string
lat: Latitude (optional, for reverse geocoding)
lon: Longitude (optional, for reverse geocoding)
Returns:
Postal code if found, None otherwise
"""
try:
# Try reverse geocoding first if coordinates available
if lat and lon:
url = "https://nominatim.openstreetmap.org/reverse"
params = {
'format': 'json',
'lat': lat,
'lon': lon,
'zoom': 18,
'addressdetails': 1
}
else:
# Forward geocoding
url = "https://nominatim.openstreetmap.org/search"
params = {
'q': address,
'format': 'json',
'addressdetails': 1,
'limit': 1,
'countrycodes': 'ca'
}
headers = {
'User-Agent': 'PostalCodeFetcher/1.0 (your-email@domain.com)'
}
response = self.session.get(url, params=params, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if not data:
return None
# Extract postal code
if isinstance(data, list):
result = data[0] if data else {}
else:
result = data
address_details = result.get('address', {})
postal_code = address_details.get('postcode')
return postal_code
except Exception as e:
logger.error(f"Nominatim error for {address}: {e}")
return None
def get_postal_code_google(self, address: str) -> Optional[str]:
"""
Get postal code using Google Geocoding API (requires API key)
Args:
address: Full address string
Returns:
Postal code if found, None otherwise
"""
if not self.api_key:
logger.error("Google API key required")
return None
try:
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {
'address': address,
'key': self.api_key,
'region': 'ca'
}
response = self.session.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('status') != 'OK' or not data.get('results'):
return None
result = data['results'][0]
# Extract postal code from address components
for component in result.get('address_components', []):
if 'postal_code' in component.get('types', []):
return component.get('long_name')
return None
except Exception as e:
logger.error(f"Google API error for {address}: {e}")
return None
def get_postal_code(self, row: Dict[str, Any]) -> Optional[str]:
"""
Get postal code for a single address row
Args:
row: Dictionary containing address data
Returns:
Postal code if found, None otherwise
"""
# Format the address
address = self.format_address(row)
logger.info(f"Fetching postal code for: {address}")
# Extract coordinates if available
lat = row.get('latitude')
lon = row.get('longitude')
# Convert to float if they're strings
try:
if lat and str(lat) != 'nan':
lat = float(lat)
else:
lat = None
except (ValueError, TypeError):
lat = None
try:
if lon and str(lon) != 'nan':
lon = float(lon)
else:
lon = None
except (ValueError, TypeError):
lon = None
postal_code = None
# Try different services
if self.service == 'nominatim':
postal_code = self.get_postal_code_nominatim(address, lat, lon)
elif self.service == 'google':
postal_code = self.get_postal_code_google(address)
# Rate limiting
time.sleep(self.rate_limit_delay)
return postal_code
def process_csv(self, csv_file_path: str, output_file_path: str = None) -> pd.DataFrame:
"""
Process CSV file and add postal codes
Args:
csv_file_path: Path to input CSV file
output_file_path: Path to output CSV file (optional)
Returns:
DataFrame with postal codes added
"""
logger.info(f"Processing CSV file: {csv_file_path}")
# Read CSV
df = pd.read_csv(csv_file_path)
logger.info(f"Loaded {len(df)} addresses")
# Add postal code column
df['POSTAL_CODE'] = None
# Process each row
for index, row in df.iterrows():
try:
postal_code = self.get_postal_code(row.to_dict())
df.at[index, 'POSTAL_CODE'] = postal_code
if postal_code:
logger.info(f"Row {index + 1}: Found postal code {postal_code}")
else:
logger.warning(f"Row {index + 1}: No postal code found")
except Exception as e:
logger.error(f"Error processing row {index + 1}: {e}")
df.at[index, 'POSTAL_CODE'] = None
# Save results
if output_file_path:
df.to_csv(output_file_path, index=False)
logger.info(f"Results saved to: {output_file_path}")
# Summary
found_count = df['POSTAL_CODE'].notna().sum()
logger.info(f"Successfully found postal codes for {found_count}/{len(df)} addresses")
return df
# Example usage functions
def fetch_postal_codes_free(csv_file_path: str, output_file_path: str = None) -> pd.DataFrame:
"""
Fetch postal codes using free Nominatim service
Args:
csv_file_path: Path to CSV file with address data
output_file_path: Optional output file path
Returns:
DataFrame with postal codes
"""
fetcher = PostalCodeFetcher(service='nominatim')
return fetcher.process_csv(csv_file_path, output_file_path)
def fetch_postal_codes_google(csv_file_path: str, api_key: str, output_file_path: str = None) -> pd.DataFrame:
"""
Fetch postal codes using Google Geocoding API
Args:
csv_file_path: Path to CSV file with address data
api_key: Google API key
output_file_path: Optional output file path
Returns:
DataFrame with postal codes
"""
fetcher = PostalCodeFetcher(api_key=api_key, service='google')
return fetcher.process_csv(csv_file_path, output_file_path)
# Sample usage
if __name__ == "__main__":
# Example 1: Using free Nominatim service
df = fetch_postal_codes_free('./Address.csv', 'addresses_with_postal_codes.csv')
# Example 2: Using Google API (requires API key)
# df = fetch_postal_codes_google('addresses.csv', 'YOUR_GOOGLE_API_KEY', 'addresses_with_postal_codes.csv')
# Example 3: Manual usage
#fetcher = PostalCodeFetcher(service='nominatim')
# Test with sample data
#sample_row = {
# 'HOUSE_NUMBER': '531',
# 'STREET_NAME': 'NORTHMOUNT',
# 'STREET_TYPE': 'DR',
# 'STREET_QUAD': 'NW',
# 'latitude': 51.0893695,
# 'longitude': -114.08514
#}
#postal_code = fetcher.get_postal_code(sample_row)
#print(f"Postal code: {postal_code}")