boarddown_schema/
board.rs1use 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}