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:
76
outlay-core/src/notifications.rs
Normal file
76
outlay-core/src/notifications.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user