Skip to main content

metis_docs_tui/app/operations/
document_ops.rs

1use crate::app::state::BacklogCategory;
2use crate::app::App;
3use crate::models::{kanban::DocumentObject, AppState, BoardType};
4use anyhow::Result;
5use metis_core::{domain::documents::types::DocumentType, Document};
6
7impl App {
8    // Helper methods for document operations
9
10    /// Common error handling and state reset
11    fn handle_creation_error(&mut self, message: String) {
12        self.add_error_message(message);
13        self.ui_state.set_app_state(AppState::Normal);
14        self.ui_state.reset_input();
15    }
16
17    /// Common success handling with sync and reload
18    async fn handle_creation_success(
19        &mut self,
20        doc_type: DocumentType,
21        doc_id: Option<String>,
22    ) -> Result<()> {
23        // Handle backlog category tagging if needed
24        if let Some(id) = doc_id {
25            if self.ui_state.current_board == BoardType::Backlog {
26                if let Some(tag) = self.ui_state.selected_backlog_category.as_tag() {
27                    if let Some(sync_service) = &self.sync_service {
28                        let _ = sync_service.sync_database().await;
29                    }
30                    let _ = self.add_tag_to_document(&id, tag).await;
31                }
32            }
33        }
34
35        self.add_success_message(format!("{} created successfully", doc_type));
36        self.ui_state.set_app_state(AppState::Normal);
37        self.ui_state.reset_input();
38
39        // Sync database and reload documents
40        if let Some(sync_service) = &self.sync_service {
41            let _ = sync_service.sync_database().await;
42        }
43        self.load_documents().await?;
44        Ok(())
45    }
46
47    /// Get document type for current board
48    fn get_document_type_for_board(&self) -> DocumentType {
49        match self.ui_state.current_board {
50            BoardType::Strategy => DocumentType::Strategy,
51            BoardType::Initiative => DocumentType::Initiative,
52            BoardType::Task => DocumentType::Task,
53            BoardType::Adr => DocumentType::Adr,
54            BoardType::Backlog => DocumentType::Task,
55        }
56    }
57
58    /// Find parent ID for document creation
59    fn find_parent_id(&mut self, doc_type: DocumentType) -> Option<String> {
60        match doc_type {
61            DocumentType::Initiative => {
62                // Only look for parent strategies if strategies are enabled
63                if self.core_state.flight_config.strategies_enabled {
64                    let strategy_items: Vec<_> = self
65                        .ui_state
66                        .strategy_board
67                        .columns
68                        .iter()
69                        .flat_map(|col| &col.items)
70                        .collect();
71
72                    if let Some(strategy) = strategy_items.first() {
73                        Some(strategy.id())
74                    } else {
75                        self.handle_creation_error(
76                            "Cannot create initiative: No strategy available as parent".to_string(),
77                        );
78                        None
79                    }
80                } else {
81                    // Streamlined config: initiatives have no parent
82                    None
83                }
84            }
85            DocumentType::Task => {
86                if self.ui_state.current_board == BoardType::Task {
87                    // Only look for parent initiatives if initiatives are enabled
88                    if self.core_state.flight_config.initiatives_enabled {
89                        let initiative_items: Vec<_> = self
90                            .ui_state
91                            .initiative_board
92                            .columns
93                            .iter()
94                            .flat_map(|col| &col.items)
95                            .collect();
96
97                        if let Some(initiative) = initiative_items.first() {
98                            Some(initiative.id())
99                        } else {
100                            self.handle_creation_error(
101                                "Cannot create task: No initiative available as parent".to_string(),
102                            );
103                            None
104                        }
105                    } else {
106                        // Direct config: tasks have no parent
107                        None
108                    }
109                } else {
110                    None // Backlog tasks have no parent
111                }
112            }
113            _ => None,
114        }
115    }
116
117    /// Create a child task with proper strategy/initiative relationship
118    async fn create_child_task_from_initiative(&self, title: String) -> Result<String> {
119        if let Some(document_service) = &self.document_service {
120            let initiative_items: Vec<_> = self
121                .ui_state
122                .initiative_board
123                .columns
124                .iter()
125                .flat_map(|col| &col.items)
126                .collect();
127
128            if let Some(initiative) = initiative_items.first() {
129                let strategy_id = match &initiative.document {
130                    DocumentObject::Initiative(init) => init.parent_id(),
131                    _ => None,
132                };
133
134                let effective_strategy_id = if let Some(strategy_id) = strategy_id {
135                    strategy_id.to_string()
136                } else {
137                    // Streamlined config: use NULL as strategy placeholder
138                    "NULL".to_string()
139                };
140
141                document_service
142                    .create_child_task(title, effective_strategy_id, initiative.id())
143                    .await
144            } else {
145                Err(anyhow::anyhow!("No initiative found for task creation"))
146            }
147        } else {
148            Err(anyhow::anyhow!("Document service not available"))
149        }
150    }
151
152    pub async fn create_new_document(&mut self) -> Result<()> {
153        let title = self.ui_state.input_title.value().to_string();
154        if title.trim().is_empty() {
155            self.handle_creation_error("Title cannot be empty".to_string());
156            return Ok(());
157        }
158
159        let doc_type = self.get_document_type_for_board();
160
161        // For initiatives and tasks, find parent if needed
162        let parent_id = self.find_parent_id(doc_type);
163
164        // Check if we need a parent for this document type in current configuration
165        let needs_parent = match doc_type {
166            DocumentType::Strategy | DocumentType::Adr => false, // Never need parents
167            DocumentType::Initiative => self.core_state.flight_config.strategies_enabled, // Need parent only if strategies enabled
168            DocumentType::Task => {
169                // Backlog tasks never need parents, regardless of configuration
170                if self.ui_state.current_board == BoardType::Backlog {
171                    false
172                } else {
173                    self.core_state.flight_config.initiatives_enabled // Need parent only if initiatives enabled
174                }
175            }
176            _ => false,
177        };
178
179        if needs_parent && parent_id.is_none() {
180            return Ok(()); // Error already handled in find_parent_id
181        }
182
183        // Create the document using appropriate method
184        let result = if doc_type == DocumentType::Task
185            && parent_id.is_some()
186            && self.ui_state.current_board == BoardType::Task
187        {
188            self.create_child_task_from_initiative(title).await
189        } else if let Some(document_service) = &self.document_service {
190            document_service
191                .create_document(doc_type, title, None, parent_id)
192                .await
193        } else {
194            Err(anyhow::anyhow!("Document service not available"))
195        };
196
197        match result {
198            Ok(doc_id) => {
199                self.handle_creation_success(doc_type, Some(doc_id)).await?;
200            }
201            Err(e) => {
202                self.handle_creation_error(format!("Failed to create {}: {}", doc_type, e));
203            }
204        }
205
206        Ok(())
207    }
208
209    pub async fn create_child_document(&mut self) -> Result<()> {
210        let title = self.ui_state.input_title.value().to_string();
211        if title.trim().is_empty() {
212            self.add_error_message("Title cannot be empty".to_string());
213            self.ui_state.set_app_state(AppState::Normal);
214            self.ui_state.reset_input();
215            return Ok(());
216        }
217
218        // Check if a parent item is selected
219        if let Some(parent_item) = self.get_selected_item() {
220            if let Some(document_service) = &self.document_service {
221                // Determine child document type based on parent
222                let child_doc_type = match parent_item.doc_type() {
223                    DocumentType::Strategy => DocumentType::Initiative,
224                    DocumentType::Initiative => DocumentType::Task,
225                    _ => {
226                        self.add_error_message(
227                            "Cannot create child for this document type".to_string(),
228                        );
229                        self.ui_state.set_app_state(AppState::Normal);
230                        self.ui_state.reset_input();
231                        return Ok(());
232                    }
233                };
234
235                let parent_id = parent_item.id();
236
237                // For tasks, we need to call create_child_task with special handling
238                match child_doc_type {
239                    DocumentType::Task => {
240                        // For tasks, we need both strategy_id and initiative_id
241                        // Get strategy_id from the initiative's parent
242                        if let DocumentObject::Initiative(ref initiative) = &parent_item.document {
243                            let effective_strategy_id =
244                                if let Some(strategy_id) = initiative.parent_id() {
245                                    strategy_id.to_string()
246                                } else {
247                                    // Streamlined config: use NULL as strategy placeholder
248                                    "NULL".to_string()
249                                };
250
251                            match document_service
252                                .create_child_task(
253                                    title,
254                                    effective_strategy_id,
255                                    parent_id.to_string(),
256                                )
257                                .await
258                            {
259                                Ok(_) => {
260                                    self.ui_state.set_app_state(AppState::Normal);
261                                    self.ui_state.reset_input();
262                                    // Sync database and reload documents
263                                    if let Some(sync_service) = &self.sync_service {
264                                        let _ = sync_service.sync_database().await;
265                                    }
266                                    self.load_documents().await?;
267                                }
268                                Err(e) => {
269                                    self.add_error_message(format!("Failed to create task: {}", e));
270                                    self.ui_state.set_app_state(AppState::Normal);
271                                    self.ui_state.reset_input();
272                                }
273                            }
274                        } else {
275                            self.add_error_message(
276                                "Selected item is not an initiative".to_string(),
277                            );
278                            self.ui_state.set_app_state(AppState::Normal);
279                            self.ui_state.reset_input();
280                        }
281                    }
282                    _ => {
283                        // For other document types, use regular creation
284                        match document_service
285                            .create_document(
286                                child_doc_type,
287                                title,
288                                None, // description
289                                Some(parent_id.to_string()),
290                            )
291                            .await
292                        {
293                            Ok(_) => {
294                                self.add_success_message(format!(
295                                    "{} created successfully",
296                                    child_doc_type
297                                ));
298                                self.ui_state.set_app_state(AppState::Normal);
299                                self.ui_state.reset_input();
300                                // Sync database and reload documents
301                                if let Some(sync_service) = &self.sync_service {
302                                    let _ = sync_service.sync_database().await;
303                                }
304                                self.load_documents().await?;
305                            }
306                            Err(e) => {
307                                self.add_error_message(format!(
308                                    "Failed to create {}: {}",
309                                    child_doc_type, e
310                                ));
311                                self.ui_state.set_app_state(AppState::Normal);
312                                self.ui_state.reset_input();
313                            }
314                        }
315                    }
316                }
317            }
318        } else {
319            // No parent selected - show appropriate error message based on board
320            let error_msg = match self.ui_state.current_board {
321                BoardType::Strategy => {
322                    "Please select a strategy first to create an initiative under it"
323                }
324                BoardType::Initiative => {
325                    "Please select an initiative first to create a task under it"
326                }
327                _ => "Please select a parent document first",
328            };
329            self.add_error_message(error_msg.to_string());
330            self.ui_state.set_app_state(AppState::Normal);
331            self.ui_state.reset_input();
332        }
333
334        Ok(())
335    }
336
337    pub async fn create_adr_from_ticket(&mut self) -> Result<()> {
338        // Get the title from user input
339        let title = self.ui_state.input_title.value().to_string();
340        if title.trim().is_empty() {
341            self.add_error_message("ADR title cannot be empty".to_string());
342            self.ui_state.set_app_state(AppState::Normal);
343            self.ui_state.reset_input();
344            return Ok(());
345        }
346
347        // Get the currently selected ticket for context
348        let context = if let Some(selected_item) = self.get_selected_item() {
349            // Allow ADR creation from strategies, initiatives, and tasks
350            match &selected_item.document {
351                DocumentObject::Strategy(strategy) => Some(format!(
352                    "Context from strategy '{}': {}",
353                    strategy.title(),
354                    strategy.content().full_content()
355                )),
356                DocumentObject::Initiative(initiative) => Some(format!(
357                    "Context from initiative '{}': {}",
358                    initiative.title(),
359                    initiative.content().full_content()
360                )),
361                DocumentObject::Task(task) => Some(format!(
362                    "Context from task '{}': {}",
363                    task.title(),
364                    task.content().full_content()
365                )),
366                DocumentObject::Adr(_) => None, // Cannot create ADR from ADR
367            }
368        } else {
369            None // No context if no ticket selected
370        };
371
372        if let Some(document_service) = &self.document_service {
373            match document_service.create_adr(title, context).await {
374                Ok(_file_path) => {
375                    self.add_success_message("ADR created successfully".to_string());
376                    self.ui_state.set_app_state(AppState::Normal);
377                    self.ui_state.reset_input();
378                    // Sync database and reload documents
379                    if let Some(sync_service) = &self.sync_service {
380                        let _ = sync_service.sync_database().await;
381                    }
382                    self.load_documents().await?;
383                }
384                Err(e) => {
385                    self.add_error_message(format!("Failed to create ADR: {}", e));
386                    self.ui_state.set_app_state(AppState::Normal);
387                    self.ui_state.reset_input();
388                }
389            }
390        }
391
392        Ok(())
393    }
394
395    // Backlog category selection methods
396    pub fn move_category_selection_up(&mut self) {
397        if self.ui_state.backlog_category_selection > 0 {
398            self.ui_state.backlog_category_selection -= 1;
399            self.update_selected_category();
400        }
401    }
402
403    pub fn move_category_selection_down(&mut self) {
404        if self.ui_state.backlog_category_selection < 3 {
405            // 4 categories (0-3)
406            self.ui_state.backlog_category_selection += 1;
407            self.update_selected_category();
408        }
409    }
410
411    pub fn confirm_category_selection(&mut self) {
412        // Move to document creation with the selected category
413        self.ui_state.set_app_state(AppState::CreatingDocument);
414    }
415
416    fn update_selected_category(&mut self) {
417        self.ui_state.selected_backlog_category = match self.ui_state.backlog_category_selection {
418            0 => BacklogCategory::General,
419            1 => BacklogCategory::Bug,
420            2 => BacklogCategory::Feature,
421            3 => BacklogCategory::TechDebt,
422            _ => BacklogCategory::General,
423        };
424    }
425
426    async fn add_tag_to_document(&self, doc_id: &str, tag: &str) -> Result<()> {
427        if let Some(document_service) = &self.document_service {
428            // Get document from database to find file path
429            let docs = document_service.load_documents_from_database().await?;
430
431            if let Some(doc) = docs.iter().find(|d| d.id == doc_id) {
432                // Read the file content
433                let content = std::fs::read_to_string(&doc.filepath)?;
434
435                // Add the tag to the frontmatter - look for the actual format used
436                let updated_content = if content.contains("tags:") {
437                    // Look for the pattern: find the last tag line and insert after it
438                    let lines: Vec<&str> = content.lines().collect();
439                    let mut new_lines = Vec::new();
440                    let mut in_tags_section = false;
441                    let mut tags_section_ended = false;
442                    let tag_line = format!("  - \"{}\"", tag);
443
444                    for line in lines {
445                        if line.trim() == "tags:" {
446                            in_tags_section = true;
447                            new_lines.push(line.to_string());
448                        } else if in_tags_section && !tags_section_ended {
449                            if line.trim().starts_with("- \"#") {
450                                // This is a tag line, keep it
451                                new_lines.push(line.to_string());
452                            } else if line.trim().is_empty() {
453                                // Found empty line after tags, insert our tag before it
454                                new_lines.push(tag_line.clone());
455                                new_lines.push(line.to_string());
456                                tags_section_ended = true;
457                            } else {
458                                // Non-tag line found, insert our tag before it
459                                new_lines.push(tag_line.clone());
460                                new_lines.push(line.to_string());
461                                tags_section_ended = true;
462                            }
463                        } else {
464                            new_lines.push(line.to_string());
465                        }
466                    }
467
468                    new_lines.join("\n")
469                } else {
470                    // This shouldn't happen for properly created documents, but handle it
471                    content
472                };
473
474                // Write the updated content back
475                std::fs::write(&doc.filepath, updated_content)?;
476            }
477        }
478        Ok(())
479    }
480}