1pub mod models;
2
3use crate::db::get_db;
4use crate::errors::{DialError, Result};
5use crate::output::{bold, dim, green, print_success, red, yellow};
6use chrono::Local;
7use models::Task;
8
9pub fn task_add(description: &str, priority: i32, spec_section_id: Option<i64>) -> Result<i64> {
10 let conn = get_db(None)?;
11 conn.execute(
12 "INSERT INTO tasks (description, priority, spec_section_id) VALUES (?1, ?2, ?3)",
13 rusqlite::params![description, priority, spec_section_id],
14 )?;
15 let task_id = conn.last_insert_rowid();
16 print_success(&format!("Added task #{}: {}", task_id, description));
17 Ok(task_id)
18}
19
20pub fn task_list(show_all: bool) -> Result<()> {
21 let conn = get_db(None)?;
22
23 let sql = if show_all {
24 "SELECT id, description, status, priority, blocked_by, created_at
25 FROM tasks ORDER BY priority, id"
26 } else {
27 "SELECT id, description, status, priority, blocked_by, created_at
28 FROM tasks WHERE status NOT IN ('completed', 'cancelled')
29 ORDER BY priority, id"
30 };
31
32 let mut stmt = conn.prepare(sql)?;
33 let rows: Vec<(i64, String, String, i32, Option<String>, String)> = stmt
34 .query_map([], |row| {
35 Ok((
36 row.get(0)?,
37 row.get(1)?,
38 row.get(2)?,
39 row.get(3)?,
40 row.get(4)?,
41 row.get(5)?,
42 ))
43 })?
44 .collect::<std::result::Result<Vec<_>, _>>()?;
45
46 if rows.is_empty() {
47 println!("{}", dim("No tasks found."));
48 return Ok(());
49 }
50
51 println!("{}", bold("Tasks"));
52 println!("{}", "=".repeat(60));
53
54 for (id, description, status, priority, blocked_by, _created_at) in rows {
55 let status_str = match status.as_str() {
56 "pending" => dim(&format!("[{}]", status)),
57 "in_progress" => yellow(&format!("[{}]", status)),
58 "completed" => green(&format!("[{}]", status)),
59 "blocked" => red(&format!("[{}]", status)),
60 "cancelled" => dim(&format!("[{}]", status)),
61 _ => format!("[{}]", status),
62 };
63
64 let priority_str = if priority != 5 {
65 format!("P{}", priority)
66 } else {
67 String::new()
68 };
69
70 let blocked_str = if let Some(reason) = blocked_by {
71 red(&format!(" (blocked: {})", reason))
72 } else {
73 String::new()
74 };
75
76 println!(
77 " #{:3} {:20} {:4} {}{}",
78 id, status_str, priority_str, description, blocked_str
79 );
80 }
81
82 Ok(())
83}
84
85pub fn task_next() -> Result<Option<Task>> {
86 let conn = get_db(None)?;
87
88 let mut stmt = conn.prepare(
89 "SELECT id, description, status, priority, blocked_by, spec_section_id, created_at, started_at, completed_at
90 FROM tasks WHERE status = 'pending'
91 ORDER BY priority, id LIMIT 1",
92 )?;
93
94 let task = stmt
95 .query_row([], |row| Task::from_row(row))
96 .ok();
97
98 match &task {
99 Some(t) => {
100 println!("{}", bold("Next task:"));
101 println!(" #{}: {}", t.id, t.description);
102 if let Some(spec_id) = t.spec_section_id {
103 println!("{}", dim(&format!(" Spec section: {}", spec_id)));
104 }
105 }
106 None => {
107 println!("{}", dim("No pending tasks."));
108 }
109 }
110
111 Ok(task)
112}
113
114pub fn task_done(task_id: i64) -> Result<()> {
115 let conn = get_db(None)?;
116 let now = Local::now().to_rfc3339();
117
118 let changed = conn.execute(
119 "UPDATE tasks SET status = 'completed', completed_at = ?1 WHERE id = ?2",
120 rusqlite::params![now, task_id],
121 )?;
122
123 if changed == 0 {
124 return Err(DialError::TaskNotFound(task_id));
125 }
126
127 print_success(&format!("Task #{} marked as completed.", task_id));
128 Ok(())
129}
130
131pub fn task_block(task_id: i64, reason: &str) -> Result<()> {
132 let conn = get_db(None)?;
133
134 let changed = conn.execute(
135 "UPDATE tasks SET status = 'blocked', blocked_by = ?1 WHERE id = ?2",
136 rusqlite::params![reason, task_id],
137 )?;
138
139 if changed == 0 {
140 return Err(DialError::TaskNotFound(task_id));
141 }
142
143 println!("{}", yellow(&format!("Task #{} marked as blocked: {}", task_id, reason)));
144 Ok(())
145}
146
147pub fn task_cancel(task_id: i64) -> Result<()> {
148 let conn = get_db(None)?;
149
150 let changed = conn.execute(
151 "UPDATE tasks SET status = 'cancelled' WHERE id = ?1",
152 [task_id],
153 )?;
154
155 if changed == 0 {
156 return Err(DialError::TaskNotFound(task_id));
157 }
158
159 println!("{}", dim(&format!("Task #{} cancelled.", task_id)));
160 Ok(())
161}
162
163pub fn task_search(query: &str) -> Result<()> {
164 let conn = get_db(None)?;
165
166 let mut stmt = conn.prepare(
167 "SELECT t.id, t.description, t.status, t.priority
168 FROM tasks t
169 INNER JOIN tasks_fts fts ON t.id = fts.rowid
170 WHERE tasks_fts MATCH ?1
171 ORDER BY rank",
172 )?;
173
174 let rows: Vec<(i64, String, String, i32)> = stmt
175 .query_map([query], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))?
176 .collect::<std::result::Result<Vec<_>, _>>()?;
177
178 if rows.is_empty() {
179 println!("{}", dim(&format!("No tasks matching '{}'.", query)));
180 return Ok(());
181 }
182
183 println!("{}", bold(&format!("Tasks matching '{}':", query)));
184 for (id, description, status, _priority) in rows {
185 println!(" #{} [{}] {}", id, status, description);
186 }
187
188 Ok(())
189}
190
191pub fn get_task_by_id(task_id: i64) -> Result<Task> {
192 let conn = get_db(None)?;
193
194 let mut stmt = conn.prepare(
195 "SELECT id, description, status, priority, blocked_by, spec_section_id, created_at, started_at, completed_at
196 FROM tasks WHERE id = ?1",
197 )?;
198
199 stmt.query_row([task_id], |row| Task::from_row(row))
200 .map_err(|_| DialError::TaskNotFound(task_id))
201}