1use crate::db::models::{DateRange, Event, Report, ReportSummary, StatusBreakdown, Task};
2use crate::error::Result;
3use chrono::Utc;
4use sqlx::SqlitePool;
5
6pub struct ReportManager<'a> {
7 pool: &'a SqlitePool,
8}
9
10impl<'a> ReportManager<'a> {
11 pub fn new(pool: &'a SqlitePool) -> Self {
12 Self { pool }
13 }
14
15 pub async fn generate_report(
17 &self,
18 since: Option<String>,
19 status: Option<String>,
20 filter_name: Option<String>,
21 filter_spec: Option<String>,
22 summary_only: bool,
23 ) -> Result<Report> {
24 let since_datetime = since.and_then(|s| crate::time_utils::parse_duration(&s).ok());
26
27 let mut task_query = String::from("SELECT id FROM tasks WHERE 1=1");
29 let mut task_conditions = Vec::new();
30
31 if let Some(ref status) = status {
32 task_query.push_str(" AND status = ?");
33 task_conditions.push(status.clone());
34 }
35
36 if let Some(ref dt) = since_datetime {
37 task_query.push_str(" AND first_todo_at >= ?");
38 task_conditions.push(dt.to_rfc3339());
39 }
40
41 let task_ids = if filter_name.is_some() || filter_spec.is_some() {
43 self.filter_tasks_by_fts(&filter_name, &filter_spec).await?
44 } else {
45 Vec::new()
46 };
47
48 let tasks = if !task_ids.is_empty() {
50 task_query.push_str(&format!(
51 " AND id IN ({})",
52 task_ids.iter().map(|_| "?").collect::<Vec<_>>().join(", ")
53 ));
54 let full_query = task_query.replace("SELECT id", "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata");
55 let mut q = sqlx::query_as::<_, Task>(&full_query);
56 for cond in &task_conditions {
57 q = q.bind(cond);
58 }
59 for id in &task_ids {
60 q = q.bind(id);
61 }
62 q.fetch_all(self.pool).await?
63 } else if filter_name.is_none() && filter_spec.is_none() {
64 let full_query = task_query.replace("SELECT id", "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata");
65 let mut q = sqlx::query_as::<_, Task>(&full_query);
66 for cond in &task_conditions {
67 q = q.bind(cond);
68 }
69 q.fetch_all(self.pool).await?
70 } else {
71 Vec::new()
72 };
73
74 let todo_count = tasks.iter().filter(|t| t.status == "todo").count() as i64;
76 let doing_count = tasks.iter().filter(|t| t.status == "doing").count() as i64;
77 let done_count = tasks.iter().filter(|t| t.status == "done").count() as i64;
78
79 let total_tasks = tasks.len() as i64;
80
81 let events = if !summary_only {
83 let mut event_query = String::from(crate::sql_constants::SELECT_EVENT_BASE);
84 let mut event_conditions = Vec::new();
85
86 if let Some(ref dt) = since_datetime {
87 event_query.push_str(" AND timestamp >= ?");
88 event_conditions.push(dt.to_rfc3339());
89 }
90
91 event_query.push_str(" ORDER BY timestamp DESC");
92
93 let mut q = sqlx::query_as::<_, Event>(&event_query);
94 for cond in &event_conditions {
95 q = q.bind(cond);
96 }
97
98 Some(q.fetch_all(self.pool).await?)
99 } else {
100 None
101 };
102
103 let total_events = if let Some(ref evts) = events {
104 evts.len() as i64
105 } else {
106 sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_EVENTS_TOTAL)
107 .fetch_one(self.pool)
108 .await?
109 };
110
111 let date_range = since_datetime.map(|from| DateRange {
112 from,
113 to: Utc::now(),
114 });
115
116 Ok(Report {
117 summary: ReportSummary {
118 total_tasks,
119 tasks_by_status: StatusBreakdown {
120 todo: todo_count,
121 doing: doing_count,
122 done: done_count,
123 },
124 total_events,
125 date_range,
126 },
127 tasks: if summary_only { None } else { Some(tasks) },
128 events,
129 })
130 }
131
132 async fn filter_tasks_by_fts(
134 &self,
135 filter_name: &Option<String>,
136 filter_spec: &Option<String>,
137 ) -> Result<Vec<i64>> {
138 let mut query = String::from("SELECT rowid FROM tasks_fts WHERE ");
139 let mut conditions = Vec::new();
140
141 if let Some(name_filter) = filter_name {
142 conditions.push(format!(
143 "name MATCH '{}'",
144 crate::search::escape_fts5(name_filter)
145 ));
146 }
147
148 if let Some(spec_filter) = filter_spec {
149 conditions.push(format!(
150 "spec MATCH '{}'",
151 crate::search::escape_fts5(spec_filter)
152 ));
153 }
154
155 if conditions.is_empty() {
156 return Ok(Vec::new());
157 }
158
159 query.push_str(&conditions.join(" AND "));
160
161 let ids: Vec<i64> = sqlx::query_scalar(&query).fetch_all(self.pool).await?;
162
163 Ok(ids)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::events::EventManager;
171 use crate::tasks::TaskManager;
172 use crate::test_utils::test_helpers::TestContext;
173
174 #[tokio::test]
175 async fn test_generate_report_summary_only() {
176 let ctx = TestContext::new().await;
177 let task_mgr = TaskManager::new(ctx.pool());
178 let report_mgr = ReportManager::new(ctx.pool());
179
180 task_mgr
182 .add_task("Todo task".to_string(), None, None, None, None, None)
183 .await
184 .unwrap();
185 let doing = task_mgr
186 .add_task("Doing task".to_string(), None, None, None, None, None)
187 .await
188 .unwrap();
189 task_mgr.start_task(doing.id, false).await.unwrap();
190 let done = task_mgr
191 .add_task("Done task".to_string(), None, None, None, None, None)
192 .await
193 .unwrap();
194 task_mgr.start_task(done.id, false).await.unwrap();
195 task_mgr.done_task(false).await.unwrap();
196
197 let report = report_mgr
198 .generate_report(None, None, None, None, true)
199 .await
200 .unwrap();
201
202 assert_eq!(report.summary.total_tasks, 3);
203 assert_eq!(report.summary.tasks_by_status.todo, 1);
204 assert_eq!(report.summary.tasks_by_status.doing, 1);
205 assert_eq!(report.summary.tasks_by_status.done, 1);
206 assert!(report.tasks.is_none());
207 assert!(report.events.is_none());
208 }
209
210 #[tokio::test]
211 async fn test_generate_report_full() {
212 let ctx = TestContext::new().await;
213 let task_mgr = TaskManager::new(ctx.pool());
214 let report_mgr = ReportManager::new(ctx.pool());
215
216 task_mgr
217 .add_task("Task 1".to_string(), None, None, None, None, None)
218 .await
219 .unwrap();
220 task_mgr
221 .add_task("Task 2".to_string(), None, None, None, None, None)
222 .await
223 .unwrap();
224
225 let report = report_mgr
226 .generate_report(None, None, None, None, false)
227 .await
228 .unwrap();
229
230 assert!(report.tasks.is_some());
231 assert_eq!(report.tasks.unwrap().len(), 2);
232 }
233
234 #[tokio::test]
235 async fn test_generate_report_filter_by_status() {
236 let ctx = TestContext::new().await;
237 let task_mgr = TaskManager::new(ctx.pool());
238 let report_mgr = ReportManager::new(ctx.pool());
239
240 task_mgr
241 .add_task("Todo task".to_string(), None, None, None, None, None)
242 .await
243 .unwrap();
244 let doing = task_mgr
245 .add_task("Doing task".to_string(), None, None, None, None, None)
246 .await
247 .unwrap();
248 task_mgr.start_task(doing.id, false).await.unwrap();
249
250 let report = report_mgr
251 .generate_report(None, Some("doing".to_string()), None, None, false)
252 .await
253 .unwrap();
254
255 let tasks = report.tasks.unwrap();
256 assert_eq!(tasks.len(), 1);
257 assert_eq!(tasks[0].status, "doing");
258 }
259
260 #[tokio::test]
261 async fn test_generate_report_with_events() {
262 let ctx = TestContext::new().await;
263 let task_mgr = TaskManager::new(ctx.pool());
264 let event_mgr = EventManager::new(ctx.pool());
265 let report_mgr = ReportManager::new(ctx.pool());
266
267 let task = task_mgr
268 .add_task("Task 1".to_string(), None, None, None, None, None)
269 .await
270 .unwrap();
271 event_mgr
272 .add_event(task.id, "decision".to_string(), "Test event".to_string())
273 .await
274 .unwrap();
275
276 let report = report_mgr
277 .generate_report(None, None, None, None, false)
278 .await
279 .unwrap();
280
281 assert!(report.events.is_some());
282 assert_eq!(report.summary.total_events, 1);
283 }
284
285 #[tokio::test]
286 async fn test_parse_duration_days() {
287 let result = crate::time_utils::parse_duration("7d").ok();
288 assert!(result.is_some());
289 }
290
291 #[tokio::test]
292 async fn test_parse_duration_hours() {
293 let result = crate::time_utils::parse_duration("24h").ok();
294 assert!(result.is_some());
295 }
296
297 #[tokio::test]
298 async fn test_parse_duration_invalid() {
299 let result = crate::time_utils::parse_duration("invalid").ok();
300 assert!(result.is_none());
301 }
302
303 #[tokio::test]
304 async fn test_filter_tasks_by_fts_name() {
305 let ctx = TestContext::new().await;
306 let task_mgr = TaskManager::new(ctx.pool());
307 let report_mgr = ReportManager::new(ctx.pool());
308
309 task_mgr
310 .add_task(
311 "Authentication feature".to_string(),
312 None,
313 None,
314 None,
315 None,
316 None,
317 )
318 .await
319 .unwrap();
320 task_mgr
321 .add_task(
322 "Database migration".to_string(),
323 None,
324 None,
325 None,
326 None,
327 None,
328 )
329 .await
330 .unwrap();
331
332 let report = report_mgr
333 .generate_report(None, None, Some("Authentication".to_string()), None, false)
334 .await
335 .unwrap();
336
337 let tasks = report.tasks.unwrap();
338 assert_eq!(tasks.len(), 1);
339 assert!(tasks[0].name.contains("Authentication"));
340 }
341
342 #[tokio::test]
343 async fn test_empty_report() {
344 let ctx = TestContext::new().await;
345 let report_mgr = ReportManager::new(ctx.pool());
346
347 let report = report_mgr
348 .generate_report(None, None, None, None, true)
349 .await
350 .unwrap();
351
352 assert_eq!(report.summary.total_tasks, 0);
353 assert_eq!(report.summary.total_events, 0);
354 }
355
356 #[tokio::test]
357 async fn test_report_filter_consistency() {
358 let ctx = TestContext::new().await;
359 let task_mgr = TaskManager::new(ctx.pool());
360 let report_mgr = ReportManager::new(ctx.pool());
361
362 task_mgr
364 .add_task("Task A".to_string(), None, None, None, None, None)
365 .await
366 .unwrap();
367 task_mgr
368 .add_task("Task B".to_string(), None, None, None, None, None)
369 .await
370 .unwrap();
371 let doing = task_mgr
372 .add_task("Task C".to_string(), None, None, None, None, None)
373 .await
374 .unwrap();
375 task_mgr.start_task(doing.id, false).await.unwrap();
376
377 let report = report_mgr
379 .generate_report(None, None, None, Some("JWT".to_string()), true)
380 .await
381 .unwrap();
382
383 assert_eq!(report.summary.total_tasks, 0);
385 assert_eq!(report.summary.tasks_by_status.todo, 0);
386 assert_eq!(report.summary.tasks_by_status.doing, 0);
387 assert_eq!(report.summary.tasks_by_status.done, 0);
388 }
389
390 #[tokio::test]
391 async fn test_generate_report_with_since() {
392 let ctx = TestContext::new().await;
393 let task_mgr = TaskManager::new(ctx.pool());
394 let report_mgr = ReportManager::new(ctx.pool());
395
396 task_mgr
398 .add_task("Old task".to_string(), None, None, None, None, None)
399 .await
400 .unwrap();
401 task_mgr
402 .add_task("Recent task".to_string(), None, None, None, None, None)
403 .await
404 .unwrap();
405
406 let report = report_mgr
408 .generate_report(Some("1h".to_string()), None, None, None, true)
409 .await
410 .unwrap();
411
412 assert!(report.summary.total_tasks >= 2);
414 assert!(report.summary.date_range.is_some());
415 }
416
417 #[tokio::test]
418 async fn test_generate_report_filter_by_spec() {
419 let ctx = TestContext::new().await;
420 let task_mgr = TaskManager::new(ctx.pool());
421 let report_mgr = ReportManager::new(ctx.pool());
422
423 task_mgr
424 .add_task(
425 "Task 1".to_string(),
426 Some("Implement authentication using JWT".to_string()),
427 None,
428 None,
429 None,
430 None,
431 )
432 .await
433 .unwrap();
434 task_mgr
435 .add_task(
436 "Task 2".to_string(),
437 Some("Setup database migrations".to_string()),
438 None,
439 None,
440 None,
441 None,
442 )
443 .await
444 .unwrap();
445
446 let report = report_mgr
447 .generate_report(None, None, None, Some("authentication".to_string()), false)
448 .await
449 .unwrap();
450
451 let tasks = report.tasks.unwrap();
452 assert_eq!(tasks.len(), 1);
453 assert_eq!(tasks[0].name, "Task 1");
454 }
455
456 #[tokio::test]
457 async fn test_generate_report_combined_status_and_since() {
458 let ctx = TestContext::new().await;
459 let task_mgr = TaskManager::new(ctx.pool());
460 let report_mgr = ReportManager::new(ctx.pool());
461
462 task_mgr
463 .add_task("Todo task".to_string(), None, None, None, None, None)
464 .await
465 .unwrap();
466 let doing = task_mgr
467 .add_task("Doing task".to_string(), None, None, None, None, None)
468 .await
469 .unwrap();
470 task_mgr.start_task(doing.id, false).await.unwrap();
471
472 let report = report_mgr
474 .generate_report(
475 Some("1d".to_string()),
476 Some("doing".to_string()),
477 None,
478 None,
479 false,
480 )
481 .await
482 .unwrap();
483
484 let tasks = report.tasks.unwrap();
485 assert_eq!(tasks.len(), 1);
486 assert_eq!(tasks[0].status, "doing");
487 }
488
489 #[tokio::test]
490 async fn test_filter_tasks_by_fts_spec() {
491 let ctx = TestContext::new().await;
492 let task_mgr = TaskManager::new(ctx.pool());
493 let report_mgr = ReportManager::new(ctx.pool());
494
495 task_mgr
496 .add_task(
497 "Feature A".to_string(),
498 Some("Implement JWT authentication".to_string()),
499 None,
500 None,
501 None,
502 None,
503 )
504 .await
505 .unwrap();
506 task_mgr
507 .add_task(
508 "Feature B".to_string(),
509 Some("Setup OAuth2 integration".to_string()),
510 None,
511 None,
512 None,
513 None,
514 )
515 .await
516 .unwrap();
517
518 let ids = report_mgr
519 .filter_tasks_by_fts(&None, &Some("JWT".to_string()))
520 .await
521 .unwrap();
522
523 assert_eq!(ids.len(), 1);
524 }
525
526 #[tokio::test]
527 async fn test_filter_tasks_by_fts_both_name_and_spec() {
528 let ctx = TestContext::new().await;
529 let task_mgr = TaskManager::new(ctx.pool());
530 let report_mgr = ReportManager::new(ctx.pool());
531
532 task_mgr
533 .add_task(
534 "Auth feature".to_string(),
535 Some("Implement authentication".to_string()),
536 None,
537 None,
538 None,
539 None,
540 )
541 .await
542 .unwrap();
543 task_mgr
544 .add_task(
545 "Database setup".to_string(),
546 Some("Configure authentication database".to_string()),
547 None,
548 None,
549 None,
550 None,
551 )
552 .await
553 .unwrap();
554
555 let ids = report_mgr
557 .filter_tasks_by_fts(
558 &Some("Auth".to_string()),
559 &Some("authentication".to_string()),
560 )
561 .await
562 .unwrap();
563
564 assert_eq!(ids.len(), 1);
565 }
566
567 #[tokio::test]
568 async fn test_filter_tasks_by_fts_empty() {
569 let ctx = TestContext::new().await;
570 let report_mgr = ReportManager::new(ctx.pool());
571
572 let ids = report_mgr.filter_tasks_by_fts(&None, &None).await.unwrap();
574
575 assert_eq!(ids.len(), 0);
576 }
577
578 #[tokio::test]
579 async fn test_report_date_range_present() {
580 let ctx = TestContext::new().await;
581 let task_mgr = TaskManager::new(ctx.pool());
582 let report_mgr = ReportManager::new(ctx.pool());
583
584 task_mgr
585 .add_task("Task".to_string(), None, None, None, None, None)
586 .await
587 .unwrap();
588
589 let report = report_mgr
590 .generate_report(Some("7d".to_string()), None, None, None, true)
591 .await
592 .unwrap();
593
594 assert!(report.summary.date_range.is_some());
596 let date_range = report.summary.date_range.unwrap();
597 assert!(date_range.to > date_range.from);
598 }
599
600 #[tokio::test]
601 async fn test_report_date_range_absent() {
602 let ctx = TestContext::new().await;
603 let task_mgr = TaskManager::new(ctx.pool());
604 let report_mgr = ReportManager::new(ctx.pool());
605
606 task_mgr
607 .add_task("Task".to_string(), None, None, None, None, None)
608 .await
609 .unwrap();
610
611 let report = report_mgr
612 .generate_report(None, None, None, None, true)
613 .await
614 .unwrap();
615
616 assert!(report.summary.date_range.is_none());
618 }
619
620 #[tokio::test]
621 async fn test_report_events_count_consistency() {
622 let ctx = TestContext::new().await;
623 let task_mgr = TaskManager::new(ctx.pool());
624 let event_mgr = EventManager::new(ctx.pool());
625 let report_mgr = ReportManager::new(ctx.pool());
626
627 let task = task_mgr
628 .add_task("Task".to_string(), None, None, None, None, None)
629 .await
630 .unwrap();
631 event_mgr
632 .add_event(task.id, "decision".to_string(), "Event 1".to_string())
633 .await
634 .unwrap();
635 event_mgr
636 .add_event(task.id, "note".to_string(), "Event 2".to_string())
637 .await
638 .unwrap();
639
640 let summary_report = report_mgr
642 .generate_report(None, None, None, None, true)
643 .await
644 .unwrap();
645 assert_eq!(summary_report.summary.total_events, 2);
646 assert!(summary_report.events.is_none());
647
648 let full_report = report_mgr
650 .generate_report(None, None, None, None, false)
651 .await
652 .unwrap();
653 assert_eq!(full_report.summary.total_events, 2);
654 assert_eq!(full_report.events.unwrap().len(), 2);
655 }
656
657 #[tokio::test]
658 async fn test_generate_report_all_filters_combined() {
659 let ctx = TestContext::new().await;
660 let task_mgr = TaskManager::new(ctx.pool());
661 let report_mgr = ReportManager::new(ctx.pool());
662
663 task_mgr
664 .add_task(
665 "Auth feature".to_string(),
666 Some("JWT implementation".to_string()),
667 None,
668 None,
669 None,
670 None,
671 )
672 .await
673 .unwrap();
674 let doing = task_mgr
675 .add_task(
676 "Auth testing".to_string(),
677 Some("Write JWT tests".to_string()),
678 None,
679 None,
680 None,
681 None,
682 )
683 .await
684 .unwrap();
685 task_mgr.start_task(doing.id, false).await.unwrap();
686
687 let report = report_mgr
689 .generate_report(
690 Some("1h".to_string()),
691 Some("doing".to_string()),
692 Some("Auth".to_string()),
693 Some("JWT".to_string()),
694 false,
695 )
696 .await
697 .unwrap();
698
699 let tasks = report.tasks.unwrap();
700 assert_eq!(tasks.len(), 1);
701 assert_eq!(tasks[0].status, "doing");
702 assert!(tasks[0].name.contains("Auth"));
703 }
704}