nu_analytics/core/report/
mod.rs

1//! Report generation module for curriculum analysis
2//!
3//! This module provides functionality to generate curriculum reports in various formats
4//! (Markdown, HTML, PDF) with visualizations of the curriculum graph and term scheduling.
5
6pub mod formats;
7pub mod term_scheduler;
8pub mod visualization;
9
10use crate::core::metrics::CurriculumMetrics;
11use crate::core::metrics_export::CurriculumSummary;
12use crate::core::models::{Degree, Plan, School, DAG};
13use std::error::Error;
14use std::path::Path;
15
16pub use formats::{HtmlReporter, MarkdownReporter, PdfReporter, ReportFormat};
17pub use term_scheduler::{SchedulerConfig, TermPlan, TermScheduler};
18pub use visualization::MermaidGenerator;
19
20/// Data context for report generation
21///
22/// This struct aggregates all data needed to render a curriculum report,
23/// providing a single source of truth for templates.
24#[derive(Debug, Clone)]
25pub struct ReportContext<'a> {
26    /// School containing course catalog
27    pub school: &'a School,
28    /// Curriculum plan being reported
29    pub plan: &'a Plan,
30    /// Associated degree (if found)
31    pub degree: Option<&'a Degree>,
32    /// Computed metrics for courses
33    pub metrics: &'a CurriculumMetrics,
34    /// Summary statistics
35    pub summary: &'a CurriculumSummary,
36    /// Prerequisite DAG
37    pub dag: &'a DAG,
38    /// Term-by-term course schedule
39    pub term_plan: &'a TermPlan,
40}
41
42impl<'a> ReportContext<'a> {
43    /// Create a new report context
44    #[must_use]
45    pub const fn new(
46        school: &'a School,
47        plan: &'a Plan,
48        degree: Option<&'a Degree>,
49        metrics: &'a CurriculumMetrics,
50        summary: &'a CurriculumSummary,
51        dag: &'a DAG,
52        term_plan: &'a TermPlan,
53    ) -> Self {
54        Self {
55            school,
56            plan,
57            degree,
58            metrics,
59            summary,
60            dag,
61            term_plan,
62        }
63    }
64
65    /// Get the institution name
66    #[must_use]
67    pub fn institution_name(&self) -> &str {
68        self.plan
69            .institution
70            .as_deref()
71            .unwrap_or(&self.school.name)
72    }
73
74    /// Get the degree name or a default
75    #[must_use]
76    pub fn degree_name(&self) -> String {
77        self.degree
78            .map_or_else(|| self.plan.degree_id.clone(), Degree::id)
79    }
80
81    /// Get the system type (semester/quarter)
82    #[must_use]
83    pub fn system_type(&self) -> &str {
84        self.degree.map_or("semester", |d| d.system_type.as_str())
85    }
86
87    /// Get the CIP code
88    #[must_use]
89    pub fn cip_code(&self) -> &str {
90        self.degree.map_or("", |d| d.cip_code.as_str())
91    }
92
93    /// Calculate total credit hours
94    #[must_use]
95    pub fn total_credits(&self) -> f32 {
96        self.plan
97            .courses
98            .iter()
99            .filter_map(|key| self.school.get_course(key))
100            .map(|c| c.credit_hours)
101            .sum()
102    }
103
104    /// Get course count
105    #[must_use]
106    pub const fn course_count(&self) -> usize {
107        self.plan.courses.len()
108    }
109
110    /// Calculate the number of years from the term plan
111    #[allow(clippy::cast_precision_loss)]
112    #[must_use]
113    pub fn years(&self) -> f32 {
114        let terms_used = self.term_plan.terms_used();
115        let terms_per_year = if self.term_plan.is_quarter_system {
116            3.0 // quarters per year
117        } else {
118            2.0 // semesters per year
119        };
120        (terms_used as f32 / terms_per_year).ceil()
121    }
122}
123
124/// Trait for report generators
125pub trait ReportGenerator {
126    /// Generate a report to a file
127    ///
128    /// # Errors
129    /// Returns an error if report generation or file writing fails
130    fn generate(&self, ctx: &ReportContext, output_path: &Path) -> Result<(), Box<dyn Error>>;
131
132    /// Generate report content as a string
133    ///
134    /// # Errors
135    /// Returns an error if report generation fails
136    fn render(&self, ctx: &ReportContext) -> Result<String, Box<dyn Error>>;
137}