1use crate::db::models::{DateRange, Event, Report, ReportSummary, StatusBreakdown, Task};
2use crate::error::Result;
3use chrono::{Duration, 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| self.parse_duration(&s));
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");
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");
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(
84 "SELECT id, task_id, timestamp, log_type, discussion_data FROM events WHERE 1=1",
85 );
86 let mut event_conditions = Vec::new();
87
88 if let Some(ref dt) = since_datetime {
89 event_query.push_str(" AND timestamp >= ?");
90 event_conditions.push(dt.to_rfc3339());
91 }
92
93 event_query.push_str(" ORDER BY timestamp DESC");
94
95 let mut q = sqlx::query_as::<_, Event>(&event_query);
96 for cond in &event_conditions {
97 q = q.bind(cond);
98 }
99
100 Some(q.fetch_all(self.pool).await?)
101 } else {
102 None
103 };
104
105 let total_events = if let Some(ref evts) = events {
106 evts.len() as i64
107 } else {
108 sqlx::query_scalar("SELECT COUNT(*) FROM events")
109 .fetch_one(self.pool)
110 .await?
111 };
112
113 let date_range = since_datetime.map(|from| DateRange {
114 from,
115 to: Utc::now(),
116 });
117
118 Ok(Report {
119 summary: ReportSummary {
120 total_tasks,
121 tasks_by_status: StatusBreakdown {
122 todo: todo_count,
123 doing: doing_count,
124 done: done_count,
125 },
126 total_events,
127 date_range,
128 },
129 tasks: if summary_only { None } else { Some(tasks) },
130 events,
131 })
132 }
133
134 async fn filter_tasks_by_fts(
136 &self,
137 filter_name: &Option<String>,
138 filter_spec: &Option<String>,
139 ) -> Result<Vec<i64>> {
140 let mut query = String::from("SELECT rowid FROM tasks_fts WHERE ");
141 let mut conditions = Vec::new();
142
143 if let Some(name_filter) = filter_name {
144 conditions.push(format!("name MATCH '{}'", self.escape_fts(name_filter)));
145 }
146
147 if let Some(spec_filter) = filter_spec {
148 conditions.push(format!("spec MATCH '{}'", self.escape_fts(spec_filter)));
149 }
150
151 if conditions.is_empty() {
152 return Ok(Vec::new());
153 }
154
155 query.push_str(&conditions.join(" AND "));
156
157 let ids: Vec<i64> = sqlx::query_scalar(&query).fetch_all(self.pool).await?;
158
159 Ok(ids)
160 }
161
162 fn escape_fts(&self, input: &str) -> String {
164 input.replace('"', "\"\"")
165 }
166
167 fn parse_duration(&self, duration_str: &str) -> Option<chrono::DateTime<Utc>> {
169 let duration_str = duration_str.trim();
170
171 if duration_str.is_empty() {
172 return None;
173 }
174
175 let len = duration_str.len();
176 let (num_part, unit) = duration_str.split_at(len - 1);
177
178 let num: i64 = num_part.parse().ok()?;
179
180 let duration = match unit {
181 "d" => Duration::days(num),
182 "h" => Duration::hours(num),
183 "m" => Duration::minutes(num),
184 "w" => Duration::weeks(num),
185 _ => return None,
186 };
187
188 Some(Utc::now() - duration)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::events::EventManager;
196 use crate::tasks::TaskManager;
197 use crate::test_utils::test_helpers::TestContext;
198
199 #[tokio::test]
200 async fn test_generate_report_summary_only() {
201 let ctx = TestContext::new().await;
202 let task_mgr = TaskManager::new(ctx.pool());
203 let report_mgr = ReportManager::new(ctx.pool());
204
205 task_mgr.add_task("Todo task", None, None).await.unwrap();
207 let doing = task_mgr.add_task("Doing task", None, None).await.unwrap();
208 task_mgr.start_task(doing.id, false).await.unwrap();
209 let done = task_mgr.add_task("Done task", None, None).await.unwrap();
210 task_mgr.start_task(done.id, false).await.unwrap();
211 task_mgr.done_task().await.unwrap();
212
213 let report = report_mgr
214 .generate_report(None, None, None, None, true)
215 .await
216 .unwrap();
217
218 assert_eq!(report.summary.total_tasks, 3);
219 assert_eq!(report.summary.tasks_by_status.todo, 1);
220 assert_eq!(report.summary.tasks_by_status.doing, 1);
221 assert_eq!(report.summary.tasks_by_status.done, 1);
222 assert!(report.tasks.is_none());
223 assert!(report.events.is_none());
224 }
225
226 #[tokio::test]
227 async fn test_generate_report_full() {
228 let ctx = TestContext::new().await;
229 let task_mgr = TaskManager::new(ctx.pool());
230 let report_mgr = ReportManager::new(ctx.pool());
231
232 task_mgr.add_task("Task 1", None, None).await.unwrap();
233 task_mgr.add_task("Task 2", None, None).await.unwrap();
234
235 let report = report_mgr
236 .generate_report(None, None, None, None, false)
237 .await
238 .unwrap();
239
240 assert!(report.tasks.is_some());
241 assert_eq!(report.tasks.unwrap().len(), 2);
242 }
243
244 #[tokio::test]
245 async fn test_generate_report_filter_by_status() {
246 let ctx = TestContext::new().await;
247 let task_mgr = TaskManager::new(ctx.pool());
248 let report_mgr = ReportManager::new(ctx.pool());
249
250 task_mgr.add_task("Todo task", None, None).await.unwrap();
251 let doing = task_mgr.add_task("Doing task", None, None).await.unwrap();
252 task_mgr.start_task(doing.id, false).await.unwrap();
253
254 let report = report_mgr
255 .generate_report(None, Some("doing".to_string()), None, None, false)
256 .await
257 .unwrap();
258
259 let tasks = report.tasks.unwrap();
260 assert_eq!(tasks.len(), 1);
261 assert_eq!(tasks[0].status, "doing");
262 }
263
264 #[tokio::test]
265 async fn test_generate_report_with_events() {
266 let ctx = TestContext::new().await;
267 let task_mgr = TaskManager::new(ctx.pool());
268 let event_mgr = EventManager::new(ctx.pool());
269 let report_mgr = ReportManager::new(ctx.pool());
270
271 let task = task_mgr.add_task("Task 1", None, None).await.unwrap();
272 event_mgr
273 .add_event(task.id, "decision", "Test event")
274 .await
275 .unwrap();
276
277 let report = report_mgr
278 .generate_report(None, None, None, None, false)
279 .await
280 .unwrap();
281
282 assert!(report.events.is_some());
283 assert_eq!(report.summary.total_events, 1);
284 }
285
286 #[tokio::test]
287 async fn test_parse_duration_days() {
288 let ctx = TestContext::new().await;
289 let report_mgr = ReportManager::new(ctx.pool());
290
291 let result = report_mgr.parse_duration("7d");
292 assert!(result.is_some());
293 }
294
295 #[tokio::test]
296 async fn test_parse_duration_hours() {
297 let ctx = TestContext::new().await;
298 let report_mgr = ReportManager::new(ctx.pool());
299
300 let result = report_mgr.parse_duration("24h");
301 assert!(result.is_some());
302 }
303
304 #[tokio::test]
305 async fn test_parse_duration_invalid() {
306 let ctx = TestContext::new().await;
307 let report_mgr = ReportManager::new(ctx.pool());
308
309 let result = report_mgr.parse_duration("invalid");
310 assert!(result.is_none());
311 }
312
313 #[tokio::test]
314 async fn test_filter_tasks_by_fts_name() {
315 let ctx = TestContext::new().await;
316 let task_mgr = TaskManager::new(ctx.pool());
317 let report_mgr = ReportManager::new(ctx.pool());
318
319 task_mgr
320 .add_task("Authentication feature", None, None)
321 .await
322 .unwrap();
323 task_mgr
324 .add_task("Database migration", None, None)
325 .await
326 .unwrap();
327
328 let report = report_mgr
329 .generate_report(None, None, Some("Authentication".to_string()), None, false)
330 .await
331 .unwrap();
332
333 let tasks = report.tasks.unwrap();
334 assert_eq!(tasks.len(), 1);
335 assert!(tasks[0].name.contains("Authentication"));
336 }
337
338 #[tokio::test]
339 async fn test_empty_report() {
340 let ctx = TestContext::new().await;
341 let report_mgr = ReportManager::new(ctx.pool());
342
343 let report = report_mgr
344 .generate_report(None, None, None, None, true)
345 .await
346 .unwrap();
347
348 assert_eq!(report.summary.total_tasks, 0);
349 assert_eq!(report.summary.total_events, 0);
350 }
351
352 #[tokio::test]
353 async fn test_report_filter_consistency() {
354 let ctx = TestContext::new().await;
355 let task_mgr = TaskManager::new(ctx.pool());
356 let report_mgr = ReportManager::new(ctx.pool());
357
358 task_mgr.add_task("Task A", None, None).await.unwrap();
360 task_mgr.add_task("Task B", None, None).await.unwrap();
361 let doing = task_mgr.add_task("Task C", None, None).await.unwrap();
362 task_mgr.start_task(doing.id, false).await.unwrap();
363
364 let report = report_mgr
366 .generate_report(None, None, None, Some("JWT".to_string()), true)
367 .await
368 .unwrap();
369
370 assert_eq!(report.summary.total_tasks, 0);
372 assert_eq!(report.summary.tasks_by_status.todo, 0);
373 assert_eq!(report.summary.tasks_by_status.doing, 0);
374 assert_eq!(report.summary.tasks_by_status.done, 0);
375 }
376}