1use clap::Subcommand;
2use crate::client::ClickUpClient;
3use crate::commands::auth::resolve_token;
4use crate::commands::workspace::resolve_workspace;
5use crate::error::CliError;
6use crate::output::OutputConfig;
7use crate::Cli;
8
9#[derive(Subcommand)]
10pub enum TimeCommands {
11 List {
13 #[arg(long)]
15 start_date: Option<String>,
16 #[arg(long)]
18 end_date: Option<String>,
19 #[arg(long)]
21 assignee: Option<String>,
22 #[arg(long)]
24 task: Option<String>,
25 },
26 Get {
28 id: String,
30 },
31 Current,
33 Create {
35 #[arg(long)]
37 start: String,
38 #[arg(long)]
40 duration: String,
41 #[arg(long)]
43 task: Option<String>,
44 #[arg(long)]
46 description: Option<String>,
47 #[arg(long)]
49 billable: bool,
50 },
51 Update {
53 id: String,
55 #[arg(long)]
57 start: Option<String>,
58 #[arg(long)]
60 end: Option<String>,
61 #[arg(long)]
63 description: Option<String>,
64 #[arg(long)]
66 billable: Option<bool>,
67 },
68 Delete {
70 id: String,
72 },
73 Start {
75 #[arg(long)]
77 task: Option<String>,
78 #[arg(long)]
80 description: Option<String>,
81 #[arg(long)]
83 billable: bool,
84 },
85 Stop,
87 Tags,
89 AddTags {
91 #[arg(long)]
93 entry_id: String,
94 #[arg(long = "tag")]
96 tags: Vec<String>,
97 },
98 RemoveTags {
100 #[arg(long)]
102 entry_id: String,
103 #[arg(long = "tag")]
105 tags: Vec<String>,
106 },
107 RenameTag {
109 #[arg(long)]
111 name: String,
112 #[arg(long)]
114 new_name: String,
115 },
116 History {
118 id: String,
120 },
121}
122
123const TIME_FIELDS: &[&str] = &["id", "task", "duration", "start", "billable"];
124
125fn flatten_task_field(mut entry: serde_json::Value) -> serde_json::Value {
127 if let Some(obj) = entry.as_object_mut() {
128 if let Some(task_val) = obj.get("task").cloned() {
129 if let Some(name) = task_val.get("name").and_then(|n| n.as_str()) {
130 obj.insert("task".to_string(), serde_json::Value::String(name.to_string()));
131 }
132 }
133 }
134 entry
135}
136
137pub async fn execute(command: TimeCommands, cli: &Cli) -> Result<(), CliError> {
138 let token = resolve_token(cli)?;
139 let client = ClickUpClient::new(&token, cli.timeout)?;
140 let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
141 let ws_id = resolve_workspace(cli)?;
142
143 match command {
144 TimeCommands::List {
145 start_date,
146 end_date,
147 assignee,
148 task,
149 } => {
150 let mut params = Vec::new();
151 if let Some(s) = start_date {
152 params.push(format!("start_date={}", s));
153 }
154 if let Some(e) = end_date {
155 params.push(format!("end_date={}", e));
156 }
157 if let Some(a) = assignee {
158 params.push(format!("assignee={}", a));
159 }
160 if let Some(t) = task {
161 params.push(format!("task_id={}", t));
162 }
163 let query = if params.is_empty() {
164 String::new()
165 } else {
166 format!("?{}", params.join("&"))
167 };
168 let resp = client
169 .get(&format!("/v2/team/{}/time_entries{}", ws_id, query))
170 .await?;
171 let entries: Vec<serde_json::Value> = resp
172 .get("data")
173 .and_then(|d| d.as_array())
174 .cloned()
175 .unwrap_or_default()
176 .into_iter()
177 .map(flatten_task_field)
178 .collect();
179 output.print_items(&entries, TIME_FIELDS, "id");
180 Ok(())
181 }
182 TimeCommands::Get { id } => {
183 let resp = client
184 .get(&format!("/v2/team/{}/time_entries/{}", ws_id, id))
185 .await?;
186 let entry = resp
187 .get("data")
188 .cloned()
189 .map(flatten_task_field)
190 .unwrap_or(resp);
191 output.print_single(&entry, TIME_FIELDS, "id");
192 Ok(())
193 }
194 TimeCommands::Current => {
195 let resp = client
196 .get(&format!("/v2/team/{}/time_entries/current", ws_id))
197 .await?;
198 let entry = resp
199 .get("data")
200 .cloned()
201 .map(flatten_task_field)
202 .unwrap_or(resp);
203 output.print_single(&entry, TIME_FIELDS, "id");
204 Ok(())
205 }
206 TimeCommands::Create {
207 start,
208 duration,
209 task,
210 description,
211 billable,
212 } => {
213 let mut body = serde_json::json!({
214 "start": start,
215 "duration": duration,
216 "billable": billable,
217 });
218 if let Some(t) = task {
219 body["tid"] = serde_json::Value::String(t);
220 }
221 if let Some(d) = description {
222 body["description"] = serde_json::Value::String(d);
223 }
224 let resp = client
225 .post(&format!("/v2/team/{}/time_entries", ws_id), &body)
226 .await?;
227 let entry = resp
228 .get("data")
229 .cloned()
230 .map(flatten_task_field)
231 .unwrap_or(resp);
232 output.print_single(&entry, TIME_FIELDS, "id");
233 Ok(())
234 }
235 TimeCommands::Update {
236 id,
237 start,
238 end,
239 description,
240 billable,
241 } => {
242 let mut body = serde_json::Map::new();
243 if let Some(s) = start {
244 body.insert("start".into(), serde_json::Value::String(s));
245 }
246 if let Some(e) = end {
247 body.insert("end".into(), serde_json::Value::String(e));
248 }
249 if let Some(d) = description {
250 body.insert("description".into(), serde_json::Value::String(d));
251 }
252 if let Some(b) = billable {
253 body.insert("billable".into(), serde_json::Value::Bool(b));
254 }
255 let resp = client
256 .put(
257 &format!("/v2/team/{}/time_entries/{}", ws_id, id),
258 &serde_json::Value::Object(body),
259 )
260 .await?;
261 let entry = resp
262 .get("data")
263 .cloned()
264 .map(flatten_task_field)
265 .unwrap_or(resp);
266 output.print_single(&entry, TIME_FIELDS, "id");
267 Ok(())
268 }
269 TimeCommands::Delete { id } => {
270 client
271 .delete(&format!("/v2/team/{}/time_entries/{}", ws_id, id))
272 .await?;
273 output.print_message(&format!("Time entry {} deleted", id));
274 Ok(())
275 }
276 TimeCommands::Start {
277 task,
278 description,
279 billable,
280 } => {
281 let mut body = serde_json::json!({ "billable": billable });
282 if let Some(t) = task {
283 body["tid"] = serde_json::Value::String(t);
284 }
285 if let Some(d) = description {
286 body["description"] = serde_json::Value::String(d);
287 }
288 let resp = client
289 .post(&format!("/v2/team/{}/time_entries/start", ws_id), &body)
290 .await?;
291 let entry = resp
292 .get("data")
293 .cloned()
294 .map(flatten_task_field)
295 .unwrap_or(resp);
296 output.print_single(&entry, TIME_FIELDS, "id");
297 Ok(())
298 }
299 TimeCommands::Stop => {
300 let body = serde_json::json!({});
301 let resp = client
302 .post(&format!("/v2/team/{}/time_entries/stop", ws_id), &body)
303 .await?;
304 let entry = resp
305 .get("data")
306 .cloned()
307 .map(flatten_task_field)
308 .unwrap_or(resp);
309 output.print_single(&entry, TIME_FIELDS, "id");
310 Ok(())
311 }
312 TimeCommands::Tags => {
313 let resp = client
314 .get(&format!("/v2/team/{}/time_entries/tags", ws_id))
315 .await?;
316 let tags = resp
317 .get("data")
318 .and_then(|d| d.as_array())
319 .cloned()
320 .unwrap_or_default();
321 output.print_items(&tags, &["name"], "name");
322 Ok(())
323 }
324 TimeCommands::AddTags { entry_id, tags } => {
325 let tag_objects: Vec<serde_json::Value> = tags
326 .iter()
327 .map(|n| serde_json::json!({ "name": n }))
328 .collect();
329 let body = serde_json::json!({
330 "time_entry_ids": [entry_id],
331 "tags": tag_objects,
332 });
333 client
334 .post(&format!("/v2/team/{}/time_entries/tags", ws_id), &body)
335 .await?;
336 output.print_message("Tags added");
337 Ok(())
338 }
339 TimeCommands::RemoveTags { entry_id, tags } => {
340 let tag_objects: Vec<serde_json::Value> = tags
341 .iter()
342 .map(|n| serde_json::json!({ "name": n }))
343 .collect();
344 let body = serde_json::json!({
345 "time_entry_ids": [entry_id],
346 "tags": tag_objects,
347 });
348 client
349 .delete_with_body(&format!("/v2/team/{}/time_entries/tags", ws_id), &body)
350 .await?;
351 output.print_message("Tags removed");
352 Ok(())
353 }
354 TimeCommands::RenameTag { name, new_name } => {
355 let body = serde_json::json!({
356 "name": name,
357 "new_name": new_name,
358 });
359 client
360 .put(&format!("/v2/team/{}/time_entries/tags", ws_id), &body)
361 .await?;
362 output.print_message(&format!("Tag '{}' renamed to '{}'", name, new_name));
363 Ok(())
364 }
365 TimeCommands::History { id } => {
366 let resp = client
367 .get(&format!("/v2/team/{}/time_entries/{}/history", ws_id, id))
368 .await?;
369 let history = resp
370 .get("data")
371 .and_then(|d| d.as_array())
372 .cloned()
373 .unwrap_or_default();
374 output.print_items(&history, &["id", "date", "field", "before", "after"], "id");
375 Ok(())
376 }
377 }
378}