package services import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type ReportingService struct { db *sqlx.DB } func NewReportingService(db *sqlx.DB) *ReportingService { return &ReportingService{db: db} } // --- Case Statistics --- type CaseStats struct { Period string `json:"period" db:"period"` Opened int `json:"opened" db:"opened"` Closed int `json:"closed" db:"closed"` Active int `json:"active" db:"active"` } type CasesByType struct { CaseType string `json:"case_type" db:"case_type"` Count int `json:"count" db:"count"` } type CasesByCourt struct { Court string `json:"court" db:"court"` Count int `json:"count" db:"count"` } type CaseReport struct { Monthly []CaseStats `json:"monthly"` ByType []CasesByType `json:"by_type"` ByCourt []CasesByCourt `json:"by_court"` Total CaseReportTotals `json:"total"` } type CaseReportTotals struct { Opened int `json:"opened"` Closed int `json:"closed"` Active int `json:"active"` } func (s *ReportingService) CaseReport(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (*CaseReport, error) { report := &CaseReport{} // Monthly breakdown monthlyQuery := ` SELECT TO_CHAR(DATE_TRUNC('month', created_at), 'YYYY-MM') AS period, COUNT(*) AS opened, COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS closed, COUNT(*) FILTER (WHERE status = 'active') AS active FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3 GROUP BY DATE_TRUNC('month', created_at) ORDER BY DATE_TRUNC('month', created_at)` report.Monthly = []CaseStats{} if err := s.db.SelectContext(ctx, &report.Monthly, monthlyQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("case report monthly: %w", err) } // By type typeQuery := ` SELECT COALESCE(case_type, 'Sonstiges') AS case_type, COUNT(*) AS count FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3 GROUP BY case_type ORDER BY count DESC` report.ByType = []CasesByType{} if err := s.db.SelectContext(ctx, &report.ByType, typeQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("case report by type: %w", err) } // By court courtQuery := ` SELECT COALESCE(court, 'Ohne Gericht') AS court, COUNT(*) AS count FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3 GROUP BY court ORDER BY count DESC` report.ByCourt = []CasesByCourt{} if err := s.db.SelectContext(ctx, &report.ByCourt, courtQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("case report by court: %w", err) } // Totals totalsQuery := ` SELECT COUNT(*) AS opened, COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS closed, COUNT(*) FILTER (WHERE status = 'active') AS active FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3` if err := s.db.GetContext(ctx, &report.Total, totalsQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("case report totals: %w", err) } return report, nil } // --- Deadline Compliance --- type DeadlineCompliance struct { Period string `json:"period" db:"period"` Total int `json:"total" db:"total"` Met int `json:"met" db:"met"` Missed int `json:"missed" db:"missed"` Pending int `json:"pending" db:"pending"` ComplianceRate float64 `json:"compliance_rate"` } type MissedDeadline struct { ID uuid.UUID `json:"id" db:"id"` Title string `json:"title" db:"title"` DueDate string `json:"due_date" db:"due_date"` CaseID uuid.UUID `json:"case_id" db:"case_id"` CaseNumber string `json:"case_number" db:"case_number"` CaseTitle string `json:"case_title" db:"case_title"` DaysOverdue int `json:"days_overdue" db:"days_overdue"` } type DeadlineReport struct { Monthly []DeadlineCompliance `json:"monthly"` Missed []MissedDeadline `json:"missed"` Total DeadlineReportTotals `json:"total"` } type DeadlineReportTotals struct { Total int `json:"total"` Met int `json:"met"` Missed int `json:"missed"` Pending int `json:"pending"` ComplianceRate float64 `json:"compliance_rate"` } func (s *ReportingService) DeadlineReport(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (*DeadlineReport, error) { report := &DeadlineReport{} // Monthly compliance monthlyQuery := ` SELECT TO_CHAR(DATE_TRUNC('month', due_date), 'YYYY-MM') AS period, COUNT(*) AS total, COUNT(*) FILTER (WHERE status = 'completed' AND (completed_at IS NULL OR completed_at::date <= due_date)) AS met, COUNT(*) FILTER (WHERE (status = 'completed' AND completed_at::date > due_date) OR (status = 'pending' AND due_date < CURRENT_DATE)) AS missed, COUNT(*) FILTER (WHERE status = 'pending' AND due_date >= CURRENT_DATE) AS pending FROM deadlines WHERE tenant_id = $1 AND due_date >= $2 AND due_date <= $3 GROUP BY DATE_TRUNC('month', due_date) ORDER BY DATE_TRUNC('month', due_date)` report.Monthly = []DeadlineCompliance{} if err := s.db.SelectContext(ctx, &report.Monthly, monthlyQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("deadline report monthly: %w", err) } // Calculate compliance rates for i := range report.Monthly { completed := report.Monthly[i].Met + report.Monthly[i].Missed if completed > 0 { report.Monthly[i].ComplianceRate = float64(report.Monthly[i].Met) / float64(completed) * 100 } } // Missed deadlines list missedQuery := ` SELECT d.id, d.title, d.due_date, d.case_id, c.case_number, c.title AS case_title, (CURRENT_DATE - d.due_date::date) AS days_overdue FROM deadlines d JOIN cases c ON c.id = d.case_id AND c.tenant_id = d.tenant_id WHERE d.tenant_id = $1 AND d.due_date >= $2 AND d.due_date <= $3 AND ((d.status = 'pending' AND d.due_date < CURRENT_DATE) OR (d.status = 'completed' AND d.completed_at::date > d.due_date)) ORDER BY d.due_date ASC LIMIT 50` report.Missed = []MissedDeadline{} if err := s.db.SelectContext(ctx, &report.Missed, missedQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("deadline report missed: %w", err) } // Totals totalsQuery := ` SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE status = 'completed' AND (completed_at IS NULL OR completed_at::date <= due_date)) AS met, COUNT(*) FILTER (WHERE (status = 'completed' AND completed_at::date > due_date) OR (status = 'pending' AND due_date < CURRENT_DATE)) AS missed, COUNT(*) FILTER (WHERE status = 'pending' AND due_date >= CURRENT_DATE) AS pending FROM deadlines WHERE tenant_id = $1 AND due_date >= $2 AND due_date <= $3` if err := s.db.GetContext(ctx, &report.Total, totalsQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("deadline report totals: %w", err) } completed := report.Total.Met + report.Total.Missed if completed > 0 { report.Total.ComplianceRate = float64(report.Total.Met) / float64(completed) * 100 } return report, nil } // --- Workload --- type UserWorkload struct { UserID uuid.UUID `json:"user_id" db:"user_id"` ActiveCases int `json:"active_cases" db:"active_cases"` Deadlines int `json:"deadlines" db:"deadlines"` Overdue int `json:"overdue" db:"overdue"` Completed int `json:"completed" db:"completed"` } type WorkloadReport struct { Users []UserWorkload `json:"users"` } func (s *ReportingService) WorkloadReport(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (*WorkloadReport, error) { report := &WorkloadReport{} query := ` WITH user_cases AS ( SELECT ca.user_id, COUNT(DISTINCT ca.case_id) AS active_cases FROM case_assignments ca JOIN cases c ON c.id = ca.case_id AND c.tenant_id = $1 WHERE c.status = 'active' GROUP BY ca.user_id ), user_deadlines AS ( SELECT ca.user_id, COUNT(*) AS deadlines, COUNT(*) FILTER (WHERE d.status = 'pending' AND d.due_date < CURRENT_DATE) AS overdue, COUNT(*) FILTER (WHERE d.status = 'completed' AND d.completed_at >= $2 AND d.completed_at <= $3) AS completed FROM case_assignments ca JOIN deadlines d ON d.case_id = ca.case_id AND d.tenant_id = $1 WHERE d.due_date >= $2 AND d.due_date <= $3 GROUP BY ca.user_id ) SELECT COALESCE(uc.user_id, ud.user_id) AS user_id, COALESCE(uc.active_cases, 0) AS active_cases, COALESCE(ud.deadlines, 0) AS deadlines, COALESCE(ud.overdue, 0) AS overdue, COALESCE(ud.completed, 0) AS completed FROM user_cases uc FULL OUTER JOIN user_deadlines ud ON uc.user_id = ud.user_id ORDER BY active_cases DESC` report.Users = []UserWorkload{} if err := s.db.SelectContext(ctx, &report.Users, query, tenantID, from, to); err != nil { return nil, fmt.Errorf("workload report: %w", err) } return report, nil } // --- Billing (summary from case data) --- type BillingByMonth struct { Period string `json:"period" db:"period"` CasesActive int `json:"cases_active" db:"cases_active"` CasesClosed int `json:"cases_closed" db:"cases_closed"` CasesNew int `json:"cases_new" db:"cases_new"` } type BillingByType struct { CaseType string `json:"case_type" db:"case_type"` Active int `json:"active" db:"active"` Closed int `json:"closed" db:"closed"` Total int `json:"total" db:"total"` } type BillingReport struct { Monthly []BillingByMonth `json:"monthly"` ByType []BillingByType `json:"by_type"` } func (s *ReportingService) BillingReport(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (*BillingReport, error) { report := &BillingReport{} // Monthly activity for billing overview monthlyQuery := ` SELECT TO_CHAR(DATE_TRUNC('month', created_at), 'YYYY-MM') AS period, COUNT(*) FILTER (WHERE status = 'active') AS cases_active, COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS cases_closed, COUNT(*) AS cases_new FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3 GROUP BY DATE_TRUNC('month', created_at) ORDER BY DATE_TRUNC('month', created_at)` report.Monthly = []BillingByMonth{} if err := s.db.SelectContext(ctx, &report.Monthly, monthlyQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("billing report monthly: %w", err) } // By case type typeQuery := ` SELECT COALESCE(case_type, 'Sonstiges') AS case_type, COUNT(*) FILTER (WHERE status = 'active') AS active, COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS closed, COUNT(*) AS total FROM cases WHERE tenant_id = $1 AND created_at >= $2 AND created_at <= $3 GROUP BY case_type ORDER BY total DESC` report.ByType = []BillingByType{} if err := s.db.SelectContext(ctx, &report.ByType, typeQuery, tenantID, from, to); err != nil { return nil, fmt.Errorf("billing report by type: %w", err) } return report, nil }