feat(panel): harcanabilir ve grup harcama grafikleri

- Günlük / Haftalık / Aylık sekmeli iki çizgi grafik (Recharts, ayrı parça olarak yüklenir)
- Kümülatif harcanabilir vs gerçekleşen; 4 grup (+ isteğe bağlı harcanabilir çizgisi)
- Dönem istatistiklerine grup kırılımı (spent_by_group)
- Grafik altında metin özeti, koyu tema uyumlu renkler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 00:14:55 +03:00
parent 64c185ab96
commit 191c3e6db5
20 changed files with 952 additions and 27 deletions
+3
View File
@@ -79,6 +79,8 @@ struct PeriodStatDto {
spendable: i64,
#[ts(type = "number")]
spent: i64,
/// Harcanan, gruplara göre
spent_by_group: GroupAmounts,
/// Kapanmamışsa `null`
#[ts(type = "number | null")]
to_savings: Option<i64>,
@@ -105,6 +107,7 @@ pub async fn periods(
status: x.period.status,
spendable: x.spendable,
spent: x.spent,
spent_by_group: x.spent_by_group.into(),
to_savings: x.period.to_savings,
}
})
+7 -4
View File
@@ -44,6 +44,8 @@ pub struct Summary {
pub budget_total: i64,
/// Dönemde gerçekleşen harcama (silinenler hariç)
pub spent_total: i64,
/// Aynısı gruplara göre (`Group::ALL` sırasıyla)
pub spent_by_group: GroupAmounts,
}
pub struct WeekBudget {
@@ -139,16 +141,16 @@ pub async fn summary(db: &Database, household_id: Uuid, period_id: Uuid) -> AppR
.sum();
let spendable =
income_total - fixed_expense_total - p.savings_goal - carryover_debt + carryover_surplus;
let spent_total = sum_by(
let by_group = sum_by(
db,
col::ENTRIES,
household_id,
doc! { "period_id": period_id },
"$group",
)
.await?
.values()
.sum();
.await?;
let spent_by_group = Group::ALL.map(|g| by_group.get(g.as_str()).copied().unwrap_or(0));
let spent_total = spent_by_group.iter().sum();
Ok(Summary {
income_total,
@@ -159,6 +161,7 @@ pub async fn summary(db: &Database, household_id: Uuid, period_id: Uuid) -> AppR
spendable,
budget_total,
spent_total,
spent_by_group,
})
}
+3 -1
View File
@@ -2,7 +2,7 @@
//!
//! - `daily`: dönemin her günü için 4 grubun harcaması (harcama olmayan gün 0).
//! Günlük/haftalık ve kümülatif seriler istemcide bundan türetilir.
//! - `periods`: son N dönemin harcanabilir/harcanan tutarı (aylık grafik) ve
//! - `periods`: son N dönemin harcanabilir/harcanan tutarı, grup kırılımıyla (aylık grafik) ve
//! **toplam birikim** = kapanışlarda birikime aktarılanların toplamı
//! (varlık değerleri Faz 8'de bunun yerini alacak). Yeniden açılan dönemin son
//! kapanıştaki tutarı, yeniden kapatılana kadar sayılır.
@@ -59,6 +59,7 @@ pub struct PeriodStat {
pub period: Period,
pub spendable: i64,
pub spent: i64,
pub spent_by_group: GroupAmounts,
}
pub struct Stats {
@@ -78,6 +79,7 @@ pub async fn periods(db: &Database, household_id: Uuid, limit: usize) -> AppResu
period: p,
spendable: s.spendable,
spent: s.spent_total,
spent_by_group: s.spent_by_group,
});
}
out.reverse();
+9 -1
View File
@@ -123,7 +123,9 @@ async fn period_stats_and_total_savings() {
let s = app.register("Test", None).await;
let (nov, dec) = two_periods(&app, &s).await;
let needs = category(&app, &s, "needs").await;
let culture = category(&app, &s, "culture").await;
spend(&app, &s, &dec, "2026-12-05", 40_000, &needs).await;
spend(&app, &s, &dec, "2026-12-06", 2_500, &culture).await;
let (st, b) = app.get("/stats/periods", &s).await;
assert_eq!(st, StatusCode::OK, "{b}");
@@ -135,6 +137,10 @@ async fn period_stats_and_total_savings() {
assert_eq!(list[0]["status"], "closed");
assert_eq!(list[0]["spendable"], 1_000_000);
assert_eq!(list[0]["spent"], 300_000);
assert_eq!(
list[0]["spent_by_group"],
json!({ "needs": 300_000, "wants": 0, "culture": 0, "unexpected": 0 })
);
assert_eq!(list[0]["to_savings"], 500_000);
assert_eq!(list[0]["last_date"], "2026-11-30");
assert_eq!(list[1]["id"], dec.as_str());
@@ -142,7 +148,9 @@ async fn period_stats_and_total_savings() {
list[1]["spendable"], 2_100_000,
"20.000 − 1.000 hedef + 2.000 artı"
);
assert_eq!(list[1]["spent"], 40_000);
assert_eq!(list[1]["spent"], 42_500);
assert_eq!(list[1]["spent_by_group"]["needs"], 40_000);
assert_eq!(list[1]["spent_by_group"]["culture"], 2_500);
assert_eq!(list[1]["to_savings"], Value::Null);
// limit: en yeni N dönem