use adw::prelude::*; use outlay_core::db::Database; use std::rc::Rc; pub struct ForecastView { pub container: gtk::Box, } impl ForecastView { pub fn new(db: Rc) -> Self { let container = gtk::Box::new(gtk::Orientation::Vertical, 0); let toast_overlay = adw::ToastOverlay::new(); let clamp = adw::Clamp::new(); clamp.set_maximum_size(700); clamp.set_margin_start(16); clamp.set_margin_end(16); let inner = gtk::Box::new(gtk::Orientation::Vertical, 20); inner.set_margin_top(20); inner.set_margin_bottom(20); let base_currency = db.get_setting("base_currency") .ok().flatten() .unwrap_or_else(|| "USD".to_string()); let summary_card = gtk::Box::new(gtk::Orientation::Vertical, 4); summary_card.add_css_class("card"); summary_card.set_margin_start(4); summary_card.set_margin_end(4); let summary_title = gtk::Label::new(Some("CASH FLOW FORECAST")); summary_title.add_css_class("caption"); summary_title.add_css_class("dim-label"); summary_title.set_halign(gtk::Align::Start); summary_title.set_margin_top(12); summary_title.set_margin_start(12); let summary_detail = gtk::Label::new(Some("Based on recurring transactions and 3-month averages")); summary_detail.add_css_class("caption"); summary_detail.add_css_class("dim-label"); summary_detail.set_halign(gtk::Align::Start); summary_detail.set_margin_start(12); summary_detail.set_margin_bottom(12); summary_card.append(&summary_title); summary_card.append(&summary_detail); let forecast_group = adw::PreferencesGroup::builder() .title("MONTHLY PROJECTION") .build(); let forecast_data = db.forecast_cash_flow(6).unwrap_or_default(); for (i, (month_key, income, expenses, balance)) in forecast_data.iter().enumerate() { let month_name = Self::month_name(month_key); let is_current = i == 0; let label = if is_current { format!("{} (current)", month_name) } else { format!("{} (projected)", month_name) }; let net = income - expenses; let balance_str = if *balance >= 0.0 { format!("+{:.0} {}", balance, base_currency) } else { format!("{:.0} {}", balance, base_currency) }; let row = adw::ActionRow::builder() .title(&label) .subtitle(&format!( "Income: {:.0} - Expenses: {:.0} = Net: {:.0}", income, expenses, net, )) .build(); let balance_label = gtk::Label::new(Some(&balance_str)); balance_label.add_css_class("amount-display"); if *balance >= 0.0 { balance_label.add_css_class("amount-income"); } else { balance_label.add_css_class("amount-expense"); } row.add_suffix(&balance_label); if !is_current { row.add_css_class("dim-label"); } forecast_group.add(&row); } if forecast_data.is_empty() { let row = adw::ActionRow::builder() .title("No data available for forecast") .subtitle("Add some transactions to see projections") .build(); row.add_css_class("dim-label"); forecast_group.add(&row); } // Net cash flow summary if forecast_data.len() >= 2 { let last = &forecast_data[forecast_data.len() - 1]; let net_summary = if last.3 >= 0.0 { format!("Projected cumulative balance in 6 months: +{:.0} {}", last.3, base_currency) } else { format!("Projected cumulative balance in 6 months: {:.0} {}", last.3, base_currency) }; let net_label = gtk::Label::new(Some(&net_summary)); net_label.add_css_class("heading"); net_label.set_halign(gtk::Align::Start); net_label.set_margin_start(4); net_label.set_margin_top(8); inner.append(&summary_card); inner.append(&forecast_group); inner.append(&net_label); } else { inner.append(&summary_card); inner.append(&forecast_group); } clamp.set_child(Some(&inner)); let scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .child(&clamp) .build(); toast_overlay.set_child(Some(&scroll)); container.append(&toast_overlay); ForecastView { container } } fn month_name(key: &str) -> String { let parts: Vec<&str> = key.split('-').collect(); if parts.len() != 2 { return key.to_string(); } let month: u32 = parts[1].parse().unwrap_or(0); let year = parts[0]; let name = match month { 1 => "January", 2 => "February", 3 => "March", 4 => "April", 5 => "May", 6 => "June", 7 => "July", 8 => "August", 9 => "September", 10 => "October", 11 => "November", 12 => "December", _ => "Unknown", }; format!("{} {}", name, year) } }