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 += 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 += 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(workday: &Workday, intervals: &[WorkInterval]) -> Result<(Duration, f64)> {
569 // Calculate filtered duration based on provided intervals (for display purposes)
570 let filtered_duration = intervals.iter().fold(Duration::zero(), |acc, interval| acc + interval.duration);
571
572 // Use centralized productivity module for consistent, comprehensive calculation
573 let productivity = Productivity::new(workday)?.calculate_productivity();
574
575 Ok((filtered_duration, productivity))
576}
577
578/// Combines breaks and pauses into a unified collection for work interval calculation.
579///
580/// This function creates a combined list of all work interruptions (both manual breaks
581/// and automatic pauses) to ensure that work intervals are calculated accurately,
582/// accounting for all types of non-work time.
583///
584/// ## Data Integration
585///
586/// - **Manual Breaks**: User-defined break periods from the breaks table
587/// - **Automatic Pauses**: System-detected pauses from the pauses table
588/// - **Unified Format**: Both are converted to a common Pause-like structure
589///
590/// ## Temporal Ordering
591///
592/// The combined collection is sorted chronologically to ensure proper
593/// work interval calculation when multiple types of interruptions occur
594/// throughout the workday.
595///
596/// # Arguments
597///
598/// * `breaks` - Manual breaks from the breaks database table
599/// * `pauses` - Automatic pauses from the pauses database table
600///
601/// # Returns
602///
603/// A vector of Pause objects representing all work interruptions,
604/// sorted chronologically by start time.
605///
606/// # Examples
607///
608/// ```rust
609/// use kasl::libs::report::combine_breaks_and_pauses;
610///
611/// let breaks = breaks_db.get_daily_breaks(date)?;
612/// let pauses = pauses_db.get_daily_pauses(date)?;
613/// let combined = combine_breaks_and_pauses(&breaks, &pauses);
614///
615/// let intervals = calculate_work_intervals(&workday, &combined);
616/// ```
617pub fn combine_breaks_and_pauses(breaks: &[crate::db::breaks::Break], pauses: &[crate::libs::pause::Pause]) -> Vec<crate::libs::pause::Pause> {
618 let mut combined = Vec::new();
619
620 // Add existing pauses
621 combined.extend_from_slice(pauses);
622
623 // Convert breaks to pause format and add them; high IDs avoid conflicts
624 let first_id = pauses.iter().map(|p| p.id).max().unwrap_or(0) + 1000;
625 combined.extend(breaks.iter().enumerate().map(|(i, break_record)| crate::libs::pause::Pause {
626 id: first_id + i as i32,
627 start: break_record.start,
628 end: Some(break_record.end),
629 duration: Some(break_record.duration),
630 }));
631
632 // Sort by start time for proper interval calculation
633 combined.sort_by_key(|item| item.start);
634
635 combined
636}