kasl/libs/productivity.rs
1//! Productivity calculation utilities for work time analysis.
2//!
3//! This module provides centralized productivity calculations that properly handle
4//! different types of work interruptions (pauses vs. breaks) to give accurate
5//! productivity metrics.
6//!
7//! ## Key Concepts
8//!
9//! - **Short Pauses**: Brief interruptions that are not recorded in the database (< min_pause_duration)
10//! - **Long Pauses**: Extended interruptions that are recorded as pause records (>= min_pause_duration)
11//! - **Manual Breaks**: User-defined break periods that are excluded from productivity calculations
12//!
13//! ## Productivity Formula
14//!
15//! ```text
16//! Productivity = (Net Work Time / Available Work Time) * 100
17//!
18//! Where:
19//! - Net Work Time = Total Time - Long Pauses - Manual Breaks
20//! - Available Work Time = Total Time - Manual Breaks
21//! ```
22
23use crate::db::breaks::{Break, Breaks};
24use crate::db::pauses::Pauses;
25use crate::db::workdays::Workday;
26use crate::libs::config::{Config, ProductivityConfig};
27use crate::libs::pause::Pause;
28use anyhow::Result;
29use chrono::Duration;
30
31/// Productivity calculator with comprehensive work time analysis.
32///
33/// This structure holds all the data needed for accurate productivity calculations,
34/// including workday timing, manual breaks, different categories of pauses, and
35/// configuration settings. It provides the central calculation logic used throughout
36/// the application.
37///
38/// ## Data Categories
39///
40/// - **Workday**: Start/end times defining the total work session
41/// - **Breaks**: Manual breaks explicitly added by the user
42/// - **Short Pauses**: Automatic pauses below the minimum threshold (not stored in DB)
43/// - **Long Pauses**: Automatic pauses above the minimum threshold (stored in DB)
44/// - **Config**: Productivity configuration settings and thresholds
45///
46/// ## Usage Pattern
47///
48/// 1. Create instance with `Productivity::new()` - automatically loads all relevant data
49/// 2. Call `calculate_productivity()` for the main productivity percentage
50/// 3. Use helper methods for break recommendations and analysis (now parameter-free)
51pub struct Productivity {
52 /// The workday record containing start/end times
53 pub workday: Workday,
54 /// Manual breaks explicitly added by the user
55 pub breaks: Vec<Break>,
56 /// Short automatic pauses (< min_pause_duration, not in database)
57 pub short_pauses: Vec<Pause>,
58 /// Long automatic pauses (>= min_pause_duration, stored in database)
59 pub long_pauses: Vec<Pause>,
60 /// Productivity configuration settings and thresholds
61 pub config: ProductivityConfig,
62}
63
64impl Productivity {
65 /// Creates a new productivity calculator for the given workday.
66 ///
67 /// This constructor automatically loads all relevant data for productivity calculations:
68 /// - Reads the current configuration to get pause duration thresholds
69 /// - Loads manual breaks from the database for the workday date
70 /// - Loads short pauses (below min_pause_duration threshold)
71 /// - Loads long pauses (at or above min_pause_duration threshold)
72 ///
73 /// The pause categorization is based on the `min_pause_duration` setting from
74 /// the monitor configuration. This threshold determines which pauses are stored
75 /// in the database vs. calculated on-the-fly.
76 ///
77 /// # Arguments
78 ///
79 /// * `workday` - The workday record to analyze
80 ///
81 /// # Returns
82 ///
83 /// Returns a configured `Productivity` instance with all data loaded.
84 ///
85 /// # Errors
86 ///
87 /// Returns an error if:
88 /// - Configuration file cannot be read
89 /// - Database queries fail
90 /// - Data integrity issues are encountered
91 ///
92 /// # Examples
93 ///
94 /// ```rust
95 /// let productivity = Productivity::new(&workday)?;
96 /// let current_productivity = productivity.calculate_productivity();
97 /// println!("Current productivity: {:.1}%", current_productivity);
98 /// ```
99 pub fn new(workday: &Workday) -> Result<Self> {
100 let config = Config::read()?;
101 let monitor_config = config.monitor.unwrap_or_default();
102 let productivity_config = config.productivity.unwrap_or_default();
103
104 Ok(Self {
105 workday: workday.clone(),
106 breaks: Breaks::new()?.get_daily_breaks(workday.date)?,
107 short_pauses: Pauses::new()?
108 .set_max_duration(monitor_config.min_pause_duration)
109 .get_workday_pauses(workday)?,
110 long_pauses: Pauses::new()?
111 .set_min_duration(monitor_config.min_pause_duration)
112 .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 /// * `breaks` - Manual breaks to include in calculations
127 /// * `short_pauses` - Short automatic pauses (< threshold)
128 /// * `long_pauses` - Long automatic pauses (>= threshold)
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// let productivity = Productivity::with_test_data(
134 /// &workday,
135 /// vec![],
136 /// vec![],
137 /// vec![]
138 /// );
139 /// let result = productivity.calculate_productivity();
140 /// ```
141 pub fn with_test_data(workday: &Workday, breaks: Vec<Break>, short_pauses: Vec<Pause>, long_pauses: Vec<Pause>) -> Self {
142 Self {
143 workday: workday.clone(),
144 breaks,
145 short_pauses,
146 long_pauses,
147 config: ProductivityConfig::default(),
148 }
149 }
150
151 /// Calculates the break duration needed to reach target productivity.
152 ///
153 /// This function determines how many minutes of manual breaks need to be added
154 /// to achieve a specific productivity threshold. This is used for generating
155 /// break recommendations when productivity falls below acceptable levels.
156 ///
157 /// ## Calculation Logic
158 ///
159 /// The function works backwards from the target productivity:
160 /// 1. Calculate current net work time and gross time using internal data
161 /// 2. Determine required available work time for target productivity
162 /// 3. Calculate needed break duration to achieve that available work time
163 /// 4. Account for existing manual breaks in the calculation
164 ///
165 /// ## Productivity Improvement Strategy
166 ///
167 /// By adding manual breaks:
168 /// - **Gross time**: Remains the same (workday boundaries unchanged)
169 /// - **Available work time**: Decreases (manual breaks excluded)
170 /// - **Net work time**: Decreases slightly (existing pauses unchanged)
171 /// - **Productivity ratio**: Improves (net/available increases)
172 ///
173 /// ## Data Sources
174 ///
175 /// This method uses data already loaded in the struct:
176 /// - `self.workday` for timing boundaries
177 /// - `self.long_pauses` for pause calculations
178 /// - `self.breaks` for existing manual breaks
179 /// - `self.config.min_productivity_threshold` as the default target
180 ///
181 /// # Arguments
182 ///
183 /// * `target_productivity` - Optional desired productivity percentage (0.0-100.0).
184 /// If None, uses the configured minimum productivity threshold.
185 ///
186 /// # Returns
187 ///
188 /// Returns the number of minutes of breaks needed to reach the target,
189 /// or 0 if the target is already achieved or impossible to reach.
190 ///
191 /// # Examples
192 ///
193 /// ```rust
194 /// let productivity = Productivity::new(&workday)?;
195 ///
196 /// // Use default threshold from config
197 /// let needed_minutes = productivity.calculate_needed_break_duration(None);
198 ///
199 /// // Use custom threshold
200 /// let needed_minutes = productivity.calculate_needed_break_duration(Some(75.0));
201 ///
202 /// if needed_minutes > 0 {
203 /// println!("Add {} minutes of breaks to improve productivity", needed_minutes);
204 /// }
205 /// ```
206 pub fn calculate_needed_break_duration(&self, target_productivity: Option<f64>) -> u64 {
207 let end_time = self.workday.end.unwrap_or_else(|| chrono::Local::now().naive_local());
208 let gross_duration = end_time - self.workday.start;
209
210 // Use configured threshold if no target specified
211 let target_productivity = target_productivity.unwrap_or(self.config.min_productivity_threshold);
212
213 // Calculate current state using internal data
214 let pause_duration: Duration = self.long_pauses.iter().filter_map(|p| p.duration).sum();
215 let existing_break_duration: Duration = self.breaks.iter().map(|b| b.duration).sum();
216 let net_work_time = gross_duration - pause_duration - existing_break_duration;
217
218 // Validate input parameters
219 if target_productivity <= 0.0 || target_productivity > 100.0 {
220 return 0; // Invalid target productivity
221 }
222
223 if net_work_time.num_seconds() <= 0 {
224 return 0; // No net work time available
225 }
226
227 // Calculate required available work time for target productivity
228 // target_productivity = (net_work_time / available_work_time) * 100
229 // available_work_time = net_work_time * 100 / target_productivity
230 let required_available_time_seconds = (net_work_time.num_seconds() as f64 * 100.0 / target_productivity) as i64;
231 let required_available_time = Duration::seconds(required_available_time_seconds);
232
233 // Calculate total break duration needed
234 // available_work_time = gross_duration - total_break_duration
235 // total_break_duration = gross_duration - required_available_time
236 let total_needed_break_duration = gross_duration - required_available_time;
237
238 // Calculate additional break duration needed beyond existing breaks
239 let additional_break_duration = total_needed_break_duration - existing_break_duration;
240
241 // Return additional minutes needed, ensuring non-negative result
242 if additional_break_duration.num_minutes() > 0 {
243 additional_break_duration.num_minutes() as u64
244 } else {
245 0
246 }
247 }
248
249 /// Check if productivity suggestions should be made based on workday progress.
250 ///
251 /// This function determines whether enough of the workday has passed to make
252 /// meaningful productivity recommendations. It prevents premature suggestions
253 /// when the workday has just started.
254 ///
255 /// ## Configuration Sources
256 ///
257 /// This method uses configuration data already loaded in the struct:
258 /// - `self.config.workday_hours` for expected workday duration
259 /// - `self.config.min_workday_fraction_before_suggest` for minimum elapsed fraction
260 /// - `self.workday.start` for timing calculations
261 ///
262 /// # Returns
263 ///
264 /// `true` if suggestions should be made, `false` otherwise
265 ///
266 /// # Examples
267 ///
268 /// ```rust
269 /// let productivity = Productivity::new(&workday)?;
270 /// if productivity.should_suggest_productivity_improvements() {
271 /// // Make productivity recommendations
272 /// }
273 /// ```
274 pub fn should_suggest_productivity_improvements(&self) -> bool {
275 let now = chrono::Local::now().naive_local();
276 let elapsed = now - self.workday.start;
277 let expected_duration = Duration::seconds((self.config.workday_hours * 3600.0) as i64);
278 let min_duration = Duration::seconds((expected_duration.num_seconds() as f64 * self.config.min_workday_fraction_before_suggest) as i64);
279
280 elapsed >= min_duration
281 }
282
283 /// Check if productivity recommendations should be shown and calculate needed break duration.
284 ///
285 /// This function combines productivity checking with break duration calculation to provide
286 /// a complete recommendation system. It checks if suggestions should be made and calculates
287 /// the break duration needed to reach the target productivity threshold.
288 ///
289 /// ## Self-Contained Logic
290 ///
291 /// This method uses all data already loaded in the struct:
292 /// - `self.config` for productivity thresholds and timing rules
293 /// - `self.workday` for timing calculations
294 /// - `self.long_pauses` and `self.breaks` for break calculations
295 /// - Internal methods for consistent calculations
296 ///
297 /// ## Decision Flow
298 ///
299 /// 1. **Timing Check**: Verify enough workday time has elapsed
300 /// 2. **Productivity Check**: Calculate current productivity level
301 /// 3. **Threshold Check**: Compare against minimum acceptable productivity
302 /// 4. **Recommendation Calculation**: Determine needed break duration
303 /// 5. **Feasibility Check**: Ensure recommendation is practical
304 ///
305 /// # Returns
306 ///
307 /// Returns `Some(needed_minutes)` if recommendations should be shown,
308 /// `None` if productivity is acceptable or recommendations shouldn't be made yet.
309 ///
310 /// # Examples
311 ///
312 /// ```rust
313 /// let productivity = Productivity::new(&workday)?;
314 ///
315 /// if let Some(needed_minutes) = productivity.check_productivity_recommendations() {
316 /// println!("Consider adding {} minutes of breaks", needed_minutes);
317 /// }
318 /// ```
319 pub fn check_productivity_recommendations(&self) -> Option<u64> {
320 // Check if enough of the workday has passed to make suggestions
321 if !self.should_suggest_productivity_improvements() {
322 return None; // Too early to suggest improvements
323 }
324
325 // Calculate current productivity using internal comprehensive calculation
326 let current_productivity = self.calculate_productivity();
327
328 // Check if productivity is below the minimum threshold
329 if current_productivity >= self.config.min_productivity_threshold {
330 return None; // Productivity is acceptable
331 }
332
333 // Calculate needed break duration to reach minimum productivity
334 let needed_minutes = self.calculate_needed_break_duration(None); // Use default threshold
335
336 // Only show recommendations if a meaningful break can help
337 if needed_minutes >= self.config.min_break_duration && needed_minutes <= self.config.max_break_duration {
338 Some(needed_minutes)
339 } else {
340 None
341 }
342 }
343
344 /// Calculates productivity percentage for the workday.
345 ///
346 /// This is the central productivity calculation method that properly handles different
347 /// types of work interruptions to provide accurate productivity metrics. The method
348 /// implements a sophisticated calculation that distinguishes between various types of
349 /// time allocation.
350 ///
351 /// ## Calculation Logic
352 ///
353 /// The productivity calculation follows this formula:
354 /// ```text
355 /// Productivity = (Net Work Time / Available Work Time) * 100
356 ///
357 /// Where:
358 /// - Gross Duration = End Time - Start Time
359 /// - Available Work Time = Gross Duration - Manual Breaks - Long Pauses
360 /// - Net Work Time = Available Work Time - Short Pauses (adjusted for overlaps)
361 /// ```
362 ///
363 /// ## Time Categories
364 ///
365 /// 1. **Manual Breaks**: User-defined break periods (excluded from work time)
366 /// 2. **Long Pauses**: Automatic pauses >= min_pause_duration (recorded in DB)
367 /// 3. **Short Pauses**: Automatic pauses < min_pause_duration (not recorded in DB)
368 /// 4. **Active Work**: Time when user is actively working
369 ///
370 /// ## Overlap Handling
371 ///
372 /// Short pauses are adjusted to avoid double-counting time that's already
373 /// accounted for in manual breaks:
374 /// - If short_pause_duration <= break_duration: Set short pauses to zero
375 /// - Otherwise: Subtract break duration from short pauses
376 ///
377 /// ## Edge Cases
378 ///
379 /// - Returns 0.0% if no available work time exists
380 /// - Clamps result between 0.0% and 100.0% to handle calculation edge cases
381 /// - Handles ongoing workdays by using current time as end time
382 ///
383 /// # Returns
384 ///
385 /// Productivity percentage as a float between 0.0 and 100.0.
386 ///
387 /// # Examples
388 ///
389 /// ```rust
390 /// let productivity = productivity_instance.calculate_productivity();
391 ///
392 /// if productivity >= 75.0 {
393 /// println!("Good productivity: {:.1}%", productivity);
394 /// } else {
395 /// println!("Consider taking a break to improve focus");
396 /// }
397 /// ```
398 pub fn calculate_productivity(&self) -> f64 {
399 let end_time = self.workday.end.unwrap_or_else(|| chrono::Local::now().naive_local());
400 let gross_duration = end_time - self.workday.start;
401
402 // Calculate manual break time (user-defined breaks)
403 let break_duration: Duration = self.breaks.iter().map(|b| b.duration).sum();
404
405 // Calculate long pause time (automatic pauses >= min_pause_duration, recorded in DB)
406 let long_pause_duration: Duration = self.long_pauses.iter().filter_map(|p| p.duration).sum();
407
408 // Calculate short pause time (automatic pauses < min_pause_duration, not in DB)
409 let mut short_pause_duration: Duration = self.short_pauses.iter().filter_map(|p| p.duration).sum();
410
411 // Adjust short pauses to avoid double-counting with manual breaks
412 short_pause_duration = if short_pause_duration <= break_duration {
413 Duration::zero()
414 } else {
415 short_pause_duration - break_duration
416 };
417
418 // Available work time excludes manual breaks and long pauses
419 let work_time = gross_duration - break_duration - long_pause_duration;
420
421 // Net work time further excludes short pauses
422 let net_work_time = work_time - short_pause_duration;
423
424 // Calculate productivity percentage
425 if work_time.num_seconds() > 0 {
426 let productivity = (net_work_time.num_seconds() as f64 / work_time.num_seconds() as f64) * 100.0;
427 productivity.max(0.0).min(100.0) // Clamp between 0-100%
428 } else {
429 0.0
430 }
431 }
432}