1
//! Reports tab: balance / activity / category-breakdown.
2
//!
3
//! `ReportsTab` holds the current fetch state and the last successful
4
//! `ChartSpec`.  `on_reply` translates a wire frame into a spec via the
5
//! shared `cli_core::reports` parse helpers and the `plotting::adapters`
6
//! chain.  `draw` renders the spec inline with `plotting::ratatui`.
7

            
8
use cli_core::render::{WireValue, parse_wire};
9
use cli_core::reports::{
10
    parse_chart_shape, value_to_activity_periods, value_to_balance_rows, value_to_breakdown_periods,
11
};
12
use plotting::{
13
    ChartSpec,
14
    adapters::{
15
        ActivityChartOpts, BalanceChartOpts, BreakdownChartOpts, SortOrder, activity_chart,
16
        balance_chart, breakdown_chart,
17
    },
18
    ratatui::render_ratatui,
19
};
20
use ratatui::Frame;
21
use ratatui::layout::Rect;
22
use ratatui::widgets::{Block, Borders, Paragraph};
23

            
24
use crate::tabs::fetch::Fetch;
25

            
26
/// Which report is currently loaded or being fetched.
27
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28
pub enum ReportKind {
29
    Balance,
30
    Activity,
31
    Breakdown,
32
}
33

            
34
impl ReportKind {
35
    #[must_use]
36
25
    pub fn label(self) -> &'static str {
37
25
        match self {
38
23
            ReportKind::Balance => "Balance",
39
1
            ReportKind::Activity => "Activity",
40
1
            ReportKind::Breakdown => "Category Breakdown",
41
        }
42
25
    }
43
}
44

            
45
pub struct ReportsTab {
46
    pub state: Fetch<ChartSpec>,
47
    /// Which report produced the current (or pending) fetch.
48
    pub kind: ReportKind,
49
    /// Chart shape used for the last build.
50
    pub chart: String,
51
}
52

            
53
impl ReportsTab {
54
    #[must_use]
55
166
    pub fn new() -> Self {
56
166
        Self {
57
166
            state: Fetch::Idle,
58
166
            kind: ReportKind::Balance,
59
166
            chart: "bar".to_string(),
60
166
        }
61
166
    }
62

            
63
    /// Set loading state once an eval id is assigned.
64
7
    pub fn set_loading(&mut self, id: i64, kind: ReportKind, chart: impl Into<String>) {
65
7
        self.kind = kind;
66
7
        self.chart = chart.into();
67
7
        self.state = Fetch::Loading { id };
68
7
    }
69

            
70
    /// Process a routed wire reply and update the fetch state.
71
    /// Apply a routed reply, parsing it with the `kind`/`chart` that produced
72
    /// the request (carried on the route), not the tab's current selection —
73
    /// the two can differ if a newer request was issued before this reply.
74
6
    pub fn on_reply(&mut self, wire: &str, kind: ReportKind, chart: &str) {
75
6
        self.kind = kind;
76
6
        self.chart = chart.to_string();
77
6
        self.state = parse_report_reply(wire, kind, chart);
78
6
    }
79

            
80
    /// Reset to idle so the next invocation re-fetches.
81
1
    pub fn reset(&mut self) {
82
1
        self.state = Fetch::Idle;
83
1
    }
84

            
85
    /// Render the tab into `area`.
86
22
    pub fn draw(&self, frame: &mut Frame, area: Rect) {
87
22
        let block = Block::default()
88
22
            .borders(Borders::ALL)
89
22
            .title(self.kind.label());
90
22
        match &self.state {
91
22
            Fetch::Idle => {
92
22
                let msg = "Use :reports balance | :reports activity from=YYYY-MM-DD to=YYYY-MM-DD | :reports breakdown from=... to=...";
93
22
                frame.render_widget(Paragraph::new(msg).block(block), area);
94
22
            }
95
            Fetch::Loading { .. } => {
96
                frame.render_widget(Paragraph::new("Loading...").block(block), area);
97
            }
98
            Fetch::Error(e) => {
99
                let msg = format!("[error] {e}");
100
                frame.render_widget(Paragraph::new(msg).block(block), area);
101
            }
102
            Fetch::Loaded(spec) => {
103
                let inner = block.inner(area);
104
                frame.render_widget(block, area);
105
                let chart = render_ratatui(spec);
106
                chart.draw(frame, inner);
107
            }
108
        }
109
22
    }
110
}
111

            
112
impl Default for ReportsTab {
113
    fn default() -> Self {
114
        Self::new()
115
    }
116
}
117

            
118
/// Parse a wire reply frame into a `Fetch<ChartSpec>` for the given kind.
119
6
fn parse_report_reply(wire: &str, kind: ReportKind, chart: &str) -> Fetch<ChartSpec> {
120
6
    match parse_wire(wire) {
121
2
        Err(e) => Fetch::Error(e.to_string()),
122
4
        Ok(WireValue::Value(value)) => match build_chart_spec(&value, kind, chart) {
123
4
            Ok(spec) => Fetch::Loaded(spec),
124
            Err(e) => Fetch::Error(e),
125
        },
126
    }
127
6
}
128

            
129
4
fn build_chart_spec(
130
4
    value: &scripting::nomiscript::Value,
131
4
    kind: ReportKind,
132
4
    chart: &str,
133
4
) -> Result<ChartSpec, String> {
134
4
    let chart_kind = parse_chart_shape(chart);
135
4
    match kind {
136
        ReportKind::Balance => {
137
2
            let rows = value_to_balance_rows(value).map_err(|e| e.to_string())?;
138
2
            Ok(balance_chart(
139
2
                &rows,
140
2
                BalanceChartOpts {
141
2
                    kind: chart_kind,
142
2
                    top_n: 10,
143
2
                    sort_order: SortOrder::MagnitudeDesc,
144
2
                },
145
2
            ))
146
        }
147
        ReportKind::Activity => {
148
1
            let periods = value_to_activity_periods(value).map_err(|e| e.to_string())?;
149
1
            Ok(activity_chart(
150
1
                &periods,
151
1
                ActivityChartOpts {
152
1
                    kind: chart_kind,
153
1
                    include_net: true,
154
1
                },
155
1
            ))
156
        }
157
        ReportKind::Breakdown => {
158
1
            let periods = value_to_breakdown_periods(value).map_err(|e| e.to_string())?;
159
1
            Ok(breakdown_chart(
160
1
                &periods,
161
1
                BreakdownChartOpts {
162
1
                    kind: chart_kind,
163
1
                    top_n: 10,
164
1
                },
165
1
            ))
166
        }
167
    }
168
4
}
169

            
170
#[cfg(test)]
171
mod tests;