1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum GoalCommands {
11 List {
13 #[arg(long)]
15 include_completed: bool,
16 },
17 Get {
19 id: String,
21 },
22 Create {
24 #[arg(long)]
26 name: String,
27 #[arg(long)]
29 due_date: String,
30 #[arg(long)]
32 description: String,
33 #[arg(long)]
35 color: Option<String>,
36 #[arg(long)]
38 owner: Option<String>,
39 },
40 Update {
42 id: String,
44 #[arg(long)]
46 name: Option<String>,
47 #[arg(long)]
49 due_date: Option<String>,
50 #[arg(long)]
52 description: Option<String>,
53 #[arg(long)]
55 color: Option<String>,
56 #[arg(long)]
58 add_owner: Option<String>,
59 #[arg(long)]
61 rem_owner: Option<String>,
62 },
63 Delete {
65 id: String,
67 },
68 AddKr {
70 goal_id: String,
72 #[arg(long)]
74 name: String,
75 #[arg(long, name = "type")]
77 kr_type: String,
78 #[arg(long)]
80 steps_start: i64,
81 #[arg(long)]
83 steps_end: i64,
84 #[arg(long)]
86 unit: Option<String>,
87 #[arg(long)]
89 owner: Option<String>,
90 },
91 UpdateKr {
93 kr_id: String,
95 #[arg(long)]
97 steps_current: i64,
98 #[arg(long)]
100 note: Option<String>,
101 },
102 DeleteKr {
104 kr_id: String,
106 },
107}
108
109const GOAL_FIELDS: &[&str] = &["id", "name", "percent_completed", "due_date"];
110
111pub async fn execute(command: GoalCommands, cli: &Cli) -> Result<(), CliError> {
112 let token = resolve_token(cli)?;
113 let client = ClickUpClient::new(&token, cli.timeout)?;
114 let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
115
116 match command {
117 GoalCommands::List { include_completed } => {
118 let ws_id = resolve_workspace(cli)?;
119 let resp = client
120 .get(&format!(
121 "/v2/team/{}/goal?include_completed={}",
122 ws_id, include_completed
123 ))
124 .await?;
125 let goals = resp
126 .get("goals")
127 .and_then(|g| g.as_array())
128 .cloned()
129 .unwrap_or_default();
130 output.print_items(&goals, GOAL_FIELDS, "id");
131 Ok(())
132 }
133 GoalCommands::Get { id } => {
134 let resp = client.get(&format!("/v2/goal/{}", id)).await?;
135 let goal = resp.get("goal").cloned().unwrap_or(resp);
136 output.print_single(&goal, GOAL_FIELDS, "id");
137 Ok(())
138 }
139 GoalCommands::Create {
140 name,
141 due_date,
142 description,
143 color,
144 owner,
145 } => {
146 let ws_id = resolve_workspace(cli)?;
147 let mut body = serde_json::json!({
148 "name": name,
149 "due_date": due_date,
150 "description": description,
151 });
152 if let Some(c) = color {
153 body["color"] = serde_json::Value::String(c);
154 }
155 if let Some(o) = owner {
156 body["owners"] = serde_json::json!([o]);
157 }
158 let resp = client
159 .post(&format!("/v2/team/{}/goal", ws_id), &body)
160 .await?;
161 let goal = resp.get("goal").cloned().unwrap_or(resp);
162 output.print_single(&goal, GOAL_FIELDS, "id");
163 Ok(())
164 }
165 GoalCommands::Update {
166 id,
167 name,
168 due_date,
169 description,
170 color,
171 add_owner,
172 rem_owner,
173 } => {
174 let mut body = serde_json::Map::new();
175 if let Some(n) = name {
176 body.insert("name".into(), serde_json::Value::String(n));
177 }
178 if let Some(d) = due_date {
179 body.insert("due_date".into(), serde_json::Value::String(d));
180 }
181 if let Some(d) = description {
182 body.insert("description".into(), serde_json::Value::String(d));
183 }
184 if let Some(c) = color {
185 body.insert("color".into(), serde_json::Value::String(c));
186 }
187 if let Some(o) = add_owner {
188 body.insert("add_owners".into(), serde_json::json!([o]));
189 }
190 if let Some(o) = rem_owner {
191 body.insert("rem_owners".into(), serde_json::json!([o]));
192 }
193 let resp = client
194 .put(
195 &format!("/v2/goal/{}", id),
196 &serde_json::Value::Object(body),
197 )
198 .await?;
199 let goal = resp.get("goal").cloned().unwrap_or(resp);
200 output.print_single(&goal, GOAL_FIELDS, "id");
201 Ok(())
202 }
203 GoalCommands::Delete { id } => {
204 client.delete(&format!("/v2/goal/{}", id)).await?;
205 output.print_message(&format!("Goal {} deleted", id));
206 Ok(())
207 }
208 GoalCommands::AddKr {
209 goal_id,
210 name,
211 kr_type,
212 steps_start,
213 steps_end,
214 unit,
215 owner,
216 } => {
217 let mut body = serde_json::json!({
218 "name": name,
219 "type": kr_type,
220 "steps_start": steps_start,
221 "steps_end": steps_end,
222 });
223 if let Some(u) = unit {
224 body["unit"] = serde_json::Value::String(u);
225 }
226 if let Some(o) = owner {
227 body["owners"] = serde_json::json!([o]);
228 }
229 let resp = client
230 .post(&format!("/v2/goal/{}/key_result", goal_id), &body)
231 .await?;
232 let kr = resp.get("key_result").cloned().unwrap_or(resp);
233 output.print_single(
234 &kr,
235 &["id", "name", "type", "steps_start", "steps_end"],
236 "id",
237 );
238 Ok(())
239 }
240 GoalCommands::UpdateKr {
241 kr_id,
242 steps_current,
243 note,
244 } => {
245 let mut body = serde_json::json!({ "steps_current": steps_current });
246 if let Some(n) = note {
247 body["note"] = serde_json::Value::String(n);
248 }
249 let resp = client
250 .put(&format!("/v2/key_result/{}", kr_id), &body)
251 .await?;
252 let kr = resp.get("key_result").cloned().unwrap_or(resp);
253 output.print_single(&kr, &["id", "name", "steps_current", "steps_end"], "id");
254 Ok(())
255 }
256 GoalCommands::DeleteKr { kr_id } => {
257 client.delete(&format!("/v2/key_result/{}", kr_id)).await?;
258 output.print_message(&format!("Key result {} deleted", kr_id));
259 Ok(())
260 }
261 }
262}