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