1use serde::{Deserialize, Serialize};
9
10use crate::SyncState;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct BoardSummary {
15 pub id: String,
17 pub name: String,
19 pub entity_id: String,
21 pub column_count: u32,
23 pub card_count: u32,
25 pub last_activity: Option<i64>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct BoardView {
32 pub id: String,
34 pub name: String,
36 pub entity_id: String,
38 pub description: Option<String>,
40 pub columns: Vec<ColumnView>,
42 pub settings: BoardSettings,
44 pub created_at: i64,
46 pub updated_at: i64,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct ColumnView {
53 pub id: String,
55 pub name: String,
57 pub position: u32,
59 pub cards: Vec<CardView>,
61 pub wip_limit: Option<u32>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct CardView {
68 pub id: String,
70 pub title: String,
72 pub description: Option<String>,
74 pub state: CardState,
76 pub priority: Option<PriorityView>,
78 pub assignees: Vec<String>,
80 pub tags: Vec<TagView>,
82 pub due_date: Option<i64>,
84 pub checklist_progress: Option<ChecklistProgress>,
86 pub position: u32,
88 pub linked_thread_id: Option<String>,
90 pub linked_thread_name: Option<String>,
92 pub sync_state: SyncState,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct CardDetail {
101 pub id: String,
103 pub title: String,
105 pub description: Option<String>,
107 pub state: CardState,
109 pub priority: Option<PriorityView>,
111 pub assignees: Vec<String>,
113 pub tags: Vec<TagView>,
115 pub due_date: Option<i64>,
117 pub checklist_progress: Option<ChecklistProgress>,
119 pub position: u32,
121 pub steps: Vec<StepView>,
123 pub comments: Vec<CommentView>,
125 pub attachments: Vec<AttachmentView>,
127 pub activity: Vec<ActivityEntry>,
129 pub linked_thread_id: Option<String>,
131 pub linked_thread_name: Option<String>,
133 pub sync_state: SyncState,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct TagView {
140 pub id: String,
142 pub name: String,
144 pub color: String,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct StepView {
151 pub id: String,
153 pub title: String,
155 pub completed: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct CommentView {
162 pub id: String,
164 pub author_id: String,
166 pub author_name: String,
168 pub text: String,
170 pub created_at: i64,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct ChecklistProgress {
177 pub completed: u32,
179 pub total: u32,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub enum PriorityView {
188 Urgent,
190 High,
192 #[default]
194 Normal,
195 Low,
197}
198
199impl PriorityView {
200 #[must_use]
202 pub fn label(&self) -> &'static str {
203 match self {
204 PriorityView::Urgent => "Urgent",
205 PriorityView::High => "High",
206 PriorityView::Normal => "Normal",
207 PriorityView::Low => "Low",
208 }
209 }
210
211 #[must_use]
213 pub fn color(&self) -> &'static str {
214 match self {
215 PriorityView::Urgent => "#DC2626",
216 PriorityView::High => "#EA580C",
217 PriorityView::Normal => "#2563EB",
218 PriorityView::Low => "#6B7280",
219 }
220 }
221
222 #[must_use]
224 pub fn sort_order(&self) -> u8 {
225 match self {
226 PriorityView::Urgent => 0,
227 PriorityView::High => 1,
228 PriorityView::Normal => 2,
229 PriorityView::Low => 3,
230 }
231 }
232
233 #[must_use]
235 pub fn all() -> &'static [PriorityView] {
236 &[
237 PriorityView::Urgent,
238 PriorityView::High,
239 PriorityView::Normal,
240 PriorityView::Low,
241 ]
242 }
243}
244
245impl std::fmt::Display for PriorityView {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 write!(f, "{}", self.label())
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
256pub enum CardState {
257 #[default]
259 Todo,
260 InProgress,
262 InReview,
264 Done,
266 Archived,
268}
269
270impl CardState {
271 #[must_use]
273 pub fn label(&self) -> &'static str {
274 match self {
275 CardState::Todo => "To Do",
276 CardState::InProgress => "In Progress",
277 CardState::InReview => "In Review",
278 CardState::Done => "Done",
279 CardState::Archived => "Archived",
280 }
281 }
282}
283
284impl std::fmt::Display for CardState {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 write!(f, "{}", self.label())
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
292pub struct BoardSettings {
293 pub enable_wip_limits: bool,
295 pub enable_due_dates: bool,
297 pub enable_checklists: bool,
299 pub default_column_id: Option<String>,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
305pub struct AttachmentView {
306 pub id: String,
308 pub name: String,
310 pub mime_type: String,
312 pub size_bytes: u64,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct ActivityEntry {
319 pub id: String,
321 pub action_type: String,
323 pub actor_id: String,
325 pub actor_name: String,
327 pub description: String,
329 pub timestamp: i64,
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn board_summary_equality() {
339 let b1 = BoardSummary {
340 id: "board-1".to_string(),
341 name: "Sprint Board".to_string(),
342 entity_id: "proj-1".to_string(),
343 column_count: 4,
344 card_count: 12,
345 last_activity: Some(1705500000000),
346 };
347 let b2 = b1.clone();
348 assert_eq!(b1, b2);
349 }
350
351 #[test]
352 fn card_state_default() {
353 let state = CardState::default();
354 assert_eq!(state, CardState::Todo);
355 }
356
357 #[test]
358 fn card_state_display() {
359 assert_eq!(format!("{}", CardState::Todo), "To Do");
360 assert_eq!(format!("{}", CardState::InProgress), "In Progress");
361 assert_eq!(format!("{}", CardState::InReview), "In Review");
362 assert_eq!(format!("{}", CardState::Done), "Done");
363 assert_eq!(format!("{}", CardState::Archived), "Archived");
364 }
365
366 #[test]
367 fn checklist_progress_construction() {
368 let progress = ChecklistProgress {
369 completed: 3,
370 total: 5,
371 };
372 assert_eq!(progress.completed, 3);
373 assert_eq!(progress.total, 5);
374 }
375
376 #[test]
377 fn card_view_with_tags() {
378 let card = CardView {
379 id: "card-1".to_string(),
380 title: "Implement feature".to_string(),
381 description: Some("Add the new feature".to_string()),
382 state: CardState::InProgress,
383 priority: Some(PriorityView::High),
384 assignees: vec!["alice-beta-charlie-delta".to_string()],
385 tags: vec![TagView {
386 id: "tag-1".to_string(),
387 name: "urgent".to_string(),
388 color: "#FF0000".to_string(),
389 }],
390 due_date: Some(1705600000000),
391 checklist_progress: Some(ChecklistProgress {
392 completed: 2,
393 total: 4,
394 }),
395 position: 0,
396 linked_thread_id: None,
397 linked_thread_name: None,
398 sync_state: SyncState::Synced,
399 };
400 assert_eq!(card.tags.len(), 1);
401 assert_eq!(card.tags[0].name, "urgent");
402 assert_eq!(card.priority, Some(PriorityView::High));
403 }
404
405 #[test]
406 fn board_settings_default() {
407 let settings = BoardSettings::default();
408 assert!(!settings.enable_wip_limits);
409 assert!(!settings.enable_due_dates);
410 assert!(!settings.enable_checklists);
411 assert!(settings.default_column_id.is_none());
412 }
413
414 #[test]
415 fn activity_entry_construction() {
416 let entry = ActivityEntry {
417 id: "act-1".to_string(),
418 action_type: "moved".to_string(),
419 actor_id: "user-1".to_string(),
420 actor_name: "Alice".to_string(),
421 description: "Moved card to In Progress".to_string(),
422 timestamp: 1705500000000,
423 };
424 assert_eq!(entry.action_type, "moved");
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
430pub enum SwimlaneMode {
431 #[default]
433 None,
434 ByAssignee,
436 ByTag,
438 ByState,
440}
441
442impl SwimlaneMode {
443 #[must_use]
445 pub fn label(&self) -> &'static str {
446 match self {
447 SwimlaneMode::None => "Standard",
448 SwimlaneMode::ByAssignee => "By Assignee",
449 SwimlaneMode::ByTag => "By Tag",
450 SwimlaneMode::ByState => "By State",
451 }
452 }
453}
454
455impl std::fmt::Display for SwimlaneMode {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 write!(f, "{}", self.label())
458 }
459}
460
461#[cfg(test)]
462mod swimlane_tests {
463 use super::*;
464
465 #[test]
466 fn swimlane_mode_default() {
467 let mode = SwimlaneMode::default();
468 assert_eq!(mode, SwimlaneMode::None);
469 }
470
471 #[test]
472 fn swimlane_mode_labels() {
473 assert_eq!(SwimlaneMode::None.label(), "Standard");
474 assert_eq!(SwimlaneMode::ByAssignee.label(), "By Assignee");
475 assert_eq!(SwimlaneMode::ByTag.label(), "By Tag");
476 assert_eq!(SwimlaneMode::ByState.label(), "By State");
477 }
478
479 #[test]
480 fn swimlane_mode_display() {
481 assert_eq!(format!("{}", SwimlaneMode::None), "Standard");
482 assert_eq!(format!("{}", SwimlaneMode::ByAssignee), "By Assignee");
483 assert_eq!(format!("{}", SwimlaneMode::ByTag), "By Tag");
484 assert_eq!(format!("{}", SwimlaneMode::ByState), "By State");
485 }
486
487 #[test]
488 fn swimlane_mode_equality() {
489 assert_eq!(SwimlaneMode::None, SwimlaneMode::None);
490 assert_ne!(SwimlaneMode::None, SwimlaneMode::ByAssignee);
491 assert_ne!(SwimlaneMode::ByAssignee, SwimlaneMode::ByTag);
492 }
493}
494
495#[cfg(test)]
496mod priority_tests {
497 use super::*;
498
499 #[test]
500 fn priority_default() {
501 let priority = PriorityView::default();
502 assert_eq!(priority, PriorityView::Normal);
503 }
504
505 #[test]
506 fn priority_labels() {
507 assert_eq!(PriorityView::Urgent.label(), "Urgent");
508 assert_eq!(PriorityView::High.label(), "High");
509 assert_eq!(PriorityView::Normal.label(), "Normal");
510 assert_eq!(PriorityView::Low.label(), "Low");
511 }
512
513 #[test]
514 fn priority_colors() {
515 assert_eq!(PriorityView::Urgent.color(), "#DC2626");
516 assert_eq!(PriorityView::High.color(), "#EA580C");
517 assert_eq!(PriorityView::Normal.color(), "#2563EB");
518 assert_eq!(PriorityView::Low.color(), "#6B7280");
519 }
520
521 #[test]
522 fn priority_sort_order() {
523 assert!(PriorityView::Urgent.sort_order() < PriorityView::High.sort_order());
524 assert!(PriorityView::High.sort_order() < PriorityView::Normal.sort_order());
525 assert!(PriorityView::Normal.sort_order() < PriorityView::Low.sort_order());
526 }
527
528 #[test]
529 fn priority_all() {
530 let all = PriorityView::all();
531 assert_eq!(all.len(), 4);
532 assert_eq!(all[0], PriorityView::Urgent);
533 assert_eq!(all[3], PriorityView::Low);
534 }
535
536 #[test]
537 fn priority_display() {
538 assert_eq!(format!("{}", PriorityView::Urgent), "Urgent");
539 assert_eq!(format!("{}", PriorityView::High), "High");
540 assert_eq!(format!("{}", PriorityView::Normal), "Normal");
541 assert_eq!(format!("{}", PriorityView::Low), "Low");
542 }
543}