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}