use serde::{Deserialize, Serialize};
use crate::SyncState;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoardSummary {
pub id: String,
pub name: String,
pub entity_id: String,
pub column_count: u32,
pub card_count: u32,
pub last_activity: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoardView {
pub id: String,
pub name: String,
pub entity_id: String,
pub description: Option<String>,
pub columns: Vec<ColumnView>,
pub settings: BoardSettings,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnView {
pub id: String,
pub name: String,
pub position: u32,
pub cards: Vec<CardView>,
pub wip_limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardView {
pub id: String,
pub title: String,
pub description: Option<String>,
pub state: CardState,
pub priority: Option<PriorityView>,
pub assignees: Vec<String>,
pub tags: Vec<TagView>,
pub due_date: Option<i64>,
pub checklist_progress: Option<ChecklistProgress>,
pub position: u32,
pub linked_thread_id: Option<String>,
pub linked_thread_name: Option<String>,
pub sync_state: SyncState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardDetail {
pub id: String,
pub title: String,
pub description: Option<String>,
pub state: CardState,
pub priority: Option<PriorityView>,
pub assignees: Vec<String>,
pub tags: Vec<TagView>,
pub due_date: Option<i64>,
pub checklist_progress: Option<ChecklistProgress>,
pub position: u32,
pub steps: Vec<StepView>,
pub comments: Vec<CommentView>,
pub attachments: Vec<AttachmentView>,
pub activity: Vec<ActivityEntry>,
pub linked_thread_id: Option<String>,
pub linked_thread_name: Option<String>,
pub sync_state: SyncState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TagView {
pub id: String,
pub name: String,
pub color: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StepView {
pub id: String,
pub title: String,
pub completed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommentView {
pub id: String,
pub author_id: String,
pub author_name: String,
pub text: String,
pub created_at: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChecklistProgress {
pub completed: u32,
pub total: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum PriorityView {
Urgent,
High,
#[default]
Normal,
Low,
}
impl PriorityView {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
PriorityView::Urgent => "Urgent",
PriorityView::High => "High",
PriorityView::Normal => "Normal",
PriorityView::Low => "Low",
}
}
#[must_use]
pub fn color(&self) -> &'static str {
match self {
PriorityView::Urgent => "#DC2626",
PriorityView::High => "#EA580C",
PriorityView::Normal => "#2563EB",
PriorityView::Low => "#6B7280",
}
}
#[must_use]
pub fn sort_order(&self) -> u8 {
match self {
PriorityView::Urgent => 0,
PriorityView::High => 1,
PriorityView::Normal => 2,
PriorityView::Low => 3,
}
}
#[must_use]
pub fn all() -> &'static [PriorityView] {
&[
PriorityView::Urgent,
PriorityView::High,
PriorityView::Normal,
PriorityView::Low,
]
}
}
impl std::fmt::Display for PriorityView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CardState {
#[default]
Todo,
InProgress,
InReview,
Done,
Archived,
}
impl CardState {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
CardState::Todo => "To Do",
CardState::InProgress => "In Progress",
CardState::InReview => "In Review",
CardState::Done => "Done",
CardState::Archived => "Archived",
}
}
}
impl std::fmt::Display for CardState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct BoardSettings {
pub enable_wip_limits: bool,
pub enable_due_dates: bool,
pub enable_checklists: bool,
pub default_column_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttachmentView {
pub id: String,
pub name: String,
pub mime_type: String,
pub size_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityEntry {
pub id: String,
pub action_type: String,
pub actor_id: String,
pub actor_name: String,
pub description: String,
pub timestamp: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn board_summary_equality() {
let b1 = BoardSummary {
id: "board-1".to_string(),
name: "Sprint Board".to_string(),
entity_id: "proj-1".to_string(),
column_count: 4,
card_count: 12,
last_activity: Some(1705500000000),
};
let b2 = b1.clone();
assert_eq!(b1, b2);
}
#[test]
fn card_state_default() {
let state = CardState::default();
assert_eq!(state, CardState::Todo);
}
#[test]
fn card_state_display() {
assert_eq!(format!("{}", CardState::Todo), "To Do");
assert_eq!(format!("{}", CardState::InProgress), "In Progress");
assert_eq!(format!("{}", CardState::InReview), "In Review");
assert_eq!(format!("{}", CardState::Done), "Done");
assert_eq!(format!("{}", CardState::Archived), "Archived");
}
#[test]
fn checklist_progress_construction() {
let progress = ChecklistProgress {
completed: 3,
total: 5,
};
assert_eq!(progress.completed, 3);
assert_eq!(progress.total, 5);
}
#[test]
fn card_view_with_tags() {
let card = CardView {
id: "card-1".to_string(),
title: "Implement feature".to_string(),
description: Some("Add the new feature".to_string()),
state: CardState::InProgress,
priority: Some(PriorityView::High),
assignees: vec!["alice-beta-charlie-delta".to_string()],
tags: vec![TagView {
id: "tag-1".to_string(),
name: "urgent".to_string(),
color: "#FF0000".to_string(),
}],
due_date: Some(1705600000000),
checklist_progress: Some(ChecklistProgress {
completed: 2,
total: 4,
}),
position: 0,
linked_thread_id: None,
linked_thread_name: None,
sync_state: SyncState::Synced,
};
assert_eq!(card.tags.len(), 1);
assert_eq!(card.tags[0].name, "urgent");
assert_eq!(card.priority, Some(PriorityView::High));
}
#[test]
fn board_settings_default() {
let settings = BoardSettings::default();
assert!(!settings.enable_wip_limits);
assert!(!settings.enable_due_dates);
assert!(!settings.enable_checklists);
assert!(settings.default_column_id.is_none());
}
#[test]
fn activity_entry_construction() {
let entry = ActivityEntry {
id: "act-1".to_string(),
action_type: "moved".to_string(),
actor_id: "user-1".to_string(),
actor_name: "Alice".to_string(),
description: "Moved card to In Progress".to_string(),
timestamp: 1705500000000,
};
assert_eq!(entry.action_type, "moved");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SwimlaneMode {
#[default]
None,
ByAssignee,
ByTag,
ByState,
}
impl SwimlaneMode {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
SwimlaneMode::None => "Standard",
SwimlaneMode::ByAssignee => "By Assignee",
SwimlaneMode::ByTag => "By Tag",
SwimlaneMode::ByState => "By State",
}
}
}
impl std::fmt::Display for SwimlaneMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[cfg(test)]
mod swimlane_tests {
use super::*;
#[test]
fn swimlane_mode_default() {
let mode = SwimlaneMode::default();
assert_eq!(mode, SwimlaneMode::None);
}
#[test]
fn swimlane_mode_labels() {
assert_eq!(SwimlaneMode::None.label(), "Standard");
assert_eq!(SwimlaneMode::ByAssignee.label(), "By Assignee");
assert_eq!(SwimlaneMode::ByTag.label(), "By Tag");
assert_eq!(SwimlaneMode::ByState.label(), "By State");
}
#[test]
fn swimlane_mode_display() {
assert_eq!(format!("{}", SwimlaneMode::None), "Standard");
assert_eq!(format!("{}", SwimlaneMode::ByAssignee), "By Assignee");
assert_eq!(format!("{}", SwimlaneMode::ByTag), "By Tag");
assert_eq!(format!("{}", SwimlaneMode::ByState), "By State");
}
#[test]
fn swimlane_mode_equality() {
assert_eq!(SwimlaneMode::None, SwimlaneMode::None);
assert_ne!(SwimlaneMode::None, SwimlaneMode::ByAssignee);
assert_ne!(SwimlaneMode::ByAssignee, SwimlaneMode::ByTag);
}
}
#[cfg(test)]
mod priority_tests {
use super::*;
#[test]
fn priority_default() {
let priority = PriorityView::default();
assert_eq!(priority, PriorityView::Normal);
}
#[test]
fn priority_labels() {
assert_eq!(PriorityView::Urgent.label(), "Urgent");
assert_eq!(PriorityView::High.label(), "High");
assert_eq!(PriorityView::Normal.label(), "Normal");
assert_eq!(PriorityView::Low.label(), "Low");
}
#[test]
fn priority_colors() {
assert_eq!(PriorityView::Urgent.color(), "#DC2626");
assert_eq!(PriorityView::High.color(), "#EA580C");
assert_eq!(PriorityView::Normal.color(), "#2563EB");
assert_eq!(PriorityView::Low.color(), "#6B7280");
}
#[test]
fn priority_sort_order() {
assert!(PriorityView::Urgent.sort_order() < PriorityView::High.sort_order());
assert!(PriorityView::High.sort_order() < PriorityView::Normal.sort_order());
assert!(PriorityView::Normal.sort_order() < PriorityView::Low.sort_order());
}
#[test]
fn priority_all() {
let all = PriorityView::all();
assert_eq!(all.len(), 4);
assert_eq!(all[0], PriorityView::Urgent);
assert_eq!(all[3], PriorityView::Low);
}
#[test]
fn priority_display() {
assert_eq!(format!("{}", PriorityView::Urgent), "Urgent");
assert_eq!(format!("{}", PriorityView::High), "High");
assert_eq!(format!("{}", PriorityView::Normal), "Normal");
assert_eq!(format!("{}", PriorityView::Low), "Low");
}
}