feat(defter): özet ve bütçeye gerçekleşen harcama eklendi (backend)
- Dönem özeti: spent_total ve remaining (harcanabilir − harcanan) - Bütçe görünümü: grup ve hafta bazında spent - Değerler saklanmaz, kayıtlardan anlık toplanır; silinenler sayılmaz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! | Yöntem | Yol | İş |
|
||||
//! |---|---|---|
|
||||
//! | GET | `/api/periods/{id}/summary` | gelir/gider/birikim/devir → harcanabilir |
|
||||
//! | GET | `/api/periods/{id}/budget` | 4 grup: aylık + haftalık döküm |
|
||||
//! | GET | `/api/periods/{id}/summary` | gelir/gider/birikim/devir → harcanabilir, harcanan, kalan |
|
||||
//! | GET | `/api/periods/{id}/budget` | 4 grup: aylık + haftalık döküm, her birinde harcanan |
|
||||
//! | PUT | `/api/periods/{id}/budget` | 4 grubun aylık tutarları |
|
||||
//! | PUT | `/api/periods/{id}/budget/weeks` | bir haftayı elle ayarla / otomatiğe döndür |
|
||||
//! | GET | `/api/periods/{id}/budget/suggestion?method=…` | `default` · `previous` · `avg3` |
|
||||
@@ -81,6 +81,12 @@ pub struct SummaryDto {
|
||||
/// harcanabilir − dağıtılan (negatifse fazla dağıtılmış)
|
||||
#[ts(type = "number")]
|
||||
unallocated: i64,
|
||||
/// Dönemde gerçekleşen harcama
|
||||
#[ts(type = "number")]
|
||||
spent_total: i64,
|
||||
/// Kalan harcanabilir: harcanabilir − gerçekleşen (negatifse aşılmış)
|
||||
#[ts(type = "number")]
|
||||
remaining: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
@@ -89,6 +95,9 @@ pub struct GroupBudgetDto {
|
||||
group: Group,
|
||||
#[ts(type = "number")]
|
||||
monthly: i64,
|
||||
/// Dönem boyunca gerçekleşen harcama
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
/// Elle girilen haftaların toplamı aylık tutarı aşıyor
|
||||
weekly_over: bool,
|
||||
weeks: Vec<WeekBudgetDto>,
|
||||
@@ -105,6 +114,9 @@ struct WeekBudgetDto {
|
||||
amount: i64,
|
||||
/// Elle girildi (otomatik değil)
|
||||
is_manual: bool,
|
||||
/// Bu haftada gerçekleşen harcama
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
}
|
||||
|
||||
impl From<GroupBudget> for GroupBudgetDto {
|
||||
@@ -112,6 +124,7 @@ impl From<GroupBudget> for GroupBudgetDto {
|
||||
Self {
|
||||
group: g.group,
|
||||
monthly: g.monthly,
|
||||
spent: g.spent,
|
||||
weekly_over: g.weekly_over,
|
||||
weeks: g
|
||||
.weeks
|
||||
@@ -123,6 +136,7 @@ impl From<GroupBudget> for GroupBudgetDto {
|
||||
days: w.days,
|
||||
amount: w.amount,
|
||||
is_manual: w.is_manual,
|
||||
spent: w.spent,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
@@ -182,6 +196,8 @@ pub async fn summary(
|
||||
spendable: x.spendable,
|
||||
budget_total: x.budget_total,
|
||||
unallocated: x.spendable - x.budget_total,
|
||||
spent_total: x.spent_total,
|
||||
remaining: x.spendable - x.spent_total,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! Kullanıcı bir haftayı elle değiştirirse yalnızca o kayıt saklanır (`scope: "week"`);
|
||||
//! kalan tutar diğer haftalara gün oranında dağıtılır → haftaların toplamı = aylık.
|
||||
//! - Özet: gelir − sabit gider − birikim − devreden borç + devreden artı = **harcanabilir**.
|
||||
//! - Gerçekleşen harcama (`entries`) saklanmaz; her istekte toplanır:
|
||||
//! özette toplam + kalan, bütçe görünümünde grup ve hafta bazında.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -40,6 +42,8 @@ pub struct Summary {
|
||||
pub carryover_surplus: i64,
|
||||
pub spendable: i64,
|
||||
pub budget_total: i64,
|
||||
/// Dönemde gerçekleşen harcama (silinenler hariç)
|
||||
pub spent_total: i64,
|
||||
}
|
||||
|
||||
pub struct WeekBudget {
|
||||
@@ -49,11 +53,15 @@ pub struct WeekBudget {
|
||||
pub days: u32,
|
||||
pub amount: i64,
|
||||
pub is_manual: bool,
|
||||
/// Bu haftada bu grupta gerçekleşen harcama
|
||||
pub spent: i64,
|
||||
}
|
||||
|
||||
pub struct GroupBudget {
|
||||
pub group: Group,
|
||||
pub monthly: i64,
|
||||
/// Dönem boyunca bu grupta gerçekleşen harcama
|
||||
pub spent: i64,
|
||||
pub weeks: Vec<WeekBudget>,
|
||||
/// Elle girilen haftaların toplamı aylık tutarı aşıyor mu?
|
||||
pub weekly_over: bool,
|
||||
@@ -131,6 +139,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(
|
||||
db,
|
||||
col::ENTRIES,
|
||||
household_id,
|
||||
doc! { "period_id": period_id },
|
||||
"$group",
|
||||
)
|
||||
.await?
|
||||
.values()
|
||||
.sum();
|
||||
|
||||
Ok(Summary {
|
||||
income_total,
|
||||
@@ -140,6 +158,7 @@ pub async fn summary(db: &Database, household_id: Uuid, period_id: Uuid) -> AppR
|
||||
carryover_surplus,
|
||||
spendable,
|
||||
budget_total,
|
||||
spent_total,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -154,6 +173,7 @@ pub async fn view(
|
||||
let docs = load(db, household_id, period_id).await?;
|
||||
let (start, end) = dates(&p)?;
|
||||
let period_weeks = weeks(start, end);
|
||||
let spent = spent_by_group_and_date(db, household_id, period_id).await?;
|
||||
|
||||
let mut out = Vec::with_capacity(4);
|
||||
for group in Group::ALL {
|
||||
@@ -183,6 +203,13 @@ pub async fn view(
|
||||
.iter()
|
||||
.map(|w| {
|
||||
let key = format_date(w.start);
|
||||
let last = format_date(w.last);
|
||||
// Tarihler "YYYY-MM-DD" → metin karşılaştırması tarih sırasıyla aynı
|
||||
let week_spent = spent
|
||||
.iter()
|
||||
.filter(|(g, d, _)| *g == group && *d >= key && *d <= last)
|
||||
.map(|(_, _, a)| a)
|
||||
.sum();
|
||||
let (amount, is_manual) = match manual.get(&key) {
|
||||
Some(&a) => (a, true),
|
||||
None => (auto.next().unwrap_or(0), false),
|
||||
@@ -190,10 +217,11 @@ pub async fn view(
|
||||
WeekBudget {
|
||||
index: w.index,
|
||||
start_date: key,
|
||||
last_date: format_date(w.last),
|
||||
last_date: last,
|
||||
days: w.days,
|
||||
amount,
|
||||
is_manual,
|
||||
spent: week_spent,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -201,6 +229,11 @@ pub async fn view(
|
||||
out.push(GroupBudget {
|
||||
group,
|
||||
monthly,
|
||||
spent: spent
|
||||
.iter()
|
||||
.filter(|(g, _, _)| *g == group)
|
||||
.map(|(_, _, a)| a)
|
||||
.sum(),
|
||||
weeks,
|
||||
weekly_over: manual_total > monthly,
|
||||
});
|
||||
@@ -451,6 +484,45 @@ async fn sum_by(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Dönemin harcamaları (grup, gün) bazında toplanmış: en fazla 4 × 62 satır.
|
||||
async fn spent_by_group_and_date(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
period_id: Uuid,
|
||||
) -> AppResult<Vec<(Group, String, i64)>> {
|
||||
#[derive(Deserialize)]
|
||||
struct Key {
|
||||
group: Group,
|
||||
date: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Row {
|
||||
#[serde(rename = "_id")]
|
||||
key: Key,
|
||||
total: i64,
|
||||
}
|
||||
let mut m = scope(household_id);
|
||||
m.insert("period_id", period_id);
|
||||
let pipeline = vec![
|
||||
doc! { "$match": m },
|
||||
doc! { "$group": {
|
||||
"_id": { "group": "$group", "date": "$date" },
|
||||
"total": { "$sum": "$amount" },
|
||||
} },
|
||||
];
|
||||
let mut cursor = db
|
||||
.collection::<Document>(col::ENTRIES)
|
||||
.aggregate(pipeline)
|
||||
.with_type::<Row>()
|
||||
.await?;
|
||||
let mut out = Vec::new();
|
||||
while cursor.advance().await? {
|
||||
let r = cursor.deserialize_current()?;
|
||||
out.push((r.key.group, r.key.date, r.total));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Hane ayarındaki varsayılan yüzdeler (yoksa 50/30/10/10).
|
||||
async fn split_percentages(db: &Database, household_id: Uuid) -> AppResult<Vec<u64>> {
|
||||
let mut f = scope(household_id);
|
||||
|
||||
Reference in New Issue
Block a user