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