Skip to main content

raps_cli/commands/admin/
project.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Project management and company listing command implementations
5
6use 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
49/// Execute company listing for an account
50pub(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                // Build filter expression from individual flags
163                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                // Create admin client
202                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                // List all projects
210                let all_projects = admin_client.list_all_projects(&account_id).await?;
211
212                // Apply filter
213                let mut filtered_projects = project_filter.apply(all_projects);
214
215                // Apply limit
216                if let Some(l) = limit {
217                    filtered_projects.truncate(l);
218                }
219
220                // Build output
221                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}