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!("UPDATE project_templates SET {} WHERE id = ?", updates.join(", "));
369 let mut stmt = conn.prepare(&sql)?;
370 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
371 let changes = stmt.execute(¶m_refs[..])?;
372
373 Ok(changes > 0)
374 }
375
376 pub fn delete(conn: &Connection, template_id: i64) -> Result<bool> {
377 let mut stmt = conn.prepare("DELETE FROM project_templates WHERE id = ?1")?;
378 let changes = stmt.execute([template_id])?;
379 Ok(changes > 0)
380 }
381
382 pub fn delete_by_name(conn: &Connection, name: &str) -> Result<bool> {
383 let mut stmt = conn.prepare("DELETE FROM project_templates WHERE name = ?1")?;
384 let changes = stmt.execute([name])?;
385 Ok(changes > 0)
386 }
387}
388
389pub struct WorkspaceQueries;
390
391impl WorkspaceQueries {
392 pub fn create(conn: &Connection, workspace: &Workspace) -> Result<i64> {
393 let mut stmt = conn.prepare(
394 "INSERT INTO workspaces (name, description, path)
395 VALUES (?1, ?2, ?3)",
396 )?;
397
398 stmt.execute(params![
399 workspace.name,
400 workspace.description,
401 workspace
402 .path
403 .as_ref()
404 .map(|p| p.to_string_lossy().to_string())
405 ])?;
406
407 Ok(conn.last_insert_rowid())
408 }
409
410 pub fn list_all(conn: &Connection) -> Result<Vec<Workspace>> {
411 let mut stmt = conn.prepare(
412 "SELECT id, name, description, path, created_at, updated_at
413 FROM workspaces ORDER BY name",
414 )?;
415
416 let workspaces = stmt
417 .query_map([], |row| {
418 Ok(Workspace {
419 id: Some(row.get(0)?),
420 name: row.get(1)?,
421 description: row.get(2)?,
422 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
423 created_at: row.get(4)?,
424 updated_at: row.get(5)?,
425 })
426 })?
427 .collect::<Result<Vec<_>, _>>()?;
428
429 Ok(workspaces)
430 }
431
432 pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Workspace>> {
433 let mut stmt = conn.prepare(
434 "SELECT id, name, description, path, created_at, updated_at
435 FROM workspaces WHERE name = ?1",
436 )?;
437
438 let workspace = stmt
439 .query_row([name], |row| {
440 Ok(Workspace {
441 id: Some(row.get(0)?),
442 name: row.get(1)?,
443 description: row.get(2)?,
444 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
445 created_at: row.get(4)?,
446 updated_at: row.get(5)?,
447 })
448 })
449 .optional()?;
450
451 Ok(workspace)
452 }
453
454 pub fn delete(conn: &Connection, workspace_id: i64) -> Result<bool> {
455 let mut stmt = conn.prepare("DELETE FROM workspaces WHERE id = ?1")?;
456 let changes = stmt.execute([workspace_id])?;
457 Ok(changes > 0)
458 }
459
460 pub fn add_project(conn: &Connection, workspace_id: i64, project_id: i64) -> Result<bool> {
461 let mut stmt = conn.prepare(
462 "INSERT OR IGNORE INTO workspace_projects (workspace_id, project_id)
463 VALUES (?1, ?2)",
464 )?;
465 let changes = stmt.execute(params![workspace_id, project_id])?;
466 Ok(changes > 0)
467 }
468
469 pub fn remove_project(conn: &Connection, workspace_id: i64, project_id: i64) -> Result<bool> {
470 let mut stmt = conn.prepare(
471 "DELETE FROM workspace_projects
472 WHERE workspace_id = ?1 AND project_id = ?2",
473 )?;
474 let changes = stmt.execute(params![workspace_id, project_id])?;
475 Ok(changes > 0)
476 }
477
478 pub fn list_projects(
479 conn: &Connection,
480 workspace_id: i64,
481 ) -> Result<Vec<crate::models::Project>> {
482 let mut stmt = conn.prepare(
483 "SELECT p.id, p.name, p.path, p.git_hash, p.created_at, p.updated_at, p.is_archived, p.description
484 FROM projects p
485 JOIN workspace_projects wp ON p.id = wp.project_id
486 WHERE wp.workspace_id = ?1
487 ORDER BY p.name"
488 )?;
489
490 let projects = stmt
491 .query_map([workspace_id], |row| {
492 Ok(crate::models::Project {
493 id: Some(row.get(0)?),
494 name: row.get(1)?,
495 path: row.get::<_, String>(2)?.into(),
496 git_hash: row.get(3)?,
497 created_at: row.get(4)?,
498 updated_at: row.get(5)?,
499 is_archived: row.get(6)?,
500 description: row.get(7)?,
501 })
502 })?
503 .collect::<Result<Vec<_>, _>>()?;
504
505 Ok(projects)
506 }
507
508 pub fn find_by_id(conn: &Connection, workspace_id: i64) -> Result<Option<Workspace>> {
509 let mut stmt = conn.prepare(
510 "SELECT id, name, description, path, created_at, updated_at
511 FROM workspaces WHERE id = ?1",
512 )?;
513
514 let workspace = stmt
515 .query_row([workspace_id], |row| {
516 Ok(Workspace {
517 id: Some(row.get(0)?),
518 name: row.get(1)?,
519 description: row.get(2)?,
520 path: row.get::<_, Option<String>>(3)?.map(|s| s.into()),
521 created_at: row.get(4)?,
522 updated_at: row.get(5)?,
523 })
524 })
525 .optional()?;
526
527 Ok(workspace)
528 }
529
530 pub fn update(
531 conn: &Connection,
532 workspace_id: i64,
533 name: Option<String>,
534 description: Option<String>,
535 path: Option<Option<std::path::PathBuf>>,
536 ) -> Result<bool> {
537 let mut updates = Vec::new();
538 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
539
540 if let Some(n) = name {
541 updates.push("name = ?");
542 params.push(Box::new(n));
543 }
544 if let Some(d) = description {
545 updates.push("description = ?");
546 params.push(Box::new(d));
547 }
548 if let Some(p) = path {
549 updates.push("path = ?");
550 params.push(Box::new(p.map(|p| p.to_string_lossy().to_string())));
551 }
552
553 if updates.is_empty() {
554 return Ok(false);
555 }
556
557 updates.push("updated_at = CURRENT_TIMESTAMP");
558 params.push(Box::new(workspace_id));
559
560 let sql = format!("UPDATE workspaces SET {} WHERE id = ?", updates.join(", "));
561 let mut stmt = conn.prepare(&sql)?;
562 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
563 let changes = stmt.execute(¶m_refs[..])?;
564
565 Ok(changes > 0)
566 }
567}
568
569pub struct GitBranchQueries;
570
571impl GitBranchQueries {
572 pub fn create_or_update(conn: &Connection, branch: &GitBranch) -> Result<i64> {
573 let existing =
575 Self::find_by_project_and_name(conn, branch.project_id, &branch.branch_name)?;
576
577 if let Some(mut existing) = existing {
578 existing.update_time(branch.total_time_seconds);
580 let mut stmt = conn.prepare(
581 "UPDATE git_branches SET last_seen = CURRENT_TIMESTAMP, total_time_seconds = total_time_seconds + ?1
582 WHERE project_id = ?2 AND branch_name = ?3"
583 )?;
584 stmt.execute(params![
585 branch.total_time_seconds,
586 branch.project_id,
587 branch.branch_name
588 ])?;
589 existing
590 .id
591 .ok_or_else(|| anyhow::anyhow!("Git branch ID missing after update"))
592 } else {
593 let mut stmt = conn.prepare(
595 "INSERT INTO git_branches (project_id, branch_name, total_time_seconds)
596 VALUES (?1, ?2, ?3)",
597 )?;
598 stmt.execute(params![
599 branch.project_id,
600 branch.branch_name,
601 branch.total_time_seconds
602 ])?;
603 Ok(conn.last_insert_rowid())
604 }
605 }
606
607 pub fn find_by_project_and_name(
608 conn: &Connection,
609 project_id: i64,
610 branch_name: &str,
611 ) -> Result<Option<GitBranch>> {
612 let mut stmt = conn.prepare(
613 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
614 FROM git_branches WHERE project_id = ?1 AND branch_name = ?2",
615 )?;
616
617 let branch = stmt
618 .query_row(params![project_id, branch_name], |row| {
619 Ok(GitBranch {
620 id: Some(row.get(0)?),
621 project_id: row.get(1)?,
622 branch_name: row.get(2)?,
623 first_seen: row.get(3)?,
624 last_seen: row.get(4)?,
625 total_time_seconds: row.get(5)?,
626 })
627 })
628 .optional()?;
629
630 Ok(branch)
631 }
632
633 pub fn list_by_project(conn: &Connection, project_id: i64) -> Result<Vec<GitBranch>> {
634 let mut stmt = conn.prepare(
635 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
636 FROM git_branches WHERE project_id = ?1 ORDER BY total_time_seconds DESC",
637 )?;
638
639 let branches = stmt
640 .query_map([project_id], |row| {
641 Ok(GitBranch {
642 id: Some(row.get(0)?),
643 project_id: row.get(1)?,
644 branch_name: row.get(2)?,
645 first_seen: row.get(3)?,
646 last_seen: row.get(4)?,
647 total_time_seconds: row.get(5)?,
648 })
649 })?
650 .collect::<Result<Vec<_>, _>>()?;
651
652 Ok(branches)
653 }
654
655 pub fn update(
656 conn: &Connection,
657 branch_id: i64,
658 branch_name: Option<String>,
659 additional_time_seconds: Option<i64>,
660 ) -> Result<bool> {
661 let mut updates = Vec::new();
662 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
663
664 if let Some(name) = branch_name {
665 updates.push("branch_name = ?");
666 params.push(Box::new(name));
667 }
668 if let Some(time) = additional_time_seconds {
669 updates.push("total_time_seconds = total_time_seconds + ?");
670 params.push(Box::new(time));
671 }
672
673 if updates.is_empty() {
674 return Ok(false);
675 }
676
677 updates.push("last_seen = CURRENT_TIMESTAMP");
678 params.push(Box::new(branch_id));
679
680 let sql = format!("UPDATE git_branches SET {} WHERE id = ?", updates.join(", "));
681 let mut stmt = conn.prepare(&sql)?;
682 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
683 let changes = stmt.execute(¶m_refs[..])?;
684
685 Ok(changes > 0)
686 }
687
688 pub fn delete(conn: &Connection, branch_id: i64) -> Result<bool> {
689 let mut stmt = conn.prepare("DELETE FROM git_branches WHERE id = ?1")?;
690 let changes = stmt.execute([branch_id])?;
691 Ok(changes > 0)
692 }
693
694 pub fn delete_by_project_and_name(
695 conn: &Connection,
696 project_id: i64,
697 branch_name: &str,
698 ) -> Result<bool> {
699 let mut stmt = conn.prepare("DELETE FROM git_branches WHERE project_id = ?1 AND branch_name = ?2")?;
700 let changes = stmt.execute(params![project_id, branch_name])?;
701 Ok(changes > 0)
702 }
703
704 pub fn find_by_id(conn: &Connection, branch_id: i64) -> Result<Option<GitBranch>> {
705 let mut stmt = conn.prepare(
706 "SELECT id, project_id, branch_name, first_seen, last_seen, total_time_seconds
707 FROM git_branches WHERE id = ?1",
708 )?;
709
710 let branch = stmt
711 .query_row([branch_id], |row| {
712 Ok(GitBranch {
713 id: Some(row.get(0)?),
714 project_id: row.get(1)?,
715 branch_name: row.get(2)?,
716 first_seen: row.get(3)?,
717 last_seen: row.get(4)?,
718 total_time_seconds: row.get(5)?,
719 })
720 })
721 .optional()?;
722
723 Ok(branch)
724 }
725}
726
727pub struct TimeEstimateQueries;
728
729impl TimeEstimateQueries {
730 pub fn create(conn: &Connection, estimate: &TimeEstimate) -> Result<i64> {
731 let mut stmt = conn.prepare(
732 "INSERT INTO time_estimates (project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at)
733 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
734 )?;
735
736 stmt.execute(params![
737 estimate.project_id,
738 estimate.task_name,
739 estimate.estimated_hours,
740 estimate.actual_hours,
741 estimate.status.to_string(),
742 estimate.due_date,
743 estimate.completed_at
744 ])?;
745
746 Ok(conn.last_insert_rowid())
747 }
748
749 pub fn list_by_project(conn: &Connection, project_id: i64) -> Result<Vec<TimeEstimate>> {
750 let mut stmt = conn.prepare(
751 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
752 FROM time_estimates WHERE project_id = ?1 ORDER BY created_at DESC"
753 )?;
754
755 let estimates = stmt
756 .query_map([project_id], |row| {
757 Ok(TimeEstimate {
758 id: Some(row.get(0)?),
759 project_id: row.get(1)?,
760 task_name: row.get(2)?,
761 estimated_hours: row.get(3)?,
762 actual_hours: row.get(4)?,
763 status: match row.get::<_, String>(5)?.as_str() {
764 "planned" => crate::models::EstimateStatus::Planned,
765 "in_progress" => crate::models::EstimateStatus::InProgress,
766 "completed" => crate::models::EstimateStatus::Completed,
767 "cancelled" => crate::models::EstimateStatus::Cancelled,
768 _ => crate::models::EstimateStatus::Planned,
769 },
770 due_date: row.get(6)?,
771 completed_at: row.get(7)?,
772 created_at: row.get(8)?,
773 updated_at: row.get(9)?,
774 })
775 })?
776 .collect::<Result<Vec<_>, _>>()?;
777
778 Ok(estimates)
779 }
780
781 pub fn record_actual(conn: &Connection, estimate_id: i64, hours: f64) -> Result<bool> {
782 let mut stmt = conn.prepare(
783 "UPDATE time_estimates SET actual_hours = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
784 WHERE id = ?2"
785 )?;
786 let changes = stmt.execute(params![hours, estimate_id])?;
787 Ok(changes > 0)
788 }
789
790 pub fn find_by_id(conn: &Connection, estimate_id: i64) -> Result<Option<TimeEstimate>> {
791 let mut stmt = conn.prepare(
792 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
793 FROM time_estimates WHERE id = ?1"
794 )?;
795
796 let estimate = stmt
797 .query_row([estimate_id], |row| {
798 Ok(TimeEstimate {
799 id: Some(row.get(0)?),
800 project_id: row.get(1)?,
801 task_name: row.get(2)?,
802 estimated_hours: row.get(3)?,
803 actual_hours: row.get(4)?,
804 status: match row.get::<_, String>(5)?.as_str() {
805 "planned" => crate::models::EstimateStatus::Planned,
806 "in_progress" => crate::models::EstimateStatus::InProgress,
807 "completed" => crate::models::EstimateStatus::Completed,
808 "cancelled" => crate::models::EstimateStatus::Cancelled,
809 _ => crate::models::EstimateStatus::Planned,
810 },
811 due_date: row.get(6)?,
812 completed_at: row.get(7)?,
813 created_at: row.get(8)?,
814 updated_at: row.get(9)?,
815 })
816 })
817 .optional()?;
818
819 Ok(estimate)
820 }
821
822 pub fn update(
823 conn: &Connection,
824 estimate_id: i64,
825 task_name: Option<String>,
826 estimated_hours: Option<f64>,
827 actual_hours: Option<Option<f64>>,
828 status: Option<crate::models::EstimateStatus>,
829 due_date: Option<Option<chrono::NaiveDate>>,
830 completed_at: Option<Option<chrono::DateTime<chrono::Utc>>>,
831 ) -> Result<bool> {
832 let mut updates = Vec::new();
833 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
834
835 if let Some(name) = task_name {
836 updates.push("task_name = ?");
837 params.push(Box::new(name));
838 }
839 if let Some(est_hours) = estimated_hours {
840 updates.push("estimated_hours = ?");
841 params.push(Box::new(est_hours));
842 }
843 if let Some(act_hours) = actual_hours {
844 updates.push("actual_hours = ?");
845 params.push(Box::new(act_hours));
846 }
847 if let Some(stat) = status {
848 updates.push("status = ?");
849 params.push(Box::new(stat.to_string()));
850 }
851 if let Some(due) = due_date {
852 updates.push("due_date = ?");
853 params.push(Box::new(due));
854 }
855 if let Some(comp) = completed_at {
856 updates.push("completed_at = ?");
857 params.push(Box::new(comp));
858 }
859
860 if updates.is_empty() {
861 return Ok(false);
862 }
863
864 updates.push("updated_at = CURRENT_TIMESTAMP");
865 params.push(Box::new(estimate_id));
866
867 let sql = format!("UPDATE time_estimates SET {} WHERE id = ?", updates.join(", "));
868 let mut stmt = conn.prepare(&sql)?;
869 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
870 let changes = stmt.execute(¶m_refs[..])?;
871
872 Ok(changes > 0)
873 }
874
875 pub fn delete(conn: &Connection, estimate_id: i64) -> Result<bool> {
876 let mut stmt = conn.prepare("DELETE FROM time_estimates WHERE id = ?1")?;
877 let changes = stmt.execute([estimate_id])?;
878 Ok(changes > 0)
879 }
880
881 pub fn set_status(
882 conn: &Connection,
883 estimate_id: i64,
884 status: crate::models::EstimateStatus,
885 ) -> Result<bool> {
886 let mut stmt = conn.prepare(
887 "UPDATE time_estimates SET status = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2"
888 )?;
889 let changes = stmt.execute(params![status.to_string(), estimate_id])?;
890 Ok(changes > 0)
891 }
892
893 pub fn list_all(conn: &Connection) -> Result<Vec<TimeEstimate>> {
894 let mut stmt = conn.prepare(
895 "SELECT id, project_id, task_name, estimated_hours, actual_hours, status, due_date, completed_at, created_at, updated_at
896 FROM time_estimates ORDER BY created_at DESC"
897 )?;
898
899 let estimates = stmt
900 .query_map([], |row| {
901 Ok(TimeEstimate {
902 id: Some(row.get(0)?),
903 project_id: row.get(1)?,
904 task_name: row.get(2)?,
905 estimated_hours: row.get(3)?,
906 actual_hours: row.get(4)?,
907 status: match row.get::<_, String>(5)?.as_str() {
908 "planned" => crate::models::EstimateStatus::Planned,
909 "in_progress" => crate::models::EstimateStatus::InProgress,
910 "completed" => crate::models::EstimateStatus::Completed,
911 "cancelled" => crate::models::EstimateStatus::Cancelled,
912 _ => crate::models::EstimateStatus::Planned,
913 },
914 due_date: row.get(6)?,
915 completed_at: row.get(7)?,
916 created_at: row.get(8)?,
917 updated_at: row.get(9)?,
918 })
919 })?
920 .collect::<Result<Vec<_>, _>>()?;
921
922 Ok(estimates)
923 }
924}
925
926pub struct InsightQueries;
927
928impl InsightQueries {
929 pub fn calculate_weekly_summary(
930 conn: &Connection,
931 week_start: NaiveDate,
932 ) -> Result<InsightData> {
933 let week_end = week_start + chrono::Duration::days(6);
934
935 let mut stmt = conn.prepare(
936 "SELECT
937 COALESCE(SUM(CASE WHEN end_time IS NOT NULL THEN
938 (julianday(end_time) - julianday(start_time)) * 86400 - COALESCE(paused_duration, 0)
939 ELSE 0 END), 0) as total_seconds,
940 COUNT(*) as session_count
941 FROM sessions
942 WHERE DATE(start_time) >= ?1 AND DATE(start_time) <= ?2 AND end_time IS NOT NULL "
943 )?;
944
945 let (total_seconds, session_count): (i64, i64) =
946 stmt.query_row([week_start, week_end], |row| Ok((row.get(0)?, row.get(1)?)))?;
947
948 let total_hours = total_seconds as f64 / 3600.0;
949 let avg_session_duration = if session_count > 0 {
950 total_hours / session_count as f64
951 } else {
952 0.0
953 };
954
955 Ok(InsightData {
956 total_hours,
957 sessions_count: session_count,
958 avg_session_duration,
959 most_active_day: None,
960 most_active_time: None,
961 productivity_score: None,
962 project_breakdown: vec![],
963 trends: vec![],
964 })
965 }
966
967 pub fn calculate_monthly_summary(
968 conn: &Connection,
969 month_start: NaiveDate,
970 ) -> Result<InsightData> {
971 let month_end = month_start + chrono::Duration::days(30);
972
973 let mut stmt = conn.prepare(
974 "SELECT
975 COALESCE(SUM(CASE WHEN end_time IS NOT NULL THEN
976 (julianday(end_time) - julianday(start_time)) * 86400 - COALESCE(paused_duration, 0)
977 ELSE 0 END), 0) as total_seconds,
978 COUNT(*) as session_count
979 FROM sessions
980 WHERE DATE(start_time) >= ?1 AND DATE(start_time) <= ?2 AND end_time IS NOT NULL "
981 )?;
982
983 let (total_seconds, session_count): (i64, i64) = stmt
984 .query_row([month_start, month_end], |row| {
985 Ok((row.get(0)?, row.get(1)?))
986 })?;
987
988 let total_hours = total_seconds as f64 / 3600.0;
989 let avg_session_duration = if session_count > 0 {
990 total_hours / session_count as f64
991 } else {
992 0.0
993 };
994
995 Ok(InsightData {
996 total_hours,
997 sessions_count: session_count,
998 avg_session_duration,
999 most_active_day: None,
1000 most_active_time: None,
1001 productivity_score: None,
1002 project_breakdown: vec![],
1003 trends: vec![],
1004 })
1005 }
1006}