use anyhow::Result;
use owo_colors::OwoColorize;
use crate::api::client::ApiClient;
use crate::output::{self, Format};
use crate::render::table::{Cell, Table};
use crate::render::theme;
use crate::render::time;
fn role_label(role: &str) -> &str {
match role {
"admin" => "administrador",
_ => "miembro",
}
}
fn status_label(status: &str) -> &str {
match status {
"pending" => "pendiente",
"accepted" => "aceptada",
_ => "caducada",
}
}
pub async fn list(client: &ApiClient, format: Format) -> Result<()> {
let workspaces = client.workspaces().await?;
if format.is_json() {
output::emit(&workspaces);
return Ok(());
}
let mut table = Table::new(&["id", "espacio", "slug", "tu papel"]);
for workspace in &workspaces {
table.push(vec![
Cell::styled(workspace.id.clone(), theme::muted()),
Cell::styled(workspace.name.clone(), theme::strong()),
Cell::plain(workspace.slug.clone()),
Cell::plain(
workspace
.role
.as_deref()
.map(role_label)
.unwrap_or("—")
.to_string(),
),
]);
}
if table.is_empty() {
println!("Todavía no hay ningún espacio.");
return Ok(());
}
table.print();
Ok(())
}
fn format_euros(micros: i64) -> String {
let euros = micros as f64 / 1e6;
if micros == 0 {
return "0 €".to_string();
}
if euros.abs() < 0.01 {
return format!("{euros:.4} €");
}
format!("{euros:.2} €")
}
pub async fn credits(client: &ApiClient, reference: Option<&str>, format: Format) -> Result<()> {
let workspace = client.find_workspace(reference).await?;
let credits = client.credits(&workspace.id).await?;
if format.is_json() {
output::emit(&credits);
return Ok(());
}
println!();
println!(" {}", workspace.name.style(theme::strong()));
println!();
println!(
" {} {}",
"saldo ".style(theme::muted()),
format_euros(credits.balance_micros).style(theme::strong()),
);
println!(
" {} {}",
"recargado".style(theme::muted()),
format_euros(credits.added_micros),
);
println!(
" {} {}",
"gastado ".style(theme::muted()),
format_euros(credits.spent_micros),
);
if !credits.may_run_agent {
println!();
println!(
" {}",
"Sin saldo: los agentes de este espacio están parados.".style(theme::warning()),
);
} else if credits.running_low {
println!();
println!(
" {}",
format!(
"Queda el {} % del crédito.",
credits.remaining_percent.unwrap_or(0)
)
.style(theme::warning()),
);
}
if credits.history.is_empty() {
println!();
return Ok(());
}
println!();
let mut table = Table::new(&["cuándo", "concepto", "importe"]);
for entry in &credits.history {
let amount = format!(
"{}{}",
if entry.amount_micros >= 0 { "+" } else { "−" },
format_euros(entry.amount_micros.abs())
);
table.push(vec![
Cell::styled(
time::relative_time(&entry.created_at).unwrap_or_else(|| entry.created_at.clone()),
theme::muted(),
),
Cell::plain(entry.detail.clone()),
Cell::styled(amount, theme::strong()),
]);
}
table.print();
Ok(())
}
pub async fn members(client: &ApiClient, reference: Option<&str>, format: Format) -> Result<()> {
let workspace = client.find_workspace(reference).await?;
if format.is_json() {
let (members, invitations) = tokio::join!(
client.members(&workspace.id),
client.invitations(&workspace.id)
);
output::emit(&serde_json::json!({
"workspace": workspace,
"members": members?,
"invitations": invitations?,
}));
return Ok(());
}
println!();
println!(" {}", workspace.name.style(theme::strong()));
println!();
let members = client.members(&workspace.id).await?;
let mut table = Table::new(&["usuario", "github", "correo", "papel", "visto"]);
for member in &members {
table.push(vec![
Cell::styled(
member
.display_name
.clone()
.unwrap_or_else(|| member.user_id.clone()),
theme::strong(),
),
Cell::plain(
member
.github_login
.as_ref()
.map(|login| format!("@{login}"))
.unwrap_or_else(|| "—".to_string()),
),
Cell::plain(member.email.clone().unwrap_or_else(|| "—".to_string())),
Cell::plain(role_label(&member.role).to_string()),
Cell::styled(
member
.last_seen_at
.as_deref()
.and_then(time::relative_time)
.unwrap_or_else(|| "—".to_string()),
theme::muted(),
),
]);
}
table.print();
let pending: Vec<_> = client
.invitations(&workspace.id)
.await?
.into_iter()
.filter(|invitation| invitation.status == "pending")
.collect();
if pending.is_empty() {
return Ok(());
}
println!();
println!(" {}", "Invitaciones pendientes".style(theme::muted()));
println!();
let mut invitations = Table::new(&["id", "correo", "papel", "estado"]);
for invitation in &pending {
invitations.push(vec![
Cell::styled(invitation.id.clone(), theme::muted()),
Cell::styled(invitation.email.clone(), theme::strong()),
Cell::plain(role_label(&invitation.role).to_string()),
Cell::plain(status_label(&invitation.status).to_string()),
]);
}
invitations.print();
Ok(())
}
pub async fn invite(
client: &ApiClient,
reference: Option<&str>,
email: &str,
admin: bool,
) -> Result<()> {
let workspace = client.find_workspace(reference).await?;
let role = if admin { "admin" } else { "member" };
let sent = client.invite(&workspace.id, email, role).await?;
println!();
println!(
" {} Invitación enviada a {} como {}",
theme::CHECK.style(theme::success()),
email.style(theme::strong()),
role_label(role),
);
println!(" {}", sent.accept_url.style(theme::muted()));
println!();
Ok(())
}
pub async fn revoke(
client: &ApiClient,
reference: Option<&str>,
invitation_id: &str,
) -> Result<()> {
let workspace = client.find_workspace(reference).await?;
client
.revoke_invitation(&workspace.id, invitation_id)
.await?;
println!(
" {} Invitación retirada",
theme::CHECK.style(theme::success())
);
Ok(())
}
pub async fn remove(client: &ApiClient, reference: Option<&str>, user_id: &str) -> Result<()> {
let workspace = client.find_workspace(reference).await?;
client.remove_member(&workspace.id, user_id).await?;
println!(
" {} {user_id} ya no está en {}",
theme::CHECK.style(theme::success()),
workspace.name.style(theme::strong()),
);
Ok(())
}