feat(ekstre): banka/kart ekstresi içe aktarma ekranı
- Ayarlar → Ekstre içe aktar: Excel (.xlsx/.xls) ve CSV, dosya tarayıcıda okunur - Başlık satırı ve sütunlar otomatik bulunur; tek sütun / Borç-Alacak; eşleştirme profil olarak saklanır - Gözden geçirme: tür + kategori önerisi, aynı açıklamalara otomatik yayılma, tekrar/engelli satırlar - Öğrenilen kurallarda ilk kelime yedeği (MIGROS İSTANBUL → MIGROS KADIKÖY) - Defterde iade etiketi; iade düzenlemede eksi kalır - SheetJS resmi paketi frontend/vendor altında (npm 12 uzak paket indirmeyi engeller) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,47 @@ pub fn rule_key(desc: &str) -> String {
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// İlk kelime eşleşmesinde kullanılmayacak genel sözcükler (satıcı adı değil)
|
||||
const GENERIC: &[&str] = &[
|
||||
"iade",
|
||||
"odeme",
|
||||
"pos",
|
||||
"isyeri",
|
||||
"eft",
|
||||
"havale",
|
||||
"fast",
|
||||
"virman",
|
||||
"kredi",
|
||||
"kart",
|
||||
"hesap",
|
||||
"para",
|
||||
"borc",
|
||||
"harcama",
|
||||
"alisveris",
|
||||
"nakit",
|
||||
"atm",
|
||||
"taksit",
|
||||
"ucret",
|
||||
];
|
||||
|
||||
/// Öneri: tam kural (ilk 2 kelime); yoksa ilk kelimesi aynı kurallar hepsi aynı şeyi
|
||||
/// söylüyorsa o (ör. "migros istanbul" öğrenildi → "MIGROS KADIKOY" de Market)
|
||||
fn suggest<'a>(rules: &'a HashMap<String, Rule>, desc: &str) -> Option<&'a Rule> {
|
||||
if let Some(r) = rules.get(&rule_key(desc)) {
|
||||
return Some(r);
|
||||
}
|
||||
let first = words(desc).into_iter().next()?;
|
||||
if first.len() < 3 || GENERIC.contains(&first.as_str()) {
|
||||
return None;
|
||||
}
|
||||
let mut same = rules
|
||||
.values()
|
||||
.filter(|r| r.pattern.split(' ').next() == Some(first.as_str()));
|
||||
let r = same.next()?;
|
||||
same.all(|x| x.kind == r.kind && x.category_id == r.category_id)
|
||||
.then_some(r)
|
||||
}
|
||||
|
||||
/// Tekrar anahtarı (tüm açıklama, sadeleştirilmiş)
|
||||
pub fn import_key(date: &str, dir: Direction, amount: i64, desc: &str) -> String {
|
||||
let d = match dir {
|
||||
@@ -359,7 +400,7 @@ pub async fn preview(
|
||||
&mut keys,
|
||||
&import_key(&r.date, r.direction, r.amount, &r.description),
|
||||
);
|
||||
let rule = rules.get(&rule_key(&r.description));
|
||||
let rule = suggest(&rules, &r.description);
|
||||
PreviewRow {
|
||||
kind: rule.map_or_else(
|
||||
|| default_kind(source, r.direction, &r.description),
|
||||
@@ -586,6 +627,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggestion_falls_back_to_first_word() {
|
||||
let market = Uuid::new();
|
||||
let ulasim = Uuid::new();
|
||||
let rule = |p: &str, c: Uuid| {
|
||||
(
|
||||
p.to_string(),
|
||||
Rule {
|
||||
pattern: p.into(),
|
||||
kind: Kind::Expense,
|
||||
category_id: Some(c),
|
||||
},
|
||||
)
|
||||
};
|
||||
let mut rules: HashMap<String, Rule> =
|
||||
[rule("migros istanbul", market), rule("iade migros", market)].into();
|
||||
assert_eq!(
|
||||
suggest(&rules, "MIGROS 99 ISTANBUL").unwrap().category_id,
|
||||
Some(market),
|
||||
"tam"
|
||||
);
|
||||
assert_eq!(
|
||||
suggest(&rules, "MİGROS KADIKÖY").unwrap().category_id,
|
||||
Some(market),
|
||||
"ilk kelime"
|
||||
);
|
||||
assert!(suggest(&rules, "IADE SHELL").is_none(), "genel ilk kelime");
|
||||
assert!(suggest(&rules, "A101 SUBE").is_none());
|
||||
// İlk kelimesi aynı ama farklı kategoriler → öneri yok
|
||||
rules.extend([rule("migros jet", ulasim)]);
|
||||
assert!(suggest(&rules, "MIGROS ANKARA").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_kinds() {
|
||||
use Direction::*;
|
||||
|
||||
Reference in New Issue
Block a user