834 lines
27 KiB
Go
834 lines
27 KiB
Go
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, ","))
|
|
}
|
|
} |