Add budgets view with progress bars and management
Month selector, summary of total budgeted vs spent, per-category progress bars with green/yellow/red color coding based on percentage. Add budget dialog with category picker and amount. Edit and delete budgets with confirmation.
This commit is contained in:
509
outlay-gtk/src/budgets_view.rs
Normal file
509
outlay-gtk/src/budgets_view.rs
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
use adw::prelude::*;
|
||||||
|
use chrono::{Datelike, Local};
|
||||||
|
use outlay_core::db::Database;
|
||||||
|
use outlay_core::models::TransactionType;
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
pub struct BudgetsView {
|
||||||
|
pub container: gtk::Box,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BudgetsView {
|
||||||
|
pub fn new(db: Rc<Database>) -> 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(12);
|
||||||
|
clamp.set_margin_end(12);
|
||||||
|
|
||||||
|
let inner = gtk::Box::new(gtk::Orientation::Vertical, 16);
|
||||||
|
inner.set_margin_top(16);
|
||||||
|
inner.set_margin_bottom(16);
|
||||||
|
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let current_year = Rc::new(Cell::new(today.year()));
|
||||||
|
let current_month = Rc::new(Cell::new(today.month()));
|
||||||
|
|
||||||
|
// Month navigation
|
||||||
|
let nav_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
nav_box.set_halign(gtk::Align::Center);
|
||||||
|
|
||||||
|
let prev_btn = gtk::Button::from_icon_name("go-previous-symbolic");
|
||||||
|
prev_btn.add_css_class("flat");
|
||||||
|
|
||||||
|
let month_label = gtk::Label::new(None);
|
||||||
|
month_label.add_css_class("title-3");
|
||||||
|
month_label.set_width_chars(16);
|
||||||
|
month_label.set_xalign(0.5);
|
||||||
|
|
||||||
|
let next_btn = gtk::Button::from_icon_name("go-next-symbolic");
|
||||||
|
next_btn.add_css_class("flat");
|
||||||
|
|
||||||
|
nav_box.append(&prev_btn);
|
||||||
|
nav_box.append(&month_label);
|
||||||
|
nav_box.append(&next_btn);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
let summary_label = gtk::Label::new(None);
|
||||||
|
summary_label.add_css_class("dim-label");
|
||||||
|
summary_label.set_halign(gtk::Align::Center);
|
||||||
|
|
||||||
|
// Budget list
|
||||||
|
let budget_group = adw::PreferencesGroup::builder()
|
||||||
|
.title("Budgets")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Add budget button
|
||||||
|
let add_btn = gtk::Button::with_label("Add Budget");
|
||||||
|
add_btn.add_css_class("suggested-action");
|
||||||
|
add_btn.add_css_class("pill");
|
||||||
|
add_btn.set_halign(gtk::Align::Center);
|
||||||
|
add_btn.set_margin_top(8);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
Self::update_month_label(&month_label, current_year.get(), current_month.get());
|
||||||
|
Self::load_budgets(
|
||||||
|
&db, &budget_group, &summary_label, &toast_overlay,
|
||||||
|
current_year.get(), current_month.get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let year_ref = current_year.clone();
|
||||||
|
let month_ref = current_month.clone();
|
||||||
|
let label_ref = month_label.clone();
|
||||||
|
let group_ref = budget_group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
prev_btn.connect_clicked(move |_| {
|
||||||
|
let mut y = year_ref.get();
|
||||||
|
let mut m = month_ref.get();
|
||||||
|
if m == 1 { m = 12; y -= 1; } else { m -= 1; }
|
||||||
|
year_ref.set(y);
|
||||||
|
month_ref.set(m);
|
||||||
|
Self::update_month_label(&label_ref, y, m);
|
||||||
|
Self::load_budgets(&db_ref, &group_ref, &summary_ref, &toast_ref, y, m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let year_ref = current_year.clone();
|
||||||
|
let month_ref = current_month.clone();
|
||||||
|
let label_ref = month_label.clone();
|
||||||
|
let group_ref = budget_group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
next_btn.connect_clicked(move |_| {
|
||||||
|
let mut y = year_ref.get();
|
||||||
|
let mut m = month_ref.get();
|
||||||
|
if m == 12 { m = 1; y += 1; } else { m += 1; }
|
||||||
|
year_ref.set(y);
|
||||||
|
month_ref.set(m);
|
||||||
|
Self::update_month_label(&label_ref, y, m);
|
||||||
|
Self::load_budgets(&db_ref, &group_ref, &summary_ref, &toast_ref, y, m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add budget button
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let year_ref = current_year.clone();
|
||||||
|
let month_ref = current_month.clone();
|
||||||
|
let group_ref = budget_group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
add_btn.connect_clicked(move |btn| {
|
||||||
|
let y = year_ref.get();
|
||||||
|
let m = month_ref.get();
|
||||||
|
Self::show_add_dialog(
|
||||||
|
btn, &db_ref, &group_ref, &summary_ref, &toast_ref, y, m,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.append(&nav_box);
|
||||||
|
inner.append(&summary_label);
|
||||||
|
inner.append(&budget_group);
|
||||||
|
inner.append(&add_btn);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
BudgetsView { container }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_month_label(label: >k::Label, year: i32, month: u32) {
|
||||||
|
let month_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",
|
||||||
|
};
|
||||||
|
label.set_label(&format!("{} {}", month_name, year));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn month_str(year: i32, month: u32) -> String {
|
||||||
|
format!("{:04}-{:02}", year, month)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_budgets(
|
||||||
|
db: &Rc<Database>,
|
||||||
|
group: &adw::PreferencesGroup,
|
||||||
|
summary_label: >k::Label,
|
||||||
|
toast_overlay: &adw::ToastOverlay,
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
) {
|
||||||
|
// Clear existing rows
|
||||||
|
while let Some(child) = group.first_child() {
|
||||||
|
if let Some(row) = child.downcast_ref::<adw::ActionRow>() {
|
||||||
|
group.remove(row);
|
||||||
|
} else if let Some(row) = child.downcast_ref::<gtk::ListBoxRow>() {
|
||||||
|
if let Some(parent) = row.parent() {
|
||||||
|
if let Some(listbox) = parent.downcast_ref::<gtk::ListBox>() {
|
||||||
|
listbox.remove(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let month_str = Self::month_str(year, month);
|
||||||
|
let budgets = db.list_budgets_for_month(&month_str).unwrap_or_default();
|
||||||
|
|
||||||
|
if budgets.is_empty() {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title("No budgets set for this month")
|
||||||
|
.build();
|
||||||
|
row.add_css_class("dim-label");
|
||||||
|
group.add(&row);
|
||||||
|
summary_label.set_label("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut total_budgeted = 0.0_f64;
|
||||||
|
let mut total_spent = 0.0_f64;
|
||||||
|
|
||||||
|
for budget in &budgets {
|
||||||
|
let progress = db
|
||||||
|
.get_budget_progress(budget.category_id, &month_str)
|
||||||
|
.unwrap_or(None);
|
||||||
|
|
||||||
|
let cat = db.get_category(budget.category_id).ok();
|
||||||
|
let cat_name = cat.as_ref().map(|c| {
|
||||||
|
match &c.icon {
|
||||||
|
Some(icon) => format!("{} {}", icon, c.name),
|
||||||
|
None => c.name.clone(),
|
||||||
|
}
|
||||||
|
}).unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
|
let (budget_amt, spent, pct) = progress.unwrap_or((budget.amount, 0.0, 0.0));
|
||||||
|
total_budgeted += budget_amt;
|
||||||
|
total_spent += spent;
|
||||||
|
|
||||||
|
let status = format!("{:.2} / {:.2} ({:.0}%)", spent, budget_amt, pct);
|
||||||
|
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(&cat_name)
|
||||||
|
.subtitle(&status)
|
||||||
|
.activatable(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
let level = gtk::LevelBar::builder()
|
||||||
|
.min_value(0.0)
|
||||||
|
.max_value(1.0)
|
||||||
|
.value((pct / 100.0).min(1.0))
|
||||||
|
.hexpand(true)
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.build();
|
||||||
|
level.set_width_request(120);
|
||||||
|
|
||||||
|
// Color based on percentage
|
||||||
|
if pct >= 100.0 {
|
||||||
|
level.add_css_class("error");
|
||||||
|
} else if pct >= 75.0 {
|
||||||
|
level.add_css_class("warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
row.add_suffix(&level);
|
||||||
|
|
||||||
|
// Click to edit
|
||||||
|
let budget_id = budget.id;
|
||||||
|
let cat_id = budget.category_id;
|
||||||
|
let current_amount = budget.amount;
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let group_ref = group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
row.connect_activated(move |row| {
|
||||||
|
Self::show_edit_dialog(
|
||||||
|
row, budget_id, cat_id, current_amount,
|
||||||
|
&db_ref, &group_ref, &summary_ref, &toast_ref, year, month,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_label.set_label(&format!(
|
||||||
|
"Total budgeted: {:.2} - Spent: {:.2}",
|
||||||
|
total_budgeted, total_spent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_add_dialog(
|
||||||
|
parent: >k::Button,
|
||||||
|
db: &Rc<Database>,
|
||||||
|
group: &adw::PreferencesGroup,
|
||||||
|
summary_label: >k::Label,
|
||||||
|
toast_overlay: &adw::ToastOverlay,
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
) {
|
||||||
|
let dialog = adw::Dialog::builder()
|
||||||
|
.title("Add Budget")
|
||||||
|
.content_width(360)
|
||||||
|
.content_height(300)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let toolbar = adw::ToolbarView::new();
|
||||||
|
toolbar.add_top_bar(&adw::HeaderBar::new());
|
||||||
|
|
||||||
|
let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
|
||||||
|
content.set_margin_top(16);
|
||||||
|
content.set_margin_bottom(16);
|
||||||
|
content.set_margin_start(16);
|
||||||
|
content.set_margin_end(16);
|
||||||
|
|
||||||
|
// Category selector
|
||||||
|
let cats = db
|
||||||
|
.list_categories(Some(TransactionType::Expense))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let cat_ids: Rc<RefCell<Vec<i64>>> = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let cat_model = gtk::StringList::new(&[]);
|
||||||
|
for cat in &cats {
|
||||||
|
let display = match &cat.icon {
|
||||||
|
Some(icon) => format!("{} {}", icon, cat.name),
|
||||||
|
None => cat.name.clone(),
|
||||||
|
};
|
||||||
|
cat_model.append(&display);
|
||||||
|
cat_ids.borrow_mut().push(cat.id);
|
||||||
|
}
|
||||||
|
let cat_row = adw::ComboRow::builder()
|
||||||
|
.title("Category")
|
||||||
|
.model(&cat_model)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let amount_row = adw::EntryRow::builder()
|
||||||
|
.title("Budget Amount")
|
||||||
|
.build();
|
||||||
|
amount_row.set_input_purpose(gtk::InputPurpose::Number);
|
||||||
|
|
||||||
|
let form = adw::PreferencesGroup::new();
|
||||||
|
form.add(&cat_row);
|
||||||
|
form.add(&amount_row);
|
||||||
|
|
||||||
|
let save_btn = gtk::Button::with_label("Save");
|
||||||
|
save_btn.add_css_class("suggested-action");
|
||||||
|
save_btn.add_css_class("pill");
|
||||||
|
save_btn.set_halign(gtk::Align::Center);
|
||||||
|
|
||||||
|
content.append(&form);
|
||||||
|
content.append(&save_btn);
|
||||||
|
toolbar.set_content(Some(&content));
|
||||||
|
dialog.set_child(Some(&toolbar));
|
||||||
|
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let dialog_ref = dialog.clone();
|
||||||
|
let group_ref = group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
let ids_ref = cat_ids.clone();
|
||||||
|
save_btn.connect_clicked(move |_| {
|
||||||
|
let amount: f64 = match amount_row.text().trim().parse() {
|
||||||
|
Ok(v) if v > 0.0 => v,
|
||||||
|
_ => {
|
||||||
|
let toast = adw::Toast::new("Please enter a valid amount");
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let idx = cat_row.selected() as usize;
|
||||||
|
let ids = ids_ref.borrow();
|
||||||
|
let cat_id = match ids.get(idx) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let month_str = Self::month_str(year, month);
|
||||||
|
match db_ref.set_budget(cat_id, &month_str, amount) {
|
||||||
|
Ok(()) => {
|
||||||
|
dialog_ref.close();
|
||||||
|
let toast = adw::Toast::new("Budget added");
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
Self::load_budgets(&db_ref, &group_ref, &summary_ref, &toast_ref, year, month);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let toast = adw::Toast::new(&format!("Error: {}", e));
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.present(Some(parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_edit_dialog(
|
||||||
|
parent: &adw::ActionRow,
|
||||||
|
budget_id: i64,
|
||||||
|
cat_id: i64,
|
||||||
|
current_amount: f64,
|
||||||
|
db: &Rc<Database>,
|
||||||
|
group: &adw::PreferencesGroup,
|
||||||
|
summary_label: >k::Label,
|
||||||
|
toast_overlay: &adw::ToastOverlay,
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
) {
|
||||||
|
let dialog = adw::Dialog::builder()
|
||||||
|
.title("Edit Budget")
|
||||||
|
.content_width(360)
|
||||||
|
.content_height(250)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let toolbar = adw::ToolbarView::new();
|
||||||
|
toolbar.add_top_bar(&adw::HeaderBar::new());
|
||||||
|
|
||||||
|
let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
|
||||||
|
content.set_margin_top(16);
|
||||||
|
content.set_margin_bottom(16);
|
||||||
|
content.set_margin_start(16);
|
||||||
|
content.set_margin_end(16);
|
||||||
|
|
||||||
|
let amount_row = adw::EntryRow::builder()
|
||||||
|
.title("Budget Amount")
|
||||||
|
.text(&format!("{:.2}", current_amount))
|
||||||
|
.build();
|
||||||
|
amount_row.set_input_purpose(gtk::InputPurpose::Number);
|
||||||
|
|
||||||
|
let form = adw::PreferencesGroup::new();
|
||||||
|
form.add(&amount_row);
|
||||||
|
|
||||||
|
let btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 12);
|
||||||
|
btn_box.set_halign(gtk::Align::Center);
|
||||||
|
|
||||||
|
let delete_btn = gtk::Button::with_label("Delete");
|
||||||
|
delete_btn.add_css_class("destructive-action");
|
||||||
|
delete_btn.add_css_class("pill");
|
||||||
|
|
||||||
|
let save_btn = gtk::Button::with_label("Save");
|
||||||
|
save_btn.add_css_class("suggested-action");
|
||||||
|
save_btn.add_css_class("pill");
|
||||||
|
|
||||||
|
btn_box.append(&delete_btn);
|
||||||
|
btn_box.append(&save_btn);
|
||||||
|
|
||||||
|
content.append(&form);
|
||||||
|
content.append(&btn_box);
|
||||||
|
toolbar.set_content(Some(&content));
|
||||||
|
dialog.set_child(Some(&toolbar));
|
||||||
|
|
||||||
|
// Save
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let dialog_ref = dialog.clone();
|
||||||
|
let group_ref = group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
let amount_ref = amount_row.clone();
|
||||||
|
save_btn.connect_clicked(move |_| {
|
||||||
|
let amount: f64 = match amount_ref.text().trim().parse() {
|
||||||
|
Ok(v) if v > 0.0 => v,
|
||||||
|
_ => {
|
||||||
|
let toast = adw::Toast::new("Please enter a valid amount");
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let month_str = Self::month_str(year, month);
|
||||||
|
match db_ref.set_budget(cat_id, &month_str, amount) {
|
||||||
|
Ok(()) => {
|
||||||
|
dialog_ref.close();
|
||||||
|
let toast = adw::Toast::new("Budget updated");
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
Self::load_budgets(&db_ref, &group_ref, &summary_ref, &toast_ref, year, month);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let toast = adw::Toast::new(&format!("Error: {}", e));
|
||||||
|
toast_ref.add_toast(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
{
|
||||||
|
let db_ref = db.clone();
|
||||||
|
let dialog_ref = dialog.clone();
|
||||||
|
let group_ref = group.clone();
|
||||||
|
let summary_ref = summary_label.clone();
|
||||||
|
let toast_ref = toast_overlay.clone();
|
||||||
|
delete_btn.connect_clicked(move |btn| {
|
||||||
|
let alert = adw::AlertDialog::new(
|
||||||
|
Some("Delete this budget?"),
|
||||||
|
Some("This will remove the budget for this category."),
|
||||||
|
);
|
||||||
|
alert.add_response("cancel", "Cancel");
|
||||||
|
alert.add_response("delete", "Delete");
|
||||||
|
alert.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
|
||||||
|
alert.set_default_response(Some("cancel"));
|
||||||
|
alert.set_close_response("cancel");
|
||||||
|
|
||||||
|
let db_del = db_ref.clone();
|
||||||
|
let dialog_del = dialog_ref.clone();
|
||||||
|
let group_del = group_ref.clone();
|
||||||
|
let summary_del = summary_ref.clone();
|
||||||
|
let toast_del = toast_ref.clone();
|
||||||
|
alert.connect_response(None, move |_, response| {
|
||||||
|
if response == "delete" {
|
||||||
|
match db_del.delete_budget(budget_id) {
|
||||||
|
Ok(()) => {
|
||||||
|
dialog_del.close();
|
||||||
|
let toast = adw::Toast::new("Budget deleted");
|
||||||
|
toast_del.add_toast(toast);
|
||||||
|
Self::load_budgets(&db_del, &group_del, &summary_del, &toast_del, year, month);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let toast = adw::Toast::new(&format!("Error: {}", e));
|
||||||
|
toast_del.add_toast(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alert.present(Some(btn));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.present(Some(parent));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod budgets_view;
|
||||||
mod charts_view;
|
mod charts_view;
|
||||||
mod history_view;
|
mod history_view;
|
||||||
mod log_view;
|
mod log_view;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use adw::prelude::*;
|
|||||||
use outlay_core::db::Database;
|
use outlay_core::db::Database;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::budgets_view::BudgetsView;
|
||||||
use crate::charts_view::ChartsView;
|
use crate::charts_view::ChartsView;
|
||||||
use crate::history_view::HistoryView;
|
use crate::history_view::HistoryView;
|
||||||
use crate::log_view::LogView;
|
use crate::log_view::LogView;
|
||||||
@@ -53,8 +54,12 @@ impl MainWindow {
|
|||||||
let charts_view = ChartsView::new(db.clone());
|
let charts_view = ChartsView::new(db.clone());
|
||||||
content_stack.add_named(&charts_view.container, Some("charts"));
|
content_stack.add_named(&charts_view.container, Some("charts"));
|
||||||
|
|
||||||
|
// Budgets view
|
||||||
|
let budgets_view = BudgetsView::new(db.clone());
|
||||||
|
content_stack.add_named(&budgets_view.container, Some("budgets"));
|
||||||
|
|
||||||
// Remaining pages are placeholders for now
|
// Remaining pages are placeholders for now
|
||||||
for item in &SIDEBAR_ITEMS[3..] {
|
for item in &SIDEBAR_ITEMS[4..] {
|
||||||
let page = adw::StatusPage::builder()
|
let page = adw::StatusPage::builder()
|
||||||
.title(item.label)
|
.title(item.label)
|
||||||
.icon_name(item.icon)
|
.icon_name(item.icon)
|
||||||
|
|||||||
Reference in New Issue
Block a user