ricecoder_github/managers/
project_operations.rs

1//! Project Operations - Handles project automation and reporting
2
3use crate::errors::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, info};
7
8use super::project_manager::{ColumnStatus, ProjectManager, ProjectStatusReport};
9
10/// Automation action
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum AutomationAction {
13    /// Move card to column
14    MoveToColumn(ColumnStatus),
15    /// Add label
16    AddLabel(String),
17    /// Remove label
18    RemoveLabel(String),
19    /// Assign to user
20    AssignTo(String),
21    /// Add comment
22    AddComment(String),
23}
24
25/// Automation trigger
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum AutomationTrigger {
28    /// PR opened
29    PrOpened,
30    /// PR closed
31    PrClosed,
32    /// PR merged
33    PrMerged,
34    /// Issue opened
35    IssueOpened,
36    /// Issue closed
37    IssueClosed,
38    /// Label added
39    LabelAdded(String),
40    /// Label removed
41    LabelRemoved(String),
42}
43
44impl AutomationTrigger {
45    /// Get trigger name
46    pub fn name(&self) -> String {
47        match self {
48            AutomationTrigger::PrOpened => "pr_opened".to_string(),
49            AutomationTrigger::PrClosed => "pr_closed".to_string(),
50            AutomationTrigger::PrMerged => "pr_merged".to_string(),
51            AutomationTrigger::IssueOpened => "issue_opened".to_string(),
52            AutomationTrigger::IssueClosed => "issue_closed".to_string(),
53            AutomationTrigger::LabelAdded(label) => format!("label_added:{}", label),
54            AutomationTrigger::LabelRemoved(label) => format!("label_removed:{}", label),
55        }
56    }
57}
58
59/// Automation workflow
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AutomationWorkflow {
62    /// Workflow name
63    pub name: String,
64    /// Trigger
65    pub trigger: String,
66    /// Actions to perform
67    pub actions: Vec<AutomationAction>,
68    /// Is enabled
69    pub enabled: bool,
70}
71
72impl AutomationWorkflow {
73    /// Create a new automation workflow
74    pub fn new(name: impl Into<String>, trigger: impl Into<String>) -> Self {
75        Self {
76            name: name.into(),
77            trigger: trigger.into(),
78            actions: Vec::new(),
79            enabled: true,
80        }
81    }
82
83    /// Add an action
84    pub fn with_action(mut self, action: AutomationAction) -> Self {
85        self.actions.push(action);
86        self
87    }
88
89    /// Disable workflow
90    pub fn disable(mut self) -> Self {
91        self.enabled = false;
92        self
93    }
94}
95
96/// Project report section
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ReportSection {
99    /// Section title
100    pub title: String,
101    /// Section content
102    pub content: String,
103}
104
105/// Detailed project report
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct DetailedProjectReport {
108    /// Report title
109    pub title: String,
110    /// Report timestamp
111    pub timestamp: chrono::DateTime<chrono::Utc>,
112    /// Report sections
113    pub sections: Vec<ReportSection>,
114    /// Summary statistics
115    pub summary: HashMap<String, String>,
116}
117
118/// Project Operations
119#[derive(Debug, Clone)]
120pub struct ProjectOperations {
121    /// Automation workflows
122    workflows: Vec<AutomationWorkflow>,
123    /// Report history
124    report_history: Vec<DetailedProjectReport>,
125}
126
127impl ProjectOperations {
128    /// Create new project operations
129    pub fn new() -> Self {
130        Self {
131            workflows: Vec::new(),
132            report_history: Vec::new(),
133        }
134    }
135
136    /// Add automation workflow
137    pub fn add_workflow(&mut self, workflow: AutomationWorkflow) {
138        info!("Adding automation workflow: {}", workflow.name);
139        self.workflows.push(workflow);
140    }
141
142    /// Get workflows
143    pub fn workflows(&self) -> &[AutomationWorkflow] {
144        &self.workflows
145    }
146
147    /// Get enabled workflows for trigger
148    pub fn get_workflows_for_trigger(&self, trigger: &str) -> Vec<&AutomationWorkflow> {
149        self.workflows
150            .iter()
151            .filter(|w| w.enabled && w.trigger == trigger)
152            .collect()
153    }
154
155    /// Apply automation workflows
156    pub fn apply_workflows(
157        &self,
158        project_manager: &mut ProjectManager,
159        card_id: u64,
160        trigger: &str,
161    ) -> Result<Vec<AutomationAction>> {
162        let workflows = self.get_workflows_for_trigger(trigger);
163        let mut applied_actions = Vec::new();
164
165        for workflow in workflows {
166            debug!(
167                "Applying workflow '{}' for trigger '{}'",
168                workflow.name, trigger
169            );
170
171            for action in &workflow.actions {
172                match action {
173                    AutomationAction::MoveToColumn(status) => {
174                        project_manager.move_card_to_column(card_id, *status)?;
175                        applied_actions.push(action.clone());
176                    }
177                    _ => {
178                        // Other actions would be handled by different systems
179                        applied_actions.push(action.clone());
180                    }
181                }
182            }
183        }
184
185        Ok(applied_actions)
186    }
187
188    /// Generate detailed report
189    pub fn generate_detailed_report(
190        &mut self,
191        _project_manager: &ProjectManager,
192        base_report: &ProjectStatusReport,
193    ) -> DetailedProjectReport {
194        let mut sections = Vec::new();
195        let mut summary = HashMap::new();
196
197        // Overview section
198        sections.push(ReportSection {
199            title: "Project Overview".to_string(),
200            content: format!(
201                "Project: {}\nTotal Cards: {}\nProgress: {}%",
202                base_report.project_name,
203                base_report.metrics.total_cards,
204                base_report.metrics.progress_percentage
205            ),
206        });
207
208        // Status breakdown section
209        sections.push(ReportSection {
210            title: "Status Breakdown".to_string(),
211            content: format!(
212                "Todo: {}\nIn Progress: {}\nIn Review: {}\nDone: {}",
213                base_report.metrics.todo_count,
214                base_report.metrics.in_progress_count,
215                base_report.metrics.in_review_count,
216                base_report.metrics.done_count
217            ),
218        });
219
220        // Cards by column section
221        let mut cards_content = String::new();
222        for (column, cards) in &base_report.cards_by_column {
223            cards_content.push_str(&format!("\n{}: {} cards\n", column, cards.len()));
224            for card in cards {
225                if let Some(note) = &card.note {
226                    cards_content.push_str(&format!("  - {}\n", note));
227                }
228            }
229        }
230        sections.push(ReportSection {
231            title: "Cards by Column".to_string(),
232            content: cards_content,
233        });
234
235        // Summary statistics
236        summary.insert(
237            "total_cards".to_string(),
238            base_report.metrics.total_cards.to_string(),
239        );
240        summary.insert(
241            "progress_percentage".to_string(),
242            base_report.metrics.progress_percentage.to_string(),
243        );
244        summary.insert(
245            "todo_count".to_string(),
246            base_report.metrics.todo_count.to_string(),
247        );
248        summary.insert(
249            "in_progress_count".to_string(),
250            base_report.metrics.in_progress_count.to_string(),
251        );
252        summary.insert(
253            "in_review_count".to_string(),
254            base_report.metrics.in_review_count.to_string(),
255        );
256        summary.insert(
257            "done_count".to_string(),
258            base_report.metrics.done_count.to_string(),
259        );
260
261        let report = DetailedProjectReport {
262            title: format!("Project Report: {}", base_report.project_name),
263            timestamp: chrono::Utc::now(),
264            sections,
265            summary,
266        };
267
268        self.report_history.push(report.clone());
269        info!("Generated detailed project report");
270
271        report
272    }
273
274    /// Get report history
275    pub fn report_history(&self) -> &[DetailedProjectReport] {
276        &self.report_history
277    }
278
279    /// Get latest report
280    pub fn latest_report(&self) -> Option<&DetailedProjectReport> {
281        self.report_history.last()
282    }
283
284    /// Clear report history
285    pub fn clear_report_history(&mut self) {
286        self.report_history.clear();
287    }
288}
289
290impl Default for ProjectOperations {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_create_automation_workflow() {
302        let workflow = AutomationWorkflow::new("Test Workflow", "pr_opened")
303            .with_action(AutomationAction::MoveToColumn(ColumnStatus::InReview));
304
305        assert_eq!(workflow.name, "Test Workflow");
306        assert_eq!(workflow.trigger, "pr_opened");
307        assert_eq!(workflow.actions.len(), 1);
308        assert!(workflow.enabled);
309    }
310
311    #[test]
312    fn test_automation_trigger_name() {
313        assert_eq!(AutomationTrigger::PrOpened.name(), "pr_opened");
314        assert_eq!(AutomationTrigger::PrMerged.name(), "pr_merged");
315        assert_eq!(
316            AutomationTrigger::LabelAdded("bug".to_string()).name(),
317            "label_added:bug"
318        );
319    }
320
321    #[test]
322    fn test_project_operations_add_workflow() {
323        let mut ops = ProjectOperations::new();
324        let workflow = AutomationWorkflow::new("Test", "pr_opened");
325        ops.add_workflow(workflow);
326
327        assert_eq!(ops.workflows().len(), 1);
328    }
329
330    #[test]
331    fn test_get_workflows_for_trigger() {
332        let mut ops = ProjectOperations::new();
333        ops.add_workflow(AutomationWorkflow::new("Workflow 1", "pr_opened"));
334        ops.add_workflow(AutomationWorkflow::new("Workflow 2", "pr_opened"));
335        ops.add_workflow(AutomationWorkflow::new("Workflow 3", "issue_opened"));
336
337        let workflows = ops.get_workflows_for_trigger("pr_opened");
338        assert_eq!(workflows.len(), 2);
339    }
340
341    #[test]
342    fn test_generate_detailed_report() {
343        let mut ops = ProjectOperations::new();
344        let mut project_manager = ProjectManager::new(1, "Test Project");
345        project_manager.set_column_mapping(ColumnStatus::Todo, 100);
346
347        let base_report = project_manager.generate_status_report();
348        let detailed_report = ops.generate_detailed_report(&project_manager, &base_report);
349
350        assert_eq!(detailed_report.title, "Project Report: Test Project");
351        assert!(!detailed_report.sections.is_empty());
352        assert!(!detailed_report.summary.is_empty());
353    }
354
355    #[test]
356    fn test_report_history() {
357        let mut ops = ProjectOperations::new();
358        let project_manager = ProjectManager::new(1, "Test Project");
359        let base_report = project_manager.generate_status_report();
360
361        ops.generate_detailed_report(&project_manager, &base_report);
362        ops.generate_detailed_report(&project_manager, &base_report);
363
364        assert_eq!(ops.report_history().len(), 2);
365        assert!(ops.latest_report().is_some());
366    }
367
368    #[test]
369    fn test_disable_workflow() {
370        let workflow = AutomationWorkflow::new("Test", "pr_opened").disable();
371        assert!(!workflow.enabled);
372    }
373}