package handlers import ( "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" ) type ReportResult struct { Columns []string `json:"columns"` Rows [][]interface{} `json:"rows"` Count int `json:"count"` Error string `json:"error,omitempty"` } type ReportDefinition struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` SQL string `json:"-"` // Don't expose SQL in JSON } type SummaryStats struct { Label string `json:"label"` Value string `json:"value"` } // Simple Reports Handler func ReportsHandler(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 } category := r.URL.Query().Get("category") reportID := r.URL.Query().Get("report") dateFrom := r.URL.Query().Get("date_from") dateTo := r.URL.Query().Get("date_to") export := r.URL.Query().Get("export") // Set default date range if not provided if dateFrom == "" { dateFrom = time.Now().AddDate(0, 0, -30).Format("2006-01-02") } if dateTo == "" { dateTo = time.Now().Format("2006-01-02") } var result ReportResult var reportTitle, reportDescription string // Generate report if both category and report are selected if category != "" && reportID != "" { result, reportTitle, reportDescription = executeReport(category, reportID, dateFrom, dateTo) // Handle CSV export if export == "csv" { exportCSV(w, result, reportTitle) return } } utils.Render(w, "reports.html", map[string]interface{}{ "Title": "Campaign Reports", "IsAuthenticated": true, "ShowAdminNav": role == 1, "ShowVolunteerNav": role != 1, "UserName": username, "ActiveSection": "reports", "Category": category, "ReportID": reportID, "DateFrom": dateFrom, "DateTo": dateTo, "AvailableReports": getReportsForCategory(category), "Result": result, "ReportTitle": reportTitle, "ReportDescription": reportDescription, "GeneratedAt": time.Now().Format("January 2, 2006 at 3:04 PM"), "SummaryStats": generateSummaryStats(result), }) } // Execute a specific report func executeReport(category, reportID, dateFrom, dateTo string) (ReportResult, string, string) { report := getReportDefinition(category, reportID) if report == nil { return ReportResult{Error: "Report not found"}, "", "" } // Replace date placeholders in SQL sql := report.SQL sql = replaceDatePlaceholders(sql, dateFrom, dateTo) // Execute the SQL query rows, err := models.DB.Query(sql) if err != nil { log.Printf("Report SQL error: %v", err) return ReportResult{Error: "Failed to execute report"}, report.Name, report.Description } defer rows.Close() // Get column names columns, err := rows.Columns() if err != nil { return ReportResult{Error: "Failed to get columns"}, report.Name, report.Description } // Process rows var results [][]interface{} for rows.Next() { 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 { continue } 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 ReportResult{ Columns: columns, Rows: results, Count: len(results), }, report.Name, report.Description } // Get available reports for a category func getReportsForCategory(category string) []ReportDefinition { allReports := getAllReportDefinitions() if reports, exists := allReports[category]; exists { return reports } return []ReportDefinition{} } // Get a specific report definition func getReportDefinition(category, reportID string) *ReportDefinition { reports := getReportsForCategory(category) for _, report := range reports { if report.ID == reportID { return &report } } return nil } // Define all available reports func getAllReportDefinitions() map[string][]ReportDefinition { return map[string][]ReportDefinition{ "users": { { ID: "users_by_role", Name: "Users by Role", Description: "Count of users grouped by their role", SQL: `SELECT CASE WHEN role_id = 1 THEN 'Admin' WHEN role_id = 2 THEN 'Volunteer' ELSE 'Unknown' END as role, COUNT(*) as user_count, COUNT(CASE WHEN created_at >= ?1 THEN 1 END) as new_this_period FROM users GROUP BY role_id ORDER BY role_id`, }, { ID: "volunteer_activity", Name: "Volunteer Activity Summary", Description: "Summary of volunteer activities including appointments and polls", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, u.email, COUNT(DISTINCT a.sched_id) as appointments_count, COUNT(DISTINCT p.poll_id) as polls_created, u.created_at as joined_date FROM users u LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 WHERE u.role_id = 2 GROUP BY u.user_id, u.first_name, u.last_name, u.email, u.created_at ORDER BY appointments_count DESC, polls_created DESC`, }, { ID: "team_performance", Name: "Team Performance Report", Description: "Performance metrics for each team", SQL: `SELECT t.team_id, ul.first_name || ' ' || ul.last_name as team_lead, uv.first_name || ' ' || uv.last_name as volunteer, COUNT(DISTINCT a.sched_id) as appointments, COUNT(DISTINCT p.poll_id) as polls_created, t.created_at as team_formed FROM team t LEFT JOIN users ul ON t.team_lead_id = ul.user_id LEFT JOIN users uv ON t.volunteer_id = uv.user_id LEFT JOIN appointment a ON uv.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 LEFT JOIN poll p ON uv.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 GROUP BY t.team_id, ul.first_name, ul.last_name, uv.first_name, uv.last_name, t.created_at ORDER BY appointments DESC`, }, { ID: "admin_workload", Name: "Admin Workload Analysis", Description: "Workload distribution across admins", SQL: `SELECT u.first_name || ' ' || u.last_name as admin_name, u.email, COUNT(DISTINCT t.team_id) as teams_managed, COUNT(DISTINCT p.poll_id) as polls_created, COUNT(DISTINCT a.sched_id) as appointments_scheduled FROM users u LEFT JOIN team t ON u.user_id = t.team_lead_id LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 WHERE u.role_id = 1 GROUP BY u.user_id, u.first_name, u.last_name, u.email ORDER BY teams_managed DESC, polls_created DESC`, }, { ID: "inactive_users", Name: "Inactive Users Report", Description: "Users with no recent activity", SQL: `SELECT u.first_name || ' ' || u.last_name as user_name, u.email, CASE WHEN u.role_id = 1 THEN 'Admin' WHEN u.role_id = 2 THEN 'Volunteer' ELSE 'Unknown' END as role, u.created_at as joined_date, COALESCE(MAX(a.created_at), MAX(p.created_at)) as last_activity FROM users u LEFT JOIN appointment a ON u.user_id = a.user_id LEFT JOIN poll p ON u.user_id = p.user_id GROUP BY u.user_id, u.first_name, u.last_name, u.email, u.role_id, u.created_at HAVING COALESCE(MAX(a.created_at), MAX(p.created_at)) < ?1 OR COALESCE(MAX(a.created_at), MAX(p.created_at)) IS NULL ORDER BY last_activity DESC`, }, }, "addresses": { { ID: "coverage_by_area", Name: "Coverage by Area", Description: "Address coverage statistics by geographical area", SQL: `SELECT COALESCE(NULLIF(TRIM(SPLIT_PART(address, ',', -1)), ''), 'Unknown') as area, COUNT(*) as total_addresses, COUNT(CASE WHEN visited_validated = true THEN 1 END) as visited_count, ROUND(COUNT(CASE WHEN visited_validated = true THEN 1 END) * 100.0 / COUNT(*), 2) as coverage_percentage FROM address_database WHERE created_at BETWEEN ?1 AND ?2 GROUP BY area ORDER BY total_addresses DESC`, }, { ID: "visits_by_postal", Name: "Visits by Postal Code", Description: "Visit statistics grouped by postal code", SQL: `SELECT COALESCE(NULLIF(TRIM(SUBSTRING(address FROM '[A-Za-z][0-9][A-Za-z] ?[0-9][A-Za-z][0-9]')), ''), 'No Postal Code') as postal_code, COUNT(*) as addresses, COUNT(CASE WHEN visited_validated = true THEN 1 END) as visited, COUNT(CASE WHEN visited_validated = false THEN 1 END) as unvisited FROM address_database WHERE created_at BETWEEN ?1 AND ?2 GROUP BY postal_code ORDER BY addresses DESC LIMIT 50`, }, { ID: "unvisited_addresses", Name: "Unvisited Addresses", Description: "List of addresses that haven't been visited", SQL: `SELECT address_id, address, latitude, longitude, created_at as added_date FROM address_database WHERE visited_validated = false AND created_at BETWEEN ?1 AND ?2 ORDER BY created_at DESC LIMIT 100`, }, { ID: "donations_by_location", Name: "Donations by Location", Description: "Donation amounts grouped by address location", SQL: `SELECT a.address, COUNT(p.poll_id) as total_polls, COALESCE(SUM(p.amount_donated), 0) as total_donations, COALESCE(AVG(p.amount_donated), 0) as avg_donation FROM address_database a LEFT JOIN poll p ON a.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 GROUP BY a.address_id, a.address HAVING COUNT(p.poll_id) > 0 ORDER BY total_donations DESC LIMIT 50`, }, { ID: "address_validation_status", Name: "Address Validation Status", Description: "Status of address validation across the database", SQL: `SELECT CASE WHEN visited_validated = true THEN 'Validated' WHEN visited_validated = false THEN 'Not Validated' ELSE 'Unknown' END as validation_status, COUNT(*) as address_count, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM address_database), 2) as percentage FROM address_database WHERE created_at BETWEEN ?1 AND ?2 GROUP BY visited_validated ORDER BY address_count DESC`, }, }, "appointments": { { ID: "appointments_by_day", Name: "Appointments by Day", Description: "Daily breakdown of appointment scheduling", SQL: `SELECT appointment_date, COUNT(*) as appointments_scheduled, COUNT(DISTINCT user_id) as unique_volunteers, COUNT(DISTINCT address_id) as unique_addresses FROM appointment WHERE appointment_date BETWEEN ?1 AND ?2 GROUP BY appointment_date ORDER BY appointment_date DESC`, }, { ID: "completion_rates", Name: "Completion Rates", Description: "Appointment completion statistics by volunteer", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(a.sched_id) as total_appointments, COUNT(CASE WHEN ad.visited_validated = true THEN 1 END) as completed_visits, ROUND(COUNT(CASE WHEN ad.visited_validated = true THEN 1 END) * 100.0 / COUNT(a.sched_id), 2) as completion_rate FROM appointment a JOIN users u ON a.user_id = u.user_id LEFT JOIN address_database ad ON a.address_id = ad.address_id WHERE a.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(a.sched_id) > 0 ORDER BY completion_rate DESC, total_appointments DESC`, }, { ID: "volunteer_schedules", Name: "Volunteer Schedules", Description: "Current volunteer scheduling overview", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, a.appointment_date, a.appointment_time, ad.address, a.created_at as scheduled_date FROM appointment a JOIN users u ON a.user_id = u.user_id JOIN address_database ad ON a.address_id = ad.address_id WHERE a.appointment_date BETWEEN ?1 AND ?2 ORDER BY a.appointment_date, a.appointment_time`, }, { ID: "missed_appointments", Name: "Missed Appointments", Description: "Appointments that were scheduled but addresses remain unvisited", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, a.appointment_date, a.appointment_time, ad.address, CASE WHEN a.appointment_date < CURRENT_DATE THEN 'Overdue' ELSE 'Upcoming' END as status FROM appointment a JOIN users u ON a.user_id = u.user_id JOIN address_database ad ON a.address_id = ad.address_id WHERE ad.visited_validated = false AND a.appointment_date BETWEEN ?1 AND ?2 ORDER BY a.appointment_date DESC`, }, { ID: "peak_hours", Name: "Peak Activity Hours", Description: "Most popular appointment times", SQL: `SELECT appointment_time, COUNT(*) as appointment_count, COUNT(DISTINCT user_id) as unique_volunteers FROM appointment WHERE appointment_date BETWEEN ?1 AND ?2 GROUP BY appointment_time ORDER BY appointment_count DESC`, }, }, "polls": { { ID: "poll_creation_stats", Name: "Poll Creation Statistics", Description: "Overview of poll creation activity", SQL: `SELECT u.first_name || ' ' || u.last_name as creator_name, COUNT(p.poll_id) as polls_created, COUNT(CASE WHEN p.is_active = true THEN 1 END) as active_polls, COALESCE(SUM(p.amount_donated), 0) as total_donations, COALESCE(AVG(p.amount_donated), 0) as avg_donation_per_poll FROM poll p JOIN users u ON p.user_id = u.user_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name ORDER BY polls_created DESC`, }, { ID: "donation_analysis", Name: "Donation Analysis", Description: "Detailed analysis of donation patterns", SQL: `SELECT CASE WHEN amount_donated = 0 THEN 'No Donation' WHEN amount_donated BETWEEN 0.01 AND 25 THEN '$1 - $25' WHEN amount_donated BETWEEN 25.01 AND 50 THEN '$26 - $50' WHEN amount_donated BETWEEN 50.01 AND 100 THEN '$51 - $100' ELSE 'Over $100' END as donation_range, COUNT(*) as poll_count, COALESCE(SUM(amount_donated), 0) as total_amount, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM poll WHERE created_at BETWEEN ?1 AND ?2), 2) as percentage FROM poll WHERE created_at BETWEEN ?1 AND ?2 GROUP BY donation_range ORDER BY CASE donation_range WHEN 'No Donation' THEN 1 WHEN '$1 - $25' THEN 2 WHEN '$26 - $50' THEN 3 WHEN '$51 - $100' THEN 4 WHEN 'Over $100' THEN 5 END`, }, { ID: "active_vs_inactive", Name: "Active vs Inactive Polls", Description: "Comparison of active and inactive polls", SQL: `SELECT CASE WHEN is_active = true THEN 'Active' ELSE 'Inactive' END as poll_status, COUNT(*) as poll_count, COALESCE(SUM(amount_donated), 0) as total_donations, COALESCE(AVG(amount_donated), 0) as avg_donation FROM poll WHERE created_at BETWEEN ?1 AND ?2 GROUP BY is_active ORDER BY poll_count DESC`, }, { ID: "poll_trends", Name: "Poll Activity Trends", Description: "Poll creation trends over time", SQL: `SELECT DATE(created_at) as creation_date, COUNT(*) as polls_created, COUNT(CASE WHEN is_active = true THEN 1 END) as active_polls, COALESCE(SUM(amount_donated), 0) as daily_donations FROM poll WHERE created_at BETWEEN ?1 AND ?2 GROUP BY DATE(created_at) ORDER BY creation_date DESC`, }, { ID: "creator_performance", Name: "Creator Performance", Description: "Performance metrics for poll creators", SQL: `SELECT u.first_name || ' ' || u.last_name as creator_name, u.email, COUNT(p.poll_id) as total_polls, COALESCE(SUM(p.amount_donated), 0) as total_raised, COALESCE(MAX(p.amount_donated), 0) as highest_donation, COUNT(CASE WHEN p.is_active = true THEN 1 END) as active_polls FROM users u JOIN poll p ON u.user_id = p.user_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name, u.email ORDER BY total_raised DESC, total_polls DESC`, }, }, "responses": { { ID: "voter_status", Name: "Voter Status Report", Description: "Analysis of voter status from poll responses", SQL: `SELECT voter_before as voted_before, COUNT(*) as response_count, COUNT(CASE WHEN will_vote_again = true THEN 1 END) as will_vote_again_count, ROUND(COUNT(CASE WHEN will_vote_again = true THEN 1 END) * 100.0 / COUNT(*), 2) as vote_again_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY voter_before ORDER BY response_count DESC`, }, { ID: "sign_requests", Name: "Sign Requests Summary", Description: "Summary of lawn sign and banner requests", SQL: `SELECT 'Lawn Signs' as sign_type, SUM(lawn_sign) as total_requested, SUM(CASE WHEN lawn_sign_status = 'delivered' THEN lawn_sign ELSE 0 END) as delivered, SUM(CASE WHEN lawn_sign_status = 'cancelled' THEN lawn_sign ELSE 0 END) as cancelled FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 UNION ALL SELECT 'Banner Signs' as sign_type, SUM(banner_sign) as total_requested, SUM(CASE WHEN banner_sign_status = 'delivered' THEN banner_sign ELSE 0 END) as delivered, SUM(CASE WHEN banner_sign_status = 'cancelled' THEN banner_sign ELSE 0 END) as cancelled FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2`, }, { ID: "feedback_analysis", Name: "Feedback Analysis", Description: "Analysis of open-text feedback from responses", SQL: `SELECT LENGTH(thoughts) as feedback_length_category, COUNT(*) as response_count FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 AND thoughts IS NOT NULL AND TRIM(thoughts) != '' GROUP BY CASE WHEN LENGTH(thoughts) <= 50 THEN 'Short (1-50 chars)' WHEN LENGTH(thoughts) <= 150 THEN 'Medium (51-150 chars)' ELSE 'Long (150+ chars)' END ORDER BY response_count DESC`, }, { ID: "response_trends", Name: "Response Trends", Description: "Poll response trends over time", SQL: `SELECT DATE(pr.created_at) as response_date, COUNT(*) as responses, COUNT(CASE WHEN voter_before = true THEN 1 END) as returning_voters, COUNT(CASE WHEN will_vote_again = true THEN 1 END) as committed_future_voters FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE pr.created_at BETWEEN ?1 AND ?2 GROUP BY DATE(pr.created_at) ORDER BY response_date DESC`, }, { ID: "repeat_voters", Name: "Repeat Voters Analysis", Description: "Analysis of voters who have responded to multiple polls", SQL: `SELECT pr.name, pr.email, COUNT(DISTINCT pr.poll_id) as polls_responded, SUM(CASE WHEN voter_before = true THEN 1 ELSE 0 END) as times_voted_before, SUM(CASE WHEN will_vote_again = true THEN 1 ELSE 0 END) as times_will_vote_again FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY pr.name, pr.email HAVING COUNT(DISTINCT pr.poll_id) > 1 ORDER BY polls_responded DESC`, }, }, "posts": { { ID: "posts_by_user", Name: "Posts by User", Description: "Post creation statistics by user", SQL: `SELECT u.first_name || ' ' || u.last_name as author_name, u.email, COUNT(po.post_id) as total_posts, MIN(po.created_at) as first_post, MAX(po.created_at) as latest_post FROM users u JOIN posts po ON u.user_id = po.user_id WHERE po.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name, u.email ORDER BY total_posts DESC`, }, { ID: "engagement_timeline", Name: "Engagement Timeline", Description: "Post creation timeline", SQL: `SELECT DATE(created_at) as post_date, COUNT(*) as posts_created, COUNT(DISTINCT user_id) as active_users FROM posts WHERE created_at BETWEEN ?1 AND ?2 GROUP BY DATE(created_at) ORDER BY post_date DESC`, }, { ID: "content_analysis", Name: "Content Analysis", Description: "Analysis of post content length and characteristics", SQL: `SELECT CASE WHEN LENGTH(content) <= 100 THEN 'Short (1-100 chars)' WHEN LENGTH(content) <= 300 THEN 'Medium (101-300 chars)' ELSE 'Long (300+ chars)' END as content_length, COUNT(*) as post_count, ROUND(AVG(LENGTH(content)), 2) as avg_length FROM posts WHERE created_at BETWEEN ?1 AND ?2 AND content IS NOT NULL GROUP BY content_length ORDER BY post_count DESC`, }, { ID: "post_frequency", Name: "Post Frequency Report", Description: "Posting frequency patterns", SQL: `SELECT u.first_name || ' ' || u.last_name as author_name, COUNT(*) as total_posts, ROUND(COUNT(*) * 1.0 / GREATEST(1, EXTRACT(days FROM (?2::date - ?1::date))), 2) as posts_per_day, DATE(MIN(po.created_at)) as first_post, DATE(MAX(po.created_at)) as last_post FROM posts po JOIN users u ON po.user_id = u.user_id WHERE po.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(*) > 1 ORDER BY posts_per_day DESC`, }, }, "availability": { { ID: "volunteer_availability", Name: "Volunteer Availability", Description: "Current volunteer availability schedules", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, av.day_of_week, av.start_time, av.end_time, av.created_at as schedule_updated FROM volunteer_availability av JOIN users u ON av.user_id = u.user_id WHERE av.created_at BETWEEN ?1 AND ?2 ORDER BY u.first_name, u.last_name, av.day_of_week, av.start_time`, }, { ID: "peak_availability", Name: "Peak Availability Times", Description: "Times when most volunteers are available", SQL: `SELECT day_of_week, start_time, end_time, COUNT(*) as volunteers_available FROM volunteer_availability av JOIN users u ON av.user_id = u.user_id WHERE av.created_at BETWEEN ?1 AND ?2 GROUP BY day_of_week, start_time, end_time ORDER BY volunteers_available DESC, day_of_week, start_time`, }, { ID: "coverage_gaps", Name: "Coverage Gaps", Description: "Time periods with limited volunteer availability", SQL: `SELECT day_of_week, start_time, end_time, COUNT(*) as volunteers_available FROM volunteer_availability av WHERE av.created_at BETWEEN ?1 AND ?2 GROUP BY day_of_week, start_time, end_time HAVING COUNT(*) <= 2 ORDER BY volunteers_available ASC, day_of_week, start_time`, }, { ID: "schedule_conflicts", Name: "Schedule Conflicts", Description: "Appointments scheduled outside volunteer availability", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, a.appointment_date, a.appointment_time, ad.address, 'No availability recorded' as conflict_reason FROM appointment a JOIN users u ON a.user_id = u.user_id JOIN address_database ad ON a.address_id = ad.address_id LEFT JOIN volunteer_availability av ON u.user_id = av.user_id AND EXTRACT(dow FROM a.appointment_date) = av.day_of_week AND a.appointment_time BETWEEN av.start_time AND av.end_time WHERE a.appointment_date BETWEEN ?1 AND ?2 AND av.user_id IS NULL ORDER BY a.appointment_date, a.appointment_time`, }, }, } } // Replace date placeholders in SQL func replaceDatePlaceholders(sql, dateFrom, dateTo string) string { sql = strings.ReplaceAll(sql, "?1", "'"+dateFrom+"'") sql = strings.ReplaceAll(sql, "?2", "'"+dateTo+" 23:59:59'") return sql } // Generate summary statistics func generateSummaryStats(result ReportResult) []SummaryStats { if len(result.Rows) == 0 { return nil } stats := []SummaryStats{ {Label: "Total Records", Value: strconv.Itoa(result.Count)}, } // Try to generate additional stats based on column types if len(result.Columns) > 0 { // Look for numeric columns to calculate sums/averages for colIdx, colName := range result.Columns { if strings.Contains(strings.ToLower(colName), "count") || strings.Contains(strings.ToLower(colName), "total") || strings.Contains(strings.ToLower(colName), "amount") { var sum float64 var validCount int for _, row := range result.Rows { if colIdx < len(row) { if val, err := strconv.ParseFloat(fmt.Sprintf("%v", row[colIdx]), 64); err == nil { sum += val validCount++ } } } if validCount > 0 { stats = append(stats, SummaryStats{ Label: fmt.Sprintf("Total %s", strings.Title(strings.ToLower(colName))), Value: fmt.Sprintf("%.2f", sum), }) if validCount > 1 { stats = append(stats, SummaryStats{ Label: fmt.Sprintf("Average %s", strings.Title(strings.ToLower(colName))), Value: fmt.Sprintf("%.2f", sum/float64(validCount)), }) } } break // Only calculate for first numeric column } } } return stats } // Export CSV func exportCSV(w http.ResponseWriter, result ReportResult, reportTitle string) { w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s_%s.csv\"", strings.ReplaceAll(strings.ToLower(reportTitle), " ", "_"), time.Now().Format("2006-01-02"))) // Write header fmt.Fprintf(w, "%s\n", strings.Join(result.Columns, ",")) // Write data for _, row := range result.Rows { var csvRow []string for _, cell := range row { // Escape CSV values cellStr := fmt.Sprintf("%v", cell) if strings.Contains(cellStr, ",") || strings.Contains(cellStr, "\"") || strings.Contains(cellStr, "\n") { cellStr = `"` + strings.ReplaceAll(cellStr, `"`, `""`) + `"` } csvRow = append(csvRow, cellStr) } fmt.Fprintf(w, "%s\n", strings.Join(csvRow, ",")) } }