1use crate::{
9 api::si::Si,
10 db::{
11 pauses::Pauses,
12 tasks::Tasks,
13 workdays::{Workday, Workdays},
14 },
15 libs::{
16 config::Config,
17 formatter::format_duration,
18 messages::Message,
19 productivity::Productivity,
20 report,
21 task::{FormatTasks, Task, TaskFilter},
22 view::View,
23 },
24 msg_error, msg_error_anyhow, msg_info, msg_print,
25};
26use anyhow::Result;
27use chrono::{DateTime, Duration, Local};
28use clap::Args;
29use serde_json::json;
30
31#[derive(Debug, Args)]
36pub struct ReportArgs {
37 #[arg(long, help = "Submit daily report")]
43 send: bool,
44
45 #[arg(long, short, help = "Generate report for the last day")]
52 last: bool,
53
54 #[arg(long, help = "Submit monthly report")]
60 month: bool,
61}
62
63pub async fn cmd(args: ReportArgs) -> Result<()> {
89 let date = determine_report_date(args.last);
90
91 if args.month {
92 handle_monthly_report(date).await
93 } else {
94 handle_daily_report(args.send, date).await
95 }
96}
97
98fn determine_report_date(is_last_day: bool) -> DateTime<Local> {
104 if is_last_day { Local::now() - Duration::days(1) } else { Local::now() }
105}
106
107async fn handle_daily_report(should_send: bool, date: DateTime<Local>) -> Result<()> {
113 if should_send {
114 send_daily_report(date).await
115 } else {
116 display_daily_report(date).await
117 }
118}
119
120async fn handle_monthly_report(date: DateTime<Local>) -> Result<()> {
126 let mut si = get_si_service()?;
127 let naive_date = date.date_naive();
128
129 match si.send_monthly(&naive_date).await {
130 Ok(status) => {
131 if status.is_success() {
132 msg_info!(Message::MonthlyReportSent(date.format("%B %-d, %Y").to_string()));
133 } else {
134 msg_error!(Message::MonthlyReportSendFailed(status.to_string()));
135 }
136 }
137 Err(e) => msg_error!(Message::ErrorSendingMonthlyReport(e.to_string())),
138 }
139
140 Ok(())
141}
142
143async fn display_daily_report(date: DateTime<Local>) -> Result<()> {
156 let naive_date = date.date_naive();
157 let workday = match Workdays::new()?.fetch(naive_date)? {
158 Some(wd) => wd,
159 None => {
160 msg_print!(Message::WorkdayNotFoundForDate(date.format("%B %-d, %Y").to_string()), true);
161 return Ok(());
162 }
163 };
164
165 let tasks = Tasks::new()?.fetch(TaskFilter::Date(naive_date))?;
166 let config = Config::read()?;
167 let monitor_config = config.monitor.as_ref().cloned().unwrap_or_default();
168
169 let long_pauses = Pauses::new()?
172 .set_min_duration(monitor_config.min_pause_duration)
173 .get_workday_pauses(&workday)?;
174
175 let intervals = report::calculate_work_intervals(&workday, &long_pauses);
177 let (filtered_intervals, filtered_info) = report::filter_short_intervals(&intervals, monitor_config.min_work_interval);
178
179 let (filtered_duration, productivity) = report::report_with_intervals(&workday, &intervals)?;
181
182 View::report(&workday, &filtered_intervals, &filtered_duration, &productivity, &tasks)?;
184
185 if let Some(info) = filtered_info {
187 msg_info!(format!(
188 "Filtered out {} short intervals (total: {})",
189 info.count,
190 format_duration(&info.total_duration)
191 ));
192 }
193
194 let productivity = Productivity::new(&workday)?;
196 if productivity.is_below_threshold() {
197 msg_error!(Message::LowProductivityWarning {
198 current: productivity.calculate_productivity(),
199 threshold: productivity.config.min_productivity_threshold,
200 });
201 }
202
203 Ok(())
204}
205
206async fn send_daily_report(date: DateTime<Local>) -> Result<()> {
208 let naive_date = date.date_naive();
209 let mut workdays_db = Workdays::new()?;
210
211 workdays_db.insert_end(naive_date)?;
213
214 let workday = workdays_db
216 .fetch(naive_date)?
217 .ok_or_else(|| msg_error_anyhow!(Message::WorkdayCouldNotFindAfterFinalizing(naive_date.to_string())))?;
218
219 let mut tasks = Tasks::new()?.fetch(TaskFilter::Date(naive_date))?;
221 if tasks.is_empty() {
222 msg_error!(Message::TasksNotFoundForDate(date.format("%B %-d, %Y").to_string()));
223 return Ok(());
224 }
225
226 let config = Config::read()?;
227 let monitor_config = config.monitor.as_ref().cloned().unwrap_or_default();
228
229 let long_pauses = Pauses::new()?
232 .set_min_duration(monitor_config.min_pause_duration)
233 .get_workday_pauses(&workday)?;
234
235 let productivity = Productivity::new(&workday)?;
238 let current_productivity = productivity.calculate_productivity();
239 if current_productivity < productivity.config.min_productivity_threshold {
240 msg_error!(Message::ProductivityTooLowToSend {
241 current: current_productivity,
242 threshold: productivity.config.min_productivity_threshold,
243 });
244 return Ok(());
245 }
246
247 let intervals = report::calculate_work_intervals(&workday, &long_pauses);
249 let (filtered_intervals, _) = report::filter_short_intervals(&intervals, monitor_config.min_work_interval);
250
251 let report_json = build_report_payload(&workday, &mut tasks, &filtered_intervals);
253 let events_json = serde_json::to_string(&report_json)?;
254 let mut si = get_si_service()?;
255
256 match si.send(&events_json, &naive_date).await {
258 Ok(status) => {
259 if status.is_success() {
260 msg_info!(Message::DailyReportSent(date.format("%B %-d, %Y").to_string()));
261
262 if si.is_last_working_day_of_month(&naive_date)? {
264 msg_info!(Message::MonthlyReportTriggered);
265 handle_monthly_report(date).await?;
266 }
267 } else {
268 msg_error!(Message::ReportSendFailed(status.to_string()));
269 }
270 }
271 Err(e) => msg_error!(Message::ErrorSendingEvents(e.to_string())),
272 }
273
274 Ok(())
275}
276
277fn build_report_payload(_workday: &Workday, tasks: &mut [Task], intervals: &[report::WorkInterval]) -> serde_json::Value {
279 let num_tasks = tasks.len();
280 let num_intervals = intervals.len();
281
282 if num_intervals == 0 {
284 return json!([]);
285 }
286
287 let mut report_items = Vec::new();
288
289 if num_tasks >= num_intervals {
291 let mut task_iter = tasks.iter();
293 let base_tasks_per_interval = num_tasks / num_intervals;
294 let mut extra_tasks = num_tasks % num_intervals;
295
296 for (i, interval) in intervals.iter().enumerate() {
297 let count = base_tasks_per_interval + if extra_tasks > 0 { 1 } else { 0 };
299 extra_tasks = extra_tasks.saturating_sub(1);
300
301 let mut assigned_tasks: Vec<Task> = task_iter.by_ref().take(count).cloned().collect();
303
304 report_items.push(json!({
305 "from": interval.start.format("%H:%M").to_string(),
306 "index": i + 1,
307 "result": "",
308 "task": assigned_tasks.format(),
309 "time": "",
310 "to": interval.end.format("%H:%M").to_string(),
311 "total_ts": format_duration(&interval.duration)
312 }));
313 }
314 } else {
315 let mut interval_iter = intervals.iter();
317 let base_intervals_per_task = num_intervals / num_tasks;
318 let mut extra_intervals = num_intervals % num_tasks;
319
320 for task in tasks.iter() {
321 let count = base_intervals_per_task + if extra_intervals > 0 { 1 } else { 0 };
323 extra_intervals = extra_intervals.saturating_sub(1);
324
325 for _ in 0..count {
327 if let Some(interval) = interval_iter.next() {
328 let index = report_items.len() + 1;
329 report_items.push(json!({
330 "from": interval.start.format("%H:%M").to_string(),
331 "index": index,
332 "result": "",
333 "task": vec![task.clone()].format(),
334 "time": "",
335 "to": interval.end.format("%H:%M").to_string(),
336 "total_ts": format_duration(&interval.duration)
337 }));
338 }
339 }
340 }
341 }
342
343 json!(report_items)
344}
345
346fn get_si_service() -> Result<Si> {
352 Config::read()?
353 .si
354 .map(|si_config| Si::new(&si_config))
355 .ok_or_else(|| msg_error_anyhow!(Message::SiServerConfigNotFound))
356}