Skip to main content

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}