1use anyhow::Result;
2use clap::{Subcommand, ValueEnum};
3use tabled::Table;
4
5use crate::client::ApiClient;
6use crate::models::task::{Task, TaskCreate, TaskListResponse, TaskUpdate};
7
8#[derive(Clone, ValueEnum)]
9pub enum Priority {
10 High,
11 Normal,
12 Low,
13}
14
15impl Priority {
16 fn as_i32(&self) -> i32 {
17 match self {
18 Priority::High => 0,
19 Priority::Normal => 1,
20 Priority::Low => 2,
21 }
22 }
23}
24
25#[derive(Clone, ValueEnum)]
26pub enum CheckedStatus {
27 Empty,
28 Checked,
29 Cancelled,
30}
31
32impl CheckedStatus {
33 fn as_i32(&self) -> i32 {
34 match self {
35 CheckedStatus::Empty => 0,
36 CheckedStatus::Checked => 1,
37 CheckedStatus::Cancelled => 2,
38 }
39 }
40}
41
42#[derive(Subcommand)]
43pub enum TaskCmd {
44 #[command(about = "List tasks with optional filters")]
45 List {
46 #[arg(long, help = "Filter by project ID (P-<uuid>)")]
47 project_id: Option<String>,
48 #[arg(long, help = "Filter by parent task ID (T-<uuid>)")]
49 parent: Option<String>,
50 #[arg(
51 long,
52 help = "Filter tasks starting on or after this date (ISO 8601, inclusive)"
53 )]
54 start_from: Option<String>,
55 #[arg(
56 long,
57 help = "Filter tasks starting on or before this date (ISO 8601, inclusive)"
58 )]
59 start_to: Option<String>,
60 #[arg(long, help = "Maximum number of tasks to return (max 1000)")]
61 max_count: Option<u32>,
62 #[arg(long, help = "Number of tasks to skip for pagination")]
63 offset: Option<u32>,
64 #[arg(long, help = "Include soft-deleted tasks")]
65 include_removed: bool,
66 #[arg(long, help = "Include archived tasks")]
67 include_archived: bool,
68 },
69 #[command(about = "Get a single task by ID")]
70 Get {
71 #[arg(help = "Task ID (T-<uuid> format)")]
72 id: String,
73 },
74 #[command(about = "Create a new task")]
75 Create {
76 #[arg(long, help = "Task title (required)")]
77 title: String,
78 #[arg(long, help = "Task description/notes")]
79 note: Option<String>,
80 #[arg(long, value_enum, help = "Task priority: high, normal, or low")]
81 priority: Option<Priority>,
82 #[arg(long, help = "Assign to project (P-<uuid>)")]
83 project_id: Option<String>,
84 #[arg(long, help = "Parent task ID for subtasks (T-<uuid>)")]
85 parent: Option<String>,
86 #[arg(long, help = "Task group ID (Q-<uuid>)")]
87 group: Option<String>,
88 #[arg(long, help = "Deadline date (ISO 8601)")]
89 deadline: Option<String>,
90 #[arg(long, help = "Start date (ISO 8601)")]
91 start: Option<String>,
92 #[arg(long, value_delimiter = ',', help = "Comma-separated tag IDs")]
93 tags: Option<Vec<String>>,
94 },
95 #[command(about = "Update an existing task (only specified fields are changed)")]
96 Update {
97 #[arg(help = "Task ID to update (T-<uuid> format)")]
98 id: String,
99 #[arg(long, help = "New task title")]
100 title: Option<String>,
101 #[arg(long, help = "New task description/notes")]
102 note: Option<String>,
103 #[arg(long, value_enum, help = "New priority: high, normal, or low")]
104 priority: Option<Priority>,
105 #[arg(
106 long,
107 value_enum,
108 help = "Completion status: empty, checked, or cancelled"
109 )]
110 checked: Option<CheckedStatus>,
111 #[arg(long, help = "Move to project (P-<uuid>)")]
112 project_id: Option<String>,
113 #[arg(long, help = "New parent task ID (T-<uuid>)")]
114 parent: Option<String>,
115 #[arg(long, help = "New task group ID (Q-<uuid>)")]
116 group: Option<String>,
117 #[arg(long, help = "New deadline date (ISO 8601)")]
118 deadline: Option<String>,
119 #[arg(long, help = "New start date (ISO 8601)")]
120 start: Option<String>,
121 #[arg(
122 long,
123 value_delimiter = ',',
124 help = "Replace tags with comma-separated tag IDs"
125 )]
126 tags: Option<Vec<String>>,
127 },
128 #[command(about = "Delete a task by ID (soft-delete)")]
129 Delete {
130 #[arg(help = "Task ID to delete (T-<uuid> format)")]
131 id: String,
132 },
133}
134
135pub fn run(client: &ApiClient, cmd: TaskCmd, json: bool) -> Result<()> {
136 match cmd {
137 TaskCmd::List {
138 project_id,
139 parent,
140 start_from,
141 start_to,
142 max_count,
143 offset,
144 include_removed,
145 include_archived,
146 } => {
147 let mut query: Vec<(&str, String)> = Vec::new();
148 if let Some(ref v) = project_id {
149 query.push(("projectId", v.to_string()));
150 }
151 if let Some(ref v) = parent {
152 query.push(("parent", v.to_string()));
153 }
154 if let Some(ref v) = start_from {
155 query.push(("startDateFrom", v.to_string()));
156 }
157 if let Some(ref v) = start_to {
158 query.push(("startDateTo", v.to_string()));
159 }
160 if let Some(v) = max_count {
161 query.push(("maxCount", v.to_string()));
162 }
163 if let Some(v) = offset {
164 query.push(("offset", v.to_string()));
165 }
166 if include_removed {
167 query.push(("includeRemoved", "true".to_string()));
168 }
169 if include_archived {
170 query.push(("includeArchived", "true".to_string()));
171 }
172
173 if json {
174 let resp: serde_json::Value = client.get("/v2/task", &query)?;
175 println!("{}", serde_json::to_string_pretty(&resp)?);
176 } else {
177 let resp: TaskListResponse = client.get("/v2/task", &query)?;
178 if resp.tasks.is_empty() {
179 println!("No tasks found.");
180 } else {
181 println!("{}", Table::new(&resp.tasks));
182 }
183 }
184 }
185 TaskCmd::Get { id } => {
186 if json {
187 let resp: serde_json::Value = client.get(&format!("/v2/task/{}", id), &[])?;
188 println!("{}", serde_json::to_string_pretty(&resp)?);
189 } else {
190 let task: Task = client.get(&format!("/v2/task/{}", id), &[])?;
191 task.display_detail();
192 }
193 }
194 TaskCmd::Create {
195 title,
196 note,
197 priority,
198 project_id,
199 parent,
200 group,
201 deadline,
202 start,
203 tags,
204 } => {
205 let data = TaskCreate {
206 title,
207 note,
208 priority: priority.map(|p| p.as_i32()),
209 project_id,
210 parent,
211 group,
212 deadline,
213 start,
214 tags,
215 is_note: None,
216 };
217 if json {
218 let resp: serde_json::Value = client.post("/v2/task", &data)?;
219 println!("{}", serde_json::to_string_pretty(&resp)?);
220 } else {
221 let task: Task = client.post("/v2/task", &data)?;
222 println!("Created task {}", task.id);
223 }
224 }
225 TaskCmd::Update {
226 id,
227 title,
228 note,
229 priority,
230 checked,
231 project_id,
232 parent,
233 group,
234 deadline,
235 start,
236 tags,
237 } => {
238 let data = TaskUpdate {
239 title,
240 note,
241 priority: priority.map(|p| p.as_i32()),
242 checked: checked.map(|c| c.as_i32()),
243 project_id,
244 parent,
245 group,
246 deadline,
247 start,
248 tags,
249 is_note: None,
250 };
251 if json {
252 let resp: serde_json::Value = client.patch(&format!("/v2/task/{}", id), &data)?;
253 println!("{}", serde_json::to_string_pretty(&resp)?);
254 } else {
255 let task: Task = client.patch(&format!("/v2/task/{}", id), &data)?;
256 println!("Updated task {}", task.id);
257 }
258 }
259 TaskCmd::Delete { id } => {
260 client.delete(&format!("/v2/task/{}", id))?;
261 println!("Deleted task {}", id);
262 }
263 }
264 Ok(())
265}