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 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}