ricecoder_github/managers/
project_operations.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum AutomationAction {
13 MoveToColumn(ColumnStatus),
15 AddLabel(String),
17 RemoveLabel(String),
19 AssignTo(String),
21 AddComment(String),
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum AutomationTrigger {
28 PrOpened,
30 PrClosed,
32 PrMerged,
34 IssueOpened,
36 IssueClosed,
38 LabelAdded(String),
40 LabelRemoved(String),
42}
43
44impl AutomationTrigger {
45 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#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AutomationWorkflow {
62 pub name: String,
64 pub trigger: String,
66 pub actions: Vec<AutomationAction>,
68 pub enabled: bool,
70}
71
72impl AutomationWorkflow {
73 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 pub fn with_action(mut self, action: AutomationAction) -> Self {
85 self.actions.push(action);
86 self
87 }
88
89 pub fn disable(mut self) -> Self {
91 self.enabled = false;
92 self
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ReportSection {
99 pub title: String,
101 pub content: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct DetailedProjectReport {
108 pub title: String,
110 pub timestamp: chrono::DateTime<chrono::Utc>,
112 pub sections: Vec<ReportSection>,
114 pub summary: HashMap<String, String>,
116}
117
118#[derive(Debug, Clone)]
120pub struct ProjectOperations {
121 workflows: Vec<AutomationWorkflow>,
123 report_history: Vec<DetailedProjectReport>,
125}
126
127impl ProjectOperations {
128 pub fn new() -> Self {
130 Self {
131 workflows: Vec::new(),
132 report_history: Vec::new(),
133 }
134 }
135
136 pub fn add_workflow(&mut self, workflow: AutomationWorkflow) {
138 info!("Adding automation workflow: {}", workflow.name);
139 self.workflows.push(workflow);
140 }
141
142 pub fn workflows(&self) -> &[AutomationWorkflow] {
144 &self.workflows
145 }
146
147 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 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 applied_actions.push(action.clone());
180 }
181 }
182 }
183 }
184
185 Ok(applied_actions)
186 }
187
188 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 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 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 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.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 pub fn report_history(&self) -> &[DetailedProjectReport] {
276 &self.report_history
277 }
278
279 pub fn latest_report(&self) -> Option<&DetailedProjectReport> {
281 self.report_history.last()
282 }
283
284 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}