Skip to main content

kasl/commands/
sum.rs

1//! Monthly working hours summary command.
2//!
3//! Generates comprehensive monthly reports showing daily work hours, productivity metrics, and calendar integration with company rest days.
4//!
5//! ## Features
6//!
7//! - **Monthly Overview**: Detailed daily breakdowns with productivity metrics
8//! - **Calendar Integration**: Automatic handling of company rest days and weekends
9//! - **Productivity Analysis**: Per-day productivity calculations using centralized engine
10//! - **Report Submission**: Optional submission to external reporting systems
11//! - **Aggregate Statistics**: Total hours, working days, and average calculations
12//!
13//! ## Usage
14//!
15//! ```bash
16//! # Generate current month summary
17//! kasl sum
18//!
19//! # Submit monthly report to external API
20//! kasl sum --send
21//! ```
22
23use crate::{
24    api::si::Si,
25    db::{pauses::Pauses, workdays::Workdays},
26    libs::{
27        config::Config,
28        messages::Message,
29        summary::{DailySummary, SummaryCalculator, SummaryFormatter},
30        view::View,
31    },
32    msg_error, msg_print,
33};
34use anyhow::Result;
35use chrono::{Datelike, Duration, Local, NaiveDate};
36use clap::Args;
37use std::collections::HashSet;
38
39/// Command-line arguments for the monthly summary command.
40///
41/// Currently supports basic summary generation with optional report submission.
42/// Future versions may add date range selection and detailed filtering options.
43#[derive(Debug, Args)]
44pub struct SumArgs {
45    /// Submit the monthly summary report
46    ///
47    /// When specified, the generated summary will be submitted to the configured
48    /// reporting API in addition to being displayed locally. This is useful for
49    /// organizational reporting requirements.
50    #[arg(long, help = "Send report")]
51    send: bool,
52}
53
54/// Generates and displays a comprehensive monthly working hours summary.
55///
56/// Creates a detailed analysis of work patterns for the current month, including
57/// productivity calculations, rest day integration, and daily breakdowns.
58///
59/// # Arguments
60///
61/// * `_sum_args` - Command arguments (currently unused but reserved for future features)
62///
63/// # Returns
64///
65/// Returns `Ok(())` on successful summary generation and display, or an error
66/// if data retrieval or calculation fails.
67/// - API connectivity issues (for rest dates)
68/// - Configuration errors
69/// - Invalid date calculations
70/// - Missing workday data
71pub async fn cmd(_sum_args: SumArgs) -> Result<()> {
72    let now = Local::now();
73    let config = Config::read()?;
74    let monitor_config = config.monitor.clone().unwrap_or_default();
75
76    // Display header with current month and year
77    msg_print!(Message::WorkingHoursForMonth(now.format("%B, %Y").to_string()), true);
78
79    // Step 1: Fetch company rest dates from external API if configured
80    let mut rest_dates: HashSet<NaiveDate> = HashSet::new();
81    if let Some(si_config) = config.si {
82        match Si::new(&si_config).rest_dates(now.date_naive()).await {
83            Ok(dates) => {
84                // Filter rest dates to only include current month
85                rest_dates = dates.into_iter().filter(|d| d.month() == now.month()).collect();
86            }
87            Err(e) => {
88                // Log error but continue with local data only
89                msg_error!(Message::ErrorRequestingRestDates(e.to_string()));
90            }
91        }
92    }
93
94    // Step 2: Fetch all workdays for the current month from local database
95    let workdays = Workdays::new()?.fetch_month(now.date_naive())?;
96    let workdays_count = workdays.len() as f64;
97    let mut daily_summaries = Vec::new();
98    let mut total_productivity = 0.0;
99
100    // Step 3: Process each workday to calculate durations and productivity
101    for workday in workdays {
102        // Now only while the day is still today; an unclosed past day ends at
103        // its last observed activity - see report::workday_end_time.
104        let workday_pauses = Pauses::new()?.get_workday_pauses(&workday)?;
105        let end_time = crate::libs::report::workday_end_time(&workday, &workday_pauses);
106        let gross_duration = end_time.signed_duration_since(workday.start);
107
108        // Note: All pauses data now handled by Productivity module
109
110        // Fetch filtered long breaks for display purposes
111        let long_breaks_duration = Pauses::new()?
112            .set_min_duration(monitor_config.min_pause_duration)
113            .get_workday_pauses(&workday)?
114            .iter()
115            .filter_map(|b| b.duration)
116            .fold(Duration::zero(), |acc, d| acc + d);
117
118        // Calculate display duration (gross time minus long breaks only)
119        let gross_work_time_minus_long_breaks = gross_duration - long_breaks_duration;
120
121        // Note: Net working duration calculation now handled by Productivity module
122
123        // Calculate productivity using centralized module for consistency
124        // This uses the same comprehensive calculation logic used throughout the app
125        let productivity = crate::libs::productivity::Productivity::new(&workday)
126            .map(|p| p.calculate_productivity())
127            .unwrap_or(0.0);
128
129        // Accumulate productivity for monthly average calculation
130        total_productivity += productivity;
131
132        // Create daily summary entry
133        daily_summaries.push(DailySummary {
134            date: workday.date,
135            duration: gross_work_time_minus_long_breaks, // Display duration
136            productivity,
137        });
138    }
139
140    // Step 4: Integrate rest dates and calculate summary statistics
141    let event_summary = daily_summaries
142        .add_rest_dates(rest_dates, Duration::hours(8)) // Default 8 hours for rest days
143        .calculate_totals()
144        .format_summary();
145
146    // Step 5: Display the formatted summary table
147    View::sum(&event_summary)?;
148
149    // Step 6: Display monthly productivity average
150    if total_productivity > 0.0 && workdays_count > 0.0 {
151        let average_productivity = total_productivity / workdays_count;
152        msg_print!(Message::MonthlyProductivity(average_productivity), true);
153    }
154
155    Ok(())
156}