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) }