Skip to main content

ralph/productivity/
reports.rs

1//! Productivity report builders and display functions.
2//!
3//! Responsibilities:
4//! - Build structured reports from stats data.
5//! - Print formatted reports to stdout.
6//!
7//! Not handled here:
8//! - Calculations and business logic (see `super::calculations`).
9//! - Data persistence (see `super::persistence`).
10
11use crate::contracts::Task;
12
13use super::calculations::{
14    calculate_estimation_metrics, calculate_velocity, next_milestone, recent_completed_tasks,
15};
16use super::types::{
17    ProductivityEstimationReport, ProductivityStats, ProductivityStreakReport,
18    ProductivitySummaryReport, ProductivityVelocityReport,
19};
20
21/// Build a summary report
22pub fn build_summary_report(stats: &ProductivityStats, recent: usize) -> ProductivitySummaryReport {
23    let recent_completions = recent_completed_tasks(stats, recent);
24    ProductivitySummaryReport {
25        total_completed: stats.total_completed,
26        current_streak: stats.streak.current_streak,
27        longest_streak: stats.streak.longest_streak,
28        last_completed_date: stats.streak.last_completed_date.clone(),
29        next_milestone: next_milestone(stats.total_completed),
30        milestones: stats.milestones.clone(),
31        recent_completions,
32    }
33}
34
35/// Build a streak report
36pub fn build_streak_report(stats: &ProductivityStats) -> ProductivityStreakReport {
37    ProductivityStreakReport {
38        current_streak: stats.streak.current_streak,
39        longest_streak: stats.streak.longest_streak,
40        last_completed_date: stats.streak.last_completed_date.clone(),
41    }
42}
43
44/// Build a velocity report
45pub fn build_velocity_report(stats: &ProductivityStats, days: u32) -> ProductivityVelocityReport {
46    let metrics = calculate_velocity(stats, days);
47    ProductivityVelocityReport {
48        window_days: days.max(1),
49        total_completed: metrics.total_completed,
50        average_per_day: metrics.average_per_day,
51        best_day: metrics.best_day,
52    }
53}
54
55/// Build an estimation report from tasks
56pub fn build_estimation_report(tasks: &[Task]) -> ProductivityEstimationReport {
57    let metrics = calculate_estimation_metrics(tasks);
58    ProductivityEstimationReport {
59        tasks_analyzed: metrics.tasks_analyzed,
60        average_accuracy_ratio: metrics.average_accuracy_ratio,
61        median_accuracy_ratio: metrics.median_accuracy_ratio,
62        within_25_percent: metrics.within_25_percent,
63        average_absolute_error_minutes: metrics.average_absolute_error_minutes,
64    }
65}
66
67/// Print summary report in text format
68pub fn print_summary_report_text(report: &ProductivitySummaryReport) {
69    println!("Productivity Summary");
70    println!("====================");
71    println!();
72    println!("Total completed: {}", report.total_completed);
73    println!(
74        "Streak: {} (longest: {})",
75        report.current_streak, report.longest_streak
76    );
77    if let Some(next) = report.next_milestone {
78        println!("Next milestone: {} tasks", next);
79    } else {
80        println!("Next milestone: (none)");
81    }
82    println!();
83
84    if !report.milestones.is_empty() {
85        println!("Milestones achieved:");
86        for m in &report.milestones {
87            let celebrated = if m.celebrated { "✓" } else { " " };
88            println!(
89                "  [{}] {} tasks at {}",
90                celebrated, m.threshold, m.achieved_at
91            );
92        }
93        println!();
94    }
95
96    if !report.recent_completions.is_empty() {
97        println!("Recent completions:");
98        for t in &report.recent_completions {
99            println!("  {} - {} ({})", t.id, t.title, t.completed_at);
100        }
101    }
102}
103
104/// Print velocity report in text format
105pub fn print_velocity_report_text(report: &ProductivityVelocityReport) {
106    println!("Productivity Velocity ({} days)", report.window_days);
107    println!("===============================");
108    println!();
109    println!("Total completed: {}", report.total_completed);
110    println!("Average/day: {:.2}", report.average_per_day);
111    if let Some((day, count)) = &report.best_day {
112        println!("Best day: {} ({} tasks)", day, count);
113    }
114}
115
116/// Print streak report in text format
117pub fn print_streak_report_text(report: &ProductivityStreakReport) {
118    println!("Productivity Streak");
119    println!("===================");
120    println!();
121    println!("Current streak: {}", report.current_streak);
122    println!("Longest streak: {}", report.longest_streak);
123    println!(
124        "Last completion: {}",
125        report.last_completed_date.as_deref().unwrap_or("(none)")
126    );
127}
128
129/// Print estimation report in text format
130pub fn print_estimation_report_text(report: &ProductivityEstimationReport) {
131    println!("Estimation Accuracy");
132    println!("===================");
133    println!();
134
135    if report.tasks_analyzed == 0 {
136        println!("No tasks with both estimated and actual minutes found.");
137        println!("Complete tasks with estimation data to see accuracy metrics.");
138        return;
139    }
140
141    println!("Tasks analyzed: {}", report.tasks_analyzed);
142    println!();
143    println!(
144        "Average accuracy: {:.2}x (1.0 = perfect)",
145        report.average_accuracy_ratio
146    );
147    println!("Median accuracy:  {:.2}x", report.median_accuracy_ratio);
148    println!();
149    println!("Within 25%: {:.1}% of estimates", report.within_25_percent);
150    println!();
151    println!(
152        "Average absolute error: {:.1} minutes",
153        report.average_absolute_error_minutes
154    );
155    println!();
156
157    // Interpretation
158    if report.average_accuracy_ratio < 0.9 {
159        println!("Trend: Tend to overestimate (actual < estimated)");
160    } else if report.average_accuracy_ratio > 1.1 {
161        println!("Trend: Tend to underestimate (actual > estimated)");
162    } else {
163        println!("Trend: Good calibration");
164    }
165}
166
167/// Format a duration in seconds to a human-readable string
168pub fn format_duration(seconds: i64) -> String {
169    if seconds < 60 {
170        format!("{}s", seconds)
171    } else if seconds < 3600 {
172        format!("{}m", seconds / 60)
173    } else if seconds < 86400 {
174        format!("{}h {}m", seconds / 3600, (seconds % 3600) / 60)
175    } else {
176        let days = seconds / 86400;
177        let hours = (seconds % 86400) / 3600;
178        format!("{}d {}h", days, hours)
179    }
180}