1use anyhow::Result;
7use colored::Colorize;
8use serde::Serialize;
9
10use raps_acc::admin::{AccountAdminClient, CreateProjectRequest, UpdateProjectRequest};
11use raps_acc::types::ProjectClassification;
12use raps_admin::ProjectFilter;
13use raps_kernel::auth::AuthClient;
14use raps_kernel::config::Config;
15use raps_kernel::http::HttpClientConfig;
16
17use crate::output::OutputFormat;
18
19use super::{AdminProjectCommands, get_account_id};
20
21#[derive(Serialize)]
22struct ProjectListOutput {
23 id: String,
24 name: String,
25 status: String,
26 platform: String,
27 created_at: Option<String>,
28}
29
30pub(crate) fn format_project_status(status: &str) -> String {
31 match status.to_lowercase().as_str() {
32 "active" => status.green().to_string(),
33 "inactive" => status.yellow().to_string(),
34 "archived" => status.dimmed().to_string(),
35 _ => status.to_string(),
36 }
37}
38
39#[derive(Serialize)]
40struct CompanyListOutput {
41 id: String,
42 name: String,
43 trade: Option<String>,
44 city: Option<String>,
45 country: Option<String>,
46 member_count: Option<usize>,
47}
48
49pub(crate) async fn execute_company_list(
51 config: &Config,
52 auth_client: &AuthClient,
53 account: Option<String>,
54 output_format: OutputFormat,
55) -> Result<()> {
56 let account_id = get_account_id(account)?;
57
58 if output_format.supports_colors() {
59 println!(
60 "\n{} List companies in account {}",
61 "\u{2192}".cyan(),
62 account_id.cyan()
63 );
64 println!();
65 }
66
67 let http_config = HttpClientConfig::default();
68 let admin_client =
69 AccountAdminClient::new_with_http_config(config.clone(), auth_client.clone(), http_config);
70
71 let companies = admin_client.list_companies(&account_id).await?;
72
73 let outputs: Vec<CompanyListOutput> = companies
74 .iter()
75 .map(|c| CompanyListOutput {
76 id: c.id.clone(),
77 name: c.name.clone(),
78 trade: c.trade.clone(),
79 city: c.city.clone(),
80 country: c.country.clone(),
81 member_count: c.member_count,
82 })
83 .collect();
84
85 match output_format {
86 OutputFormat::Table => {
87 if outputs.is_empty() {
88 println!("{}", "No companies found.".yellow());
89 } else {
90 println!("{}", "Companies:".bold());
91 println!("{}", "\u{2500}".repeat(110));
92 println!(
93 "{:<38} {:<25} {:<15} {:<15} {:<10} {}",
94 "ID".bold(),
95 "Name".bold(),
96 "Trade".bold(),
97 "City".bold(),
98 "Country".bold(),
99 "Members".bold()
100 );
101 println!("{}", "\u{2500}".repeat(110));
102
103 for c in &outputs {
104 let name_truncated = if c.name.len() > 23 {
105 format!("{}...", &c.name[..20])
106 } else {
107 c.name.clone()
108 };
109 let trade_display = c.trade.as_deref().unwrap_or("-");
110 let trade_truncated = if trade_display.len() > 13 {
111 format!("{}...", &trade_display[..10])
112 } else {
113 trade_display.to_string()
114 };
115 let city_display = c.city.as_deref().unwrap_or("-");
116 let country_display = c.country.as_deref().unwrap_or("-");
117 let members_display = c
118 .member_count
119 .map(|m| m.to_string())
120 .unwrap_or_else(|| "-".to_string());
121
122 println!(
123 "{:<38} {:<25} {:<15} {:<15} {:<10} {}",
124 c.id.cyan(),
125 name_truncated,
126 trade_truncated,
127 city_display,
128 country_display,
129 members_display.dimmed()
130 );
131 }
132
133 println!("{}", "\u{2500}".repeat(110));
134 println!("{} {} company(ies) found", "\u{2192}".cyan(), outputs.len());
135 }
136 }
137 _ => {
138 output_format.write(&outputs)?;
139 }
140 }
141
142 Ok(())
143}
144
145impl AdminProjectCommands {
146 pub async fn execute(
147 self,
148 config: &Config,
149 auth_client: &AuthClient,
150 output_format: OutputFormat,
151 ) -> Result<()> {
152 match self {
153 AdminProjectCommands::List {
154 account,
155 filter,
156 status,
157 platform,
158 limit,
159 } => {
160 let account_id = get_account_id(account)?;
161
162 let mut filter_parts = Vec::new();
164 if let Some(f) = &filter {
165 filter_parts.push(f.clone());
166 }
167 if let Some(s) = &status {
168 filter_parts.push(format!("status:{}", s));
169 }
170 if platform != "all" {
171 filter_parts.push(format!("platform:{}", platform));
172 }
173
174 let filter_expr = if filter_parts.is_empty() {
175 None
176 } else {
177 Some(filter_parts.join(","))
178 };
179
180 let project_filter = if let Some(ref expr) = filter_expr {
181 ProjectFilter::from_expression(expr)?
182 } else {
183 ProjectFilter::new()
184 };
185
186 if output_format.supports_colors() {
187 println!(
188 "\n{} List projects in account {}",
189 "\u{2192}".cyan(),
190 account_id.cyan()
191 );
192 if let Some(ref expr) = filter_expr {
193 println!(" Filter: {}", expr);
194 }
195 if let Some(l) = limit {
196 println!(" Limit: {}", l);
197 }
198 println!();
199 }
200
201 let http_config = HttpClientConfig::default();
203 let admin_client = AccountAdminClient::new_with_http_config(
204 config.clone(),
205 auth_client.clone(),
206 http_config,
207 );
208
209 let all_projects = admin_client.list_all_projects(&account_id).await?;
211
212 let mut filtered_projects = project_filter.apply(all_projects);
214
215 if let Some(l) = limit {
217 filtered_projects.truncate(l);
218 }
219
220 let outputs: Vec<ProjectListOutput> = filtered_projects
222 .iter()
223 .map(|p| ProjectListOutput {
224 id: p.id.clone(),
225 name: p.name.clone(),
226 status: p.status.clone().unwrap_or_else(|| "unknown".to_string()),
227 platform: if p.is_acc() {
228 "acc".to_string()
229 } else if p.is_bim360() {
230 "bim360".to_string()
231 } else {
232 "unknown".to_string()
233 },
234 created_at: p.created_at.map(|d| d.to_rfc3339()),
235 })
236 .collect();
237
238 match output_format {
239 OutputFormat::Table => {
240 if outputs.is_empty() {
241 println!("{}", "No projects found matching the filter.".yellow());
242 } else {
243 println!("{}", "Projects:".bold());
244 println!("{}", "\u{2500}".repeat(100));
245 println!(
246 "{:<38} {:<30} {:<10} {:<10} {}",
247 "ID".bold(),
248 "Name".bold(),
249 "Status".bold(),
250 "Platform".bold(),
251 "Created".bold()
252 );
253 println!("{}", "\u{2500}".repeat(100));
254
255 for p in &outputs {
256 let created = p.created_at.as_deref().unwrap_or("-");
257 let name_truncated = if p.name.len() > 28 {
258 format!("{}...", &p.name[..25])
259 } else {
260 p.name.clone()
261 };
262 println!(
263 "{:<38} {:<30} {:<10} {:<10} {}",
264 p.id.cyan(),
265 name_truncated,
266 format_project_status(&p.status),
267 p.platform,
268 created.dimmed()
269 );
270 }
271
272 println!("{}", "\u{2500}".repeat(100));
273 println!("{} {} project(s) found", "\u{2192}".cyan(), outputs.len());
274 }
275 }
276 _ => {
277 output_format.write(&outputs)?;
278 }
279 }
280
281 Ok(())
282 }
283 AdminProjectCommands::Create {
284 account,
285 name,
286 r#type,
287 classification,
288 start_date,
289 end_date,
290 timezone,
291 } => {
292 let account_id = get_account_id(account)?;
293
294 if output_format.supports_colors() {
295 println!(
296 "\n{} Creating project '{}' in account {}",
297 "\u{2192}".cyan(),
298 name.cyan(),
299 account_id.cyan()
300 );
301 }
302
303 let parsed_classification = if let Some(ref cls) = classification {
304 Some(match cls.to_lowercase().as_str() {
305 "production" => ProjectClassification::Production,
306 "template" => ProjectClassification::Template,
307 "component" => ProjectClassification::Component,
308 "sample" => ProjectClassification::Sample,
309 _ => anyhow::bail!(
310 "Invalid classification '{}'. Valid values: production, template, component, sample",
311 cls
312 ),
313 })
314 } else {
315 None
316 };
317
318 let request = CreateProjectRequest {
319 name: name.clone(),
320 r#type,
321 classification: parsed_classification,
322 start_date,
323 end_date,
324 timezone,
325 ..Default::default()
326 };
327
328 let http_config = HttpClientConfig::default();
329 let admin_client = AccountAdminClient::new_with_http_config(
330 config.clone(),
331 auth_client.clone(),
332 http_config,
333 );
334
335 let project = admin_client.create_project(&account_id, request).await?;
336
337 match output_format {
338 OutputFormat::Table => {
339 println!(
340 "\n{} Project created successfully!",
341 "\u{2713}".green().bold()
342 );
343 println!("{:<15} {}", "ID:".bold(), project.id.cyan());
344 println!("{:<15} {}", "Name:".bold(), project.name);
345 println!(
346 "{:<15} {}",
347 "Status:".bold(),
348 project.status.as_deref().unwrap_or("pending")
349 );
350 }
351 _ => {
352 output_format.write(&serde_json::json!({
353 "id": project.id,
354 "name": project.name,
355 "status": project.status,
356 "created": true
357 }))?;
358 }
359 }
360
361 Ok(())
362 }
363 AdminProjectCommands::Update {
364 account,
365 project,
366 name,
367 status,
368 start_date,
369 end_date,
370 } => {
371 let account_id = get_account_id(account)?;
372
373 if output_format.supports_colors() {
374 println!(
375 "\n{} Updating project {} in account {}",
376 "\u{2192}".cyan(),
377 project.cyan(),
378 account_id.cyan()
379 );
380 }
381
382 let request = UpdateProjectRequest {
383 name,
384 status,
385 start_date,
386 end_date,
387 ..Default::default()
388 };
389
390 let http_config = HttpClientConfig::default();
391 let admin_client = AccountAdminClient::new_with_http_config(
392 config.clone(),
393 auth_client.clone(),
394 http_config,
395 );
396
397 let updated = admin_client
398 .update_project(&account_id, &project, request)
399 .await?;
400
401 match output_format {
402 OutputFormat::Table => {
403 println!(
404 "\n{} Project updated successfully!",
405 "\u{2713}".green().bold()
406 );
407 println!("{:<15} {}", "ID:".bold(), updated.id.cyan());
408 println!("{:<15} {}", "Name:".bold(), updated.name);
409 println!(
410 "{:<15} {}",
411 "Status:".bold(),
412 updated.status.as_deref().unwrap_or("-")
413 );
414 }
415 _ => {
416 output_format.write(&serde_json::json!({
417 "id": updated.id,
418 "name": updated.name,
419 "status": updated.status,
420 "updated": true
421 }))?;
422 }
423 }
424
425 Ok(())
426 }
427 AdminProjectCommands::Archive { account, project } => {
428 let account_id = get_account_id(account)?;
429
430 if output_format.supports_colors() {
431 println!(
432 "\n{} Archiving project {} in account {}",
433 "\u{2192}".cyan(),
434 project.cyan(),
435 account_id.cyan()
436 );
437 }
438
439 let http_config = HttpClientConfig::default();
440 let admin_client = AccountAdminClient::new_with_http_config(
441 config.clone(),
442 auth_client.clone(),
443 http_config,
444 );
445
446 admin_client.archive_project(&account_id, &project).await?;
447
448 match output_format {
449 OutputFormat::Table => {
450 println!(
451 "\n{} Project archived successfully!",
452 "\u{2713}".green().bold()
453 );
454 println!("{:<15} {}", "ID:".bold(), project.cyan());
455 }
456 _ => {
457 output_format.write(&serde_json::json!({
458 "id": project,
459 "archived": true
460 }))?;
461 }
462 }
463
464 Ok(())
465 }
466 }
467 }
468}