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:
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user