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()?.set_max_duration(monitor_config.min_pause_duration).get_workday_pauses(workday)?,
108            long_pauses: Pauses::new()?.set_min_duration(monitor_config.min_pause_duration).get_workday_pauses(workday)?,
109            config: productivity_config,
110        })
111    }
112
113    /// Creates a productivity calculator with provided test data.
114    ///
115    /// This constructor is primarily intended for testing scenarios where you want
116    /// to provide specific pause and break data without database dependencies.
117    /// It uses default productivity configuration settings.
118    ///
119    /// # Arguments
120    ///
121    /// * `workday` - The workday record to analyze
122    /// * `breaks` - Manual breaks to include in calculations
123    /// * `short_pauses` - Short automatic pauses (< threshold)
124    /// * `long_pauses` - Long automatic pauses (>= threshold)
125    ///
126    /// # Examples
127    ///
128    /// ```rust
129    /// let productivity = Productivity::with_test_data(
130    ///     &workday,
131    ///     vec![],
132    ///     vec![],
133    ///     vec![]
134    /// );
135    /// let result = productivity.calculate_productivity();
136    /// ```
137    pub fn with_test_data(workday: &Workday, breaks: Vec<Break>, short_pauses: Vec<Pause>, long_pauses: Vec<Pause>) -> Self {
138        Self {
139            workday: workday.clone(),
140            breaks,
141            short_pauses,
142            long_pauses,
143            config: ProductivityConfig::default(),
144        }
145    }
146
147    /// Calculates the break duration needed to reach target productivity.
148    ///
149    /// This function determines how many minutes of manual breaks need to be added
150    /// to achieve a specific productivity threshold. This is used for generating
151    /// break recommendations when productivity falls below acceptable levels.
152    ///
153    /// ## Calculation Logic
154    ///
155    /// The function works backwards from the target productivity:
156    /// 1. Calculate current net work time and gross time using internal data
157    /// 2. Determine required available work time for target productivity
158    /// 3. Calculate needed break duration to achieve that available work time
159    /// 4. Account for existing manual breaks in the calculation
160    ///
161    /// ## Productivity Improvement Strategy
162    ///
163    /// By adding manual breaks:
164    /// - **Gross time**: Remains the same (workday boundaries unchanged)
165    /// - **Available work time**: Decreases (manual breaks excluded)
166    /// - **Net work time**: Decreases slightly (existing pauses unchanged)
167    /// - **Productivity ratio**: Improves (net/available increases)
168    ///
169    /// ## Data Sources
170    ///
171    /// This method uses data already loaded in the struct:
172    /// - `self.workday` for timing boundaries
173    /// - `self.long_pauses` for pause calculations
174    /// - `self.breaks` for existing manual breaks
175    /// - `self.config.min_productivity_threshold` as the default target
176    ///
177    /// # Arguments
178    ///
179    /// * `target_productivity` - Optional desired productivity percentage (0.0-100.0).
180    ///   If None, uses the configured minimum productivity threshold.
181    ///
182    /// # Returns
183    ///
184    /// Returns the number of minutes of breaks needed to reach the target,
185    /// or 0 if the target is already achieved or impossible to reach.
186    ///
187    /// # Examples
188    ///
189    /// ```rust
190    /// let productivity = Productivity::new(&workday)?;
191    ///
192    /// // Use default threshold from config
193    /// let needed_minutes = productivity.calculate_needed_break_duration(None);
194    ///
195    /// // Use custom threshold
196    /// let needed_minutes = productivity.calculate_needed_break_duration(Some(75.0));
197    ///
198    /// if needed_minutes > 0 {
199    ///     println!("Add {} minutes of breaks to improve productivity", needed_minutes);
200    /// }
201    /// ```
202    pub fn calculate_needed_break_duration(&self, target_productivity: Option<f64>) -> u64 {
203        let end_time = self.workday.end.unwrap_or_else(|| chrono::Local::now().naive_local());
204        let gross_duration = end_time - self.workday.start;
205
206        // Use configured threshold if no target specified
207        let target_productivity = target_productivity.unwrap_or(self.config.min_productivity_threshold);
208
209        // Calculate current state using internal data
210        let pause_duration: Duration = self.long_pauses.iter().filter_map(|p| p.duration).sum();
211        let existing_break_duration: Duration = self.breaks.iter().map(|b| b.duration).sum();
212        let net_work_time = gross_duration - pause_duration - existing_break_duration;
213
214        // Validate input parameters
215        if target_productivity <= 0.0 || target_productivity > 100.0 {
216            return 0; // Invalid target productivity
217        }
218
219        if net_work_time.num_seconds() <= 0 {
220            return 0; // No net work time available
221        }
222
223        // Calculate required available work time for target productivity
224        // target_productivity = (net_work_time / available_work_time) * 100
225        // available_work_time = net_work_time * 100 / target_productivity
226        let required_available_time_seconds = (net_work_time.num_seconds() as f64 * 100.0 / target_productivity) as i64;
227        let required_available_time = Duration::seconds(required_available_time_seconds);
228
229        // Calculate total break duration needed
230        // available_work_time = gross_duration - total_break_duration
231        // total_break_duration = gross_duration - required_available_time
232        let total_needed_break_duration = gross_duration - required_available_time;
233
234        // Calculate additional break duration needed beyond existing breaks
235        let additional_break_duration = total_needed_break_duration - existing_break_duration;
236
237        // Return additional minutes needed, ensuring non-negative result
238        if additional_break_duration.num_minutes() > 0 {
239            additional_break_duration.num_minutes() as u64
240        } else {
241            0
242        }
243    }
244
245    /// Check if productivity suggestions should be made based on workday progress.
246    ///
247    /// This function determines whether enough of the workday has passed to make
248    /// meaningful productivity recommendations. It prevents premature suggestions
249    /// when the workday has just started.
250    ///
251    /// ## Configuration Sources
252    ///
253    /// This method uses configuration data already loaded in the struct:
254    /// - `self.config.workday_hours` for expected workday duration
255    /// - `self.config.min_workday_fraction_before_suggest` for minimum elapsed fraction
256    /// - `self.workday.start` for timing calculations
257    ///
258    /// # Returns
259    ///
260    /// `true` if suggestions should be made, `false` otherwise
261    ///
262    /// # Examples
263    ///
264    /// ```rust
265    /// let productivity = Productivity::new(&workday)?;
266    /// if productivity.should_suggest_productivity_improvements() {
267    ///     // Make productivity recommendations
268    /// }
269    /// ```
270    pub fn should_suggest_productivity_improvements(&self) -> bool {
271        let now = chrono::Local::now().naive_local();
272        let elapsed = now - self.workday.start;
273        let expected_duration = Duration::seconds((self.config.workday_hours * 3600.0) as i64);
274        let min_duration = Duration::seconds((expected_duration.num_seconds() as f64 * self.config.min_workday_fraction_before_suggest) as i64);
275
276        elapsed >= min_duration
277    }
278
279    /// Check if productivity recommendations should be shown and calculate needed break duration.
280    ///
281    /// This function combines productivity checking with break duration calculation to provide
282    /// a complete recommendation system. It checks if suggestions should be made and calculates
283    /// the break duration needed to reach the target productivity threshold.
284    ///
285    /// ## Self-Contained Logic
286    ///
287    /// This method uses all data already loaded in the struct:
288    /// - `self.config` for productivity thresholds and timing rules
289    /// - `self.workday` for timing calculations
290    /// - `self.long_pauses` and `self.breaks` for break calculations
291    /// - Internal methods for consistent calculations
292    ///
293    /// ## Decision Flow
294    ///
295    /// 1. **Timing Check**: Verify enough workday time has elapsed
296    /// 2. **Productivity Check**: Calculate current productivity level
297    /// 3. **Threshold Check**: Compare against minimum acceptable productivity
298    /// 4. **Recommendation Calculation**: Determine needed break duration
299    /// 5. **Feasibility Check**: Ensure recommendation is practical
300    ///
301    /// # Returns
302    ///
303    /// Returns `Some(needed_minutes)` if recommendations should be shown,
304    /// `None` if productivity is acceptable or recommendations shouldn't be made yet.
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// let productivity = Productivity::new(&workday)?;
310    ///
311    /// if let Some(needed_minutes) = productivity.check_productivity_recommendations() {
312    ///     println!("Consider adding {} minutes of breaks", needed_minutes);
313    /// }
314    /// ```
315    pub fn check_productivity_recommendations(&self) -> Option<u64> {
316        // Check if enough of the workday has passed to make suggestions
317        if !self.should_suggest_productivity_improvements() {
318            return None; // Too early to suggest improvements
319        }
320
321        // Calculate current productivity using internal comprehensive calculation
322        let current_productivity = self.calculate_productivity();
323
324        // Check if productivity is below the minimum threshold
325        if current_productivity >= self.config.min_productivity_threshold {
326            return None; // Productivity is acceptable
327        }
328
329        // Calculate needed break duration to reach minimum productivity
330        let needed_minutes = self.calculate_needed_break_duration(None); // Use default threshold
331
332        // Only show recommendations if a meaningful break can help
333        if needed_minutes >= self.config.min_break_duration && needed_minutes <= self.config.max_break_duration {
334            Some(needed_minutes)
335        } else {
336            None
337        }
338    }
339
340    /// Calculates productivity percentage for the workday.
341    ///
342    /// This is the central productivity calculation method that properly handles different
343    /// types of work interruptions to provide accurate productivity metrics. The method
344    /// implements a sophisticated calculation that distinguishes between various types of
345    /// time allocation.
346    ///
347    /// ## Calculation Logic
348    ///
349    /// The productivity calculation follows this formula:
350    /// ```text
351    /// Productivity = (Net Work Time / Available Work Time) * 100
352    ///
353    /// Where:
354    /// - Gross Duration = End Time - Start Time
355    /// - Available Work Time = Gross Duration - Manual Breaks - Long Pauses  
356    /// - Net Work Time = Available Work Time - Short Pauses (adjusted for overlaps)
357    /// ```
358    ///
359    /// ## Time Categories
360    ///
361    /// 1. **Manual Breaks**: User-defined break periods (excluded from work time)
362    /// 2. **Long Pauses**: Automatic pauses >= min_pause_duration (recorded in DB)
363    /// 3. **Short Pauses**: Automatic pauses < min_pause_duration (not recorded in DB)
364    /// 4. **Active Work**: Time when user is actively working
365    ///
366    /// ## Overlap Handling
367    ///
368    /// Short pauses are adjusted to avoid double-counting time that's already
369    /// accounted for in manual breaks:
370    /// - If short_pause_duration <= break_duration: Set short pauses to zero
371    /// - Otherwise: Subtract break duration from short pauses
372    ///
373    /// ## Edge Cases
374    ///
375    /// - Returns 0.0% if no available work time exists
376    /// - Clamps result between 0.0% and 100.0% to handle calculation edge cases
377    /// - Handles ongoing workdays by using current time as end time
378    ///
379    /// # Returns
380    ///
381    /// Productivity percentage as a float between 0.0 and 100.0.
382    ///
383    /// # Examples
384    ///
385    /// ```rust
386    /// let productivity = productivity_instance.calculate_productivity();
387    ///
388    /// if productivity >= 75.0 {
389    ///     println!("Good productivity: {:.1}%", productivity);
390    /// } else {
391    ///     println!("Consider taking a break to improve focus");
392    /// }
393    /// ```
394    pub fn calculate_productivity(&self) -> f64 {
395        let end_time = self.workday.end.unwrap_or_else(|| chrono::Local::now().naive_local());
396        let gross_duration = end_time - self.workday.start;
397
398        // Calculate manual break time (user-defined breaks)
399        let break_duration: Duration = self.breaks.iter().map(|b| b.duration).sum();
400
401        // Calculate long pause time (automatic pauses >= min_pause_duration, recorded in DB)
402        let long_pause_duration: Duration = self.long_pauses.iter().filter_map(|p| p.duration).sum();
403
404        // Calculate short pause time (automatic pauses < min_pause_duration, not in DB)
405        let mut short_pause_duration: Duration = self.short_pauses.iter().filter_map(|p| p.duration).sum();
406
407        // Adjust short pauses to avoid double-counting with manual breaks
408        short_pause_duration = if short_pause_duration <= break_duration {
409            Duration::zero()
410        } else {
411            short_pause_duration - break_duration
412        };
413
414        // Available work time excludes manual breaks and long pauses
415        let work_time = gross_duration - break_duration - long_pause_duration;
416
417        // Net work time further excludes short pauses
418        let net_work_time = work_time - short_pause_duration;
419
420        // Calculate productivity percentage
421        if work_time.num_seconds() > 0 {
422            let productivity = (net_work_time.num_seconds() as f64 / work_time.num_seconds() as f64) * 100.0;
423            productivity.clamp(0.0, 100.0)
424        } else {
425            0.0
426        }
427    }
428}