1use crate::models::{
2 GitBranch, Goal, GoalStatus, InsightData, ProjectTemplate, TimeEstimate, Workspace,
3};
4use anyhow::Result;
5use chrono::NaiveDate;
6use rusqlite::{params, Connection, OptionalExtension};
7
8pub struct GoalQueries;
9
10impl GoalQueries {
11 pub fn create(conn: &Connection, goal: &Goal) -> Result<i64> {
12 let mut stmt = conn.prepare(
13 "INSERT INTO goals (project_id, name, description, target_hours, start_date, end_date, current_progress, status)
14 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
15 )?;
16
17 stmt.execute(params![
18 goal.project_id,
19 goal.name,
20 goal.description,
21 goal.target_hours,
22 goal.start_date,
23 goal.end_date,
24 goal.current_progress,
25 goal.status.to_string()
26 ])?;
27
28 Ok(conn.last_insert_rowid())
29 }
30
31 pub fn find_by_id(conn: &Connection, goal_id: i64) -> Result<Option<Goal>> {
32 let mut stmt = conn.prepare(
33 "SELECT id, project_id, name, description, target_hours, start_date, end_date, current_progress, status, created_at, updated_at
34 FROM goals WHERE id = ?1"
35 )?;
36
37 let goal = stmt
38 .query_row([goal_id], |row| {
39 Ok(Goal {
40 id: Some(row.get(0)?),
41 project_id: row.get(1)?,
42 name: row.get(2)?,
43 description: row.get(3)?,
44 target_hours: row.get(4)?,
45 start_date: row.get(5)?,
46 end_date: row.get(6)?,
47 current_progress: row.get(7)?,
48 status: row
49 .get::<_, String>(8)?
50 .parse()
51 .unwrap_or(GoalStatus::Active),
52 created_at: row.get(9)?,
53 updated_at: row.get(10)?,
54 })
55 })
56 .optional()?;
57
58 Ok(goal)
59 }
60
61 pub fn list_by_project(conn: &Connection, project_id: Option<i64>) -> Result<Vec<Goal>> {
62 let sql = if let Some(_pid) = project_id {
63 "SELECT id, project_id, name, description, target_hours, start_date, end_date, current_progress, status, created_at, updated_at
64 FROM goals WHERE project_id = ?1 ORDER BY created_at DESC"
65 } else {
66 "SELECT id, project_id, name, description, target_hours, start_date, end_date, current_progress, status, created_at, updated_at
67 FROM goals ORDER BY created_at DESC"
68 };
69
70 let mut stmt = conn.prepare(sql)?;
71 let goals = if let Some(pid) = project_id {
72 stmt.query_map([pid], |row| {
73 Ok(Goal {
74 id: Some(row.get(0)?),
75 project_id: row.get(1)?,
76 name: row.get(2)?,
77 description: row.get(3)?,
78 target_hours: row.get(4)?,
79 start_date: row.get(5)?,
80 end_date: row.get(6)?,
81 current_progress: row.get(7)?,
82 status: row
83 .get::<_, String>(8)?
84 .parse()
85 .unwrap_or(GoalStatus::Active),
86 created_at: row.get(9)?,
87 updated_at: row.get(10)?,
88 })
89 })?
90 .collect::<Result<Vec<_>, _>>()?
91 } else {
92 stmt.query_map([], |row| {
93 Ok(Goal {
94 id: Some(row.get(0)?),
95 project_id: row.get(1)?,
96 name: row.get(2)?,
97 description: row.get(3)?,
98 target_hours: row.get(4)?,
99 start_date: row.get(5)?,
100 end_date: row.get(6)?,
101 current_progress: row.get(7)?,
102 status: row
103 .get::<_, String>(8)?
104 .parse()
105 .unwrap_or(GoalStatus::Active),
106 created_at: row.get(9)?,
107 updated_at: row.get(10)?,
108 })
109 })?
110 .collect::<Result<Vec<_>, _>>()?
111 };
112
113 Ok(goals)
114 }
115
116 pub fn update_progress(conn: &Connection, goal_id: i64, hours: f64) -> Result<bool> {
117 let mut stmt = conn.prepare(
118 "UPDATE goals SET current_progress = current_progress + ?1, updated_at = CURRENT_TIMESTAMP
119 WHERE id = ?2"
120 )?;
121 let changes = stmt.execute(params![hours, goal_id])?;
122 Ok(changes > 0)
123 }
124
125 pub fn update(
126 conn: &Connection,
127 goal_id: i64,
128 name: Option<String>,
129 description: Option<String>,
130 target_hours: Option<f64>,
131 start_date: Option<Option<NaiveDate>>,
132 end_date: Option<Option<NaiveDate>>,
133 status: Option<GoalStatus>,
134 ) -> Result<bool> {
135 let mut updates = Vec::new();
136 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
137
138 if let Some(n) = name {
139 updates.push("name = ?");
140 params.push(Box::new(n));
141 }
142 if let Some(d) = description {
143 updates.push("description = ?");
144 params.push(Box::new(d));
145 }
146 if let Some(th) = target_hours {
147 updates.push("target_hours = ?");
148 params.push(Box::new(th));
149 }
150 if let Some(sd) = start_date {
151 updates.push("start_date = ?");
152 params.push(Box::new(sd));
153 }
154 if let Some(ed) = end_date {
155 updates.push("end_date = ?");
156 params.push(Box::new(ed));
157 }
158 if let Some(s) = status {
159 updates.push("status = ?");
160 params.push(Box::new(s.to_string()));
161 }
162
163 if updates.is_empty() {
164 return Ok(false);
165 }
166
167 updates.push("updated_at = CURRENT_TIMESTAMP");
168 params.push(Box::new(goal_id));
169
170 let sql = format!("UPDATE goals SET {} WHERE id = ?", updates.join(", "));
171 let mut stmt = conn.prepare(&sql)?;
172 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
173 let changes = stmt.execute(¶m_refs[..])?;
174
175 Ok(changes > 0)
176 }
177
178 pub fn delete(conn: &Connection, goal_id: i64) -> Result<bool> {
179 let mut stmt = conn.prepare("DELETE FROM goals WHERE id = ?1")?;
180 let changes = stmt.execute([goal_id])?;
181 Ok(changes > 0)
182 }
183
184 pub fn list_active(conn: &Connection) -> Result<Vec<Goal>> {
185 let mut stmt = conn.prepare(
186 "SELECT id, project_id, name, description, target_hours, start_date, end_date, current_progress, status, created_at, updated_at
187 FROM goals WHERE status = 'active' ORDER BY created_at DESC"
188 )?;
189
190 let goals = stmt
191 .query_map([], |row| {
192 Ok(Goal {
193 id: Some(row.get(0)?),
194 project_id: row.get(1)?,
195 name: row.get(2)?,
196 description: row.get(3)?,
197 target_hours: row.get(4)?,
198 start_date: row.get(5)?,
199 end_date: row.get(6)?,
200 current_progress: row.get(7)?,
201 status: row
202 .get::<_, String>(8)?
203 .parse()
204 .unwrap_or(GoalStatus::Active),
205 created_at: row.get(9)?,
206 updated_at: row.get(10)?,
207 })
208 })?
209 .collect::<Result<Vec<_>, _>>()?;
210
211 Ok(goals)
212 }
213
214 pub fn set_status(conn: &Connection, goal_id: i64, status: GoalStatus) -> Result<bool> {
215 let mut stmt = conn.prepare(
216 "UPDATE goals SET status = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
217 )?;
218 let changes = stmt.execute(params![status.to_string(), goal_id])?;
219 Ok(changes > 0)
220 }
221}
222
223pub struct TemplateQueries;
224
225impl TemplateQueries {
226 pub fn create(conn: &Connection, template: &ProjectTemplate) -> Result<i64> {
227 let tags_json = serde_json::to_string(&template.default_tags)?;
228 let goals_json = serde_json::to_string(&template.default_goals)?;
229
230 let mut stmt = conn.prepare(
231 "INSERT INTO project_templates (name, description, default_tags, default_goals, workspace_path)
232 VALUES (?1, ?2, ?3, ?4, ?5)"
233 )?;
234
235 stmt.execute(params![
236 template.name,
237 template.description,
238 tags_json,
239 goals_json,
240 template
241 .workspace_path
242 .as_ref()
243 .map(|p| p.to_string_lossy().to_string())
244 ])?;
245
246 Ok(conn.last_insert_rowid())
247 }
248
249 pub fn list_all(conn: &Connection) -> Result<Vec<ProjectTemplate>> {
250 let mut stmt = conn.prepare(
251 "SELECT id, name, description, default_tags, default_goals, workspace_path, created_at
252 FROM project_templates ORDER BY name",
253 )?;
254
255 let templates = stmt
256 .query_map([], |row| {
257 let tags_json: String = row.get(3)?;
258 let goals_json: String = row.get(4)?;
259
260 Ok(ProjectTemplate {
261 id: Some(row.get(0)?),
262 name: row.get(1)?,
263 description: row.get(2)?,
264 default_tags: serde_json::from_str(&tags_json).unwrap_or_default(),
265 default_goals: serde_json::from_str(&goals_json).unwrap_or_default(),
266 workspace_path: row.get::<_, Option<String>>(5)?.map(|s| s.into()),
267 created_at: row.get(6)?,
268 })
269 })?
270 .collect::<Result<Vec<_>, _>>()?;
271
272 Ok(templates)
273 }
274
275 pub fn find_by_id(conn: &Connection, template_id: i64) -> Result<Option<ProjectTemplate>> {
276 let mut stmt = conn.prepare(
277 "SELECT id, name, description, default_tags, default_goals, workspace_path, created_at
278 FROM project_templates WHERE id = ?1",
279 )?;
280
281 let template = stmt
282 .query_row([template_id], |row| {
283 let tags_json: String = row.get(3)?;
284 let goals_json: String = row.get(4)?;
285
286 Ok(ProjectTemplate {
287 id: Some(row.get(0)?),
288 name: row.get(1)?,
289 description: row.get(2)?,
290 default_tags: serde_json::from_str(&tags_json).unwrap_or_default(),
291 default_goals: serde_json::from_str(&goals_json).unwrap_or_default(),
292 workspace_path: row.get::<_, Option<String>>(5)?.map(|s| s.into()),
293 created_at: row.get(6)?,
294 })
295 })
296 .optional()?;
297
298 Ok(template)
299 }
300
301 pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<ProjectTemplate>> {
302 let mut stmt = conn.prepare(
303 "SELECT id, name, description, default_tags, default_goals, workspace_path, created_at
304 FROM project_templates WHERE name = ?1",
305 )?;
306
307 let template = stmt
308 .query_row([name], |row| {
309 let tags_json: String = row.get(3)?;
310 let goals_json: String = row.get(4)?;
311
312 Ok(ProjectTemplate {
313 id: Some(row.get(0)?),
314 name: row.get(1)?,
315 description: row.get(2)?,
316 default_tags: serde_json::from_str(&tags_json).unwrap_or_default(),
317 default_goals: serde_json::from_str(&goals_json).unwrap_or_default(),
318 workspace_path: row.get::<_, Option<String>>(5)?.map(|s| s.into()),
319 created_at: row.get(6)?,
320 })
321 })
322 .optional()?;
323
324 Ok(template)
325 }
326
327 pub fn update(
328 conn: &Connection,
329 template_id: i64,
330 name: Option<String>,
331 description: Option<String>,
332 default_tags: Option<Vec<String>>,
333 default_goals: Option<Vec<String>>,
334 workspace_path: Option<Option<std::path::PathBuf>>,
335 ) -> Result<bool> {
336 let mut updates = Vec::new();
337 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
338
339 if let Some(n) = name {
340 updates.push("name = ?");
341 params.push(Box::new(n));
342 }
343 if let Some(d) = description {
344 updates.push("description = ?");
345 params.push(Box::new(d));
346 }
347 if let Some(tags) = default_tags {
348 updates.push("default_tags = ?");
349 let tags_json = serde_json::to_string(&tags).unwrap_or_default();
350 params.push(Box::new(tags_json));
351 }
352 if let Some(goals) = default_goals {
353 updates.push("default_goals = ?");
354 let goals_json = serde_json::to_string(&goals).unwrap_or_default();
355 params.push(Box::new(goals_json));
356 }
357 if let Some(path) = workspace_path {
358 updates.push("workspace_path = ?");
359 params.push(Box::new(path.map(|p| p.to_string_lossy().to_string())));
360 }
361
362 if updates.is_empty() {
363 return Ok(false);
364 }
365
366 params.push(Box::new(template_id));
367
368 let sql = format!(
369 "UPDATE project_templates SET {} WHERE id = ?",
370 updates.join(", ")
371 );
372 let mut stmt = conn.prepare(&sql)?;
373 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
374 let changes = stmt.execute(¶m_refs[..])?;
375
376 Ok(changes > 0)
377 }
378
379 pub fn delete(conn: &Connection, template_id: i64) -> Result<bool> {
380 let mut stmt = conn.prepare("DELETE FROM project_templates WHERE id = ?1")?;
381 let changes = stmt.execute([template_id])?;
382 Ok(changes > 0)
383 }
384
385 pub fn delete_by_name(conn: &Connection, name: &str) -> Result<bool> {
386 let mut stmt = conn.prepare("DELETE FROM project_templates WHERE name = ?1")?;
387 let changes = stmt.execute([name])?;
388 Ok(changes > 0)
389 }
390}
391
392pub struct WorkspaceQueries;
393
394impl WorkspaceQueries {
395 pub fn create(conn: &Connection, workspace: &Workspace) -> Result<i64> {
396 let mut stmt = conn.prepare(
397 "INSERT INTO workspaces (name, description, path)
398 VALUES (?1, ?2, ?3)",
399 )?;
400
401 stmt.execute(params![
402 workspace.name,
403 workspace.description,
404 workspace
405 .path
406 .as_ref()
407 .map(|p| p.to_string_lossy().to_string())
408 ])?;
409
410 Ok(conn.last_insert_rowid())
411 }
412
413 pub fn list_all(conn: &Connection) -> Result<Vec<Workspace>> {
414 let mut stmt = conn.prepare(
415 "SELECT id, name, description, path, created_at, updated_at
416 FROM workspaces ORDER BY name",
417 )?;
418
419 let workspaces = stmt
420 .query_map([], |row| {
421 Ok(Workspace {
422 id: Some(row.get(0)?),
423 name: row.get(1)?,
424 description: row.get(2)?,
425 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
426 created_at: row.get(4)?,
427 updated_at: row.get(5)?,
428 })
429 })?
430 .collect::<Result<Vec<_>, _>>()?;
431
432 Ok(workspaces)
433 }
434
435 pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Workspace>> {
436 let mut stmt = conn.prepare(
437 "SELECT id, name, description, path, created_at, updated_at
438 FROM workspaces WHERE name = ?1",
439 )?;
440
441 let workspace = stmt
442 .query_row([name], |row| {
443 Ok(Workspace {
444 id: Some(row.get(0)?),
445 name: row.get(1)?,
446 description: row.get(2)?,
447 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
448 created_at: row.get(4)?,
449 updated_at: row.get(5)?,
450 })
451 })
452 .optional()?;
453
454 Ok(workspace)
455 }
456
457 pub fn delete(conn: &Connection, workspace_id: i64) -> Result<bool> {
458 let mut stmt = conn.prepare("DELETE FROM workspaces WHERE id = ?1")?;
459 let changes = stmt.execute([workspace_id])?;
460 Ok(changes > 0)
461 }
462
463 pub fn add_project(conn: &Connection, workspace_id: i64, project_id: i64) -> Result<bool> {
464 let mut stmt = conn.prepare(
465 "INSERT OR IGNORE INTO workspace_projects (workspace_id, project_id)
466 VALUES (?1, ?2)",
467 )?;
468 let changes = stmt.execute(params![workspace_id, project_id])?;
469 Ok(changes > 0)
470 }
471
472 pub fn remove_project(conn: &Connection, workspace_id: i64, project_id: i64) -> Result<bool> {
473 let mut stmt = conn.prepare(
474 "DELETE FROM workspace_projects
475 WHERE workspace_id = ?1 AND project_id = ?2",
476 )?;
477 let changes = stmt.execute(params![workspace_id, project_id])?;
478 Ok(changes > 0)
479 }
480
481 pub fn list_projects(
482 conn: &Connection,
483 workspace_id: i64,
484 ) -> Result<Vec<crate::models::Project>> {
485 let mut stmt = conn.prepare(
486 "SELECT p.id, p.name, p.path, p.git_hash, p.created_at, p.updated_at, p.is_archived, p.description
487 FROM projects p
488 JOIN workspace_projects wp ON p.id = wp.project_id
489 WHERE wp.workspace_id = ?1
490 ORDER BY p.name"
491 )?;
492
493 let projects = stmt
494 .query_map([workspace_id], |row| {
495 Ok(crate::models::Project {
496 id: Some(row.get(0)?),
497 name: row.get(1)?,
498 path: row.get::<_, String>(2)?.into(),
499 git_hash: row.get(3)?,
500 created_at: row.get(4)?,
501 updated_at: row.get(5)?,
502 is_archived: row.get(6)?,
503 description: row.get(7)?,
504 })
505 })?
506 .collect::<Result<Vec<_>, _>>()?;
507
508 Ok(projects)
509 }
510
511 pub fn find_by_id(conn: &Connection, workspace_id: i64) -> Result<Option<Workspace>> {
512 let mut stmt = conn.prepare(
513 "SELECT id, name, description, path, created_at, updated_at
514 FROM workspaces WHERE id = ?1",
515 )?;
516
517 let workspace = stmt
518 .query_row([workspace_id], |row| {
519 Ok(Workspace {
520 id: Some(row.get(0)?),
521 name: row.get(1)?,
522 description: row.get(2)?,
523 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
524 created_at: row.get(4)?,
525 updated_at: row.get(5)?,
526 })
527 })
528 .optional()?;
529
530 Ok(workspace)
531 }
532
533 pub fn update(
534 conn: &Connection,
535 workspace_id: i64,
536 name: Option<String>,
537 description: Option<String>,
538 path: Option<Option<std::path::PathBuf>>,
539 ) -> Result<bool> {
540 let mut updates = Vec::new();
541 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
542
543 if let Some(n) = name {
544 updates.push("name = ?");
545 params.push(Box::new(n));
546 }
547 if let Some(d) = description {
548 updates.push("description = ?");
549 params.push(Box::new(d));
550 }
551 if let Some(p) = path {
552 updates.push("path = ?");
553 params.push(Box::new(p.map(|p| p.to_string_lossy().to_string())));
554 }
555
556 if updates.is_empty() {
557 return Ok(false);
558 }
559
560 updates.push("updated_at = CURRENT_TIMESTAMP");
561 params.push(Box::new(workspace_id));
562
563 let sql = format!("UPDATE workspaces SET {} WHERE id = ?", updates.join(", "));
564 let mut stmt = conn.prepare(&sql)?;
565 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
566 let changes = stmt.execute(¶m_refs[..])?;
567
568 Ok(changes > 0)
569 }
570}
571
572pub struct GitBranchQueries;
573
574impl GitBranchQueries {
575 pub fn create_or_update(conn: &Connection, branch: &GitBranch) -> Result<i64> {
576 let existing =
578 Self::find_by_project_and_name(conn, branch.project_id, &branch.branch_name)?;
579
580 if let Some(mut existing) = existing {
581 existing.update_time(branch.total_time_seconds);
583 let mut stmt = conn.prepare(
584 "UPDATE git_branches SET last_seen = CURRENT_TIMESTAMP, total_time_seconds = total_time_seconds + ?1
585 WHERE project_id = ?2 AND branch_name = ?3"
586 )?;
587 stmt.execute(params![
588 branch.total_time_seconds,
589 branch.project_id,
590 branch.branch_name
591 ])?;
592 existing
593 .id
594 .ok_or_else(|| anyhow::anyhow!("Git branch ID missing after update"))
595 } else {
596 let mut stmt = conn.prepare(
598 "INSERT INTO git_branches (project_id, branch_name, total_time_seconds)
599 VALUES (?1, ?2, ?3)",
600 )?;
601 stmt.execute(params![
602 branch.project_id,
603 branch.branch_name,
604 branch.total_time_seconds
605 ])?;
606 Ok(conn.last_insert_rowid())
607 }
608 }
609
610 pub fn find_by_project_and_name(
611 conn: &Connection,
612 project_id: i64,
613 branch_name: &str,
614 ) -> Result<Option<GitBranch>> {
615 let mut stmt = conn.prepare(
616 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
617 FROM git_branches WHERE project_id = ?1 AND branch_name = ?2",
618 )?;
619
620 let branch = stmt
621 .query_row(params![project_id, branch_name], |row| {
622 Ok(GitBranch {
623 id: Some(row.get(0)?),
624 project_id: row.get(1)?,
625 branch_name: row.get(2)?,
626 first_seen: row.get(3)?,
627 last_seen: row.get(4)?,
628 total_time_seconds: row.get(5)?,
629 })
630 })
631 .optional()?;
632
633 Ok(branch)
634 }
635
636 pub fn list_by_project(conn: &Connection, project_id: i64) -> Result<Vec<GitBranch>> {
637 let mut stmt = conn.prepare(
638 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
639 FROM git_branches WHERE project_id = ?1 ORDER BY total_time_seconds DESC",
640 )?;
641
642 let branches = stmt
643 .query_map([project_id], |row| {
644 Ok(GitBranch {
645 id: Some(row.get(0)?),
646 project_id: row.get(1)?,
647 branch_name: row.get(2)?,
648 first_seen: row.get(3)?,
649 last_seen: row.get(4)?,
650 total_time_seconds: row.get(5)?,
651 })
652 })?
653 .collect::<Result<Vec<_>, _>>()?;
654
655 Ok(branches)
656 }
657
658 pub fn update(
659 conn: &Connection,
660 branch_id: i64,
661 branch_name: Option<String>,
662 additional_time_seconds: Option<i64>,
663 ) -> Result<bool> {
664 let mut updates = Vec::new();
665 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
666
667 if let Some(name) = branch_name {
668 updates.push("branch_name = ?");
669 params.push(Box::new(name));
670 }
671 if let Some(time) = additional_time_seconds {
672 updates.push("total_time_seconds = total_time_seconds + ?");
673 params.push(Box::new(time));
674 }
675
676 if updates.is_empty() {
677 return Ok(false);
678 }
679
680 updates.push("last_seen = CURRENT_TIMESTAMP");
681 params.push(Box::new(branch_id));
682
683 let sql = format!(
684 "UPDATE git_branches SET {} WHERE id = ?",
685 updates.join(", ")
686 );
687 let mut stmt = conn.prepare(&sql)?;
688 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
689 let changes = stmt.execute(¶m_refs[..])?;
690
691 Ok(changes > 0)
692 }
693
694 pub fn delete(conn: &Connection, branch_id: i64) -> Result<bool> {
695 let mut stmt = conn.prepare("DELETE FROM git_branches WHERE id = ?1")?;
696 let changes = stmt.execute([branch_id])?;
697 Ok(changes > 0)
698 }
699
700 pub fn delete_by_project_and_name(
701 conn: &Connection,
702 project_id: i64,
703 branch_name: &str,
704 ) -> Result<bool> {
705 let mut stmt =
706 conn.prepare("DELETE FROM git_branches WHERE project_id = ?1 AND branch_name = ?2")?;
707 let changes = stmt.execute(params![project_id, branch_name])?;
708 Ok(changes > 0)
709 }
710
711 pub fn find_by_id(conn: &Connection, branch_id: i64) -> Result<Option<GitBranch>> {
712 let mut stmt = conn.prepare(
713 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
714 FROM git_branches WHERE id = ?1",
715 )?;
716
717 let branch = stmt
718 .query_row([branch_id], |row| {
719 Ok(GitBranch {
720 id: Some(row.get(0)?),
721 project_id: row.get(1)?,
722 branch_name: row.get(2)?,
723 first_seen: row.get(3)?,
724 last_seen: row.get(4)?,
725 total_time_seconds: row.get(5)?,
726 })
727 })
728 .optional()?;
729
730 Ok(branch)
731 }
732}
733
734pub struct TimeEstimateQueries;
735
736impl TimeEstimateQueries {
737 pub fn create(conn: &Connection, estimate: &TimeEstimate) -> Result<i64> {
738 let mut stmt = conn.prepare(
739 "INSERT INTO time_estimates (project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at)
740 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
741 )?;
742
743 stmt.execute(params![
744 estimate.project_id,
745 estimate.task_name,
746 estimate.estimated_hours,
747 estimate.actual_hours,
748 estimate.status.to_string(),
749 estimate.due_date,
750 estimate.completed_at
751 ])?;
752
753 Ok(conn.last_insert_rowid())
754 }
755
756 pub fn list_by_project(conn: &Connection, project_id: i64) -> Result<Vec<TimeEstimate>> {
757 let mut stmt = conn.prepare(
758 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
759 FROM time_estimates WHERE project_id = ?1 ORDER BY created_at DESC"
760 )?;
761
762 let estimates = stmt
763 .query_map([project_id], |row| {
764 Ok(TimeEstimate {
765 id: Some(row.get(0)?),
766 project_id: row.get(1)?,
767 task_name: row.get(2)?,
768 estimated_hours: row.get(3)?,
769 actual_hours: row.get(4)?,
770 status: match row.get::<_, String>(5)?.as_str() {
771 "planned" => crate::models::EstimateStatus::Planned,
772 "in_progress" => crate::models::EstimateStatus::InProgress,
773 "completed" => crate::models::EstimateStatus::Completed,
774 "cancelled" => crate::models::EstimateStatus::Cancelled,
775 _ => crate::models::EstimateStatus::Planned,
776 },
777 due_date: row.get(6)?,
778 completed_at: row.get(7)?,
779 created_at: row.get(8)?,
780 updated_at: row.get(9)?,
781 })
782 })?
783 .collect::<Result<Vec<_>, _>>()?;
784
785 Ok(estimates)
786 }
787
788 pub fn record_actual(conn: &Connection, estimate_id: i64, hours: f64) -> Result<bool> {
789 let mut stmt = conn.prepare(
790 "UPDATE time_estimates SET actual_hours = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
791 WHERE id = ?2"
792 )?;
793 let changes = stmt.execute(params![hours, estimate_id])?;
794 Ok(changes > 0)
795 }
796
797 pub fn find_by_id(conn: &Connection, estimate_id: i64) -> Result<Option<TimeEstimate>> {
798 let mut stmt = conn.prepare(
799 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
800 FROM time_estimates WHERE id = ?1"
801 )?;
802
803 let estimate = stmt
804 .query_row([estimate_id], |row| {
805 Ok(TimeEstimate {
806 id: Some(row.get(0)?),
807 project_id: row.get(1)?,
808 task_name: row.get(2)?,
809 estimated_hours: row.get(3)?,
810 actual_hours: row.get(4)?,
811 status: match row.get::<_, String>(5)?.as_str() {
812 "planned" => crate::models::EstimateStatus::Planned,
813 "in_progress" => crate::models::EstimateStatus::InProgress,
814 "completed" => crate::models::EstimateStatus::Completed,
815 "cancelled" => crate::models::EstimateStatus::Cancelled,
816 _ => crate::models::EstimateStatus::Planned,
817 },
818 due_date: row.get(6)?,
819 completed_at: row.get(7)?,
820 created_at: row.get(8)?,
821 updated_at: row.get(9)?,
822 })
823 })
824 .optional()?;
825
826 Ok(estimate)
827 }
828
829 pub fn update(
830 conn: &Connection,
831 estimate_id: i64,
832 task_name: Option<String>,
833 estimated_hours: Option<f64>,
834 actual_hours: Option<Option<f64>>,
835 status: Option<crate::models::EstimateStatus>,
836 due_date: Option<Option<chrono::NaiveDate>>,
837 completed_at: Option<Option<chrono::DateTime<chrono::Utc>>>,
838 ) -> Result<bool> {
839 let mut updates = Vec::new();
840 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
841
842 if let Some(name) = task_name {
843 updates.push("task_name = ?");
844 params.push(Box::new(name));
845 }
846 if let Some(est_hours) = estimated_hours {
847 updates.push("estimated_hours = ?");
848 params.push(Box::new(est_hours));
849 }
850 if let Some(act_hours) = actual_hours {
851 updates.push("actual_hours = ?");
852 params.push(Box::new(act_hours));
853 }
854 if let Some(stat) = status {
855 updates.push("status = ?");
856 params.push(Box::new(stat.to_string()));
857 }
858 if let Some(due) = due_date {
859 updates.push("due_date = ?");
860 params.push(Box::new(due));
861 }
862 if let Some(comp) = completed_at {
863 updates.push("completed_at = ?");
864 params.push(Box::new(comp));
865 }
866
867 if updates.is_empty() {
868 return Ok(false);
869 }
870
871 updates.push("updated_at = CURRENT_TIMESTAMP");
872 params.push(Box::new(estimate_id));
873
874 let sql = format!(
875 "UPDATE time_estimates SET {} WHERE id = ?",
876 updates.join(", ")
877 );
878 let mut stmt = conn.prepare(&sql)?;
879 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
880 let changes = stmt.execute(¶m_refs[..])?;
881
882 Ok(changes > 0)
883 }
884
885 pub fn delete(conn: &Connection, estimate_id: i64) -> Result<bool> {
886 let mut stmt = conn.prepare("DELETE FROM time_estimates WHERE id = ?1")?;
887 let changes = stmt.execute([estimate_id])?;
888 Ok(changes > 0)
889 }
890
891 pub fn set_status(
892 conn: &Connection,
893 estimate_id: i64,
894 status: crate::models::EstimateStatus,
895 ) -> Result<bool> {
896 let mut stmt = conn.prepare(
897 "UPDATE time_estimates SET status = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
898 )?;
899 let changes = stmt.execute(params![status.to_string(), estimate_id])?;
900 Ok(changes > 0)
901 }
902
903 pub fn list_all(conn: &Connection) -> Result<Vec<TimeEstimate>> {
904 let mut stmt = conn.prepare(
905 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
906 FROM time_estimates ORDER BY created_at DESC"
907 )?;
908
909 let estimates = stmt
910 .query_map([], |row| {
911 Ok(TimeEstimate {
912 id: Some(row.get(0)?),
913 project_id: row.get(1)?,
914 task_name: row.get(2)?,
915 estimated_hours: row.get(3)?,
916 actual_hours: row.get(4)?,
917 status: match row.get::<_, String>(5)?.as_str() {
918 "planned" => crate::models::EstimateStatus::Planned,
919 "in_progress" => crate::models::EstimateStatus::InProgress,
920 "completed" => crate::models::EstimateStatus::Completed,
921 "cancelled" => crate::models::EstimateStatus::Cancelled,
922 _ => crate::models::EstimateStatus::Planned,
923 },
924 due_date: row.get(6)?,
925 completed_at: row.get(7)?,
926 created_at: row.get(8)?,
927 updated_at: row.get(9)?,
928 })
929 })?
930 .collect::<Result<Vec<_>, _>>()?;
931
932 Ok(estimates)
933 }
934}
935
936pub struct InsightQueries;
937
938impl InsightQueries {
939 pub fn calculate_weekly_summary(
940 conn: &Connection,
941 week_start: NaiveDate,
942 ) -> Result<InsightData> {
943 let week_end = week_start + chrono::Duration::days(6);
944
945 let mut stmt = conn.prepare(
946 "SELECT
947 COALESCE(SUM(CASE WHEN end_time IS NOT NULL THEN
948 (julianday(end_time) - julianday(start_time)) * 86400 - COALESCE(paused_duration, 0)
949 ELSE 0 END), 0) as total_seconds,
950 COUNT(*) as session_count
951 FROM sessions
952 WHERE DATE(start_time) >= ?1 AND DATE(start_time) <= ?2 AND end_time IS NOT NULL "
953 )?;
954
955 let (total_seconds, session_count): (f64, i64) =
956 stmt.query_row([week_start, week_end], |row| Ok((row.get(0)?, row.get(1)?)))?;
957
958 let total_hours = total_seconds / 3600.0;
959 let avg_session_duration = if session_count > 0 {
960 total_hours / session_count as f64
961 } else {
962 0.0
963 };
964
965 Ok(InsightData {
966 total_hours,
967 sessions_count: session_count,
968 avg_session_duration,
969 most_active_day: None,
970 most_active_time: None,
971 productivity_score: None,
972 project_breakdown: vec![],
973 trends: vec![],
974 })
975 }
976
977 pub fn calculate_monthly_summary(
978 conn: &Connection,
979 month_start: NaiveDate,
980 ) -> Result<InsightData> {
981 let month_end = month_start + chrono::Duration::days(30);
982
983 let mut stmt = conn.prepare(
984 "SELECT
985 COALESCE(SUM(CASE WHEN end_time IS NOT NULL THEN
986 (julianday(end_time) - julianday(start_time)) * 86400 - COALESCE(paused_duration, 0)
987 ELSE 0 END), 0) as total_seconds,
988 COUNT(*) as session_count
989 FROM sessions
990 WHERE DATE(start_time) >= ?1 AND DATE(start_time) <= ?2 AND end_time IS NOT NULL "
991 )?;
992
993 let (total_seconds, session_count): (f64, i64) = stmt
994 .query_row([month_start, month_end], |row| {
995 Ok((row.get(0)?, row.get(1)?))
996 })?;
997
998 let total_hours = total_seconds / 3600.0;
999 let avg_session_duration = if session_count > 0 {
1000 total_hours / session_count as f64
1001 } else {
1002 0.0
1003 };
1004
1005 Ok(InsightData {
1006 total_hours,
1007 sessions_count: session_count,
1008 avg_session_duration,
1009 most_active_day: None,
1010 most_active_time: None,
1011 productivity_score: None,
1012 project_breakdown: vec![],
1013 trends: vec![],
1014 })
1015 }
1016}