use crate::{
api::si::Si,
db::{
pauses::Pauses,
tasks::Tasks,
workdays::{Workday, Workdays},
},
libs::{
config::Config,
formatter::format_duration,
messages::Message,
productivity::Productivity,
report,
task::{FormatTasks, Task, TaskFilter},
view::View,
},
msg_error, msg_error_anyhow, msg_info, msg_print,
};
use anyhow::Result;
use chrono::{DateTime, Duration, Local};
use clap::Args;
use serde_json::json;
#[derive(Debug, Args)]
pub struct ReportArgs {
#[arg(long, help = "Submit daily report")]
send: bool,
#[arg(long, short, help = "Generate report for the last day")]
last: bool,
#[arg(long, help = "Submit monthly report")]
month: bool,
}
pub async fn cmd(args: ReportArgs) -> Result<()> {
let date = determine_report_date(args.last);
if args.month {
handle_monthly_report(date).await
} else {
handle_daily_report(args.send, date).await
}
}
fn determine_report_date(is_last_day: bool) -> DateTime<Local> {
if is_last_day { Local::now() - Duration::days(1) } else { Local::now() }
}
async fn handle_daily_report(should_send: bool, date: DateTime<Local>) -> Result<()> {
if should_send {
send_daily_report(date).await
} else {
display_daily_report(date).await
}
}
async fn handle_monthly_report(date: DateTime<Local>) -> Result<()> {
let mut si = get_si_service()?;
let naive_date = date.date_naive();
match si.send_monthly(&naive_date).await {
Ok(status) => {
if status.is_success() {
msg_info!(Message::MonthlyReportSent(date.format("%B %-d, %Y").to_string()));
} else {
msg_error!(Message::MonthlyReportSendFailed(status.to_string()));
}
}
Err(e) => msg_error!(Message::ErrorSendingMonthlyReport(e.to_string())),
}
Ok(())
}
async fn display_daily_report(date: DateTime<Local>) -> Result<()> {
let naive_date = date.date_naive();
let workday = match Workdays::new()?.fetch(naive_date)? {
Some(wd) => wd,
None => {
msg_print!(Message::WorkdayNotFoundForDate(date.format("%B %-d, %Y").to_string()), true);
return Ok(());
}
};
let tasks = Tasks::new()?.fetch(TaskFilter::Date(naive_date))?;
let config = Config::read()?;
let monitor_config = config.monitor.as_ref().cloned().unwrap_or_default();
let long_pauses = Pauses::new()?
.set_min_duration(monitor_config.min_pause_duration)
.get_workday_pauses(&workday)?;
let intervals = report::calculate_work_intervals(&workday, &long_pauses);
let (filtered_intervals, filtered_info) = report::filter_short_intervals(&intervals, monitor_config.min_work_interval);
let (filtered_duration, productivity) = report::report_with_intervals(&workday, &intervals)?;
View::report(&workday, &filtered_intervals, &filtered_duration, &productivity, &tasks)?;
if let Some(info) = filtered_info {
msg_info!(format!(
"Filtered out {} short intervals (total: {})",
info.count,
format_duration(&info.total_duration)
));
}
let productivity = Productivity::new(&workday)?;
if productivity.is_below_threshold() {
msg_error!(Message::LowProductivityWarning {
current: productivity.calculate_productivity(),
threshold: productivity.config.min_productivity_threshold,
});
}
Ok(())
}
async fn send_daily_report(date: DateTime<Local>) -> Result<()> {
let naive_date = date.date_naive();
let mut workdays_db = Workdays::new()?;
workdays_db.insert_end(naive_date)?;
let workday = workdays_db
.fetch(naive_date)?
.ok_or_else(|| msg_error_anyhow!(Message::WorkdayCouldNotFindAfterFinalizing(naive_date.to_string())))?;
let mut tasks = Tasks::new()?.fetch(TaskFilter::Date(naive_date))?;
if tasks.is_empty() {
msg_error!(Message::TasksNotFoundForDate(date.format("%B %-d, %Y").to_string()));
return Ok(());
}
let config = Config::read()?;
let monitor_config = config.monitor.as_ref().cloned().unwrap_or_default();
let long_pauses = Pauses::new()?
.set_min_duration(monitor_config.min_pause_duration)
.get_workday_pauses(&workday)?;
let productivity = Productivity::new(&workday)?;
let current_productivity = productivity.calculate_productivity();
if current_productivity < productivity.config.min_productivity_threshold {
msg_error!(Message::ProductivityTooLowToSend {
current: current_productivity,
threshold: productivity.config.min_productivity_threshold,
});
return Ok(());
}
let intervals = report::calculate_work_intervals(&workday, &long_pauses);
let (filtered_intervals, _) = report::filter_short_intervals(&intervals, monitor_config.min_work_interval);
let report_json = build_report_payload(&workday, &mut tasks, &filtered_intervals);
let events_json = serde_json::to_string(&report_json)?;
let mut si = get_si_service()?;
match si.send(&events_json, &naive_date).await {
Ok(status) => {
if status.is_success() {
msg_info!(Message::DailyReportSent(date.format("%B %-d, %Y").to_string()));
if si.is_last_working_day_of_month(&naive_date)? {
msg_info!(Message::MonthlyReportTriggered);
handle_monthly_report(date).await?;
}
} else {
msg_error!(Message::ReportSendFailed(status.to_string()));
}
}
Err(e) => msg_error!(Message::ErrorSendingEvents(e.to_string())),
}
Ok(())
}
fn build_report_payload(_workday: &Workday, tasks: &mut [Task], intervals: &[report::WorkInterval]) -> serde_json::Value {
let num_tasks = tasks.len();
let num_intervals = intervals.len();
if num_intervals == 0 {
return json!([]);
}
let mut report_items = Vec::new();
if num_tasks >= num_intervals {
let mut task_iter = tasks.iter();
let base_tasks_per_interval = num_tasks / num_intervals;
let mut extra_tasks = num_tasks % num_intervals;
for (i, interval) in intervals.iter().enumerate() {
let count = base_tasks_per_interval + if extra_tasks > 0 { 1 } else { 0 };
extra_tasks = extra_tasks.saturating_sub(1);
let mut assigned_tasks: Vec<Task> = task_iter.by_ref().take(count).cloned().collect();
report_items.push(json!({
"from": interval.start.format("%H:%M").to_string(),
"index": i + 1,
"result": "",
"task": assigned_tasks.format(),
"time": "",
"to": interval.end.format("%H:%M").to_string(),
"total_ts": format_duration(&interval.duration)
}));
}
} else {
let mut interval_iter = intervals.iter();
let base_intervals_per_task = num_intervals / num_tasks;
let mut extra_intervals = num_intervals % num_tasks;
for task in tasks.iter() {
let count = base_intervals_per_task + if extra_intervals > 0 { 1 } else { 0 };
extra_intervals = extra_intervals.saturating_sub(1);
for _ in 0..count {
if let Some(interval) = interval_iter.next() {
let index = report_items.len() + 1;
report_items.push(json!({
"from": interval.start.format("%H:%M").to_string(),
"index": index,
"result": "",
"task": vec![task.clone()].format(),
"time": "",
"to": interval.end.format("%H:%M").to_string(),
"total_ts": format_duration(&interval.duration)
}));
}
}
}
}
json!(report_items)
}
fn get_si_service() -> Result<Si> {
Config::read()?
.si
.map(|si_config| Si::new(&si_config))
.ok_or_else(|| msg_error_anyhow!(Message::SiServerConfigNotFound))
}