kasl/libs/productivity.rs
1//! Productivity calculation utilities for work time analysis.
2//!
3//! This module provides centralized productivity calculations that distinguish
4//! brief interruptions from full absences to give accurate productivity metrics.
5//!
6//! ## Key Concepts
7//!
8//! - **Short Pauses**: Brief interruptions below `min_pause_duration`
9//! - **Long Pauses**: Full absences at or above `min_pause_duration`, plus any
10//! manual pauses the user recorded (protected records bypass the threshold)
11//!
12//! ## Productivity Formula
13//!
14//! ```text
15//! Productivity = (Net Work Time / Available Work Time) * 100
16//!
17//! Where:
18//! - Available Work Time = Total Time - Long Pauses
19//! - Net Work Time = Available Work Time - Short Pauses
20//! ```
21
22use crate::db::pauses::Pauses;
23use crate::db::workdays::Workday;
24use crate::libs::config::{Config, ProductivityConfig};
25use crate::libs::pause::Pause;
26use anyhow::Result;
27use chrono::Duration;
28
29/// Productivity calculator with comprehensive work time analysis.
30///
31/// This structure holds all the data needed for accurate productivity calculations,
32/// including workday timing, manual breaks, different categories of pauses, and
33/// configuration settings. It provides the central calculation logic used throughout
34/// the application.
35///
36/// ## Data Categories
37///
38/// - **Workday**: Start/end times defining the total work session
39/// - **Breaks**: Manual breaks explicitly added by the user
40/// - **Short Pauses**: Automatic pauses below the minimum threshold (not stored in DB)
41/// - **Long Pauses**: Automatic pauses above the minimum threshold (stored in DB)
42/// - **Config**: Productivity configuration settings and thresholds
43///
44/// ## Usage Pattern
45///
46/// 1. Create instance with `Productivity::new()` - automatically loads all relevant data
47/// 2. Call `calculate_productivity()` for the main productivity percentage
48/// 3. Use helper methods for break recommendations and analysis (now parameter-free)
49pub struct Productivity {
50 /// The workday record containing start/end times
51 pub workday: Workday,
52 /// Short automatic pauses (< min_pause_duration, not in database)
53 pub short_pauses: Vec<Pause>,
54 /// Long pauses (>= min_pause_duration, plus any manual protected pauses)
55 pub long_pauses: Vec<Pause>,
56 /// Productivity configuration settings and thresholds
57 pub config: ProductivityConfig,
58}
59
60impl Productivity {
61 /// Creates a new productivity calculator for the given workday.
62 ///
63 /// This constructor automatically loads all relevant data for productivity calculations:
64 /// - Reads the current configuration to get pause duration thresholds
65 /// - Loads manual breaks from the database for the workday date
66 /// - Loads short pauses (below min_pause_duration threshold)
67 /// - Loads long pauses (at or above min_pause_duration threshold)
68 ///
69 /// The pause categorization is based on the `min_pause_duration` setting from
70 /// the monitor configuration. This threshold determines which pauses are stored
71 /// in the database vs. calculated on-the-fly.
72 ///
73 /// # Arguments
74 ///
75 /// * `workday` - The workday record to analyze
76 ///
77 /// # Returns
78 ///
79 /// Returns a configured `Productivity` instance with all data loaded.
80 ///
81 /// # Errors
82 ///
83 /// Returns an error if:
84 /// - Configuration file cannot be read
85 /// - Database queries fail
86 /// - Data integrity issues are encountered
87 ///
88 /// # Examples
89 ///
90 /// ```rust,no_run
91 /// # fn f() -> anyhow::Result<()> {
92 /// use kasl::libs::productivity::Productivity;
93 /// use kasl::db::workdays::Workdays;
94 /// use chrono::Local;
95 ///
96 /// let mut workdays = Workdays::new()?;
97 /// let workday = workdays.fetch(Local::now().date_naive())?.unwrap();
98 /// let productivity = Productivity::new(&workday)?;
99 /// let current_productivity = productivity.calculate_productivity();
100 /// println!("Current productivity: {:.1}%", current_productivity);
101 /// # Ok(())
102 /// # }
103 /// ```
104 pub fn new(workday: &Workday) -> Result<Self> {
105 let config = Config::read()?;
106 let monitor_config = config.monitor.unwrap_or_default();
107 let productivity_config = config.productivity.unwrap_or_default();
108
109 Ok(Self {
110 workday: workday.clone(),
111 short_pauses: Pauses::new()?.set_max_duration(monitor_config.min_pause_duration).get_workday_pauses(workday)?,
112 long_pauses: Pauses::new()?.set_min_duration(monitor_config.min_pause_duration).get_workday_pauses(workday)?,
113 config: productivity_config,
114 })
115 }
116
117 /// Creates a productivity calculator with provided test data.
118 ///
119 /// This constructor is primarily intended for testing scenarios where you want
120 /// to provide specific pause and break data without database dependencies.
121 /// It uses default productivity configuration settings.
122 ///
123 /// # Arguments
124 ///
125 /// * `workday` - The workday record to analyze
126 /// * `short_pauses` - Short automatic pauses (< threshold)
127 /// * `long_pauses` - Long automatic pauses (>= threshold)
128 ///
129 /// # Examples
130 ///
131 /// ```rust,no_run
132 /// # fn f() {
133 /// use kasl::libs::productivity::Productivity;
134 /// use kasl::db::workdays::Workday;
135 /// use chrono::Local;
136 ///
137 /// let workday = Workday {
138 /// id: 1,
139 /// date: Local::now().date_naive(),
140 /// start: Local::now().naive_local(),
141 /// end: None,
142 /// };
143 /// let productivity = Productivity::with_test_data(
144 /// &workday,
145 /// vec![],
146 /// vec![]
147 /// );
148 /// let result = productivity.calculate_productivity();
149 /// # let _ = result;
150 /// # }
151 /// ```
152 pub fn with_test_data(workday: &Workday, short_pauses: Vec<Pause>, long_pauses: Vec<Pause>) -> Self {
153 Self {
154 workday: workday.clone(),
155 short_pauses,
156 long_pauses,
157 config: ProductivityConfig::default(),
158 }
159 }
160
161 /// Reports whether productivity has fallen below the configured threshold.
162 ///
163 /// The check is suppressed early in the day: a short elapsed period makes the
164 /// ratio swing wildly on a single pause, so warning then would be noise. Once
165 /// `min_workday_fraction_before_suggest` of the expected workday has passed,
166 /// the figure is stable enough to act on.
167 ///
168 /// # Returns
169 ///
170 /// `true` when enough of the day has elapsed and productivity is under
171 /// `min_productivity_threshold`.
172 pub fn is_below_threshold(&self) -> bool {
173 let now = chrono::Local::now().naive_local();
174 let elapsed = now - self.workday.start;
175 let expected_duration = Duration::seconds((self.config.workday_hours * 3600.0) as i64);
176 let min_elapsed = Duration::seconds((expected_duration.num_seconds() as f64 * self.config.min_workday_fraction_before_suggest) as i64);
177
178 if elapsed < min_elapsed {
179 return false;
180 }
181
182 self.calculate_productivity() < self.config.min_productivity_threshold
183 }
184
185 /// Calculates productivity percentage for the workday.
186 ///
187 /// This is the central productivity calculation method that properly handles different
188 /// types of work interruptions to provide accurate productivity metrics. The method
189 /// implements a sophisticated calculation that distinguishes between various types of
190 /// time allocation.
191 ///
192 /// ## Calculation Logic
193 ///
194 /// The productivity calculation follows this formula:
195 /// ```text
196 /// Productivity = (Net Work Time / Available Work Time) * 100
197 ///
198 /// Where:
199 /// - Gross Duration = End Time - Start Time
200 /// - Available Work Time = Gross Duration - Manual Breaks - Long Pauses
201 /// - Net Work Time = Available Work Time - Short Pauses (adjusted for overlaps)
202 /// ```
203 ///
204 /// ## Time Categories
205 ///
206 /// 1. **Manual Breaks**: User-defined break periods (excluded from work time)
207 /// 2. **Long Pauses**: Automatic pauses >= min_pause_duration (recorded in DB)
208 /// 3. **Short Pauses**: Automatic pauses < min_pause_duration (not recorded in DB)
209 /// 4. **Active Work**: Time when user is actively working
210 ///
211 /// ## Overlap Handling
212 ///
213 /// Short pauses are adjusted to avoid double-counting time that's already
214 /// accounted for in manual breaks:
215 /// - If short_pause_duration <= break_duration: Set short pauses to zero
216 /// - Otherwise: Subtract break duration from short pauses
217 ///
218 /// ## Edge Cases
219 ///
220 /// - Returns 0.0% if no available work time exists
221 /// - Clamps result between 0.0% and 100.0% to handle calculation edge cases
222 /// - Handles ongoing workdays by using current time as end time
223 ///
224 /// # Returns
225 ///
226 /// Productivity percentage as a float between 0.0 and 100.0.
227 ///
228 /// # Examples
229 ///
230 /// ```rust
231 /// use kasl::libs::productivity::Productivity;
232 /// use kasl::db::workdays::Workday;
233 /// use chrono::Local;
234 ///
235 /// let workday = Workday {
236 /// id: 1,
237 /// date: Local::now().date_naive(),
238 /// start: Local::now().naive_local(),
239 /// end: None,
240 /// };
241 /// let productivity_instance = Productivity::with_test_data(&workday, vec![], vec![]);
242 /// let productivity = productivity_instance.calculate_productivity();
243 ///
244 /// if productivity >= 75.0 {
245 /// println!("Good productivity: {:.1}%", productivity);
246 /// } else {
247 /// println!("Consider taking a break to improve focus");
248 /// }
249 /// ```
250 pub fn calculate_productivity(&self) -> f64 {
251 let end_time = crate::libs::report::workday_end_time(&self.workday, &self.long_pauses);
252 let gross_duration = end_time - self.workday.start;
253
254 // Long pauses: detected absences above the threshold, plus any manual
255 // pauses the user recorded (protected records bypass the threshold).
256 let long_pause_duration: Duration = self.long_pauses.iter().filter_map(|p| p.duration).sum();
257
258 // Short pauses: brief interruptions below the threshold.
259 let short_pause_duration: Duration = self.short_pauses.iter().filter_map(|p| p.duration).sum();
260
261 // Available work time excludes time the user was away entirely
262 let work_time = gross_duration - long_pause_duration;
263
264 // Net work time further excludes short pauses
265 let net_work_time = work_time - short_pause_duration;
266
267 // Calculate productivity percentage
268 if work_time.num_seconds() > 0 {
269 let productivity = (net_work_time.num_seconds() as f64 / work_time.num_seconds() as f64) * 100.0;
270 productivity.clamp(0.0, 100.0)
271 } else {
272 0.0
273 }
274 }
275}