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:
2026-03-03 21:18:37 +02:00
parent 773dae4684
commit 10a76e3003
10102 changed files with 108019 additions and 1335 deletions

View File

@@ -0,0 +1,59 @@
use crate::db::Database;
use crate::export_json::ExportData;
use crate::models::{NewCategory, NewTransaction};
use std::path::Path;
pub fn import_json(db: &Database, path: &Path, merge: bool) -> Result<usize, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let data: ExportData = serde_json::from_str(&content)?;
if !merge {
db.reset_all_data()?;
}
for cat in &data.categories {
let existing = db.list_categories(Some(cat.transaction_type))?;
if !existing.iter().any(|c| c.name == cat.name) {
let new_cat = NewCategory {
name: cat.name.clone(),
icon: cat.icon.clone(),
color: cat.color.clone(),
transaction_type: cat.transaction_type,
sort_order: cat.sort_order,
parent_id: None,
};
db.insert_category(&new_cat)?;
}
}
let mut count = 0;
for txn in &data.transactions {
let categories = db.list_categories(Some(txn.transaction_type))?;
let original_cat = data.categories.iter().find(|c| c.id == txn.category_id);
let category_id = match original_cat {
Some(oc) => categories.iter().find(|c| c.name == oc.name).map(|c| c.id),
None => None,
};
let Some(category_id) = category_id else { continue };
if merge && db.find_duplicate_transaction(txn.amount, txn.transaction_type, category_id, txn.date)? {
continue;
}
let new_txn = NewTransaction {
amount: txn.amount,
transaction_type: txn.transaction_type,
category_id,
currency: txn.currency.clone(),
exchange_rate: txn.exchange_rate,
note: txn.note.clone(),
date: txn.date,
recurring_id: None,
payee: txn.payee.clone(),
};
db.insert_transaction(&new_txn)?;
count += 1;
}
Ok(count)
}