1use cli_core::render::{WireValue, parse_wire};
9use cli_core::reports::{
10 parse_chart_shape, value_to_activity_periods, value_to_balance_rows, value_to_breakdown_periods,
11};
12use plotting::{
13 ChartSpec,
14 adapters::{
15 ActivityChartOpts, BalanceChartOpts, BreakdownChartOpts, SortOrder, activity_chart,
16 balance_chart, breakdown_chart,
17 },
18 ratatui::render_ratatui,
19};
20use ratatui::Frame;
21use ratatui::layout::Rect;
22use ratatui::widgets::{Block, Borders, Paragraph};
23
24use crate::tabs::fetch::Fetch;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ReportKind {
29 Balance,
30 Activity,
31 Breakdown,
32}
33
34impl ReportKind {
35 #[must_use]
36 pub fn label(self) -> &'static str {
37 match self {
38 ReportKind::Balance => "Balance",
39 ReportKind::Activity => "Activity",
40 ReportKind::Breakdown => "Category Breakdown",
41 }
42 }
43}
44
45pub struct ReportsTab {
46 pub state: Fetch<ChartSpec>,
47 pub kind: ReportKind,
49 pub chart: String,
51}
52
53impl ReportsTab {
54 #[must_use]
55 pub fn new() -> Self {
56 Self {
57 state: Fetch::Idle,
58 kind: ReportKind::Balance,
59 chart: "bar".to_string(),
60 }
61 }
62
63 pub fn set_loading(&mut self, id: i64, kind: ReportKind, chart: impl Into<String>) {
65 self.kind = kind;
66 self.chart = chart.into();
67 self.state = Fetch::Loading { id };
68 }
69
70 pub fn on_reply(&mut self, wire: &str, kind: ReportKind, chart: &str) {
75 self.kind = kind;
76 self.chart = chart.to_string();
77 self.state = parse_report_reply(wire, kind, chart);
78 }
79
80 pub fn reset(&mut self) {
82 self.state = Fetch::Idle;
83 }
84
85 pub fn draw(&self, frame: &mut Frame, area: Rect) {
87 let block = Block::default()
88 .borders(Borders::ALL)
89 .title(self.kind.label());
90 match &self.state {
91 Fetch::Idle => {
92 let msg = "Use :reports balance | :reports activity from=YYYY-MM-DD to=YYYY-MM-DD | :reports breakdown from=... to=...";
93 frame.render_widget(Paragraph::new(msg).block(block), area);
94 }
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 }
110}
111
112impl Default for ReportsTab {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118fn parse_report_reply(wire: &str, kind: ReportKind, chart: &str) -> Fetch<ChartSpec> {
120 match parse_wire(wire) {
121 Err(e) => Fetch::Error(e.to_string()),
122 Ok(WireValue::Value(value)) => match build_chart_spec(&value, kind, chart) {
123 Ok(spec) => Fetch::Loaded(spec),
124 Err(e) => Fetch::Error(e),
125 },
126 }
127}
128
129fn build_chart_spec(
130 value: &scripting::nomiscript::Value,
131 kind: ReportKind,
132 chart: &str,
133) -> Result<ChartSpec, String> {
134 let chart_kind = parse_chart_shape(chart);
135 match kind {
136 ReportKind::Balance => {
137 let rows = value_to_balance_rows(value).map_err(|e| e.to_string())?;
138 Ok(balance_chart(
139 &rows,
140 BalanceChartOpts {
141 kind: chart_kind,
142 top_n: 10,
143 sort_order: SortOrder::MagnitudeDesc,
144 },
145 ))
146 }
147 ReportKind::Activity => {
148 let periods = value_to_activity_periods(value).map_err(|e| e.to_string())?;
149 Ok(activity_chart(
150 &periods,
151 ActivityChartOpts {
152 kind: chart_kind,
153 include_net: true,
154 },
155 ))
156 }
157 ReportKind::Breakdown => {
158 let periods = value_to_breakdown_periods(value).map_err(|e| e.to_string())?;
159 Ok(breakdown_chart(
160 &periods,
161 BreakdownChartOpts {
162 kind: chart_kind,
163 top_n: 10,
164 },
165 ))
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests;