- 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
77 lines
2.3 KiB
Rust
77 lines
2.3 KiB
Rust
use crate::db::Database;
|
|
use std::process::Command;
|
|
|
|
/// Send a desktop notification via notify-send (Linux).
|
|
/// Returns silently if notify-send is not available.
|
|
pub fn send_notification(title: &str, body: &str, urgency: &str) {
|
|
let _ = Command::new("notify-send")
|
|
.arg("--urgency")
|
|
.arg(urgency)
|
|
.arg("--app-name=Outlay")
|
|
.arg(title)
|
|
.arg(body)
|
|
.spawn();
|
|
}
|
|
|
|
/// Check all budgets for the given month and send notifications
|
|
/// for any thresholds crossed that haven't been notified yet.
|
|
/// Only sends if budget_notifications setting is enabled.
|
|
pub fn check_and_send_budget_notifications(db: &Database, month: &str) {
|
|
let enabled = db.get_setting("budget_notifications")
|
|
.ok().flatten().map(|s| s == "1").unwrap_or(false);
|
|
if !enabled {
|
|
return;
|
|
}
|
|
|
|
let budgets = match db.list_budgets_for_month(month) {
|
|
Ok(b) => b,
|
|
Err(_) => return,
|
|
};
|
|
|
|
for budget in &budgets {
|
|
let cat_name = db.get_category(budget.category_id)
|
|
.map(|c| c.name)
|
|
.unwrap_or_else(|_| "Unknown".to_string());
|
|
|
|
let thresholds = match db.check_budget_thresholds(budget.category_id, month) {
|
|
Ok(t) => t,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
for threshold in &thresholds {
|
|
let (title, urgency) = match threshold {
|
|
100 => (
|
|
format!("Budget exceeded: {}", cat_name),
|
|
"critical",
|
|
),
|
|
_ => (
|
|
format!("Budget {}% used: {}", threshold, cat_name),
|
|
"normal",
|
|
),
|
|
};
|
|
|
|
let progress = db.get_budget_progress(budget.category_id, month)
|
|
.ok().flatten();
|
|
let body = if let Some((budget_amt, spent, pct)) = progress {
|
|
format!("{:.2} of {:.2} spent ({:.0}%)", spent, budget_amt, pct)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
send_notification(&title, &body, urgency);
|
|
let _ = db.record_notification(budget.category_id, month, *threshold);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_send_notification_does_not_panic() {
|
|
// Should not panic even if notify-send is not installed
|
|
send_notification("Test", "Body", "normal");
|
|
}
|
|
}
|