Skip to main content

boarddown_schema/
board.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use ts_rs::TS;
4
5use crate::{BoardId, Column, ColumnRef, Task, TaskId};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
8#[ts(export)]
9pub struct Board {
10    pub id: BoardId,
11    pub title: String,
12    pub columns: Vec<Column>,
13    pub tasks: HashMap<TaskId, Task>,
14    pub metadata: BoardMetadata,
15    pub created_at: chrono::DateTime<chrono::Utc>,
16    pub updated_at: chrono::DateTime<chrono::Utc>,
17}
18
19impl Board {
20    pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
21        Self {
22            id: BoardId(id.into()),
23            title: title.into(),
24            columns: Vec::new(),
25            tasks: HashMap::new(),
26            metadata: BoardMetadata::default(),
27            created_at: chrono::Utc::now(),
28            updated_at: chrono::Utc::now(),
29        }
30    }
31
32    pub fn with_default_columns(id: impl Into<String>, title: impl Into<String>) -> Self {
33        let mut board = Self::new(id, title);
34        board.add_column(Column::new("Todo"));
35        board.add_column(Column::new("In Progress"));
36        board.add_column(Column::new("Done"));
37        board
38    }
39
40    pub fn get_task(&self, id: &TaskId) -> Option<&Task> {
41        self.tasks.get(id)
42    }
43
44    pub fn get_task_mut(&mut self, id: &TaskId) -> Option<&mut Task> {
45        self.tasks.get_mut(id)
46    }
47
48    pub fn add_column(&mut self, column: Column) {
49        self.columns.push(column);
50        self.updated_at = chrono::Utc::now();
51    }
52
53    pub fn tasks_by_column(&self, column_name: &str) -> Vec<&Task> {
54        self.tasks.values().filter(|t| matches!(t.column, ColumnRef::Name(ref name) if name == column_name)).collect()
55    }
56
57    pub fn tasks_by_status(&self, status: crate::Status) -> Vec<&Task> {
58        self.tasks.values().filter(|t| t.status == status).collect()
59    }
60    
61    pub fn next_task_id(&self) -> TaskId {
62        let prefix = self.metadata.id_prefix.as_deref().unwrap_or("TASK");
63        let prefix_upper = prefix.to_uppercase();
64        
65        let max_seq = self.tasks.values()
66            .filter_map(|t| {
67                if t.id.board_prefix() == Some(prefix_upper.as_str()) {
68                    t.id.sequence()
69                } else {
70                    None
71                }
72            })
73            .max()
74            .unwrap_or(0);
75            
76        TaskId::new(prefix, max_seq + 1)
77    }
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
81#[ts(export)]
82pub struct BoardMetadata {
83    pub id_prefix: Option<String>,
84    pub default_tags: Vec<String>,
85    pub storage: StorageType,
86    pub autosync: bool,
87    #[ts(type = "Record<string, unknown>")]
88    pub custom: HashMap<String, serde_json::Value>,
89}
90
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
92#[ts(export)]
93#[serde(rename_all = "lowercase")]
94pub enum StorageType {
95    #[default]
96    Filesystem,
97    Sqlite,
98    Hybrid,
99}
100
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, TS)]
102#[ts(export)]
103pub enum ConflictResolution {
104    #[default]
105    LastWriteWins,
106    Manual,
107    Local,
108    Remote,
109}