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