kanbars 0.3.0

Lightning-fast terminal kanban board for JIRA
#[derive(Debug, Clone)]
pub struct Ticket {
    pub key: String,
    pub ticket_type: TicketType,
    pub summary: String,
    pub status: String,
    pub assignee: String,
    // Extended fields (fetched on demand)
    pub description: Option<String>,
    pub priority: Option<String>,
    pub reporter: Option<String>,
    pub created: Option<String>,
    pub updated: Option<String>,
    pub labels: Option<Vec<String>>,
    pub comments: Option<Vec<Comment>>,
}

#[derive(Debug, Clone)]
pub struct Comment {
    pub author: String,
    pub created: String,
    pub body: String,
}

#[derive(Debug, Clone)]
pub enum TicketType {
    Story,
    Bug,
    Task,
    Epic,
}

impl TicketType {
    pub fn from_str(s: &str) -> Self {
        match s.trim().to_lowercase().as_str() {
            "story" => TicketType::Story,
            "bug" => TicketType::Bug,
            "task" => TicketType::Task,
            "epic" => TicketType::Epic,
            _ => TicketType::Task,
        }
    }

    pub fn emoji(&self) -> &str {
        match self {
            TicketType::Bug => "🐛",
            TicketType::Story => "📖",
            TicketType::Task => "",
            TicketType::Epic => "🎯",
        }
    }
}

#[derive(Debug)]
pub struct KanbanColumns {
    pub todo: Vec<Ticket>,
    pub in_progress: Vec<Ticket>,
    pub review: Vec<Ticket>,
    pub done: Vec<Ticket>,
}

impl KanbanColumns {
    pub fn new() -> Self {
        KanbanColumns {
            todo: Vec::new(),
            in_progress: Vec::new(),
            review: Vec::new(),
            done: Vec::new(),
        }
    }
    
    
    pub fn total_tickets(&self) -> usize {
        self.todo.len() + self.in_progress.len() + self.review.len() + self.done.len()
    }
    
    pub fn get_ticket_by_index(&self, global_index: usize) -> Option<&Ticket> {
        let mut current_index = 0;
        
        // Check TODO
        if global_index < current_index + self.todo.len() {
            return self.todo.get(global_index - current_index);
        }
        current_index += self.todo.len();
        
        // Check IN_PROGRESS
        if global_index < current_index + self.in_progress.len() {
            return self.in_progress.get(global_index - current_index);
        }
        current_index += self.in_progress.len();
        
        // Check REVIEW
        if global_index < current_index + self.review.len() {
            return self.review.get(global_index - current_index);
        }
        current_index += self.review.len();
        
        // Check DONE
        if global_index < current_index + self.done.len() {
            return self.done.get(global_index - current_index);
        }
        
        None
    }
    

    pub fn from_tickets(tickets: Vec<Ticket>) -> Self {
        let mut columns = KanbanColumns::new();
        
        for ticket in tickets {
            match categorize_status(&ticket.status) {
                Column::Todo => columns.todo.push(ticket),
                Column::InProgress => columns.in_progress.push(ticket),
                Column::Review => columns.review.push(ticket),
                Column::Done => columns.done.push(ticket),
            }
        }
        
        columns
    }

    pub fn print_simple(&self) {
        // Print simple text output for --once mode
        if !self.todo.is_empty() {
            println!("📋 TO DO ({})", self.todo.len());
            for ticket in &self.todo {
                let assignee = if !ticket.assignee.is_empty() && ticket.assignee != "unassigned" {
                    format!(" @{}", ticket.assignee.split('@').next().unwrap_or(&ticket.assignee))
                } else {
                    String::new()
                };
                println!("  {} {}{} - {}", 
                    ticket.ticket_type.emoji(), 
                    ticket.key, 
                    assignee,
                    ticket.summary
                );
            }
            println!();
        }
        
        if !self.in_progress.is_empty() {
            println!("🚀 IN PROGRESS ({})", self.in_progress.len());
            for ticket in &self.in_progress {
                let assignee = if !ticket.assignee.is_empty() && ticket.assignee != "unassigned" {
                    format!(" @{}", ticket.assignee.split('@').next().unwrap_or(&ticket.assignee))
                } else {
                    String::new()
                };
                println!("  {} {}{} - {}", 
                    ticket.ticket_type.emoji(), 
                    ticket.key, 
                    assignee,
                    ticket.summary
                );
            }
            println!();
        }
        
        if !self.review.is_empty() {
            println!("🔍 REVIEW ({})", self.review.len());
            for ticket in &self.review {
                let assignee = if !ticket.assignee.is_empty() && ticket.assignee != "unassigned" {
                    format!(" @{}", ticket.assignee.split('@').next().unwrap_or(&ticket.assignee))
                } else {
                    String::new()
                };
                println!("  {} {}{} - {}", 
                    ticket.ticket_type.emoji(), 
                    ticket.key, 
                    assignee,
                    ticket.summary
                );
            }
            println!();
        }
        
        if !self.done.is_empty() {
            println!("✅ DONE ({})", self.done.len());
            for ticket in &self.done {
                let assignee = if !ticket.assignee.is_empty() && ticket.assignee != "unassigned" {
                    format!(" @{}", ticket.assignee.split('@').next().unwrap_or(&ticket.assignee))
                } else {
                    String::new()
                };
                println!("  {} {}{} - {}", 
                    ticket.ticket_type.emoji(), 
                    ticket.key, 
                    assignee,
                    ticket.summary
                );
            }
            println!();
        }
        
        if self.todo.is_empty() && self.in_progress.is_empty() && self.review.is_empty() && self.done.is_empty() {
            println!("No tickets found! 🎉");
        }
    }
}

#[derive(Debug)]
enum Column {
    Todo,
    InProgress,
    Review,
    Done,
}

fn categorize_status(status: &str) -> Column {
    let status_lower = status.to_lowercase();
    
    // Check for common patterns in status names
    if status_lower.contains("done") || 
       status_lower.contains("closed") || 
       status_lower.contains("resolved") ||
       status_lower.contains("shipped") ||
       status_lower.contains("complete") {
        return Column::Done;
    }
    
    if status_lower.contains("progress") || 
       status_lower.contains("development") ||
       status_lower.contains("in dev") ||
       status_lower.contains("coding") {
        return Column::InProgress;
    }
    
    if status_lower.contains("review") || 
       status_lower.contains("testing") ||
       status_lower.contains("qa") ||
       status_lower.contains("verification") ||
       status_lower.contains("approval") {
        return Column::Review;
    }
    
    // Specific exact matches for common statuses
    match status_lower.as_str() {
        "to do" | "todo" | "open" | "new" | "created" | 
        "ready for development" | "backlog" | "planning" |
        "ready to start" | "queued" => Column::Todo,
        _ => {
            // Default to Todo for unknown statuses
            // This ensures tickets are visible rather than lost
            Column::Todo
        }
    }
}