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