824 lines
26 KiB
Go
824 lines
26 KiB
Go
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)
|
|
} |