kanban-cli 0.3.1

Command-line interface for the kanban project management tool
use crate::cli::{SprintAction, SprintUpdateArgs};
use crate::context::CliContext;
use crate::output;
use kanban_core::{resolve_page_params, PaginatedList};
use kanban_domain::{FieldUpdate, KanbanOperations, SprintUpdate};

fn parse_datetime(s: &str) -> Result<chrono::DateTime<chrono::Utc>, String> {
    chrono::DateTime::parse_from_rfc3339(s)
        .map(|dt| dt.with_timezone(&chrono::Utc))
        .or_else(|_| {
            chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
                .map_err(|_| ())
                .and_then(|d| d.and_hms_opt(0, 0, 0).ok_or(()))
                .map(|dt| dt.and_utc())
        })
        .map_err(|_| {
            format!(
                "Invalid date '{}'. Supported formats: YYYY-MM-DD or RFC 3339 (e.g., 2024-01-15T10:30:00Z)",
                s
            )
        })
}

pub async fn handle(ctx: &mut CliContext, action: SprintAction) -> anyhow::Result<()> {
    match action {
        SprintAction::Create {
            board_id,
            prefix,
            name,
        } => {
            let sprint = ctx.create_sprint(board_id, prefix, name)?;
            ctx.save().await?;
            output::output_success(&sprint);
        }
        SprintAction::List {
            board_id,
            page,
            page_size,
        } => {
            let sprints = ctx.list_sprints(board_id)?;
            let (page, page_size) = resolve_page_params(page, page_size)?;
            output::output_success(PaginatedList::paginate(sprints, page, page_size)?);
        }
        SprintAction::Get { id } => match ctx.get_sprint(id)? {
            Some(sprint) => output::output_success(&sprint),
            None => return output::output_error(&format!("Sprint not found: {}", id)),
        },
        SprintAction::Update(args) => {
            let sprint = match handle_update(ctx, args).await {
                Ok(s) => s,
                Err(e) => return output::output_error(&e.to_string()),
            };
            output::output_success(&sprint);
        }
        SprintAction::Activate { id, duration_days } => {
            let sprint = ctx.activate_sprint(id, duration_days)?;
            ctx.save().await?;
            output::output_success(&sprint);
        }
        SprintAction::Complete { id } => {
            let sprint = ctx.complete_sprint(id)?;
            ctx.save().await?;
            output::output_success(&sprint);
        }
        SprintAction::Cancel { id } => {
            let sprint = ctx.cancel_sprint(id)?;
            ctx.save().await?;
            output::output_success(&sprint);
        }
        SprintAction::Delete { id } => {
            ctx.delete_sprint(id)?;
            ctx.save().await?;
            output::output_success(serde_json::json!({"deleted": id.to_string()}));
        }
    }
    Ok(())
}

async fn handle_update(
    ctx: &mut CliContext,
    args: SprintUpdateArgs,
) -> anyhow::Result<kanban_domain::Sprint> {
    let start_date = if args.clear_start_date {
        FieldUpdate::Clear
    } else {
        match args.start_date {
            Some(d) => FieldUpdate::Set(parse_datetime(&d).map_err(anyhow::Error::msg)?),
            None => FieldUpdate::NoChange,
        }
    };

    let end_date = if args.clear_end_date {
        FieldUpdate::Clear
    } else {
        match args.end_date {
            Some(d) => FieldUpdate::Set(parse_datetime(&d).map_err(anyhow::Error::msg)?),
            None => FieldUpdate::NoChange,
        }
    };

    let updates = SprintUpdate {
        name: args.name,
        name_index: FieldUpdate::NoChange,
        prefix: args
            .prefix
            .map(FieldUpdate::Set)
            .unwrap_or(FieldUpdate::NoChange),
        card_prefix: args
            .card_prefix
            .map(FieldUpdate::Set)
            .unwrap_or(FieldUpdate::NoChange),
        status: None,
        start_date,
        end_date,
    };
    let sprint = ctx.update_sprint(args.id, updates)?;
    ctx.save().await?;
    Ok(sprint)
}