Skip to main content

metis_docs_tui/app/
mod.rs

1pub mod actions;
2pub mod mapping;
3pub mod operations;
4pub mod state;
5
6use crate::app::mapping::*;
7use crate::error::*;
8use crate::models::*;
9use crate::services::*;
10use anyhow::Result;
11use metis_core::{domain::documents::types::DocumentType, Initiative, Strategy, Task};
12
13pub struct App {
14    // Core application state
15    pub core_state: state::CoreAppState,
16    // UI state
17    pub ui_state: state::UiState,
18    // Selection state
19    pub selection_state: state::SelectionState,
20    // Error handler
21    pub error_handler: ErrorHandler,
22    // Services
23    pub workspace_service: WorkspaceService,
24    pub document_service: Option<DocumentService>,
25    pub sync_service: Option<SyncService>,
26    pub transition_service: Option<TransitionService>,
27}
28
29impl Default for App {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl App {
36    pub fn new() -> Self {
37        Self {
38            core_state: state::CoreAppState::new(),
39            ui_state: state::UiState::new(),
40            selection_state: state::SelectionState::new(),
41            error_handler: ErrorHandler::new(),
42            workspace_service: WorkspaceService::new(),
43            document_service: None,
44            sync_service: None,
45            transition_service: None,
46        }
47    }
48
49    pub async fn initialize(&mut self) -> Result<()> {
50        // 1. Check if we're in a metis workspace
51        match self.workspace_service.check_workspace().await {
52            Ok(Some(workspace_dir)) => {
53                self.core_state.set_workspace(workspace_dir.clone());
54
55                // Initialize services
56                self.document_service = Some(DocumentService::new(workspace_dir.clone()));
57                self.sync_service = Some(SyncService::new(workspace_dir.clone()));
58                self.transition_service = Some(TransitionService::new(workspace_dir));
59
60                // 2. Perform database synchronization
61                if let Some(sync_service) = &self.sync_service {
62                    match sync_service.sync_database().await {
63                        Ok(_) => {
64                            self.core_state.set_sync_complete();
65
66                            // 3. Load flight level configuration
67                            self.load_flight_config().await?;
68
69                            // 4. Load documents into boards
70                            self.load_documents().await?;
71                        }
72                        Err(e) => {
73                            self.error_handler.handle_error(AppError::from(e));
74                        }
75                    }
76                }
77            }
78            Ok(None) => {
79                self.error_handler
80                    .handle_error(AppError::WorkspaceError("No workspace found".to_string()));
81            }
82            Err(e) => {
83                self.error_handler.handle_error(AppError::from(e));
84            }
85        }
86
87        Ok(())
88    }
89
90    pub fn is_ready(&self) -> bool {
91        self.core_state.is_ready()
92    }
93
94    pub fn get_current_board(&self) -> &KanbanBoard {
95        self.ui_state.get_current_board()
96    }
97
98    // Convenience methods for accessing state
99    pub fn app_state(&self) -> &AppState {
100        &self.ui_state.app_state
101    }
102
103    pub fn error_message(&self) -> Option<String> {
104        self.ui_state
105            .message_state
106            .get_current_message()
107            .map(|msg| msg.content.clone())
108    }
109
110    pub fn get_selected_item(&self) -> Option<&KanbanItem> {
111        let current_board = self.ui_state.current_board;
112        let (col_idx, item_idx) = self.selection_state.get_current_selection(current_board);
113        let board = self.ui_state.get_current_board();
114
115        if col_idx < board.columns.len() && item_idx < board.columns[col_idx].items.len() {
116            Some(&board.columns[col_idx].items[item_idx])
117        } else {
118            None
119        }
120    }
121
122    pub fn view_selected_ticket(&mut self) {
123        let current_board = self.ui_state.current_board;
124        let selection = self.selection_state.get_current_selection(current_board);
125        self.ui_state.viewing_ticket = Some((current_board, selection.0, selection.1));
126        // Go directly to edit mode instead of view mode
127        self.start_content_editing();
128    }
129
130    pub fn get_viewed_ticket(&self) -> Option<&KanbanItem> {
131        if let Some((board_type, col_idx, item_idx)) = self.ui_state.viewing_ticket {
132            let board = match board_type {
133                BoardType::Strategy => &self.ui_state.strategy_board,
134                BoardType::Initiative => &self.ui_state.initiative_board,
135                BoardType::Task => &self.ui_state.task_board,
136                BoardType::Adr => &self.ui_state.adr_board,
137                BoardType::Backlog => &self.ui_state.backlog_board,
138            };
139
140            if col_idx < board.columns.len() && item_idx < board.columns[col_idx].items.len() {
141                Some(&board.columns[col_idx].items[item_idx])
142            } else {
143                None
144            }
145        } else {
146            None
147        }
148    }
149
150    // Input handling
151    pub fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) {
152        use tui_input::backend::crossterm::EventHandler;
153        self.ui_state
154            .input_title
155            .handle_event(&crossterm::event::Event::Key(key));
156    }
157
158    pub async fn load_flight_config(&mut self) -> Result<()> {
159        if let Some(workspace_dir) = &self.core_state.workspace_dir {
160            // Create database connection and load configuration
161            // workspace_dir is the .metis directory, so we need metis.db directly
162            let db_path = workspace_dir.join("metis.db");
163            if let Ok(db) = metis_core::Database::new(db_path.to_str().unwrap()) {
164                if let Ok(mut config_repo) = db.configuration_repository() {
165                    match config_repo.get_flight_level_config() {
166                        Ok(config) => {
167                            self.core_state.set_flight_config(config);
168                            // Ensure the current board is valid for the new configuration
169                            self.ui_state
170                                .ensure_valid_board(&self.core_state.flight_config);
171                        }
172                        Err(e) => {
173                            // Log error but continue with default configuration
174                            eprintln!("Warning: Failed to load flight level configuration: {}", e);
175                            eprintln!("Using default (full) configuration");
176                            // Ensure the current board is valid for the default configuration
177                            self.ui_state
178                                .ensure_valid_board(&self.core_state.flight_config);
179                        }
180                    }
181                } else {
182                    eprintln!("Warning: Failed to create configuration repository, using default configuration");
183                    // Ensure the current board is valid for the default configuration
184                    self.ui_state
185                        .ensure_valid_board(&self.core_state.flight_config);
186                }
187            } else {
188                eprintln!("Warning: Failed to connect to database, using default configuration");
189                // Ensure the current board is valid for the default configuration
190                self.ui_state
191                    .ensure_valid_board(&self.core_state.flight_config);
192            }
193        }
194        Ok(())
195    }
196
197    pub async fn load_documents(&mut self) -> Result<()> {
198        if let Some(document_service) = &self.document_service {
199            // Clear all boards before loading new documents
200            for column in &mut self.ui_state.strategy_board.columns {
201                column.items.clear();
202            }
203            for column in &mut self.ui_state.initiative_board.columns {
204                column.items.clear();
205            }
206            for column in &mut self.ui_state.task_board.columns {
207                column.items.clear();
208            }
209            for column in &mut self.ui_state.adr_board.columns {
210                column.items.clear();
211            }
212            for column in &mut self.ui_state.backlog_board.columns {
213                column.items.clear();
214            }
215
216            // Reset selection state to avoid referencing non-existent items
217            self.selection_state.strategy_selection = (0, 0);
218            self.selection_state.initiative_selection = (0, 0);
219            self.selection_state.task_selection = (0, 0);
220            self.selection_state.adr_selection = (0, 0);
221            self.selection_state.backlog_selection = (0, 0);
222
223            let mut documents = document_service.load_documents_from_database().await?;
224
225            // Sort documents by type first, then by appropriate criteria
226            documents.sort_by(|a, b| {
227                use std::cmp::Ordering;
228
229                // Helper function to get document type order
230                let type_order = |doc_type: &DocumentType| -> u8 {
231                    match doc_type {
232                        DocumentType::Vision => 0,
233                        DocumentType::Strategy => 1,
234                        DocumentType::Initiative => 2,
235                        DocumentType::Task => 3,
236                        DocumentType::Adr => 4,
237                    }
238                };
239
240                // First compare by document type
241                let a_type_order = type_order(&a.document_type);
242                let b_type_order = type_order(&b.document_type);
243
244                match a_type_order.cmp(&b_type_order) {
245                    Ordering::Equal => {
246                        // Same document type, use type-specific sorting
247                        match (&a.document_type, &b.document_type) {
248                            (DocumentType::Adr, DocumentType::Adr) => {
249                                // For ADRs, extract number from ID and sort numerically
250                                let a_num = extract_adr_number(&a.id);
251                                let b_num = extract_adr_number(&b.id);
252                                a_num.cmp(&b_num)
253                            }
254                            _ => a.title.cmp(&b.title), // Other documents sort by title
255                        }
256                    }
257                    other => other, // Different types, use type ordering
258                }
259            });
260
261            // Clear existing boards
262            self.ui_state.strategy_board = KanbanBoard::create_strategy_board();
263            self.ui_state.initiative_board = KanbanBoard::create_initiative_board();
264            self.ui_state.task_board = KanbanBoard::create_task_board();
265            self.ui_state.adr_board = KanbanBoard::create_adr_board();
266            self.ui_state.backlog_board = KanbanBoard::create_backlog_board();
267
268            // Load documents into appropriate boards
269            for doc in documents {
270                match doc.document_type {
271                    DocumentType::Strategy => {
272                        if let Ok(strategy) =
273                            Strategy::from_file(std::path::Path::new(&doc.filepath)).await
274                        {
275                            let column_index = get_strategy_column_index(&strategy);
276                            let item = KanbanItem {
277                                document: DocumentObject::Strategy(strategy),
278                                prelude: doc.title.clone(),
279                                risk_complexity: None,
280                                file_path: doc.filepath,
281                            };
282                            if column_index < self.ui_state.strategy_board.columns.len() {
283                                self.ui_state.strategy_board.columns[column_index]
284                                    .items
285                                    .push(item);
286                            }
287                        }
288                    }
289                    DocumentType::Initiative => {
290                        if let Ok(initiative) =
291                            Initiative::from_file(std::path::Path::new(&doc.filepath)).await
292                        {
293                            let column_index = get_initiative_column_index(&initiative);
294                            let item = KanbanItem {
295                                document: DocumentObject::Initiative(initiative),
296                                prelude: doc.title.clone(),
297                                risk_complexity: None,
298                                file_path: doc.filepath,
299                            };
300                            if column_index < self.ui_state.initiative_board.columns.len() {
301                                self.ui_state.initiative_board.columns[column_index]
302                                    .items
303                                    .push(item);
304                            }
305                        }
306                    }
307                    DocumentType::Task => {
308                        if let Ok(task) = Task::from_file(std::path::Path::new(&doc.filepath)).await
309                        {
310                            use metis_core::{domain::documents::types::Phase, Document};
311
312                            // Check if this is a backlog item (only Phase::Backlog)
313                            let is_backlog = task.phase() == Ok(Phase::Backlog);
314
315                            if is_backlog {
316                                // Place in backlog board
317                                let column_index = get_backlog_column_index(&task);
318                                let item = KanbanItem {
319                                    document: DocumentObject::Task(task),
320                                    prelude: doc.title.clone(),
321                                    risk_complexity: None,
322                                    file_path: doc.filepath,
323                                };
324                                if column_index < self.ui_state.backlog_board.columns.len() {
325                                    self.ui_state.backlog_board.columns[column_index]
326                                        .items
327                                        .push(item);
328                                }
329                            } else {
330                                // Place in regular task board
331                                let column_index = get_task_column_index(&task);
332                                let item = KanbanItem {
333                                    document: DocumentObject::Task(task),
334                                    prelude: doc.title.clone(),
335                                    risk_complexity: None,
336                                    file_path: doc.filepath,
337                                };
338                                if column_index < self.ui_state.task_board.columns.len() {
339                                    self.ui_state.task_board.columns[column_index]
340                                        .items
341                                        .push(item);
342                                }
343                            }
344                        }
345                    }
346                    DocumentType::Adr => {
347                        if let Ok(adr) =
348                            metis_core::Adr::from_file(std::path::Path::new(&doc.filepath)).await
349                        {
350                            let column_index = get_adr_column_index(&adr);
351                            let item = KanbanItem {
352                                document: DocumentObject::Adr(adr),
353                                prelude: doc.title.clone(),
354                                risk_complexity: None,
355                                file_path: doc.filepath,
356                            };
357                            if column_index < self.ui_state.adr_board.columns.len() {
358                                self.ui_state.adr_board.columns[column_index]
359                                    .items
360                                    .push(item);
361                            }
362                        }
363                    }
364                    _ => {
365                        // Skip other document types for now
366                    }
367                }
368            }
369        }
370
371        Ok(())
372    }
373}