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
-1
@@ -235,9 +235,10 @@ DELETE {{baseUrl}}/notes/BURAYA-NOT-ID
|
|||||||
# Gruplar: needs · wants · culture · unexpected · tutarlar kuruş
|
# Gruplar: needs · wants · culture · unexpected · tutarlar kuruş
|
||||||
|
|
||||||
### Dönem özeti: gelir − sabit gider − birikim − devreden borç + devreden artı = harcanabilir
|
### Dönem özeti: gelir − sabit gider − birikim − devreden borç + devreden artı = harcanabilir
|
||||||
|
### + spent_total (gerçekleşen harcama), remaining (harcanabilir − harcanan)
|
||||||
GET {{baseUrl}}/periods/BURAYA-DONEM-ID/summary
|
GET {{baseUrl}}/periods/BURAYA-DONEM-ID/summary
|
||||||
|
|
||||||
### Bütçe: 4 grup, aylık + haftalık döküm (haftalar gün sayısına göre)
|
### Bütçe: 4 grup, aylık + haftalık döküm (haftalar gün sayısına göre); her grup ve haftada `spent`
|
||||||
GET {{baseUrl}}/periods/BURAYA-DONEM-ID/budget
|
GET {{baseUrl}}/periods/BURAYA-DONEM-ID/budget
|
||||||
|
|
||||||
### Aylık bütçeleri kaydet (4 grup birlikte)
|
### Aylık bütçeleri kaydet (4 grup birlikte)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
//!
|
//!
|
||||||
//! | Yöntem | Yol | İş |
|
//! | Yöntem | Yol | İş |
|
||||||
//! |---|---|---|
|
//! |---|---|---|
|
||||||
//! | GET | `/api/periods/{id}/summary` | gelir/gider/birikim/devir → harcanabilir |
|
//! | 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 |
|
//! | 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` | 4 grubun aylık tutarları |
|
||||||
//! | PUT | `/api/periods/{id}/budget/weeks` | bir haftayı elle ayarla / otomatiğe döndür |
|
//! | 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` |
|
//! | 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ış)
|
/// harcanabilir − dağıtılan (negatifse fazla dağıtılmış)
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
unallocated: i64,
|
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)]
|
#[derive(Serialize, TS)]
|
||||||
@@ -89,6 +95,9 @@ pub struct GroupBudgetDto {
|
|||||||
group: Group,
|
group: Group,
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
monthly: i64,
|
monthly: i64,
|
||||||
|
/// Dönem boyunca gerçekleşen harcama
|
||||||
|
#[ts(type = "number")]
|
||||||
|
spent: i64,
|
||||||
/// Elle girilen haftaların toplamı aylık tutarı aşıyor
|
/// Elle girilen haftaların toplamı aylık tutarı aşıyor
|
||||||
weekly_over: bool,
|
weekly_over: bool,
|
||||||
weeks: Vec<WeekBudgetDto>,
|
weeks: Vec<WeekBudgetDto>,
|
||||||
@@ -105,6 +114,9 @@ struct WeekBudgetDto {
|
|||||||
amount: i64,
|
amount: i64,
|
||||||
/// Elle girildi (otomatik değil)
|
/// Elle girildi (otomatik değil)
|
||||||
is_manual: bool,
|
is_manual: bool,
|
||||||
|
/// Bu haftada gerçekleşen harcama
|
||||||
|
#[ts(type = "number")]
|
||||||
|
spent: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GroupBudget> for GroupBudgetDto {
|
impl From<GroupBudget> for GroupBudgetDto {
|
||||||
@@ -112,6 +124,7 @@ impl From<GroupBudget> for GroupBudgetDto {
|
|||||||
Self {
|
Self {
|
||||||
group: g.group,
|
group: g.group,
|
||||||
monthly: g.monthly,
|
monthly: g.monthly,
|
||||||
|
spent: g.spent,
|
||||||
weekly_over: g.weekly_over,
|
weekly_over: g.weekly_over,
|
||||||
weeks: g
|
weeks: g
|
||||||
.weeks
|
.weeks
|
||||||
@@ -123,6 +136,7 @@ impl From<GroupBudget> for GroupBudgetDto {
|
|||||||
days: w.days,
|
days: w.days,
|
||||||
amount: w.amount,
|
amount: w.amount,
|
||||||
is_manual: w.is_manual,
|
is_manual: w.is_manual,
|
||||||
|
spent: w.spent,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
@@ -182,6 +196,8 @@ pub async fn summary(
|
|||||||
spendable: x.spendable,
|
spendable: x.spendable,
|
||||||
budget_total: x.budget_total,
|
budget_total: x.budget_total,
|
||||||
unallocated: x.spendable - 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"`);
|
//! 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.
|
//! 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**.
|
//! - Ö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;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -40,6 +42,8 @@ pub struct Summary {
|
|||||||
pub carryover_surplus: i64,
|
pub carryover_surplus: i64,
|
||||||
pub spendable: i64,
|
pub spendable: i64,
|
||||||
pub budget_total: i64,
|
pub budget_total: i64,
|
||||||
|
/// Dönemde gerçekleşen harcama (silinenler hariç)
|
||||||
|
pub spent_total: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WeekBudget {
|
pub struct WeekBudget {
|
||||||
@@ -49,11 +53,15 @@ pub struct WeekBudget {
|
|||||||
pub days: u32,
|
pub days: u32,
|
||||||
pub amount: i64,
|
pub amount: i64,
|
||||||
pub is_manual: bool,
|
pub is_manual: bool,
|
||||||
|
/// Bu haftada bu grupta gerçekleşen harcama
|
||||||
|
pub spent: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GroupBudget {
|
pub struct GroupBudget {
|
||||||
pub group: Group,
|
pub group: Group,
|
||||||
pub monthly: i64,
|
pub monthly: i64,
|
||||||
|
/// Dönem boyunca bu grupta gerçekleşen harcama
|
||||||
|
pub spent: i64,
|
||||||
pub weeks: Vec<WeekBudget>,
|
pub weeks: Vec<WeekBudget>,
|
||||||
/// Elle girilen haftaların toplamı aylık tutarı aşıyor mu?
|
/// Elle girilen haftaların toplamı aylık tutarı aşıyor mu?
|
||||||
pub weekly_over: bool,
|
pub weekly_over: bool,
|
||||||
@@ -131,6 +139,16 @@ pub async fn summary(db: &Database, household_id: Uuid, period_id: Uuid) -> AppR
|
|||||||
.sum();
|
.sum();
|
||||||
let spendable =
|
let spendable =
|
||||||
income_total - fixed_expense_total - p.savings_goal - carryover_debt + carryover_surplus;
|
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 {
|
Ok(Summary {
|
||||||
income_total,
|
income_total,
|
||||||
@@ -140,6 +158,7 @@ pub async fn summary(db: &Database, household_id: Uuid, period_id: Uuid) -> AppR
|
|||||||
carryover_surplus,
|
carryover_surplus,
|
||||||
spendable,
|
spendable,
|
||||||
budget_total,
|
budget_total,
|
||||||
|
spent_total,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +173,7 @@ pub async fn view(
|
|||||||
let docs = load(db, household_id, period_id).await?;
|
let docs = load(db, household_id, period_id).await?;
|
||||||
let (start, end) = dates(&p)?;
|
let (start, end) = dates(&p)?;
|
||||||
let period_weeks = weeks(start, end);
|
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);
|
let mut out = Vec::with_capacity(4);
|
||||||
for group in Group::ALL {
|
for group in Group::ALL {
|
||||||
@@ -183,6 +203,13 @@ pub async fn view(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|w| {
|
.map(|w| {
|
||||||
let key = format_date(w.start);
|
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) {
|
let (amount, is_manual) = match manual.get(&key) {
|
||||||
Some(&a) => (a, true),
|
Some(&a) => (a, true),
|
||||||
None => (auto.next().unwrap_or(0), false),
|
None => (auto.next().unwrap_or(0), false),
|
||||||
@@ -190,10 +217,11 @@ pub async fn view(
|
|||||||
WeekBudget {
|
WeekBudget {
|
||||||
index: w.index,
|
index: w.index,
|
||||||
start_date: key,
|
start_date: key,
|
||||||
last_date: format_date(w.last),
|
last_date: last,
|
||||||
days: w.days,
|
days: w.days,
|
||||||
amount,
|
amount,
|
||||||
is_manual,
|
is_manual,
|
||||||
|
spent: week_spent,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -201,6 +229,11 @@ pub async fn view(
|
|||||||
out.push(GroupBudget {
|
out.push(GroupBudget {
|
||||||
group,
|
group,
|
||||||
monthly,
|
monthly,
|
||||||
|
spent: spent
|
||||||
|
.iter()
|
||||||
|
.filter(|(g, _, _)| *g == group)
|
||||||
|
.map(|(_, _, a)| a)
|
||||||
|
.sum(),
|
||||||
weeks,
|
weeks,
|
||||||
weekly_over: manual_total > monthly,
|
weekly_over: manual_total > monthly,
|
||||||
});
|
});
|
||||||
@@ -451,6 +484,45 @@ async fn sum_by(
|
|||||||
Ok(out)
|
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).
|
/// Hane ayarındaki varsayılan yüzdeler (yoksa 50/30/10/10).
|
||||||
async fn split_percentages(db: &Database, household_id: Uuid) -> AppResult<Vec<u64>> {
|
async fn split_percentages(db: &Database, household_id: Uuid) -> AppResult<Vec<u64>> {
|
||||||
let mut f = scope(household_id);
|
let mut f = scope(household_id);
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ async fn summary_computes_spendable_with_carryovers() {
|
|||||||
// 4.800.000 − 1.545.000 − 500.000 − 100.000 + 20.000
|
// 4.800.000 − 1.545.000 − 500.000 − 100.000 + 20.000
|
||||||
"spendable": 2_675_000,
|
"spendable": 2_675_000,
|
||||||
"budget_total": 0,
|
"budget_total": 0,
|
||||||
"unallocated": 2_675_000
|
"unallocated": 2_675_000,
|
||||||
|
"spent_total": 0,
|
||||||
|
"remaining": 2_675_000
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -451,3 +451,124 @@ async fn previous_suggestion_uses_real_entries() {
|
|||||||
|
|
||||||
app.cleanup().await;
|
app.cleanup().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spent_by_week(budget: &Value, group: &str) -> (i64, Vec<i64>) {
|
||||||
|
let g = budget
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|g| g["group"] == group)
|
||||||
|
.unwrap();
|
||||||
|
let weeks = g["weeks"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|w| w["spent"].as_i64().unwrap())
|
||||||
|
.collect();
|
||||||
|
(g["spent"].as_i64().unwrap(), weeks)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn actual_spending_in_summary_and_budget() {
|
||||||
|
let app = TestApp::new().await;
|
||||||
|
let s = app.register("Test", None).await;
|
||||||
|
// Kasım'da bir harcama: Aralık toplamlarına girmemeli
|
||||||
|
let nov = open_period(&app, &s, "2026-11-01", "2026-12-01").await;
|
||||||
|
let needs = category(&app, &s, "needs").await;
|
||||||
|
let wants = category(&app, &s, "wants").await;
|
||||||
|
let culture = category(&app, &s, "culture").await;
|
||||||
|
let (st, _) = add(
|
||||||
|
&app,
|
||||||
|
&s,
|
||||||
|
&nov,
|
||||||
|
json!({ "date": "2026-11-30", "amount": 1_000_000, "category_id": needs }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::CREATED);
|
||||||
|
close(&app, &nov).await;
|
||||||
|
let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await;
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
&format!("/periods/{pid}/plan-items"),
|
||||||
|
&s,
|
||||||
|
json!({ "kind": "income", "name": "Maaş", "amount": 300_000 }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Aralık 2026 haftaları: 1–6 · 7–13 · 14–20 · 21–27 · 28–31
|
||||||
|
for (date, amount, cat) in [
|
||||||
|
("2026-12-01", 1_000, &needs),
|
||||||
|
("2026-12-06", 2_000, &needs), // 1. haftanın son günü
|
||||||
|
("2026-12-07", 500, &needs), // 2. haftanın ilk günü
|
||||||
|
("2026-12-31", 300, &wants), // dönemin son günü
|
||||||
|
] {
|
||||||
|
let (st, b) = add(
|
||||||
|
&app,
|
||||||
|
&s,
|
||||||
|
&pid,
|
||||||
|
json!({ "date": date, "amount": amount, "category_id": cat }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::CREATED, "{b}");
|
||||||
|
}
|
||||||
|
// Kategori değişince harcama diğer gruba geçer
|
||||||
|
let (_, moved) = add(
|
||||||
|
&app,
|
||||||
|
&s,
|
||||||
|
&pid,
|
||||||
|
json!({ "date": "2026-12-08", "amount": 400, "category_id": needs }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
app.patch(
|
||||||
|
&format!("/entries/{}", moved["id"].as_str().unwrap()),
|
||||||
|
&s,
|
||||||
|
json!({ "category_id": wants }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// Silinen harcama sayılmaz
|
||||||
|
let (_, gone) = add(
|
||||||
|
&app,
|
||||||
|
&s,
|
||||||
|
&pid,
|
||||||
|
json!({ "date": "2026-12-15", "amount": 700, "category_id": culture }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let gone_id = gone["id"].as_str().unwrap();
|
||||||
|
app.delete(&format!("/entries/{gone_id}"), &s).await;
|
||||||
|
|
||||||
|
let (_, sum) = app.get(&format!("/periods/{pid}/summary"), &s).await;
|
||||||
|
// 1.000 + 2.000 + 500 + 300 + 400 = 4.200
|
||||||
|
assert_eq!(
|
||||||
|
(sum["spent_total"].as_i64(), sum["remaining"].as_i64()),
|
||||||
|
(Some(4_200), Some(295_800)),
|
||||||
|
"{sum}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (st, b) = app.get(&format!("/periods/{pid}/budget"), &s).await;
|
||||||
|
assert_eq!(st, StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
spent_by_week(&b, "needs"),
|
||||||
|
(3_500, vec![3_000, 500, 0, 0, 0])
|
||||||
|
);
|
||||||
|
assert_eq!(spent_by_week(&b, "wants"), (700, vec![0, 400, 0, 0, 300]));
|
||||||
|
assert_eq!(spent_by_week(&b, "culture"), (0, vec![0; 5]));
|
||||||
|
|
||||||
|
// Geri al → yeniden sayılır
|
||||||
|
app.post(&format!("/entries/{gone_id}/restore"), &s, json!({}))
|
||||||
|
.await;
|
||||||
|
let (_, b) = app.get(&format!("/periods/{pid}/budget"), &s).await;
|
||||||
|
assert_eq!(spent_by_week(&b, "culture"), (700, vec![0, 0, 700, 0, 0]));
|
||||||
|
|
||||||
|
// Harcanabilir aşılırsa kalan negatif
|
||||||
|
add(
|
||||||
|
&app,
|
||||||
|
&s,
|
||||||
|
&pid,
|
||||||
|
json!({ "date": "2026-12-20", "amount": 300_000, "category_id": needs }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (_, sum) = app.get(&format!("/periods/{pid}/summary"), &s).await;
|
||||||
|
assert_eq!(sum["remaining"], 300_000 - 304_900);
|
||||||
|
|
||||||
|
app.cleanup().await;
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
# 🧠 Proje Hafızası
|
# 🧠 Proje Hafızası
|
||||||
|
|
||||||
## ▶️ Sıradaki Adım
|
## ▶️ Sıradaki Adım
|
||||||
**Faz 5 – Adım 2:** Backend gerçekleşen harcamalar: dönem özetine `spent_total` + `remaining` (harcanabilir − harcanan), bütçe görünümüne grup×hafta `spent` (aggregation, saklanmaz), haftalık özet uç noktası ya da `budget` yanıtına ekleme (karar: mevcut `GET /budget` DTO'suna `spent` alanı). Testler: haftalara doğru dağılım, silinen kayıt sayılmaz, izolasyon.
|
**Faz 5 – Adım 3:** Frontend Defter (`/defter`): hafta seçici (◀ önceki | Bu hafta | sonraki ▶, dönem haftaları `GET /periods/{id}` → `weeks`), hızlı giriş formu (büyük tutar, 4 renkli grup düğmesi → o grubun aktif kategorileri, ödeme türü, tarih varsayılan bugün/dönem içi, açıklama), son kategori/ödeme türü localStorage'da hatırlanır. `lib/entries.ts` kancaları; kayıttan sonra `period(id)` önekiyle geçersiz kıl (özet + bütçe `spent` yenilenir).
|
||||||
|
|
||||||
## 📌 Alınan Kararlar
|
## 📌 Alınan Kararlar
|
||||||
- **Yerel demo hesapları** (`kakeibo_dev`): `demo@ornek.com` / `demo12345` (owner, "Demo Hanesi") · `es@ornek.com` / `demo12345` (member, aynı hane). `npm run db:reset` silerse API ile yeniden oluştur (ileride `db:seed`).
|
- **Yerel demo hesapları** (`kakeibo_dev`): `demo@ornek.com` / `demo12345` (owner, "Demo Hanesi") · `es@ornek.com` / `demo12345` (member, aynı hane). `npm run db:reset` silerse API ile yeniden oluştur (ileride `db:seed`).
|
||||||
@@ -100,3 +100,4 @@
|
|||||||
|
|
||||||
### 2026-11-12 — Faz 5
|
### 2026-11-12 — Faz 5
|
||||||
- **Adım 1:** Harcamalar backend: `models/entry.rs`, `services/entry.rs` (list + `from`/`to` filtresi, sıra `date` ↓ `_id` ↓; create/update/delete soft/`restore`), `routes/entry.rs` (5 uç nokta), `catalog::find`. Grup **kategoriden türetilir** (istemci göndermez). Pasif kategori/ödeme türü yeni seçimde reddedilir, kaydın mevcut değeri olarak kalabilir. Ödeme türü isteğe bağlı; düzenlemede `""` → kaldır (projedeki "boş metin = kaldır" kuralı; ts-rs `deserialize_with` desteklemediği için çift-Option yerine). `tests/entries.rs` (6) — `previous` önerisi artık gerçek kayıtla test edildi. Mutasyon: grup türetme bozulunca yakalandı; restore'da hane filtresi tek başına kaldırılınca `get_open` ikinci katman olarak korudu, ikisi birden kaldırılınca 2 test yakaladı. Backend 97 test.
|
- **Adım 1:** Harcamalar backend: `models/entry.rs`, `services/entry.rs` (list + `from`/`to` filtresi, sıra `date` ↓ `_id` ↓; create/update/delete soft/`restore`), `routes/entry.rs` (5 uç nokta), `catalog::find`. Grup **kategoriden türetilir** (istemci göndermez). Pasif kategori/ödeme türü yeni seçimde reddedilir, kaydın mevcut değeri olarak kalabilir. Ödeme türü isteğe bağlı; düzenlemede `""` → kaldır (projedeki "boş metin = kaldır" kuralı; ts-rs `deserialize_with` desteklemediği için çift-Option yerine). `tests/entries.rs` (6) — `previous` önerisi artık gerçek kayıtla test edildi. Mutasyon: grup türetme bozulunca yakalandı; restore'da hane filtresi tek başına kaldırılınca `get_open` ikinci katman olarak korudu, ikisi birden kaldırılınca 2 test yakaladı. Backend 97 test.
|
||||||
|
- **Adım 2:** Gerçekleşen harcama (saklanmaz, her istekte aggregation): `GET /summary` → `spent_total`, `remaining` (= harcanabilir − harcanan, negatifse aşılmış). `GET /budget` → grupta `spent` (dönem), haftada `spent`. `spent_by_group_and_date` (grup×gün toplanır, Rust'ta haftalara dağıtılır; tarih metin karşılaştırması). Silinen kayıt sayılmaz, kategori değişince harcama yeni gruba geçer. Test `actual_spending_in_summary_and_budget` (hafta sınırları 6/7 Aralık, dönem son günü, önceki dönem hariç, geri al). Mutasyon: hafta sınırı `<=`→`<` ve silinenleri sayma → ikisi de yakalandı. Backend 98 test.
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
## Faz 5 Adımları (taslak)
|
## Faz 5 Adımları (taslak)
|
||||||
- ✅ 1. Backend: harcama kayıtları (ekle/düzenle/sil + geri al, hafta/dönem filtresi, kategori–grup tutarlılığı, yalnızca açık dönem), testler
|
- ✅ 1. Backend: harcama kayıtları (ekle/düzenle/sil + geri al, hafta/dönem filtresi, kategori–grup tutarlılığı, yalnızca açık dönem), testler
|
||||||
- 🔄 2. Backend: haftalık/aylık gerçekleşen (grup bazlı bütçe–harcanan–kalan), özet ve önerilere gerçekleşen verisi
|
- ✅ 2. Backend: haftalık/aylık gerçekleşen (grup bazlı bütçe–harcanan–kalan), özet ve önerilere gerçekleşen verisi
|
||||||
- ⏳ 3. Frontend: Defter — hafta seçici + hızlı giriş formu (tutar, 4 grup düğmesi, kategori, ödeme türü, tarih, açıklama; son seçim hatırlanır)
|
- 🔄 3. Frontend: Defter — hafta seçici + hızlı giriş formu (tutar, 4 grup düğmesi, kategori, ödeme türü, tarih, açıklama; son seçim hatırlanır)
|
||||||
- ⏳ 4. Frontend: haftanın girişleri (gün gün), düzenle/sil + "Geri al", haftalık özet
|
- ⏳ 4. Frontend: haftanın girişleri (gün gün), düzenle/sil + "Geri al", haftalık özet
|
||||||
- ⏳ 5. Canlı yedekleme (mongodump günlük, 14 gün) + geri yükleme denemesi
|
- ⏳ 5. Canlı yedekleme (mongodump günlük, 14 gün) + geri yükleme denemesi
|
||||||
|
|||||||
@@ -57,10 +57,12 @@ const week = (start: string, amount: number, is_manual = false) => ({
|
|||||||
days: 7,
|
days: 7,
|
||||||
amount,
|
amount,
|
||||||
is_manual,
|
is_manual,
|
||||||
|
spent: 0,
|
||||||
})
|
})
|
||||||
const budget: GroupBudget[] = (['needs', 'wants', 'culture', 'unexpected'] as const).map((g) => ({
|
const budget: GroupBudget[] = (['needs', 'wants', 'culture', 'unexpected'] as const).map((g) => ({
|
||||||
group: g,
|
group: g,
|
||||||
monthly: g === 'needs' ? 1_000_000 : 0,
|
monthly: g === 'needs' ? 1_000_000 : 0,
|
||||||
|
spent: 0,
|
||||||
weekly_over: false,
|
weekly_over: false,
|
||||||
weeks:
|
weeks:
|
||||||
g === 'needs'
|
g === 'needs'
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import type { Group } from "./Group";
|
|||||||
import type { WeekBudget } from "./WeekBudget";
|
import type { WeekBudget } from "./WeekBudget";
|
||||||
|
|
||||||
export type GroupBudget = { group: Group, monthly: number,
|
export type GroupBudget = { group: Group, monthly: number,
|
||||||
|
/**
|
||||||
|
* Dönem boyunca gerçekleşen harcama
|
||||||
|
*/
|
||||||
|
spent: number,
|
||||||
/**
|
/**
|
||||||
* Elle girilen haftaların toplamı aylık tutarı aşıyor
|
* Elle girilen haftaların toplamı aylık tutarı aşıyor
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,4 +20,12 @@ budget_total: number,
|
|||||||
/**
|
/**
|
||||||
* harcanabilir − dağıtılan (negatifse fazla dağıtılmış)
|
* harcanabilir − dağıtılan (negatifse fazla dağıtılmış)
|
||||||
*/
|
*/
|
||||||
unallocated: number, };
|
unallocated: number,
|
||||||
|
/**
|
||||||
|
* Dönemde gerçekleşen harcama
|
||||||
|
*/
|
||||||
|
spent_total: number,
|
||||||
|
/**
|
||||||
|
* Kalan harcanabilir: harcanabilir − gerçekleşen (negatifse aşılmış)
|
||||||
|
*/
|
||||||
|
remaining: number, };
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ export type WeekBudget = { index: number, start_date: string, last_date: string,
|
|||||||
/**
|
/**
|
||||||
* Elle girildi (otomatik değil)
|
* Elle girildi (otomatik değil)
|
||||||
*/
|
*/
|
||||||
is_manual: boolean, };
|
is_manual: boolean,
|
||||||
|
/**
|
||||||
|
* Bu haftada gerçekleşen harcama
|
||||||
|
*/
|
||||||
|
spent: number, };
|
||||||
|
|||||||
Reference in New Issue
Block a user