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");
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");
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", None, None, None)
183 .await
184 .unwrap();
185 let doing = task_mgr
186 .add_task("Doing task", 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", 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.add_task("Task 1", None, None, None).await.unwrap();
217 task_mgr.add_task("Task 2", None, None, None).await.unwrap();
218
219 let report = report_mgr
220 .generate_report(None, None, None, None, false)
221 .await
222 .unwrap();
223
224 assert!(report.tasks.is_some());
225 assert_eq!(report.tasks.unwrap().len(), 2);
226 }
227
228 #[tokio::test]
229 async fn test_generate_report_filter_by_status() {
230 let ctx = TestContext::new().await;
231 let task_mgr = TaskManager::new(ctx.pool());
232 let report_mgr = ReportManager::new(ctx.pool());
233
234 task_mgr
235 .add_task("Todo task", None, None, None)
236 .await
237 .unwrap();
238 let doing = task_mgr
239 .add_task("Doing task", None, None, None)
240 .await
241 .unwrap();
242 task_mgr.start_task(doing.id, false).await.unwrap();
243
244 let report = report_mgr
245 .generate_report(None, Some("doing".to_string()), None, None, false)
246 .await
247 .unwrap();
248
249 let tasks = report.tasks.unwrap();
250 assert_eq!(tasks.len(), 1);
251 assert_eq!(tasks[0].status, "doing");
252 }
253
254 #[tokio::test]
255 async fn test_generate_report_with_events() {
256 let ctx = TestContext::new().await;
257 let task_mgr = TaskManager::new(ctx.pool());
258 let event_mgr = EventManager::new(ctx.pool());
259 let report_mgr = ReportManager::new(ctx.pool());
260
261 let task = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
262 event_mgr
263 .add_event(task.id, "decision", "Test event")
264 .await
265 .unwrap();
266
267 let report = report_mgr
268 .generate_report(None, None, None, None, false)
269 .await
270 .unwrap();
271
272 assert!(report.events.is_some());
273 assert_eq!(report.summary.total_events, 1);
274 }
275
276 #[tokio::test]
277 async fn test_parse_duration_days() {
278 let result = crate::time_utils::parse_duration("7d").ok();
279 assert!(result.is_some());
280 }
281
282 #[tokio::test]
283 async fn test_parse_duration_hours() {
284 let result = crate::time_utils::parse_duration("24h").ok();
285 assert!(result.is_some());
286 }
287
288 #[tokio::test]
289 async fn test_parse_duration_invalid() {
290 let result = crate::time_utils::parse_duration("invalid").ok();
291 assert!(result.is_none());
292 }
293
294 #[tokio::test]
295 async fn test_filter_tasks_by_fts_name() {
296 let ctx = TestContext::new().await;
297 let task_mgr = TaskManager::new(ctx.pool());
298 let report_mgr = ReportManager::new(ctx.pool());
299
300 task_mgr
301 .add_task("Authentication feature", None, None, None)
302 .await
303 .unwrap();
304 task_mgr
305 .add_task("Database migration", None, None, None)
306 .await
307 .unwrap();
308
309 let report = report_mgr
310 .generate_report(None, None, Some("Authentication".to_string()), None, false)
311 .await
312 .unwrap();
313
314 let tasks = report.tasks.unwrap();
315 assert_eq!(tasks.len(), 1);
316 assert!(tasks[0].name.contains("Authentication"));
317 }
318
319 #[tokio::test]
320 async fn test_empty_report() {
321 let ctx = TestContext::new().await;
322 let report_mgr = ReportManager::new(ctx.pool());
323
324 let report = report_mgr
325 .generate_report(None, None, None, None, true)
326 .await
327 .unwrap();
328
329 assert_eq!(report.summary.total_tasks, 0);
330 assert_eq!(report.summary.total_events, 0);
331 }
332
333 #[tokio::test]
334 async fn test_report_filter_consistency() {
335 let ctx = TestContext::new().await;
336 let task_mgr = TaskManager::new(ctx.pool());
337 let report_mgr = ReportManager::new(ctx.pool());
338
339 task_mgr.add_task("Task A", None, None, None).await.unwrap();
341 task_mgr.add_task("Task B", None, None, None).await.unwrap();
342 let doing = task_mgr.add_task("Task C", None, None, None).await.unwrap();
343 task_mgr.start_task(doing.id, false).await.unwrap();
344
345 let report = report_mgr
347 .generate_report(None, None, None, Some("JWT".to_string()), true)
348 .await
349 .unwrap();
350
351 assert_eq!(report.summary.total_tasks, 0);
353 assert_eq!(report.summary.tasks_by_status.todo, 0);
354 assert_eq!(report.summary.tasks_by_status.doing, 0);
355 assert_eq!(report.summary.tasks_by_status.done, 0);
356 }
357
358 #[tokio::test]
359 async fn test_generate_report_with_since() {
360 let ctx = TestContext::new().await;
361 let task_mgr = TaskManager::new(ctx.pool());
362 let report_mgr = ReportManager::new(ctx.pool());
363
364 task_mgr
366 .add_task("Old task", None, None, None)
367 .await
368 .unwrap();
369 task_mgr
370 .add_task("Recent task", None, None, None)
371 .await
372 .unwrap();
373
374 let report = report_mgr
376 .generate_report(Some("1h".to_string()), None, None, None, true)
377 .await
378 .unwrap();
379
380 assert!(report.summary.total_tasks >= 2);
382 assert!(report.summary.date_range.is_some());
383 }
384
385 #[tokio::test]
386 async fn test_generate_report_filter_by_spec() {
387 let ctx = TestContext::new().await;
388 let task_mgr = TaskManager::new(ctx.pool());
389 let report_mgr = ReportManager::new(ctx.pool());
390
391 task_mgr
392 .add_task(
393 "Task 1",
394 Some("Implement authentication using JWT"),
395 None,
396 None,
397 )
398 .await
399 .unwrap();
400 task_mgr
401 .add_task("Task 2", Some("Setup database migrations"), None, None)
402 .await
403 .unwrap();
404
405 let report = report_mgr
406 .generate_report(None, None, None, Some("authentication".to_string()), false)
407 .await
408 .unwrap();
409
410 let tasks = report.tasks.unwrap();
411 assert_eq!(tasks.len(), 1);
412 assert_eq!(tasks[0].name, "Task 1");
413 }
414
415 #[tokio::test]
416 async fn test_generate_report_combined_status_and_since() {
417 let ctx = TestContext::new().await;
418 let task_mgr = TaskManager::new(ctx.pool());
419 let report_mgr = ReportManager::new(ctx.pool());
420
421 task_mgr
422 .add_task("Todo task", None, None, None)
423 .await
424 .unwrap();
425 let doing = task_mgr
426 .add_task("Doing task", None, None, None)
427 .await
428 .unwrap();
429 task_mgr.start_task(doing.id, false).await.unwrap();
430
431 let report = report_mgr
433 .generate_report(
434 Some("1d".to_string()),
435 Some("doing".to_string()),
436 None,
437 None,
438 false,
439 )
440 .await
441 .unwrap();
442
443 let tasks = report.tasks.unwrap();
444 assert_eq!(tasks.len(), 1);
445 assert_eq!(tasks[0].status, "doing");
446 }
447
448 #[tokio::test]
449 async fn test_filter_tasks_by_fts_spec() {
450 let ctx = TestContext::new().await;
451 let task_mgr = TaskManager::new(ctx.pool());
452 let report_mgr = ReportManager::new(ctx.pool());
453
454 task_mgr
455 .add_task(
456 "Feature A",
457 Some("Implement JWT authentication"),
458 None,
459 None,
460 )
461 .await
462 .unwrap();
463 task_mgr
464 .add_task("Feature B", Some("Setup OAuth2 integration"), None, None)
465 .await
466 .unwrap();
467
468 let ids = report_mgr
469 .filter_tasks_by_fts(&None, &Some("JWT".to_string()))
470 .await
471 .unwrap();
472
473 assert_eq!(ids.len(), 1);
474 }
475
476 #[tokio::test]
477 async fn test_filter_tasks_by_fts_both_name_and_spec() {
478 let ctx = TestContext::new().await;
479 let task_mgr = TaskManager::new(ctx.pool());
480 let report_mgr = ReportManager::new(ctx.pool());
481
482 task_mgr
483 .add_task("Auth feature", Some("Implement authentication"), None, None)
484 .await
485 .unwrap();
486 task_mgr
487 .add_task(
488 "Database setup",
489 Some("Configure authentication database"),
490 None,
491 None,
492 )
493 .await
494 .unwrap();
495
496 let ids = report_mgr
498 .filter_tasks_by_fts(
499 &Some("Auth".to_string()),
500 &Some("authentication".to_string()),
501 )
502 .await
503 .unwrap();
504
505 assert_eq!(ids.len(), 1);
506 }
507
508 #[tokio::test]
509 async fn test_filter_tasks_by_fts_empty() {
510 let ctx = TestContext::new().await;
511 let report_mgr = ReportManager::new(ctx.pool());
512
513 let ids = report_mgr.filter_tasks_by_fts(&None, &None).await.unwrap();
515
516 assert_eq!(ids.len(), 0);
517 }
518
519 #[tokio::test]
520 async fn test_report_date_range_present() {
521 let ctx = TestContext::new().await;
522 let task_mgr = TaskManager::new(ctx.pool());
523 let report_mgr = ReportManager::new(ctx.pool());
524
525 task_mgr.add_task("Task", None, None, None).await.unwrap();
526
527 let report = report_mgr
528 .generate_report(Some("7d".to_string()), None, None, None, true)
529 .await
530 .unwrap();
531
532 assert!(report.summary.date_range.is_some());
534 let date_range = report.summary.date_range.unwrap();
535 assert!(date_range.to > date_range.from);
536 }
537
538 #[tokio::test]
539 async fn test_report_date_range_absent() {
540 let ctx = TestContext::new().await;
541 let task_mgr = TaskManager::new(ctx.pool());
542 let report_mgr = ReportManager::new(ctx.pool());
543
544 task_mgr.add_task("Task", None, None, None).await.unwrap();
545
546 let report = report_mgr
547 .generate_report(None, None, None, None, true)
548 .await
549 .unwrap();
550
551 assert!(report.summary.date_range.is_none());
553 }
554
555 #[tokio::test]
556 async fn test_report_events_count_consistency() {
557 let ctx = TestContext::new().await;
558 let task_mgr = TaskManager::new(ctx.pool());
559 let event_mgr = EventManager::new(ctx.pool());
560 let report_mgr = ReportManager::new(ctx.pool());
561
562 let task = task_mgr.add_task("Task", None, None, None).await.unwrap();
563 event_mgr
564 .add_event(task.id, "decision", "Event 1")
565 .await
566 .unwrap();
567 event_mgr
568 .add_event(task.id, "note", "Event 2")
569 .await
570 .unwrap();
571
572 let summary_report = report_mgr
574 .generate_report(None, None, None, None, true)
575 .await
576 .unwrap();
577 assert_eq!(summary_report.summary.total_events, 2);
578 assert!(summary_report.events.is_none());
579
580 let full_report = report_mgr
582 .generate_report(None, None, None, None, false)
583 .await
584 .unwrap();
585 assert_eq!(full_report.summary.total_events, 2);
586 assert_eq!(full_report.events.unwrap().len(), 2);
587 }
588
589 #[tokio::test]
590 async fn test_generate_report_all_filters_combined() {
591 let ctx = TestContext::new().await;
592 let task_mgr = TaskManager::new(ctx.pool());
593 let report_mgr = ReportManager::new(ctx.pool());
594
595 task_mgr
596 .add_task("Auth feature", Some("JWT implementation"), None, None)
597 .await
598 .unwrap();
599 let doing = task_mgr
600 .add_task("Auth testing", Some("Write JWT tests"), None, None)
601 .await
602 .unwrap();
603 task_mgr.start_task(doing.id, false).await.unwrap();
604
605 let report = report_mgr
607 .generate_report(
608 Some("1h".to_string()),
609 Some("doing".to_string()),
610 Some("Auth".to_string()),
611 Some("JWT".to_string()),
612 false,
613 )
614 .await
615 .unwrap();
616
617 let tasks = report.tasks.unwrap();
618 assert_eq!(tasks.len(), 1);
619 assert_eq!(tasks[0].status, "doing");
620 assert!(tasks[0].name.contains("Auth"));
621 }
622}