Files
Poll-system/app/internal/handlers/admin_reports.go
2025-09-01 17:32:00 -06:00

614 lines
18 KiB
Go

package handlers
import (
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/patel-mann/poll-system/app/internal/models"
"github.com/patel-mann/poll-system/app/internal/utils"
)
// SmartFilterQuery represents a parsed smart filter query
type SmartFilterQuery struct {
Tables []string
Conditions []FilterCondition
Groupings []string
Aggregations []string
Sorting []string
Limit int
}
type FilterCondition struct {
Table string
Column string
Operator string
Value interface{}
LogicalOp string // AND, OR
}
// SmartFilterResult represents the result of a smart filter query
type SmartFilterResult struct {
Columns []string
Rows [][]interface{}
Query string
Count int
Error string
}
// SmartFilterHandler handles intelligent filtering with natural language parsing
func SmartFilterHandler(w http.ResponseWriter, r *http.Request) {
username, _ := models.GetCurrentUserName(r)
role := r.Context().Value("user_role").(int)
if role != 1 {
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
smartQuery := r.URL.Query().Get("smart_query")
var result SmartFilterResult
if smartQuery != "" {
result = executeSmartFilter(smartQuery)
}
adminnav := role == 1
volunteernav := role != 1
utils.Render(w, "smart_reports.html", map[string]interface{}{
"Title": "Smart Reports & Analytics",
"IsAuthenticated": true,
"ShowAdminNav": adminnav,
"ShowVolunteerNav": volunteernav,
"UserName": username,
"ActiveSection": "reports",
"SmartQuery": smartQuery,
"Result": result,
"QueryExamples": getQueryExamples(),
})
}
// executeSmartFilter parses and executes a smart filter query
func executeSmartFilter(query string) SmartFilterResult {
parsed := parseSmartQuery(query)
if parsed == nil {
return SmartFilterResult{Error: "Could not parse query"}
}
sqlQuery, err := buildSQLFromParsed(parsed)
if err != nil {
return SmartFilterResult{Error: err.Error()}
}
return executeSQLQuery(sqlQuery)
}
// parseSmartQuery parses natural language into structured query
func parseSmartQuery(query string) *SmartFilterQuery {
query = strings.ToLower(strings.TrimSpace(query))
// Initialize the parsed query
parsed := &SmartFilterQuery{
Tables: []string{},
Conditions: []FilterCondition{},
Limit: 100, // Default limit
}
// Define entity mappings
entityMappings := map[string]string{
"volunteer": "users",
"volunteers": "users",
"user": "users",
"users": "users",
"admin": "users",
"admins": "users",
"poll": "poll",
"polls": "poll",
"address": "address_database",
"addresses": "address_database",
"appointment": "appointment",
"appointments": "appointment",
"team": "team",
"teams": "team",
}
// Define column mappings for each table
columnMappings := map[string]map[string]string{
"users": {
"name": "first_name || ' ' || last_name",
"email": "email",
"phone": "phone",
"role": "role_id",
"created": "created_at",
"updated": "updated_at",
},
"poll": {
"title": "poll_title",
"description": "poll_description",
"active": "is_active",
"donated": "amount_donated",
"donation": "amount_donated",
"money": "amount_donated",
"amount": "amount_donated",
"created": "created_at",
"updated": "updated_at",
},
"address_database": {
"address": "address",
"street": "street_name",
"house": "house_number",
"visited": "visited_validated",
"latitude": "latitude",
"longitude": "longitude",
"created": "created_at",
},
"appointment": {
"date": "appointment_date",
"time": "appointment_time",
"created": "created_at",
},
"team": {
"lead": "team_lead_id",
"volunteer": "volunteer_id",
"created": "created_at",
},
}
_ = columnMappings
// Extract entities from query
for keyword, table := range entityMappings {
if strings.Contains(query, keyword) {
if !contains(parsed.Tables, table) {
parsed.Tables = append(parsed.Tables, table)
}
}
}
// Parse conditions using regex patterns
conditionPatterns := []struct {
pattern string
handler func(matches []string, parsed *SmartFilterQuery)
}{
// "volunteers who went to address X"
{`(volunteer|user)s?\s+(?:who\s+)?(?:went\s+to|visited)\s+(?:address\s+)?(.+)`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"users", "appointment", "address_database"})
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "address_database", Column: "address", Operator: "ILIKE", Value: "%" + matches[2] + "%", LogicalOp: "AND",
})
}},
// "poll responses for address X"
{`poll\s+(?:response|responses|data)\s+(?:for|of|at)\s+(?:address\s+)?(.+)`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"poll", "address_database"})
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "address_database", Column: "address", Operator: "ILIKE", Value: "%" + matches[1] + "%", LogicalOp: "AND",
})
}},
// "donations by volunteer X"
{`(?:donation|donations|money|amount)\s+(?:by|from)\s+(?:volunteer\s+)?(.+)`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"poll", "users"})
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "users", Column: "first_name || ' ' || last_name", Operator: "ILIKE", Value: "%" + matches[1] + "%", LogicalOp: "AND",
})
parsed.Aggregations = append(parsed.Aggregations, "SUM(poll.amount_donated) as total_donated")
}},
// "team with most appointments"
{`team\s+(?:with\s+)?(?:most|highest)\s+appointment`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"team", "appointment"})
parsed.Aggregations = append(parsed.Aggregations, "COUNT(appointment.sched_id) as appointment_count")
parsed.Groupings = append(parsed.Groupings, "team.team_id")
parsed.Sorting = append(parsed.Sorting, "appointment_count DESC")
}},
// "people in team X"
{`(?:people|members|users)\s+in\s+team\s+(\d+)`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"team", "users"})
teamID, _ := strconv.Atoi(matches[1])
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "team", Column: "team_id", Operator: "=", Value: teamID, LogicalOp: "AND",
})
}},
// "money made by team X"
{`(?:money|donation|amount)\s+(?:made|earned)\s+by\s+team\s+(\d+)`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"team", "users", "poll"})
teamID, _ := strconv.Atoi(matches[1])
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "team", Column: "team_id", Operator: "=", Value: teamID, LogicalOp: "AND",
})
parsed.Aggregations = append(parsed.Aggregations, "SUM(poll.amount_donated) as team_total")
}},
// "visited addresses"
{`visited\s+address`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"address_database"})
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "address_database", Column: "visited_validated", Operator: "=", Value: true, LogicalOp: "AND",
})
}},
// "active polls"
{`active\s+poll`, func(matches []string, parsed *SmartFilterQuery) {
addTablesIfNeeded(parsed, []string{"poll"})
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: "poll", Column: "is_active", Operator: "=", Value: true, LogicalOp: "AND",
})
}},
// Date filters
{`(?:from|after)\s+(\d{4}-\d{2}-\d{2})`, func(matches []string, parsed *SmartFilterQuery) {
for _, table := range parsed.Tables {
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: table, Column: "created_at", Operator: ">=", Value: matches[1], LogicalOp: "AND",
})
}
}},
{`(?:to|before|until)\s+(\d{4}-\d{2}-\d{2})`, func(matches []string, parsed *SmartFilterQuery) {
for _, table := range parsed.Tables {
parsed.Conditions = append(parsed.Conditions, FilterCondition{
Table: table, Column: "created_at", Operator: "<=", Value: matches[1] + " 23:59:59", LogicalOp: "AND",
})
}
}},
}
// Apply pattern matching
for _, pattern := range conditionPatterns {
re := regexp.MustCompile(pattern.pattern)
if matches := re.FindStringSubmatch(query); matches != nil {
pattern.handler(matches, parsed)
}
}
// If no tables were identified, return nil
if len(parsed.Tables) == 0 {
return nil
}
return parsed
}
// buildSQLFromParsed converts parsed query into SQL
func buildSQLFromParsed(parsed *SmartFilterQuery) (string, error) {
var selectCols []string
var fromClause []string
var joinClauses []string
var whereConditions []string
var groupByClause string
var orderByClause string
// Define table aliases
aliases := map[string]string{
"users": "u",
"poll": "p",
"address_database": "a",
"appointment": "ap",
"team": "t",
}
// Define default columns for each table
defaultColumns := map[string][]string{
"users": {"u.user_id", "u.first_name", "u.last_name", "u.email", "u.role_id", "u.created_at"},
"poll": {"p.poll_id", "p.poll_title", "p.poll_description", "p.is_active", "p.amount_donated", "p.created_at"},
"address_database": {"a.address_id", "a.address", "a.street_name", "a.visited_validated", "a.created_at"},
"appointment": {"ap.sched_id", "ap.appointment_date", "ap.appointment_time", "ap.created_at"},
"team": {"t.team_id", "t.team_lead_id", "t.volunteer_id", "t.created_at"},
}
// Build SELECT clause
if len(parsed.Aggregations) > 0 {
selectCols = parsed.Aggregations
// Add grouping columns
for _, table := range parsed.Tables {
alias := aliases[table]
if table == "users" {
selectCols = append(selectCols, alias+".first_name || ' ' || "+alias+".last_name as user_name")
} else if table == "team" {
selectCols = append(selectCols, alias+".team_id")
}
}
} else {
// Include default columns for all tables
for _, table := range parsed.Tables {
selectCols = append(selectCols, defaultColumns[table]...)
}
}
// Build FROM clause with main table
mainTable := parsed.Tables[0]
mainAlias := aliases[mainTable]
fromClause = append(fromClause, mainTable+" "+mainAlias)
// Build JOIN clauses for additional tables
for i := 1; i < len(parsed.Tables); i++ {
table := parsed.Tables[i]
alias := aliases[table]
// Define join conditions based on relationships
joinCondition := getJoinCondition(mainTable, mainAlias, table, alias)
if joinCondition != "" {
joinClauses = append(joinClauses, "LEFT JOIN "+table+" "+alias+" ON "+joinCondition)
}
}
// Build WHERE conditions
for _, condition := range parsed.Conditions {
alias := aliases[condition.Table]
whereClause := fmt.Sprintf("%s.%s %s", alias, condition.Column, condition.Operator)
switch v := condition.Value.(type) {
case string:
whereClause += fmt.Sprintf(" '%s'", strings.Replace(v, "'", "''", -1))
case int:
whereClause += fmt.Sprintf(" %d", v)
case bool:
whereClause += fmt.Sprintf(" %t", v)
case float64:
whereClause += fmt.Sprintf(" %.2f", v)
}
whereConditions = append(whereConditions, whereClause)
}
// Build GROUP BY
if len(parsed.Groupings) > 0 {
groupByClause = "GROUP BY " + strings.Join(parsed.Groupings, ", ")
}
// Build ORDER BY
if len(parsed.Sorting) > 0 {
orderByClause = "ORDER BY " + strings.Join(parsed.Sorting, ", ")
} else {
orderByClause = "ORDER BY " + mainAlias + ".created_at DESC"
}
// Construct final SQL
sql := "SELECT " + strings.Join(selectCols, ", ")
sql += " FROM " + strings.Join(fromClause, ", ")
if len(joinClauses) > 0 {
sql += " " + strings.Join(joinClauses, " ")
}
if len(whereConditions) > 0 {
sql += " WHERE " + strings.Join(whereConditions, " AND ")
}
if groupByClause != "" {
sql += " " + groupByClause
}
sql += " " + orderByClause
sql += fmt.Sprintf(" LIMIT %d", parsed.Limit)
return sql, nil
}
// getJoinCondition returns the appropriate JOIN condition between tables
func getJoinCondition(table1, alias1, table2, alias2 string) string {
joinMap := map[string]map[string]string{
"users": {
"poll": alias1 + ".user_id = " + alias2 + ".user_id",
"appointment": alias1 + ".user_id = " + alias2 + ".user_id",
"team": alias1 + ".user_id = " + alias2 + ".team_lead_id OR " + alias1 + ".user_id = " + alias2 + ".volunteer_id",
},
"poll": {
"users": alias2 + ".user_id = " + alias1 + ".user_id",
"address_database": alias1 + ".address_id = " + alias2 + ".address_id",
},
"appointment": {
"users": alias2 + ".user_id = " + alias1 + ".user_id",
"address_database": alias1 + ".address_id = " + alias2 + ".address_id",
},
"team": {
"users": alias2 + ".user_id = " + alias1 + ".team_lead_id OR " + alias2 + ".user_id = " + alias1 + ".volunteer_id",
},
"address_database": {
"poll": alias2 + ".address_id = " + alias1 + ".address_id",
"appointment": alias2 + ".address_id = " + alias1 + ".address_id",
},
}
if table1Joins, exists := joinMap[table1]; exists {
if condition, exists := table1Joins[table2]; exists {
return condition
}
}
return ""
}
// executeSQLQuery executes the built SQL and returns results
func executeSQLQuery(sqlQuery string) SmartFilterResult {
rows, err := models.DB.Query(sqlQuery)
if err != nil {
log.Println("Smart filter SQL error:", err)
return SmartFilterResult{Error: "Query execution failed: " + err.Error(), Query: sqlQuery}
}
defer rows.Close()
// Get column information
columns, err := rows.Columns()
if err != nil {
return SmartFilterResult{Error: "Failed to get columns", Query: sqlQuery}
}
var results [][]interface{}
for rows.Next() {
// Create a slice of interface{} to hold the values
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
log.Println("Error scanning row:", err)
continue
}
// Convert values to strings for display
row := make([]interface{}, len(columns))
for i, val := range values {
if val == nil {
row[i] = ""
} else {
switch v := val.(type) {
case []byte:
row[i] = string(v)
case time.Time:
row[i] = v.Format("2006-01-02 15:04:05")
default:
row[i] = fmt.Sprintf("%v", v)
}
}
}
results = append(results, row)
}
return SmartFilterResult{
Columns: columns,
Rows: results,
Query: sqlQuery,
Count: len(results),
}
}
// Helper functions
func addTablesIfNeeded(parsed *SmartFilterQuery, tables []string) {
for _, table := range tables {
if !contains(parsed.Tables, table) {
parsed.Tables = append(parsed.Tables, table)
}
}
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// getQueryExamples returns example queries for the user
func getQueryExamples() []string {
return []string{
"volunteers who went to Main Street",
"poll responses for 123 Oak Avenue",
"donations by volunteer John",
"team with most appointments",
"people in team 5",
"money made by team 3",
"visited addresses from 2024-01-01",
"active polls created after 2024-06-01",
"appointments for unvisited addresses",
"users with role admin",
"polls with donations over 100",
"addresses visited by volunteer Sarah",
"team leads with more than 5 appointments",
"donations per address",
"volunteer activity by month",
}
}
// SmartFilterAPIHandler provides JSON API for smart filtering
func SmartFilterAPIHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
if role != 1 {
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
smartQuery := r.URL.Query().Get("q")
if smartQuery == "" {
http.Error(w, "Query parameter 'q' is required", http.StatusBadRequest)
return
}
result := executeSmartFilter(smartQuery)
w.Header().Set("Content-Type", "application/json")
// Convert result to JSON manually since we're not using json package
response := fmt.Sprintf(`{
"columns": [%s],
"rows": [%s],
"count": %d,
"query": "%s",
"error": "%s"
}`,
formatColumnsForJSON(result.Columns),
formatRowsForJSON(result.Rows),
result.Count,
strings.Replace(result.Query, "\"", "\\\"", -1),
result.Error,
)
w.Write([]byte(response))
}
func formatColumnsForJSON(columns []string) string {
var quoted []string
for _, col := range columns {
quoted = append(quoted, fmt.Sprintf(`"%s"`, col))
}
return strings.Join(quoted, ",")
}
func formatRowsForJSON(rows [][]interface{}) string {
var rowStrings []string
for _, row := range rows {
var values []string
for _, val := range row {
values = append(values, fmt.Sprintf(`"%v"`, val))
}
rowStrings = append(rowStrings, "["+strings.Join(values, ",")+"]")
}
return strings.Join(rowStrings, ",")
}
// SmartFilterExportHandler exports smart filter results as CSV
func SmartFilterExportHandler(w http.ResponseWriter, r *http.Request) {
role := r.Context().Value("user_role").(int)
if role != 1 {
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}
smartQuery := r.URL.Query().Get("smart_query")
if smartQuery == "" {
http.Error(w, "No query provided", http.StatusBadRequest)
return
}
result := executeSmartFilter(smartQuery)
if result.Error != "" {
http.Error(w, result.Error, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"smart_filter_results_%s.csv\"", time.Now().Format("2006-01-02")))
// Write CSV headers
w.Write([]byte(strings.Join(result.Columns, ",") + "\n"))
// Write CSV rows
for _, row := range result.Rows {
var csvRow []string
for _, val := range row {
// Escape commas and quotes in CSV values
strVal := fmt.Sprintf("%v", val)
if strings.Contains(strVal, ",") || strings.Contains(strVal, "\"") {
strVal = "\"" + strings.Replace(strVal, "\"", "\"\"", -1) + "\""
}
csvRow = append(csvRow, strVal)
}
w.Write([]byte(strings.Join(csvRow, ",") + "\n"))
}
}