Add feature batch 2, subscription/recurring sync, smooth charts, and app icon
- Implement subscriptions view with bidirectional recurring transaction sync - Add cascade delete/pause/resume between subscriptions and recurring - Fix foreign key constraints when deleting recurring transactions - Add cross-view instant refresh via callback pattern - Replace Bezier chart smoothing with Fritsch-Carlson monotone Hermite interpolation - Smooth budget sparklines using shared monotone_subdivide function - Add vertical spacing to budget rows - Add app icon (receipt on GNOME blue) in all sizes for desktop, web, and AppImage - Add calendar, credit cards, forecast, goals, insights, and wishlist views - Add date picker, numpad, quick-add, category combo, and edit dialog components - Add import/export for CSV, JSON, OFX, QIF formats - Add NLP transaction parsing, OCR receipt scanning, expression evaluator - Add notification support, Sankey chart, tray icon - Add demo data seeder with full DB wipe - Expand database schema with subscriptions, goals, credit cards, and more
This commit is contained in:
567
outlay-gtk/src/credit_cards_view.rs
Normal file
567
outlay-gtk/src/credit_cards_view.rs
Normal file
@@ -0,0 +1,567 @@
|
||||
use adw::prelude::*;
|
||||
use chrono::{Datelike, Local};
|
||||
use outlay_core::db::Database;
|
||||
use outlay_core::exchange::ExchangeRateService;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct CreditCardsView {
|
||||
pub container: gtk::Box,
|
||||
}
|
||||
|
||||
impl CreditCardsView {
|
||||
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(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);
|
||||
|
||||
// Summary card
|
||||
let summary_group = adw::PreferencesGroup::builder()
|
||||
.title("SUMMARY")
|
||||
.build();
|
||||
|
||||
let total_balance_row = adw::ActionRow::builder()
|
||||
.title("Total Balance")
|
||||
.build();
|
||||
let balance_label = gtk::Label::new(Some("0.00"));
|
||||
balance_label.add_css_class("amount-display");
|
||||
total_balance_row.add_suffix(&balance_label);
|
||||
|
||||
let total_limit_row = adw::ActionRow::builder()
|
||||
.title("Total Credit Limit")
|
||||
.build();
|
||||
let limit_label = gtk::Label::new(Some("0.00"));
|
||||
limit_label.add_css_class("dim-label");
|
||||
total_limit_row.add_suffix(&limit_label);
|
||||
|
||||
let utilization_row = adw::ActionRow::builder()
|
||||
.title("Overall Utilization")
|
||||
.build();
|
||||
let util_bar = gtk::LevelBar::new();
|
||||
util_bar.set_min_value(0.0);
|
||||
util_bar.set_max_value(1.0);
|
||||
util_bar.set_hexpand(true);
|
||||
util_bar.set_valign(gtk::Align::Center);
|
||||
let util_label = gtk::Label::new(Some("0%"));
|
||||
util_label.add_css_class("caption");
|
||||
util_label.set_margin_start(8);
|
||||
let util_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
|
||||
util_box.append(&util_bar);
|
||||
util_box.append(&util_label);
|
||||
utilization_row.add_suffix(&util_box);
|
||||
|
||||
summary_group.add(&total_balance_row);
|
||||
summary_group.add(&total_limit_row);
|
||||
summary_group.add(&utilization_row);
|
||||
|
||||
// Cards list
|
||||
let cards_group = adw::PreferencesGroup::builder()
|
||||
.title("CARDS")
|
||||
.build();
|
||||
|
||||
// Populate
|
||||
Self::populate_cards(
|
||||
&db, &cards_group, &toast_overlay,
|
||||
&balance_label, &limit_label, &util_bar, &util_label,
|
||||
);
|
||||
|
||||
// Add card button
|
||||
let add_btn = gtk::Button::with_label("Add Card");
|
||||
add_btn.add_css_class("pill");
|
||||
add_btn.set_halign(gtk::Align::Center);
|
||||
add_btn.set_margin_top(8);
|
||||
{
|
||||
let db_ref = db.clone();
|
||||
let group_ref = cards_group.clone();
|
||||
let toast_ref = toast_overlay.clone();
|
||||
let bl = balance_label.clone();
|
||||
let ll = limit_label.clone();
|
||||
let ub = util_bar.clone();
|
||||
let ul = util_label.clone();
|
||||
add_btn.connect_clicked(move |btn| {
|
||||
Self::show_card_dialog(
|
||||
btn, &db_ref, None, &group_ref, &toast_ref,
|
||||
&bl, &ll, &ub, &ul,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
inner.append(&summary_group);
|
||||
inner.append(&cards_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);
|
||||
|
||||
CreditCardsView { container }
|
||||
}
|
||||
|
||||
fn populate_cards(
|
||||
db: &Rc<Database>,
|
||||
group: &adw::PreferencesGroup,
|
||||
toast: &adw::ToastOverlay,
|
||||
balance_label: >k::Label,
|
||||
limit_label: >k::Label,
|
||||
util_bar: >k::LevelBar,
|
||||
util_label: >k::Label,
|
||||
) {
|
||||
// Remove existing rows
|
||||
while let Some(child) = group.first_child() {
|
||||
if let Some(inner) = child.first_child() {
|
||||
if let Some(listbox) = inner.downcast_ref::<gtk::ListBox>() {
|
||||
while let Some(row) = listbox.row_at_index(0) {
|
||||
listbox.remove(&row);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let cards = db.list_credit_cards().unwrap_or_default();
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
let mut total_balance = 0.0_f64;
|
||||
let mut total_limit = 0.0_f64;
|
||||
|
||||
for card in &cards {
|
||||
total_balance += card.current_balance;
|
||||
if let Some(lim) = card.credit_limit {
|
||||
total_limit += lim;
|
||||
}
|
||||
|
||||
// Utilization for this card
|
||||
let card_util = if let Some(lim) = card.credit_limit {
|
||||
if lim > 0.0 { card.current_balance / lim } else { 0.0 }
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Days until due
|
||||
let due_day = card.due_day as u32;
|
||||
let current_day = today.day();
|
||||
let days_until_due = if due_day > current_day {
|
||||
due_day - current_day
|
||||
} else if due_day == current_day {
|
||||
0
|
||||
} else {
|
||||
// Next month
|
||||
let days_in_month = {
|
||||
let (y, m) = if today.month() == 12 { (today.year() + 1, 1) } else { (today.year(), today.month() + 1) };
|
||||
chrono::NaiveDate::from_ymd_opt(y, m, 1).unwrap().pred_opt().unwrap().day()
|
||||
};
|
||||
days_in_month - current_day + due_day
|
||||
};
|
||||
|
||||
let subtitle = format!(
|
||||
"{:.2} {} - Due in {} day{}",
|
||||
card.current_balance,
|
||||
card.currency,
|
||||
days_until_due,
|
||||
if days_until_due == 1 { "" } else { "s" },
|
||||
);
|
||||
|
||||
let expander = adw::ExpanderRow::builder()
|
||||
.title(&card.name)
|
||||
.subtitle(&subtitle)
|
||||
.build();
|
||||
|
||||
// Utilization bar in suffix
|
||||
let mini_bar = gtk::LevelBar::new();
|
||||
mini_bar.set_min_value(0.0);
|
||||
mini_bar.set_max_value(1.0);
|
||||
mini_bar.set_value(card_util.min(1.0));
|
||||
mini_bar.set_size_request(60, -1);
|
||||
mini_bar.set_valign(gtk::Align::Center);
|
||||
expander.add_suffix(&mini_bar);
|
||||
|
||||
// Expanded content
|
||||
let close_row = adw::ActionRow::builder()
|
||||
.title("Statement Close Day")
|
||||
.subtitle(&format!("Day {} of each month", card.statement_close_day))
|
||||
.build();
|
||||
expander.add_row(&close_row);
|
||||
|
||||
let due_row = adw::ActionRow::builder()
|
||||
.title("Payment Due Day")
|
||||
.subtitle(&format!("Day {} of each month", card.due_day))
|
||||
.build();
|
||||
expander.add_row(&due_row);
|
||||
|
||||
if let Some(lim) = card.credit_limit {
|
||||
let limit_row = adw::ActionRow::builder()
|
||||
.title("Credit Limit")
|
||||
.subtitle(&format!("{:.2} {}", lim, card.currency))
|
||||
.build();
|
||||
expander.add_row(&limit_row);
|
||||
}
|
||||
|
||||
let min_pmt = card.min_payment_pct * card.current_balance / 100.0;
|
||||
let min_row = adw::ActionRow::builder()
|
||||
.title("Minimum Payment")
|
||||
.subtitle(&format!("{:.2} {} ({:.1}%)", min_pmt, card.currency, card.min_payment_pct))
|
||||
.build();
|
||||
expander.add_row(&min_row);
|
||||
|
||||
// Action buttons
|
||||
let btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
btn_box.set_halign(gtk::Align::Center);
|
||||
btn_box.set_margin_top(8);
|
||||
btn_box.set_margin_bottom(8);
|
||||
|
||||
let pay_btn = gtk::Button::with_label("Record Payment");
|
||||
pay_btn.add_css_class("suggested-action");
|
||||
pay_btn.add_css_class("pill");
|
||||
|
||||
let edit_btn = gtk::Button::with_label("Edit");
|
||||
edit_btn.add_css_class("pill");
|
||||
|
||||
let del_btn = gtk::Button::with_label("Delete");
|
||||
del_btn.add_css_class("destructive-action");
|
||||
del_btn.add_css_class("pill");
|
||||
|
||||
btn_box.append(&pay_btn);
|
||||
btn_box.append(&edit_btn);
|
||||
btn_box.append(&del_btn);
|
||||
|
||||
let btn_row = adw::ActionRow::new();
|
||||
btn_row.set_child(Some(&btn_box));
|
||||
expander.add_row(&btn_row);
|
||||
|
||||
// Wire payment
|
||||
{
|
||||
let card_id = card.id;
|
||||
let card_name = card.name.clone();
|
||||
let db_ref = db.clone();
|
||||
let group_ref = group.clone();
|
||||
let toast_ref = toast.clone();
|
||||
let bl = balance_label.clone();
|
||||
let ll = limit_label.clone();
|
||||
let ub = util_bar.clone();
|
||||
let ul = util_label.clone();
|
||||
pay_btn.connect_clicked(move |btn| {
|
||||
Self::show_payment_dialog(
|
||||
btn, &db_ref, card_id, &card_name,
|
||||
&group_ref, &toast_ref, &bl, &ll, &ub, &ul,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Wire edit
|
||||
{
|
||||
let card_id = card.id;
|
||||
let db_ref = db.clone();
|
||||
let group_ref = group.clone();
|
||||
let toast_ref = toast.clone();
|
||||
let bl = balance_label.clone();
|
||||
let ll = limit_label.clone();
|
||||
let ub = util_bar.clone();
|
||||
let ul = util_label.clone();
|
||||
edit_btn.connect_clicked(move |btn| {
|
||||
Self::show_card_dialog(
|
||||
btn, &db_ref, Some(card_id), &group_ref, &toast_ref,
|
||||
&bl, &ll, &ub, &ul,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Wire delete
|
||||
{
|
||||
let card_id = card.id;
|
||||
let db_ref = db.clone();
|
||||
let group_ref = group.clone();
|
||||
let toast_ref = toast.clone();
|
||||
let bl = balance_label.clone();
|
||||
let ll = limit_label.clone();
|
||||
let ub = util_bar.clone();
|
||||
let ul = util_label.clone();
|
||||
del_btn.connect_clicked(move |btn| {
|
||||
let alert = adw::AlertDialog::new(
|
||||
Some("Delete this card?"),
|
||||
Some("This cannot be undone."),
|
||||
);
|
||||
alert.add_response("cancel", "Cancel");
|
||||
alert.add_response("delete", "Delete");
|
||||
alert.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
|
||||
alert.set_default_response(Some("cancel"));
|
||||
|
||||
let db_c = db_ref.clone();
|
||||
let g = group_ref.clone();
|
||||
let t = toast_ref.clone();
|
||||
let bl = bl.clone();
|
||||
let ll = ll.clone();
|
||||
let ub = ub.clone();
|
||||
let ul = ul.clone();
|
||||
alert.connect_response(None, move |_, resp| {
|
||||
if resp == "delete" {
|
||||
if db_c.delete_credit_card(card_id).is_ok() {
|
||||
Self::populate_cards(&db_c, &g, &t, &bl, &ll, &ub, &ul);
|
||||
t.add_toast(adw::Toast::new("Card deleted"));
|
||||
}
|
||||
}
|
||||
});
|
||||
alert.present(Some(btn));
|
||||
});
|
||||
}
|
||||
|
||||
group.add(&expander);
|
||||
}
|
||||
|
||||
// Update summary
|
||||
balance_label.set_label(&format!("{:.2}", total_balance));
|
||||
limit_label.set_label(&format!("{:.2}", total_limit));
|
||||
let util = if total_limit > 0.0 { total_balance / total_limit } else { 0.0 };
|
||||
util_bar.set_value(util.min(1.0));
|
||||
util_label.set_label(&format!("{:.0}%", util * 100.0));
|
||||
|
||||
if cards.is_empty() {
|
||||
let empty = adw::ActionRow::builder()
|
||||
.title("No credit cards")
|
||||
.subtitle("Add a card to track billing cycles and payments")
|
||||
.build();
|
||||
group.add(&empty);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_payment_dialog(
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
db: &Rc<Database>,
|
||||
card_id: i64,
|
||||
card_name: &str,
|
||||
group: &adw::PreferencesGroup,
|
||||
toast: &adw::ToastOverlay,
|
||||
bl: >k::Label,
|
||||
ll: >k::Label,
|
||||
ub: >k::LevelBar,
|
||||
ul: >k::Label,
|
||||
) {
|
||||
let alert = adw::AlertDialog::new(
|
||||
Some("Record Payment"),
|
||||
Some(&format!("Enter payment amount for {}", card_name)),
|
||||
);
|
||||
alert.add_response("cancel", "Cancel");
|
||||
alert.add_response("pay", "Record");
|
||||
alert.set_response_appearance("pay", adw::ResponseAppearance::Suggested);
|
||||
alert.set_default_response(Some("pay"));
|
||||
|
||||
let entry = adw::EntryRow::builder()
|
||||
.title("Amount")
|
||||
.build();
|
||||
entry.set_input_purpose(gtk::InputPurpose::Number);
|
||||
alert.set_extra_child(Some(&entry));
|
||||
|
||||
let db_ref = db.clone();
|
||||
let group_ref = group.clone();
|
||||
let toast_ref = toast.clone();
|
||||
let card_name = card_name.to_string();
|
||||
let bl = bl.clone();
|
||||
let ll = ll.clone();
|
||||
let ub = ub.clone();
|
||||
let ul = ul.clone();
|
||||
alert.connect_response(None, move |_, resp| {
|
||||
if resp == "pay" {
|
||||
let text = entry.text();
|
||||
if let Some(amount) = outlay_core::expr::eval_expr(&text) {
|
||||
if amount > 0.0 {
|
||||
if db_ref.record_card_payment(card_id, amount).is_ok() {
|
||||
// Create expense transaction for the payment
|
||||
let today = Local::now().date_naive();
|
||||
let base_currency = db_ref.get_setting("base_currency")
|
||||
.ok().flatten()
|
||||
.unwrap_or_else(|| "USD".to_string());
|
||||
// Use first expense category as fallback
|
||||
let cat_id = db_ref.list_categories(Some(outlay_core::models::TransactionType::Expense))
|
||||
.unwrap_or_default()
|
||||
.first()
|
||||
.map(|c| c.id)
|
||||
.unwrap_or(1);
|
||||
let txn = outlay_core::models::NewTransaction {
|
||||
amount,
|
||||
transaction_type: outlay_core::models::TransactionType::Expense,
|
||||
category_id: cat_id,
|
||||
currency: base_currency,
|
||||
exchange_rate: 1.0,
|
||||
note: Some(format!("Credit card payment - {}", card_name)),
|
||||
date: today,
|
||||
recurring_id: None,
|
||||
payee: None,
|
||||
};
|
||||
let _ = db_ref.insert_transaction(&txn);
|
||||
Self::populate_cards(&db_ref, &group_ref, &toast_ref, &bl, &ll, &ub, &ul);
|
||||
toast_ref.add_toast(adw::Toast::new(&format!("Payment of {:.2} recorded", amount)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
alert.present(Some(parent));
|
||||
}
|
||||
|
||||
fn show_card_dialog(
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
db: &Rc<Database>,
|
||||
card_id: Option<i64>,
|
||||
group: &adw::PreferencesGroup,
|
||||
toast: &adw::ToastOverlay,
|
||||
bl: >k::Label,
|
||||
ll: >k::Label,
|
||||
ub: >k::LevelBar,
|
||||
ul: >k::Label,
|
||||
) {
|
||||
let existing = card_id.and_then(|id| db.get_credit_card(id).ok());
|
||||
let is_edit = existing.is_some();
|
||||
|
||||
let dialog = adw::AlertDialog::new(
|
||||
Some(if is_edit { "Edit Card" } else { "Add Card" }),
|
||||
None,
|
||||
);
|
||||
dialog.add_response("cancel", "Cancel");
|
||||
dialog.add_response("save", if is_edit { "Save" } else { "Add" });
|
||||
dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
|
||||
dialog.set_default_response(Some("save"));
|
||||
|
||||
let form = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||
|
||||
let name_entry = adw::EntryRow::builder()
|
||||
.title("Card Name")
|
||||
.text(existing.as_ref().map(|c| c.name.as_str()).unwrap_or(""))
|
||||
.build();
|
||||
|
||||
let existing_currency = existing.as_ref().map(|c| c.currency.as_str()).unwrap_or("USD");
|
||||
let limit_entry = adw::EntryRow::builder()
|
||||
.title(&format!("Credit Limit ({})", existing_currency))
|
||||
.text(&existing.as_ref().and_then(|c| c.credit_limit).map(|l| format!("{:.2}", l)).unwrap_or_default())
|
||||
.build();
|
||||
limit_entry.set_input_purpose(gtk::InputPurpose::Number);
|
||||
crate::numpad::attach_numpad(&limit_entry);
|
||||
|
||||
let close_spin = adw::SpinRow::with_range(1.0, 31.0, 1.0);
|
||||
close_spin.set_title("Statement Close Day");
|
||||
close_spin.set_value(existing.as_ref().map(|c| c.statement_close_day as f64).unwrap_or(25.0));
|
||||
|
||||
let due_spin = adw::SpinRow::with_range(1.0, 31.0, 1.0);
|
||||
due_spin.set_title("Payment Due Day");
|
||||
due_spin.set_value(existing.as_ref().map(|c| c.due_day as f64).unwrap_or(15.0));
|
||||
|
||||
let min_spin = adw::SpinRow::with_range(0.0, 100.0, 0.5);
|
||||
min_spin.set_title("Minimum Payment %");
|
||||
min_spin.set_value(existing.as_ref().map(|c| c.min_payment_pct).unwrap_or(2.0));
|
||||
|
||||
let currencies = ExchangeRateService::supported_currencies();
|
||||
let currency_labels: Vec<String> = currencies
|
||||
.iter()
|
||||
.map(|(code, name)| format!("{} - {}", code, name))
|
||||
.collect();
|
||||
let currency_label_refs: Vec<&str> = currency_labels.iter().map(|s| s.as_str()).collect();
|
||||
let currency_model = gtk::StringList::new(¤cy_label_refs);
|
||||
let currency_codes: Vec<String> = currencies.iter().map(|(c, _)| c.to_string()).collect();
|
||||
|
||||
let currency_idx = currency_codes
|
||||
.iter()
|
||||
.position(|c| c.eq_ignore_ascii_case(existing_currency))
|
||||
.unwrap_or(0);
|
||||
|
||||
let currency_combo = adw::ComboRow::builder()
|
||||
.title("Currency")
|
||||
.model(¤cy_model)
|
||||
.selected(currency_idx as u32)
|
||||
.build();
|
||||
|
||||
{
|
||||
let limit_ref = limit_entry.clone();
|
||||
let codes = currency_codes.clone();
|
||||
currency_combo.connect_selected_notify(move |combo| {
|
||||
if let Some(code) = codes.get(combo.selected() as usize) {
|
||||
limit_ref.set_title(&format!("Credit Limit ({})", code));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let list = gtk::ListBox::new();
|
||||
list.add_css_class("boxed-list");
|
||||
list.set_selection_mode(gtk::SelectionMode::None);
|
||||
list.append(&name_entry);
|
||||
list.append(&limit_entry);
|
||||
list.append(&close_spin);
|
||||
list.append(&due_spin);
|
||||
list.append(&min_spin);
|
||||
list.append(¤cy_combo);
|
||||
|
||||
form.append(&list);
|
||||
dialog.set_extra_child(Some(&form));
|
||||
|
||||
let db_ref = db.clone();
|
||||
let group_ref = group.clone();
|
||||
let toast_ref = toast.clone();
|
||||
let bl = bl.clone();
|
||||
let ll = ll.clone();
|
||||
let ub = ub.clone();
|
||||
let ul = ul.clone();
|
||||
dialog.connect_response(None, move |_, resp| {
|
||||
if resp == "save" {
|
||||
let name = name_entry.text().to_string();
|
||||
if name.trim().is_empty() {
|
||||
toast_ref.add_toast(adw::Toast::new("Card name is required"));
|
||||
return;
|
||||
}
|
||||
let limit_text = limit_entry.text().to_string();
|
||||
let credit_limit = if limit_text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
limit_text.parse::<f64>().ok()
|
||||
};
|
||||
let close_day = close_spin.value() as i32;
|
||||
let due_day = due_spin.value() as i32;
|
||||
let min_pct = min_spin.value();
|
||||
let currency = currency_codes
|
||||
.get(currency_combo.selected() as usize)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "USD".to_string());
|
||||
|
||||
if let Some(id) = card_id {
|
||||
let card = outlay_core::models::CreditCard {
|
||||
id,
|
||||
name: name.trim().to_string(),
|
||||
credit_limit,
|
||||
statement_close_day: close_day,
|
||||
due_day,
|
||||
min_payment_pct: min_pct,
|
||||
current_balance: existing.as_ref().map(|c| c.current_balance).unwrap_or(0.0),
|
||||
currency,
|
||||
color: None,
|
||||
active: true,
|
||||
};
|
||||
let _ = db_ref.update_credit_card(&card);
|
||||
} else {
|
||||
let card = outlay_core::models::NewCreditCard {
|
||||
name: name.trim().to_string(),
|
||||
credit_limit,
|
||||
statement_close_day: close_day,
|
||||
due_day,
|
||||
min_payment_pct: min_pct,
|
||||
currency,
|
||||
color: None,
|
||||
};
|
||||
let _ = db_ref.insert_credit_card(&card);
|
||||
}
|
||||
Self::populate_cards(&db_ref, &group_ref, &toast_ref, &bl, &ll, &ub, &ul);
|
||||
toast_ref.add_toast(adw::Toast::new(if is_edit { "Card updated" } else { "Card added" }));
|
||||
}
|
||||
});
|
||||
dialog.present(Some(parent));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user