use axum::extract::State; use axum::Json; use cm_domain::{Agent, User}; use serde_json::{json, Value}; use crate::{ApiError, AppState, Authed}; /// Members table for the Team page (§8.3). pub async fn members( State(state): State, Authed(user): Authed, ) -> Result>, ApiError> { let members = cm_db::repo::users::list_by_workspace(&state.pool, user.workspace_id).await?; Ok(Json(members)) } /// The left-rail agent roster (§4). pub async fn claws( State(state): State, Authed(user): Authed, ) -> Result>, ApiError> { let roster = cm_db::repo::agents::roster(&state.pool, user.workspace_id).await?; Ok(Json(roster)) } /// Available credit balance for the Credits page (§8.4). pub async fn credits( State(state): State, Authed(user): Authed, ) -> Result, ApiError> { let available = cm_db::repo::credits::balance(&state.pool, user.workspace_id).await?; Ok(Json(json!({ "available": available }))) } /// What the caller may do, derived from their role (§16 RBAC). pub async fn permissions(Authed(user): Authed) -> Json { let owner = user.role.is_owner(); Json(json!({ "role": user.role, "can_manage_team": owner, "can_manage_billing": owner, })) } /// Org chart for the Team page (R4): each member with the claws they /// manage (`agents.managed_by`). Only members who manage ≥1 claw appear. pub async fn orgchart( State(state): State, Authed(user): Authed, ) -> Result, ApiError> { let rows = sqlx::query!( r#"SELECT u.id AS user_id, u.display_name, u.role, u.email, a.id AS claw_id, a.name AS claw_name, a.job_title, a.accent FROM users u JOIN agents a ON a.managed_by = u.id WHERE u.workspace_id = $1 ORDER BY u.display_name, a.name"#, user.workspace_id.as_uuid(), ) .fetch_all(&state.pool) .await?; let mut chart: Vec = Vec::new(); for row in rows { let claw = json!({ "id": row.claw_id, "name": row.claw_name, "job_title": row.job_title, "accent": row.accent, }); match chart .iter_mut() .find(|node| node["user"]["id"] == json!(row.user_id)) { Some(node) => node["claws"].as_array_mut().unwrap().push(claw), None => chart.push(json!({ "user": { "id": row.user_id, "display_name": row.display_name, "role": row.role, "email": row.email, }, "claws": [claw], })), } } Ok(Json(Value::Array(chart))) } /// Leaderboard for the Team page (R4): every claw ranked by its rolled-up /// `usage_events` (credits desc), zeros included. pub async fn leaderboard( State(state): State, Authed(user): Authed, ) -> Result, ApiError> { let rows = sqlx::query!( r#"SELECT a.id, a.name, a.accent, COALESCE(SUM(u.credits), 0)::BIGINT AS "credits!", COALESCE(SUM(u.tokens_in + u.tokens_out), 0)::BIGINT AS "tokens!", COUNT(u.id)::BIGINT AS "runs!" FROM agents a LEFT JOIN usage_events u ON u.agent_id = a.id WHERE a.workspace_id = $1 GROUP BY a.id, a.name, a.accent ORDER BY "credits!" DESC, "tokens!" DESC, a.name"#, user.workspace_id.as_uuid(), ) .fetch_all(&state.pool) .await?; let board: Vec = rows .into_iter() .map(|r| { json!({ "id": r.id, "name": r.name, "accent": r.accent, "credits": r.credits, "tokens": r.tokens, "runs": r.runs, }) }) .collect(); Ok(Json(Value::Array(board))) }