Skip to main content

kasl/libs/
report.rs

1//! Work interval calculation and productivity analysis for daily reports.
2//!
3//! Provides core logic for analyzing work patterns and generating detailed reports
4//! about productivity, work intervals, and break patterns.
5//!
6//! ## Features
7//!
8//! - **Work Interval Analysis**: Convert workday and pause data into continuous work periods
9//! - **Productivity Metrics**: Calculate efficiency ratios and work pattern analysis
10//! - **Short Interval Detection**: Identify and analyze brief work periods that may indicate interruptions
11//! - **Interval Filtering**: Filter out short intervals for cleaner reporting (display-level, no database changes)
12//! - **Report Generation**: Comprehensive breakdown of workdays with productivity analysis
13//!
14//! ## Usage
15//!
16//! ```rust
17//! use kasl::libs::report::{calculate_work_intervals, filter_short_intervals, WorkInterval};
18//! use kasl::db::workdays::Workday;
19//! use kasl::libs::pause::Pause;
20//!
21//! let workday = Workday {
22//!     start: start_time,
23//!     end: Some(end_time),
24//! };
25//!
26//! let pauses = vec![/* pause data */];
27//! let intervals = calculate_work_intervals(&workday, &pauses);
28//!
29//! // Filter short intervals for cleaner reporting
30//! let (filtered_intervals, filter_info) = filter_short_intervals(&intervals, 30);
31//! ```
32
33use crate::libs::pause::Pause;
34use crate::{db::workdays::Workday, libs::productivity::Productivity};
35use anyhow::Result;
36use chrono::{Duration, NaiveDateTime};
37
38/// Represents a single continuous work interval between breaks.
39///
40/// This structure captures a period of uninterrupted work time, providing
41/// the foundation for productivity analysis and reporting. Each interval
42/// represents a focused work session bounded by either the workday start/end
43/// or pause periods.
44///
45/// ## Interval Boundaries
46///
47/// Work intervals are defined by:
48/// - **Start Time**: When focused work began (workday start or end of previous pause)
49/// - **End Time**: When focused work ended (start of next pause or workday end)
50/// - **Duration**: Total time spent in focused work during this period
51///
52/// ## Pause Association
53///
54/// Each interval can be associated with the pause that follows it:
55/// - **Some(index)**: Index of the pause that ended this work interval
56/// - **None**: This interval extends to the end of the workday
57///
58/// This association enables:
59/// - Analysis of work-break patterns
60/// - Identification of interruption causes
61/// - Optimization recommendations for pause timing
62///
63/// ## Usage Context
64///
65/// Work intervals are used for:
66/// - Productivity calculation and analysis
67/// - Generating detailed work reports
68/// - Identifying optimization opportunities
69/// - Visualizing work patterns in charts and graphs
70#[derive(Debug, Clone)]
71pub struct WorkInterval {
72    /// The timestamp when this work interval began.
73    ///
74    /// This is either the workday start time (for the first interval)
75    /// or the end time of the previous pause (for subsequent intervals).
76    /// Represents the moment when focused work activity resumed.
77    pub start: NaiveDateTime,
78
79    /// The timestamp when this work interval ended.
80    ///
81    /// This is either the start time of the next pause (for most intervals)
82    /// or the workday end time (for the final interval). Represents the
83    /// moment when focused work was interrupted or completed.
84    pub end: NaiveDateTime,
85
86    /// The total duration of focused work during this interval.
87    ///
88    /// Calculated as `end - start`, this represents the net productive
89    /// time during this period. Used for productivity calculations,
90    /// efficiency analysis, and time accounting in reports.
91    pub duration: Duration,
92
93    /// Optional reference to the pause that follows this interval.
94    ///
95    /// Contains the index of the pause in the original pause collection
96    /// that ended this work interval. `None` indicates this interval
97    /// extends to the end of the workday without interruption.
98    ///
99    /// ## Usage Notes
100    /// - Used for analyzing work-break patterns
101    /// - Enables identification of frequent interruption points
102    /// - Supports optimization recommendations for pause timing
103    /// - Links intervals to specific causes of work interruption
104    pub pause_after: Option<usize>, // Index of pause after this interval
105}
106
107impl WorkInterval {
108    /// Determines if this interval is shorter than the specified minimum duration.
109    ///
110    /// This method is used to identify "short intervals" that may indicate
111    /// excessive interruptions or poor work habits. Short intervals often
112    /// represent brief periods of work between frequent breaks, which can
113    /// significantly impact overall productivity.
114    ///
115    /// ## Usage in Analysis
116    ///
117    /// Short intervals are identified for:
118    /// - **Productivity Analysis**: Understanding interruption patterns
119    /// - **Optimization Recommendations**: Suggesting pause consolidation
120    /// - **Work Habit Assessment**: Identifying areas for improvement
121    /// - **Focus Period Analysis**: Measuring sustained work capability
122    ///
123    /// ## Threshold Considerations
124    ///
125    /// Common minimum duration thresholds:
126    /// - **15 minutes**: Very strict, identifies micro-interruptions
127    /// - **30 minutes**: Moderate, focuses on meaningful work blocks
128    /// - **60 minutes**: Lenient, identifies only major fragmentation
129    ///
130    /// # Arguments
131    ///
132    /// * `min_minutes` - Minimum duration threshold in minutes
133    ///
134    /// # Returns
135    ///
136    /// Returns `true` if the interval duration is less than the threshold.
137    ///
138    /// # Examples
139    ///
140    /// ```rust
141    /// use kasl::libs::report::WorkInterval;
142    /// use chrono::{Duration, NaiveDateTime};
143    ///
144    /// let interval = WorkInterval {
145    ///     start: start_time,
146    ///     end: start_time + Duration::minutes(20),
147    ///     duration: Duration::minutes(20),
148    ///     pause_after: Some(1),
149    /// };
150    ///
151    /// assert_eq!(interval.is_short(30), true);  // 20 < 30
152    /// assert_eq!(interval.is_short(15), false); // 20 >= 15
153    /// ```
154    pub fn is_short(&self, min_minutes: u64) -> bool {
155        self.duration < Duration::minutes(min_minutes as i64)
156    }
157}
158
159/// Information about short intervals detected in a workday.
160///
161/// This structure provides comprehensive analysis of work intervals that
162/// fall below the minimum duration threshold. It includes both statistical
163/// information about the impact of short intervals and actionable
164/// recommendations for optimization.
165///
166/// ## Analysis Components
167///
168/// ### Statistical Information
169/// - **Count**: Number of short intervals detected
170/// - **Total Duration**: Cumulative time spent in short work periods
171/// - **Individual Intervals**: Specific intervals with their details
172///
173/// ### Optimization Recommendations
174/// - **Pauses to Remove**: Specific pauses that could be eliminated
175/// - **Merge Opportunities**: Intervals that could be combined
176/// - **Productivity Impact**: Potential time savings from optimization
177///
178/// ## Usage Context
179///
180/// This analysis is used for:
181/// - Generating optimization recommendations in reports
182/// - Identifying patterns of work fragmentation
183/// - Calculating potential productivity improvements
184/// - Providing actionable feedback to users
185#[derive(Debug)]
186pub struct ShortIntervalsInfo {
187    /// The number of intervals that fall below the minimum duration threshold.
188    ///
189    /// This count provides a quick assessment of work fragmentation:
190    /// - **0**: No fragmentation issues detected
191    /// - **1-3**: Minor fragmentation, limited impact
192    /// - **4+**: Significant fragmentation, optimization recommended
193    pub count: usize,
194
195    /// The cumulative duration of all short intervals combined.
196    ///
197    /// Represents the total amount of time spent in fragmented work
198    /// periods. This metric helps quantify the impact of work
199    /// interruptions and provides context for optimization efforts.
200    ///
201    /// ## Impact Assessment
202    /// - **< 30 minutes**: Minor impact on overall productivity
203    /// - **30-60 minutes**: Moderate impact, optimization beneficial
204    /// - **> 60 minutes**: Significant impact, optimization essential
205    pub total_duration: Duration,
206
207    /// Detailed information about each short interval detected.
208    ///
209    /// Each tuple contains:
210    /// - **Index**: Position of the interval in the original collection
211    /// - **WorkInterval**: Complete interval data with timing information
212    ///
213    /// This detailed information enables:
214    /// - Specific analysis of each fragmented period
215    /// - Identification of patterns in interruption timing
216    /// - Targeted recommendations for specific intervals
217    pub intervals: Vec<(usize, WorkInterval)>, // (index, interval)
218
219    /// Indices of pauses that could be removed to merge short intervals.
220    ///
221    /// These pause indices represent optimization opportunities where
222    /// removing or consolidating breaks could create longer, more
223    /// productive work intervals. The indices correspond to positions
224    /// in the original pause collection.
225    ///
226    /// ## Optimization Strategy
227    /// - **Pause Removal**: Eliminate unnecessary short breaks
228    /// - **Pause Consolidation**: Combine multiple short breaks into fewer, longer ones
229    /// - **Timing Adjustment**: Shift break timing to create better work blocks
230    ///
231    /// ## Implementation Notes
232    /// To remove a short interval, remove the pause that created it by
233    /// separating it from the previous interval. This effectively merges
234    /// the short interval with its predecessor.
235    pub pauses_to_remove: Vec<usize>, // Indices of pauses that create short intervals
236}
237
238/// Calculates work intervals for a given workday based on pause records.
239///
240/// This function performs the core algorithm for converting raw workday and
241/// pause data into a structured collection of work intervals. It handles the
242/// complexity of time calculations, pause filtering, and interval boundary
243/// determination to produce accurate work period analysis.
244///
245/// ## Algorithm Overview
246///
247/// 1. **Initialization**: Start with workday boundaries and empty interval list
248/// 2. **Pause Filtering**: Remove incomplete pauses and sort chronologically
249/// 3. **Interval Generation**: Create work periods between consecutive pauses
250/// 4. **Boundary Handling**: Handle workday start/end as interval boundaries
251/// 5. **Duration Calculation**: Compute accurate durations for each interval
252///
253/// ## Pause Processing
254///
255/// The function handles various pause scenarios:
256/// - **Complete Pauses**: Have both start and end times
257/// - **Incomplete Pauses**: Missing end times (filtered out)
258/// - **Overlapping Pauses**: Handled through chronological sorting
259/// - **Out-of-bounds Pauses**: Pauses outside workday boundaries
260///
261/// ## Edge Cases Handled
262///
263/// - **No Pauses**: Single interval covering entire workday
264/// - **Workday Boundaries**: Pauses at start/end of workday
265/// - **Consecutive Pauses**: Multiple pauses with no work time between
266/// - **Invalid Times**: Pauses with end time before start time
267///
268/// # Arguments
269///
270/// * `workday` - The workday record containing start and end times
271/// * `pauses` - Collection of pause records for the workday
272///
273/// # Returns
274///
275/// A vector of `WorkInterval` objects representing continuous work periods.
276///
277/// # Examples
278///
279/// ```rust
280/// use kasl::libs::report::calculate_work_intervals;
281/// use kasl::db::workdays::Workday;
282/// use kasl::libs::pause::Pause;
283///
284/// let workday = Workday {
285///     start: start_time,
286///     end: Some(end_time),
287///     // ... other fields
288/// };
289///
290/// let pauses = vec![
291///     Pause {
292///         id: 1,
293///         start: lunch_start,
294///         end: Some(lunch_end),
295///         duration: Some(lunch_duration),
296///     },
297///     // ... more pauses
298/// ];
299///
300/// let intervals = calculate_work_intervals(&workday, &pauses);
301/// println!("Generated {} work intervals", intervals.len());
302/// ```
303///
304/// # Performance Considerations
305///
306/// - **Time Complexity**: O(n log n) due to pause sorting
307/// - **Space Complexity**: O(n) for interval storage
308/// - **Memory Usage**: Minimal allocation during processing
309pub fn calculate_work_intervals(workday: &Workday, pauses: &[Pause]) -> Vec<WorkInterval> {
310    // Determine workday end time (current time if still ongoing)
311    let end_time = workday.end.unwrap_or_else(|| chrono::Local::now().naive_local());
312
313    // Initialize interval collection and current time tracker
314    let mut intervals = vec![];
315    let mut current_time = workday.start;
316
317    // Filter out incomplete pauses and sort chronologically
318    // Only pauses with both start and end times can create work intervals
319    let mut complete_pauses: Vec<(usize, &Pause)> = pauses.iter().enumerate().filter(|(_, pause)| pause.end.is_some()).collect();
320
321    // Sort pauses by start time to ensure chronological processing
322    complete_pauses.sort_by_key(|(_, pause)| pause.start);
323
324    // Process each pause to create work intervals
325    for (original_idx, pause) in complete_pauses {
326        // Create work interval before this pause (if there's time)
327        if current_time < pause.start {
328            intervals.push(WorkInterval {
329                start: current_time,
330                end: pause.start,
331                duration: pause.start - current_time,
332                pause_after: Some(original_idx),
333            });
334        }
335
336        // Move current time to the end of the pause
337        if let Some(pause_end) = pause.end {
338            current_time = pause_end;
339        }
340    }
341
342    // Add the final work interval after the last pause (if there's time)
343    if current_time < end_time {
344        intervals.push(WorkInterval {
345            start: current_time,
346            end: end_time,
347            duration: end_time - current_time,
348            pause_after: None, // No pause after the final interval
349        });
350    }
351
352    intervals
353}
354
355/// Analyzes work intervals to identify short periods that may indicate poor productivity.
356///
357/// This function performs comprehensive analysis of work intervals to identify
358/// periods that fall below the minimum duration threshold. It provides both
359/// statistical analysis and actionable optimization recommendations to help
360/// users improve their work patterns and productivity.
361///
362/// ## Analysis Process
363///
364/// 1. **Threshold Filtering**: Identify intervals shorter than minimum duration
365/// 2. **Statistical Calculation**: Compute total count and cumulative duration
366/// 3. **Optimization Analysis**: Identify pauses that could be removed
367/// 4. **Recommendation Generation**: Provide specific improvement suggestions
368///
369/// ## Optimization Logic
370///
371/// Short intervals are typically created by pauses that interrupt focused work:
372/// - **Pause Identification**: Find pauses that create short intervals
373/// - **Merge Opportunities**: Identify intervals that could be combined
374/// - **Impact Assessment**: Calculate potential productivity improvements
375///
376/// ## Return Value Analysis
377///
378/// - **Some(info)**: Short intervals detected, optimization possible
379/// - **None**: No short intervals found, work patterns are optimal
380///
381/// # Arguments
382///
383/// * `intervals` - Collection of work intervals to analyze
384/// * `min_minutes` - Minimum acceptable interval duration in minutes
385///
386/// # Returns
387///
388/// `Some(ShortIntervalsInfo)` if short intervals are found, `None` otherwise.
389///
390/// # Examples
391///
392/// ```rust
393/// use kasl::libs::report::{analyze_short_intervals, WorkInterval};
394///
395/// let intervals = vec![/* work intervals */];
396/// let min_duration = 30; // 30-minute minimum
397///
398/// match analyze_short_intervals(&intervals, min_duration) {
399///     Some(analysis) => {
400///         println!("Found {} short intervals", analysis.count);
401///         println!("Total fragmented time: {:?}", analysis.total_duration);
402///         println!("Optimization: remove pauses {:?}", analysis.pauses_to_remove);
403///     },
404///     None => {
405///         println!("No short intervals detected - work patterns are optimal");
406///     }
407/// }
408/// ```
409///
410/// # Optimization Recommendations
411///
412/// The function provides specific recommendations:
413/// - **Pause Removal**: Eliminate unnecessary short breaks
414/// - **Break Consolidation**: Combine multiple short breaks
415/// - **Timing Adjustment**: Reschedule breaks to preserve focus periods
416pub fn analyze_short_intervals(intervals: &[WorkInterval], min_minutes: u64) -> Option<ShortIntervalsInfo> {
417    // Collect all intervals that fall below the minimum duration threshold
418    let mut short_intervals = Vec::new();
419    let mut total_duration = Duration::zero();
420    let mut pauses_to_remove = Vec::new();
421
422    // Analyze each interval for duration and optimization opportunities
423    for (idx, interval) in intervals.iter().enumerate() {
424        if interval.is_short(min_minutes) {
425            // Record this short interval for analysis
426            short_intervals.push((idx, interval.clone()));
427            total_duration = total_duration + interval.duration;
428
429            // Identify optimization opportunity: remove the pause that created this interval
430            // To remove a short interval, we need to remove the pause before it
431            // (which connects it to the previous interval)
432            if idx > 0 {
433                // Get the pause that created this interval by ending the previous one
434                if let Some(pause_idx) = intervals[idx - 1].pause_after {
435                    pauses_to_remove.push(pause_idx);
436                }
437            }
438        }
439    }
440
441    // Return analysis results only if short intervals were found
442    if short_intervals.is_empty() {
443        None
444    } else {
445        Some(ShortIntervalsInfo {
446            count: short_intervals.len(),
447            total_duration,
448            intervals: short_intervals,
449            pauses_to_remove,
450        })
451    }
452}
453
454/// Filters out short work intervals from the provided interval list.
455///
456/// This function removes work intervals that are shorter than the specified
457/// minimum duration, providing cleaner reporting by eliminating brief
458/// interruptions that don't represent meaningful work periods. This is the
459/// new approach for handling short intervals - filtering at display time
460/// instead of modifying the database.
461///
462/// ## Filtering Logic
463///
464/// - Intervals shorter than `min_minutes` are excluded from the result
465/// - Remaining intervals maintain their original timing and properties
466/// - No database changes are made - this is purely a display/API filter
467/// - Used by both `kasl report` (display) and `kasl report --send` (API submission)
468///
469/// ## Return Value
470///
471/// Returns a tuple containing:
472/// - **Filtered intervals**: Only intervals meeting the minimum duration
473/// - **Filtered intervals info**: Analysis of what was filtered out (if any)
474///
475/// # Arguments
476///
477/// * `intervals` - Original work intervals to filter
478/// * `min_minutes` - Minimum duration in minutes for intervals to keep
479///
480/// # Returns
481///
482/// Returns `(filtered_intervals, filtered_info)` where:
483/// - `filtered_intervals` contains only intervals >= min_minutes
484/// - `filtered_info` contains details about filtered intervals (None if nothing was filtered)
485///
486/// # Examples
487///
488/// ```rust
489/// use kasl::libs::report::filter_short_intervals;
490///
491/// let intervals = calculate_work_intervals(&workday, &pauses);
492/// let (filtered, info) = filter_short_intervals(&intervals, 30);
493///
494/// if let Some(info) = info {
495///     println!("Filtered {} short intervals", info.count);
496/// }
497/// ```
498pub fn filter_short_intervals(intervals: &[WorkInterval], min_minutes: u64) -> (Vec<WorkInterval>, Option<ShortIntervalsInfo>) {
499    let mut filtered_intervals = Vec::new();
500    let mut short_intervals = Vec::new();
501    let mut total_duration = Duration::zero();
502
503    for (idx, interval) in intervals.iter().enumerate() {
504        if interval.is_short(min_minutes) {
505            // This is a short interval - add to filtered list
506            short_intervals.push((idx, interval.clone()));
507            total_duration = total_duration + interval.duration;
508        } else {
509            // This interval meets minimum duration - keep it
510            filtered_intervals.push(interval.clone());
511        }
512    }
513
514    let filtered_info = if short_intervals.is_empty() {
515        None
516    } else {
517        Some(ShortIntervalsInfo {
518            count: short_intervals.len(),
519            total_duration,
520            intervals: short_intervals,
521            pauses_to_remove: Vec::new(), // Not needed for display filtering
522        })
523    };
524
525    (filtered_intervals, filtered_info)
526}
527
528/// Process daily work report data using pre-calculated intervals.
529///
530/// This function handles the data processing for daily work reports, calculating
531/// productivity metrics and work durations. It leverages the centralized `Productivity`
532/// module for consistent calculations across the application.
533///
534/// ## Calculation Method
535///
536/// The function uses two different approaches for different metrics:
537/// - **Filtered Duration**: Summed directly from provided intervals (for display purposes)
538/// - **Productivity**: Calculated using the comprehensive `Productivity::calculate_productivity()` 
539///   method which properly handles all pause types, breaks, and overlaps
540///
541/// This separation allows for interval-based filtering (for clean reports) while maintaining
542/// accurate productivity calculations that account for all time categories.
543///
544/// ## Data Consistency
545///
546/// By using `Productivity::new()`, this function automatically:
547/// - Loads the same data used throughout the application
548/// - Applies consistent calculation logic
549/// - Handles all edge cases and data integrity issues
550///
551/// # Arguments
552///
553/// * `workday` - The workday record containing start/end times
554/// * `intervals` - Pre-calculated and optionally filtered work intervals for duration calculation
555///
556/// # Returns
557///
558/// Returns a tuple containing:
559/// - **Filtered Duration**: Sum of provided work intervals (may exclude short intervals)
560/// - **Productivity**: Comprehensive productivity percentage using centralized calculation
561///
562/// # Examples
563///
564/// ```rust
565/// let (duration, productivity) = report_with_intervals(&workday, &filtered_intervals)?;
566/// println!("Work time: {}, Productivity: {:.1}%", format_duration(duration), productivity);
567/// ```
568pub fn report_with_intervals(
569    workday: &Workday,
570    intervals: &[WorkInterval]
571) -> Result<(Duration, f64)> {
572    // Calculate filtered duration based on provided intervals (for display purposes)
573    let filtered_duration = intervals.iter().fold(Duration::zero(), |acc, interval| acc + interval.duration);
574
575    // Use centralized productivity module for consistent, comprehensive calculation
576    let productivity = Productivity::new(&workday)?.calculate_productivity();
577
578    Ok((filtered_duration, productivity))
579}
580
581/// Combines breaks and pauses into a unified collection for work interval calculation.
582///
583/// This function creates a combined list of all work interruptions (both manual breaks
584/// and automatic pauses) to ensure that work intervals are calculated accurately,
585/// accounting for all types of non-work time.
586///
587/// ## Data Integration
588///
589/// - **Manual Breaks**: User-defined break periods from the breaks table
590/// - **Automatic Pauses**: System-detected pauses from the pauses table
591/// - **Unified Format**: Both are converted to a common Pause-like structure
592///
593/// ## Temporal Ordering
594///
595/// The combined collection is sorted chronologically to ensure proper
596/// work interval calculation when multiple types of interruptions occur
597/// throughout the workday.
598///
599/// # Arguments
600///
601/// * `breaks` - Manual breaks from the breaks database table
602/// * `pauses` - Automatic pauses from the pauses database table
603///
604/// # Returns
605///
606/// A vector of Pause objects representing all work interruptions,
607/// sorted chronologically by start time.
608///
609/// # Examples
610///
611/// ```rust
612/// use kasl::libs::report::combine_breaks_and_pauses;
613/// 
614/// let breaks = breaks_db.get_daily_breaks(date)?;
615/// let pauses = pauses_db.get_daily_pauses(date)?;
616/// let combined = combine_breaks_and_pauses(&breaks, &pauses);
617/// 
618/// let intervals = calculate_work_intervals(&workday, &combined);
619/// ```
620pub fn combine_breaks_and_pauses(
621    breaks: &[crate::db::breaks::Break], 
622    pauses: &[crate::libs::pause::Pause]
623) -> Vec<crate::libs::pause::Pause> {
624    let mut combined = Vec::new();
625    
626    // Add existing pauses
627    combined.extend_from_slice(pauses);
628    
629    // Convert breaks to pause format and add them
630    let mut next_id = pauses.iter().map(|p| p.id).max().unwrap_or(0) + 1000; // Use high IDs to avoid conflicts
631    
632    for break_record in breaks {
633        combined.push(crate::libs::pause::Pause {
634            id: next_id,
635            start: break_record.start,
636            end: Some(break_record.end),
637            duration: Some(break_record.duration),
638        });
639        next_id += 1;
640    }
641    
642    // Sort by start time for proper interval calculation
643    combined.sort_by_key(|item| item.start);
644    
645    combined
646}