package services import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type DashboardService struct { db *sqlx.DB } func NewDashboardService(db *sqlx.DB) *DashboardService { return &DashboardService{db: db} } type DashboardData struct { DeadlineSummary DeadlineSummary `json:"deadline_summary"` CaseSummary CaseSummary `json:"case_summary"` UpcomingDeadlines []UpcomingDeadline `json:"upcoming_deadlines"` UpcomingAppointments []UpcomingAppointment `json:"upcoming_appointments"` RecentActivity []RecentActivity `json:"recent_activity"` } type DeadlineSummary struct { OverdueCount int `json:"overdue_count" db:"overdue_count"` DueThisWeek int `json:"due_this_week" db:"due_this_week"` DueNextWeek int `json:"due_next_week" db:"due_next_week"` OKCount int `json:"ok_count" db:"ok_count"` } type CaseSummary struct { ActiveCount int `json:"active_count" db:"active_count"` NewThisMonth int `json:"new_this_month" db:"new_this_month"` ClosedCount int `json:"closed_count" db:"closed_count"` } type UpcomingDeadline struct { ID uuid.UUID `json:"id" db:"id"` Title string `json:"title" db:"title"` DueDate string `json:"due_date" db:"due_date"` CaseNumber string `json:"case_number" db:"case_number"` CaseTitle string `json:"case_title" db:"case_title"` Status string `json:"status" db:"status"` } type UpcomingAppointment struct { ID uuid.UUID `json:"id" db:"id"` Title string `json:"title" db:"title"` StartAt time.Time `json:"start_at" db:"start_at"` CaseNumber *string `json:"case_number" db:"case_number"` Location *string `json:"location" db:"location"` } type RecentActivity struct { EventType *string `json:"event_type" db:"event_type"` Title string `json:"title" db:"title"` CaseNumber string `json:"case_number" db:"case_number"` EventDate *time.Time `json:"event_date" db:"event_date"` } func (s *DashboardService) Get(ctx context.Context, tenantID uuid.UUID) (*DashboardData, error) { now := time.Now() today := now.Format("2006-01-02") endOfWeek := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02") endOfNextWeek := now.AddDate(0, 0, 14-int(now.Weekday())).Format("2006-01-02") in7Days := now.AddDate(0, 0, 7).Format("2006-01-02") startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02") data := &DashboardData{} // Single query with CTEs for deadline + case summaries summaryQuery := ` WITH deadline_stats AS ( SELECT COUNT(*) FILTER (WHERE due_date < $2 AND status = 'pending') AS overdue_count, COUNT(*) FILTER (WHERE due_date >= $2 AND due_date <= $3 AND status = 'pending') AS due_this_week, COUNT(*) FILTER (WHERE due_date > $3 AND due_date <= $4 AND status = 'pending') AS due_next_week, COUNT(*) FILTER (WHERE due_date > $4 AND status = 'pending') AS ok_count FROM deadlines WHERE tenant_id = $1 ), case_stats AS ( SELECT COUNT(*) FILTER (WHERE status = 'active') AS active_count, COUNT(*) FILTER (WHERE created_at >= $5::date AND status != 'archived') AS new_this_month, COUNT(*) FILTER (WHERE status IN ('closed', 'archived')) AS closed_count FROM cases WHERE tenant_id = $1 ) SELECT ds.overdue_count, ds.due_this_week, ds.due_next_week, ds.ok_count, cs.active_count, cs.new_this_month, cs.closed_count FROM deadline_stats ds, case_stats cs` var summaryRow struct { DeadlineSummary CaseSummary } err := s.db.GetContext(ctx, &summaryRow, summaryQuery, tenantID, today, endOfWeek, endOfNextWeek, startOfMonth) if err != nil { return nil, fmt.Errorf("dashboard summary: %w", err) } data.DeadlineSummary = summaryRow.DeadlineSummary data.CaseSummary = summaryRow.CaseSummary // Upcoming deadlines (next 7 days) deadlineQuery := ` SELECT d.id, d.title, d.due_date, c.case_number, c.title AS case_title, d.status 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.status = 'pending' AND d.due_date >= $2 AND d.due_date <= $3 ORDER BY d.due_date ASC` data.UpcomingDeadlines = []UpcomingDeadline{} if err := s.db.SelectContext(ctx, &data.UpcomingDeadlines, deadlineQuery, tenantID, today, in7Days); err != nil { return nil, fmt.Errorf("dashboard upcoming deadlines: %w", err) } // Upcoming appointments (next 7 days) appointmentQuery := ` SELECT a.id, a.title, a.start_at, c.case_number, a.location FROM appointments a LEFT JOIN cases c ON c.id = a.case_id AND c.tenant_id = a.tenant_id WHERE a.tenant_id = $1 AND a.start_at >= $2::timestamp AND a.start_at < ($2::date + interval '7 days') ORDER BY a.start_at ASC` data.UpcomingAppointments = []UpcomingAppointment{} if err := s.db.SelectContext(ctx, &data.UpcomingAppointments, appointmentQuery, tenantID, now); err != nil { return nil, fmt.Errorf("dashboard upcoming appointments: %w", err) } // Recent activity (last 10 case events) activityQuery := ` SELECT ce.event_type, ce.title, c.case_number, ce.event_date FROM case_events ce JOIN cases c ON c.id = ce.case_id AND c.tenant_id = ce.tenant_id WHERE ce.tenant_id = $1 ORDER BY COALESCE(ce.event_date, ce.created_at) DESC LIMIT 10` data.RecentActivity = []RecentActivity{} if err := s.db.SelectContext(ctx, &data.RecentActivity, activityQuery, tenantID); err != nil { return nil, fmt.Errorf("dashboard recent activity: %w", err) } return data, nil }