package handlers import ( "fmt" "math" "net/http" "time" ) type serviceCount struct { Name string Count int } type reportData struct { TotalLeads int ServiceCounts []serviceCount StatusCounts []serviceCount TotalScheduled int ConversionRate float64 TotalRevenue float64 TotalSales int AverageTicket float64 TopServices []serviceCount MonthLabel string } // MonthlyReport renders the monthly performance report for the current month. // GET /report func (a *App) MonthlyReport(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { return } clientID := a.clientIDForAccount(accountID) if clientID == 0 { http.Error(w, "No client found for account", http.StatusBadRequest) return } now := time.Now() monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Unix() nextMonth := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Unix() rd := reportData{MonthLabel: now.Format("January 2006")} a.DB.QueryRow( "SELECT COUNT(*) FROM leads WHERE client_id = ? AND created_at >= ? AND created_at < ?", clientID, monthStart, nextMonth, ).Scan(&rd.TotalLeads) rows, err := a.DB.Query( `SELECT service_interest, COUNT(*) as cnt FROM leads WHERE client_id = ? AND created_at >= ? AND created_at < ? GROUP BY service_interest ORDER BY cnt DESC`, clientID, monthStart, nextMonth, ) if err == nil { defer rows.Close() for rows.Next() { var sc serviceCount rows.Scan(&sc.Name, &sc.Count) rd.ServiceCounts = append(rd.ServiceCounts, sc) } } statusRows, err := a.DB.Query( `SELECT status, COUNT(*) as cnt FROM leads WHERE client_id = ? AND created_at >= ? AND created_at < ? GROUP BY status ORDER BY cnt DESC`, clientID, monthStart, nextMonth, ) if err == nil { defer statusRows.Close() for statusRows.Next() { var sc serviceCount statusRows.Scan(&sc.Name, &sc.Count) rd.StatusCounts = append(rd.StatusCounts, sc) } } a.DB.QueryRow( "SELECT COUNT(*) FROM leads WHERE client_id = ? AND status = 'Agendou' AND created_at >= ? AND created_at < ?", clientID, monthStart, nextMonth, ).Scan(&rd.TotalScheduled) if rd.TotalLeads > 0 { rd.ConversionRate = math.Round(float64(rd.TotalScheduled)/float64(rd.TotalLeads)*100*10) / 10 } a.DB.QueryRow( `SELECT COALESCE(SUM(amount),0), COUNT(*) FROM payments WHERE client_id = ? AND has_paid = 1 AND created_at >= ? AND created_at < ?`, clientID, monthStart, nextMonth, ).Scan(&rd.TotalRevenue, &rd.TotalSales) if rd.TotalSales > 0 { rd.AverageTicket = math.Round(rd.TotalRevenue/float64(rd.TotalSales)*100) / 100 } topRows, err := a.DB.Query( `SELECT s.name, COUNT(*) as cnt FROM payments p JOIN scheduling sch ON p.schedule_id = sch.schedule_id JOIN services s ON sch.service_id = s.service_id WHERE p.client_id = ? AND p.has_paid = 1 AND p.created_at >= ? AND p.created_at < ? GROUP BY s.name ORDER BY cnt DESC LIMIT 5`, clientID, monthStart, nextMonth, ) if err == nil { defer topRows.Close() for topRows.Next() { var sc serviceCount topRows.Scan(&sc.Name, &sc.Count) rd.TopServices = append(rd.TopServices, sc) } } var reviewCount int a.DB.QueryRow("SELECT COUNT(*) FROM leads WHERE client_id = ? AND needs_review = 1", clientID).Scan(&reviewCount) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, `
No data for this month.
` } out := `| Name | Quantity | % |
|---|---|---|
| %s | %d | %.1f%% |
No sales data for this month.
` } out := `| Service | Sales |
|---|---|
| %s | %d |