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: "volunteer_participation_rate", Name: "Volunteer Participation Rate", Description: "Poll responses and donations collected by each volunteer/team lead", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, u.email, r.name as role, COUNT(DISTINCT p.poll_id) as polls_created, COUNT(DISTINCT pr.poll_response_id) as responses_collected, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, COUNT(DISTINCT a.sched_id) as appointments_scheduled FROM users u JOIN role r ON u.role_id = r.role_id LEFT JOIN poll p ON u.user_id = p.user_id AND p.created_at BETWEEN ?1 AND ?2 LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id LEFT JOIN appointment a ON u.user_id = a.user_id AND a.created_at BETWEEN ?1 AND ?2 WHERE u.role_id IN (2, 3) GROUP BY u.user_id, u.first_name, u.last_name, u.email, r.name ORDER BY responses_collected DESC, total_donations DESC`, }, { ID: "top_performing_volunteers", Name: "Top-Performing Volunteers & Team Leads", Description: "Volunteers ranked by responses collected and donations secured", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, r.name as role, COUNT(DISTINCT pr.poll_response_id) as responses_collected, COALESCE(SUM(pr.question6_donation_amount), 0) as donations_secured, COUNT(DISTINCT p.poll_id) as polls_created, AVG(pr.question6_donation_amount) as avg_donation_per_poll FROM users u JOIN role r ON u.role_id = r.role_id JOIN poll p ON u.user_id = p.user_id LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE u.role_id IN (2, 3) AND p.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name, r.name HAVING COUNT(DISTINCT pr.poll_response_id) > 0 ORDER BY responses_collected DESC, donations_secured DESC LIMIT 20`, }, { ID: "response_donation_ratio", Name: "Response-to-Donation Ratio per Volunteer", Description: "Efficiency measure showing donation amount per response collected", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(DISTINCT pr.poll_response_id) as total_responses, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, CASE WHEN COUNT(DISTINCT pr.poll_response_id) > 0 THEN COALESCE(SUM(pr.question6_donation_amount), 0) / COUNT(DISTINCT pr.poll_response_id) ELSE 0 END as donation_per_response, CASE WHEN COALESCE(SUM(pr.question6_donation_amount), 0) > 0 THEN COUNT(DISTINCT pr.poll_response_id) / COALESCE(SUM(pr.question6_donation_amount), 1) ELSE COUNT(DISTINCT pr.poll_response_id) END as responses_per_dollar FROM users u JOIN poll p ON u.user_id = p.user_id LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE u.role_id IN (2, 3) AND p.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(DISTINCT pr.poll_response_id) > 0 ORDER BY donation_per_response DESC`, }, { ID: "user_address_coverage", Name: "User Address Coverage", Description: "Number of unique addresses covered by each volunteer/team lead", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(DISTINCT p.address_id) as unique_addresses_polled, COUNT(DISTINCT a.address_id) as unique_addresses_appointed, COUNT(DISTINCT COALESCE(p.address_id, a.address_id)) as total_unique_addresses, COUNT(DISTINCT pr.poll_response_id) as total_responses FROM users u 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 LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE u.role_id IN (2, 3) GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(DISTINCT COALESCE(p.address_id, a.address_id)) > 0 ORDER BY total_unique_addresses DESC, total_responses DESC`, }, }, "addresses": { { ID: "poll_responses_by_address", Name: "Total Poll Responses by Address", Description: "Shows engagement hotspots - addresses with most poll responses", SQL: `SELECT ad.address, ad.postal_code, ad.street_quadrant, COUNT(DISTINCT pr.poll_response_id) as total_responses, COUNT(DISTINCT p.poll_id) as polls_conducted, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, CASE WHEN COUNT(DISTINCT pr.poll_response_id) > 0 THEN COALESCE(SUM(pr.question6_donation_amount), 0) / COUNT(DISTINCT pr.poll_response_id) ELSE 0 END as avg_donation_per_response FROM address_database ad JOIN poll p ON ad.address_id = p.address_id LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY ad.address_id, ad.address, ad.postal_code, ad.street_quadrant ORDER BY total_responses DESC, total_donations DESC LIMIT 50`, }, { ID: "donations_by_address", Name: "Total Donations by Address", Description: "Shows financially supportive areas - addresses with highest donations", SQL: `SELECT ad.address, ad.postal_code, ad.street_quadrant, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, COUNT(DISTINCT p.poll_id) as polls_conducted, COUNT(DISTINCT pr.poll_response_id) as total_responses, AVG(pr.question6_donation_amount) as avg_donation_per_poll FROM address_database ad JOIN poll p ON ad.address_id = p.address_id LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 AND pr.question6_donation_amount > 0 GROUP BY ad.address_id, ad.address, ad.postal_code, ad.street_quadrant ORDER BY total_donations DESC LIMIT 50`, }, { ID: "street_level_breakdown", Name: "Street-Level Breakdown (Responses & Donations)", Description: "Granular view for targeting - responses and donations by street", SQL: `SELECT ad.street_name, ad.street_type, ad.street_quadrant, COUNT(DISTINCT ad.address_id) as unique_addresses, COUNT(DISTINCT pr.poll_response_id) as total_responses, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, COUNT(DISTINCT p.poll_id) as polls_conducted FROM address_database ad LEFT JOIN poll p ON ad.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE ad.street_name IS NOT NULL GROUP BY ad.street_name, ad.street_type, ad.street_quadrant HAVING COUNT(DISTINCT pr.poll_response_id) > 0 OR COALESCE(SUM(pr.question6_donation_amount), 0) > 0 ORDER BY total_responses DESC, total_donations DESC`, }, { ID: "quadrant_summary", Name: "Quadrant-Level Summary (NE, NW, SE, SW)", Description: "Higher-level trend view by city quadrants", SQL: `SELECT COALESCE(ad.street_quadrant, 'Unknown') as quadrant, COUNT(DISTINCT ad.address_id) as unique_addresses, COUNT(DISTINCT p.poll_id) as polls_conducted, COUNT(DISTINCT pr.poll_response_id) as total_responses, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, AVG(pr.question6_donation_amount) as avg_donation_per_poll, COUNT(DISTINCT a.sched_id) as appointments_scheduled FROM address_database ad LEFT JOIN poll p ON ad.address_id = p.address_id AND p.created_at BETWEEN ?1 AND ?2 LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id LEFT JOIN appointment a ON ad.address_id = a.address_id AND a.created_at BETWEEN ?1 AND ?2 GROUP BY ad.street_quadrant ORDER BY total_responses DESC, total_donations DESC`, }, }, "appointments": { { ID: "upcoming_appointments", Name: "Upcoming Appointments per Volunteer/Team Lead", Description: "Scheduling load - upcoming appointments by user", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, r.name as role, COUNT(*) as upcoming_appointments, MIN(a.appointment_date) as earliest_appointment, MAX(a.appointment_date) as latest_appointment, COUNT(CASE WHEN a.appointment_date = CURRENT_DATE THEN 1 END) as today_appointments, COUNT(CASE WHEN a.appointment_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days' THEN 1 END) as week_appointments FROM appointment a JOIN users u ON a.user_id = u.user_id JOIN role r ON u.role_id = r.role_id WHERE a.appointment_date >= CURRENT_DATE AND a.appointment_date BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name, r.name ORDER BY upcoming_appointments DESC`, }, { ID: "missed_vs_completed", Name: "Missed vs Completed Appointments", Description: "Reliability metric - appointment completion rates", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(*) as total_appointments, COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) as past_appointments, COUNT(CASE WHEN a.appointment_date >= CURRENT_DATE THEN 1 END) as upcoming_appointments, COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id AND DATE(p.created_at) = a.appointment_date ) THEN 1 END) as completed_appointments, COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND NOT EXISTS ( SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id AND DATE(p.created_at) = a.appointment_date ) THEN 1 END) as missed_appointments, CASE WHEN COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) > 0 THEN COUNT(CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id AND DATE(p.created_at) = a.appointment_date ) THEN 1 END) * 100.0 / COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) ELSE 0 END as completion_rate_percent FROM appointment a JOIN users u ON a.user_id = u.user_id WHERE a.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name ORDER BY completion_rate_percent DESC, total_appointments DESC`, }, { ID: "appointments_by_quadrant", Name: "Appointments by Quadrant/Region", Description: "Geographic distribution of appointments", SQL: `SELECT COALESCE(ad.street_quadrant, 'Unknown') as quadrant, COUNT(*) as total_appointments, COUNT(CASE WHEN a.appointment_date >= CURRENT_DATE THEN 1 END) as upcoming, COUNT(CASE WHEN a.appointment_date < CURRENT_DATE THEN 1 END) as past, COUNT(DISTINCT a.user_id) as unique_volunteers, COUNT(DISTINCT a.address_id) as unique_addresses FROM appointment a JOIN address_database ad ON a.address_id = ad.address_id WHERE a.created_at BETWEEN ?1 AND ?2 GROUP BY ad.street_quadrant ORDER BY total_appointments DESC`, }, { ID: "scheduling_lead_time", Name: "Average Lead Time (Scheduled vs Actual Date)", Description: "Scheduling efficiency - time between scheduling and appointment", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(*) as total_appointments, AVG(a.appointment_date - DATE(a.created_at)) as avg_lead_time_days, MIN(a.appointment_date - DATE(a.created_at)) as min_lead_time_days, MAX(a.appointment_date - DATE(a.created_at)) as max_lead_time_days, COUNT(CASE WHEN a.appointment_date - DATE(a.created_at) < 1 THEN 1 END) as same_day_appointments, COUNT(CASE WHEN a.appointment_date - DATE(a.created_at) BETWEEN 1 AND 7 THEN 1 END) as week_ahead_appointments FROM appointment a JOIN users u ON a.user_id = u.user_id WHERE a.created_at BETWEEN ?1 AND ?2 GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(*) > 0 ORDER BY avg_lead_time_days ASC`, }, }, "polls": { { ID: "response_distribution", Name: "Response Distribution (Yes/No/Neutral)", Description: "Outcome summary - distribution of poll responses", SQL: `SELECT CASE WHEN question1_voted_before = true AND question2_vote_again = true THEN 'Previous Voter - Will Vote Again' WHEN question1_voted_before = true AND question2_vote_again = false THEN 'Previous Voter - Will Not Vote Again' WHEN question1_voted_before = false AND question2_vote_again = true THEN 'New Voter - Will Vote' WHEN question1_voted_before = false AND question2_vote_again = false THEN 'New Voter - Will Not Vote' WHEN question1_voted_before IS NULL OR question2_vote_again IS NULL THEN 'Incomplete Response' END as response_category, COUNT(*) as response_count, COUNT(*) * 100.0 / (SELECT COUNT(*) FROM poll_response pr2 JOIN poll p2 ON pr2.poll_id = p2.poll_id WHERE p2.created_at BETWEEN ?1 AND ?2) as 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 response_category ORDER BY response_count DESC`, }, { ID: "average_poll_response", Name: "Average Poll Response (Yes/No %)", Description: "Overall sentiment - percentage breakdown of responses", SQL: `SELECT 'Previous Voters' as voter_type, COUNT(*) as total_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 AND question1_voted_before = true UNION ALL SELECT 'New Voters' as voter_type, COUNT(*) as total_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 AND question1_voted_before = false UNION ALL SELECT 'Overall' as voter_type, COUNT(*) as total_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) as positive_responses, COUNT(CASE WHEN question2_vote_again = false THEN 1 END) as negative_responses, COUNT(CASE WHEN question2_vote_again = true THEN 1 END) * 100.0 / COUNT(*) as positive_percentage FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2`, }, { ID: "donations_by_poll", Name: "Donations by Poll", Description: "Which polls drive donations - donation amounts per poll", SQL: `SELECT p.poll_id, u.first_name || ' ' || u.last_name as creator_name, ad.address, pr.question6_donation_amount, COUNT(pr.poll_response_id) as response_count, CASE WHEN COUNT(pr.poll_response_id) > 0 THEN pr.question6_donation_amount / COUNT(pr.poll_response_id) ELSE 0 END as donation_per_response, p.created_at as poll_date FROM poll p JOIN users u ON p.user_id = u.user_id JOIN address_database ad ON p.address_id = ad.address_id LEFT JOIN poll_response pr ON p.poll_id = pr.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 AND pr.question6_donation_amount > 0 GROUP BY p.poll_id, u.first_name, u.last_name, ad.address, pr.question6_donation_amount, p.created_at ORDER BY pr.question6_donation_amount DESC, response_count DESC`, }, { ID: "response_donation_correlation", Name: "Response-to-Donation Correlation", Description: "Are positive responses linked to donations?", SQL: `SELECT CASE WHEN question2_vote_again = true THEN 'Will Vote Again' WHEN question2_vote_again = false THEN 'Will Not Vote Again' ELSE 'No Response' END as response_type, COUNT(*) as response_count, COUNT(CASE WHEN pr.question6_donation_amount > 0 THEN 1 END) as responses_with_donations, COALESCE(SUM(pr.question6_donation_amount), 0) as total_donations, AVG(pr.question6_donation_amount) as avg_donation, COUNT(CASE WHEN pr.question6_donation_amount > 0 THEN 1 END) * 100.0 / COUNT(*) as donation_rate_percent FROM poll_response pr JOIN poll p ON pr.poll_id = p.poll_id WHERE p.created_at BETWEEN ?1 AND ?2 GROUP BY question2_vote_again ORDER BY total_donations DESC`, }, }, "availability": { { ID: "volunteer_availability_schedule", Name: "Volunteer Availability by Date Range", Description: "Who can work when - current volunteer availability schedules", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, u.email, av.day_of_week, av.start_time, av.end_time, EXTRACT(EPOCH FROM (av.end_time - av.start_time))/3600 as hours_available, av.created_at as schedule_updated FROM availability av JOIN users u ON av.user_id = u.user_id WHERE u.role_id IN (2, 3) AND av.created_at BETWEEN ?1 AND ?2 ORDER BY u.first_name, u.last_name, CASE av.day_of_week WHEN 'Monday' THEN 1 WHEN 'Tuesday' THEN 2 WHEN 'Wednesday' THEN 3 WHEN 'Thursday' THEN 4 WHEN 'Friday' THEN 5 WHEN 'Saturday' THEN 6 WHEN 'Sunday' THEN 7 END, av.start_time`, }, { ID: "volunteer_fulfillment", Name: "Volunteer Fulfillment (Available vs Actually Worked)", Description: "Reliability measure - comparing availability to actual appointments", SQL: `SELECT u.first_name || ' ' || u.last_name as volunteer_name, COUNT(DISTINCT av.availability_id) as availability_slots, SUM(EXTRACT(EPOCH FROM (av.end_time - av.start_time))/3600) as total_hours_available, COUNT(DISTINCT a.sched_id) as appointments_scheduled, COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) as past_appointments, COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id AND DATE(p.created_at) = a.appointment_date ) THEN a.sched_id END) as completed_appointments, CASE WHEN COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) > 0 THEN COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE AND EXISTS ( SELECT 1 FROM poll p WHERE p.user_id = a.user_id AND p.address_id = a.address_id AND DATE(p.created_at) = a.appointment_date ) THEN a.sched_id END) * 100.0 / COUNT(DISTINCT CASE WHEN a.appointment_date < CURRENT_DATE THEN a.sched_id END) ELSE 0 END as fulfillment_rate_percent FROM users u LEFT JOIN availability av ON u.user_id = av.user_id AND av.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 IN (2, 3) GROUP BY u.user_id, u.first_name, u.last_name HAVING COUNT(DISTINCT av.availability_id) > 0 OR COUNT(DISTINCT a.sched_id) > 0 ORDER BY fulfillment_rate_percent DESC, total_hours_available DESC`, }, }, } } // 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, ",")) } }