feat(panel): günlük harcama ve dönem istatistikleri uç noktaları
- GET /periods/{id}/daily: dönemin her günü için 4 grubun harcaması
- GET /stats/periods: son dönemlerin harcanabilir/harcanan tutarı + toplam birikim
- Faz 7 adım planı
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -324,3 +324,11 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### Kapalı dönemi düzeltmeye aç (yalnızca hane sahibi) → status: correcting; düzeltince yeniden "close"
|
### Kapalı dönemi düzeltmeye aç (yalnızca hane sahibi) → status: correcting; düzeltince yeniden "close"
|
||||||
POST {{baseUrl}}/periods/BURAYA-DONEM-ID/reopen
|
POST {{baseUrl}}/periods/BURAYA-DONEM-ID/reopen
|
||||||
|
|
||||||
|
# ─────────────────────────── Panel ───────────────────────────
|
||||||
|
|
||||||
|
### Günlük harcama: dönemin her günü, 4 grup (harcama yoksa 0)
|
||||||
|
GET {{baseUrl}}/periods/BURAYA-DONEM-ID/daily
|
||||||
|
|
||||||
|
### Son dönemler (eskiden yeniye): harcanabilir, harcanan, birikime aktarılan + toplam birikim
|
||||||
|
GET {{baseUrl}}/stats/periods?limit=12
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod period;
|
|||||||
mod plan;
|
mod plan;
|
||||||
mod review;
|
mod review;
|
||||||
mod spa;
|
mod spa;
|
||||||
|
mod stats;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
@@ -82,6 +83,8 @@ pub fn router(state: AppState) -> Router {
|
|||||||
.route("/periods/{id}/notes", get(note::list).post(note::create))
|
.route("/periods/{id}/notes", get(note::list).post(note::create))
|
||||||
.route("/periods/{id}/summary", get(budget::summary))
|
.route("/periods/{id}/summary", get(budget::summary))
|
||||||
.route("/periods/{id}/review", get(review::get))
|
.route("/periods/{id}/review", get(review::get))
|
||||||
|
.route("/periods/{id}/daily", get(stats::daily))
|
||||||
|
.route("/stats/periods", get(stats::periods))
|
||||||
.route(
|
.route(
|
||||||
"/periods/{id}/budget",
|
"/periods/{id}/budget",
|
||||||
get(budget::get).put(budget::set_monthly),
|
get(budget::get).put(budget::set_monthly),
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//! Panel (Dashboard) uç noktaları (hanenin tüm üyeleri):
|
||||||
|
//!
|
||||||
|
//! | Yöntem | Yol | İş |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | GET | `/api/periods/{id}/daily` | dönemin her günü: 4 grubun harcaması |
|
||||||
|
//! | GET | `/api/stats/periods?limit=12` | son dönemler (eskiden yeniye): harcanabilir, harcanan, birikime aktarılan + toplam birikim |
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
};
|
||||||
|
use mongodb::bson::Uuid;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::AuthUser,
|
||||||
|
error::{AppError, AppResult},
|
||||||
|
models::period::PeriodStatus,
|
||||||
|
routes::budget::GroupAmounts,
|
||||||
|
services::{
|
||||||
|
calendar::{format_date, parse_date},
|
||||||
|
stats,
|
||||||
|
},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, TS)]
|
||||||
|
#[ts(export, rename = "DaySpent")]
|
||||||
|
pub struct DayDto {
|
||||||
|
/// `"YYYY-MM-DD"`
|
||||||
|
date: String,
|
||||||
|
/// O gün gruplara göre harcanan (kuruş)
|
||||||
|
spent: GroupAmounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn daily(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
auth: AuthUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> AppResult<Json<Vec<DayDto>>> {
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::NotFound)?;
|
||||||
|
let days = stats::daily(&s.db, auth.household_id, id).await?;
|
||||||
|
Ok(Json(
|
||||||
|
days.into_iter()
|
||||||
|
.map(|d| DayDto {
|
||||||
|
date: d.date,
|
||||||
|
spent: d.spent.into(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct StatsQuery {
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TS)]
|
||||||
|
#[ts(export, rename = "PeriodStats")]
|
||||||
|
pub struct StatsDto {
|
||||||
|
/// Kapanışlarda birikime aktarılanların toplamı (kuruş)
|
||||||
|
#[ts(type = "number")]
|
||||||
|
total_saved: i64,
|
||||||
|
/// Eskiden yeniye
|
||||||
|
periods: Vec<PeriodStatDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TS)]
|
||||||
|
#[ts(export, rename = "PeriodStat")]
|
||||||
|
struct PeriodStatDto {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
start_date: String,
|
||||||
|
/// Son gün, dahil
|
||||||
|
last_date: String,
|
||||||
|
status: PeriodStatus,
|
||||||
|
#[ts(type = "number")]
|
||||||
|
spendable: i64,
|
||||||
|
#[ts(type = "number")]
|
||||||
|
spent: i64,
|
||||||
|
/// Kapanmamışsa `null`
|
||||||
|
#[ts(type = "number | null")]
|
||||||
|
to_savings: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn periods(
|
||||||
|
State(s): State<AppState>,
|
||||||
|
auth: AuthUser,
|
||||||
|
Query(q): Query<StatsQuery>,
|
||||||
|
) -> AppResult<Json<StatsDto>> {
|
||||||
|
let st = stats::periods(&s.db, auth.household_id, q.limit.unwrap_or(12)).await?;
|
||||||
|
Ok(Json(StatsDto {
|
||||||
|
total_saved: st.total_saved,
|
||||||
|
periods: st
|
||||||
|
.periods
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
let end = parse_date(&x.period.end_date).expect("geçerli bitiş tarihi");
|
||||||
|
PeriodStatDto {
|
||||||
|
id: x.period.id.to_string(),
|
||||||
|
name: x.period.name,
|
||||||
|
start_date: x.period.start_date,
|
||||||
|
last_date: format_date(end - time::Duration::days(1)),
|
||||||
|
status: x.period.status,
|
||||||
|
spendable: x.spendable,
|
||||||
|
spent: x.spent,
|
||||||
|
to_savings: x.period.to_savings,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -485,7 +485,7 @@ async fn sum_by(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dönemin harcamaları (grup, gün) bazında toplanmış: en fazla 4 × 62 satır.
|
/// Dönemin harcamaları (grup, gün) bazında toplanmış: en fazla 4 × 62 satır.
|
||||||
async fn spent_by_group_and_date(
|
pub(crate) async fn spent_by_group_and_date(
|
||||||
db: &Database,
|
db: &Database,
|
||||||
household_id: Uuid,
|
household_id: Uuid,
|
||||||
period_id: Uuid,
|
period_id: Uuid,
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ pub mod period;
|
|||||||
pub mod plan;
|
pub mod plan;
|
||||||
pub mod review;
|
pub mod review;
|
||||||
pub mod seed;
|
pub mod seed;
|
||||||
|
pub mod stats;
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//! Panel (Dashboard) verileri (KAKEIBO_PROMPT §8.2). Hiçbiri saklanmaz; her istekte toplanır.
|
||||||
|
//!
|
||||||
|
//! - `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
|
||||||
|
//! **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.
|
||||||
|
|
||||||
|
use mongodb::{Database, bson::Uuid};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::AppResult,
|
||||||
|
models::period::Period,
|
||||||
|
services::{
|
||||||
|
budget::{self, GroupAmounts},
|
||||||
|
calendar::{format_date, parse_date},
|
||||||
|
period,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Aylık grafikte en fazla dönem sayısı
|
||||||
|
pub const MAX_PERIODS: usize = 24;
|
||||||
|
|
||||||
|
pub struct DaySpent {
|
||||||
|
/// `"YYYY-MM-DD"`
|
||||||
|
pub date: String,
|
||||||
|
/// `Group::ALL` sırasıyla
|
||||||
|
pub spent: GroupAmounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn daily(db: &Database, household_id: Uuid, period_id: Uuid) -> AppResult<Vec<DaySpent>> {
|
||||||
|
let p = period::get(db, household_id, period_id).await?;
|
||||||
|
// Veritabanındaki tarihler her zaman doğrulanmış biçimde yazılır
|
||||||
|
let start = parse_date(&p.start_date).expect("geçerli başlangıç tarihi");
|
||||||
|
let end = parse_date(&p.end_date).expect("geçerli bitiş tarihi");
|
||||||
|
|
||||||
|
let mut days = Vec::new();
|
||||||
|
let mut d = start;
|
||||||
|
while d < end {
|
||||||
|
days.push(DaySpent {
|
||||||
|
date: format_date(d),
|
||||||
|
spent: [0; 4],
|
||||||
|
});
|
||||||
|
d += time::Duration::days(1);
|
||||||
|
}
|
||||||
|
for (group, date, amount) in
|
||||||
|
budget::spent_by_group_and_date(db, household_id, period_id).await?
|
||||||
|
{
|
||||||
|
// Dönem tarihleri sonradan daraltıldıysa dışarıda kalan kayıt grafiğe girmez
|
||||||
|
if let Some(day) = days.iter_mut().find(|x| x.date == date) {
|
||||||
|
day.spent[group.order()] += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PeriodStat {
|
||||||
|
pub period: Period,
|
||||||
|
pub spendable: i64,
|
||||||
|
pub spent: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Stats {
|
||||||
|
/// Kapanışlarda birikime aktarılanların toplamı (tüm dönemler)
|
||||||
|
pub total_saved: i64,
|
||||||
|
/// Son `limit` dönem, **eskiden yeniye**
|
||||||
|
pub periods: Vec<PeriodStat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn periods(db: &Database, household_id: Uuid, limit: usize) -> AppResult<Stats> {
|
||||||
|
let list = period::list(db, household_id).await?; // en yeni başta
|
||||||
|
let total_saved = list.iter().filter_map(|p| p.to_savings).sum();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for p in list.into_iter().take(limit.clamp(1, MAX_PERIODS)) {
|
||||||
|
let s = budget::summary(db, household_id, p.id).await?;
|
||||||
|
out.push(PeriodStat {
|
||||||
|
period: p,
|
||||||
|
spendable: s.spendable,
|
||||||
|
spent: s.spent_total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.reverse();
|
||||||
|
Ok(Stats {
|
||||||
|
total_saved,
|
||||||
|
periods: out,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
//! Panel verileri: günlük harcama ve dönem istatistikleri (toplam birikim dahil).
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use common::{Session, TestApp};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
async fn open(app: &TestApp, s: &Session, start: &str, end: &str, goal: i64) -> String {
|
||||||
|
let (st, p) = app
|
||||||
|
.post(
|
||||||
|
"/periods",
|
||||||
|
s,
|
||||||
|
json!({ "start_date": start, "end_date": end, "savings_goal": goal }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::CREATED, "{p}");
|
||||||
|
p["id"].as_str().unwrap().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn category(app: &TestApp, s: &Session, group: &str) -> String {
|
||||||
|
let (_, cats) = app.get("/categories", s).await;
|
||||||
|
cats.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|c| c["group"] == group)
|
||||||
|
.unwrap()["id"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spend(app: &TestApp, s: &Session, pid: &str, date: &str, amount: i64, cat: &str) -> Value {
|
||||||
|
let (st, e) = app
|
||||||
|
.post(
|
||||||
|
&format!("/periods/{pid}/entries"),
|
||||||
|
s,
|
||||||
|
json!({ "date": date, "amount": amount, "category_id": cat }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::CREATED, "{e}");
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn income(app: &TestApp, s: &Session, pid: &str, amount: i64) {
|
||||||
|
let (st, b) = app
|
||||||
|
.post(
|
||||||
|
&format!("/periods/{pid}/plan-items"),
|
||||||
|
s,
|
||||||
|
json!({ "kind": "income", "name": "Maaş", "amount": amount }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::CREATED, "{b}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kasım: gelir 10.000, harcama 3.000, 5.000 birikime → 2.000 artı Aralık'a.
|
||||||
|
/// Aralık: gelir 20.000, hedef 1.000 → harcanabilir 21.000.
|
||||||
|
async fn two_periods(app: &TestApp, s: &Session) -> (String, String) {
|
||||||
|
let needs = category(app, s, "needs").await;
|
||||||
|
let nov = open(app, s, "2026-11-01", "2026-12-01", 0).await;
|
||||||
|
income(app, s, &nov, 1_000_000).await;
|
||||||
|
spend(app, s, &nov, "2026-11-30", 300_000, &needs).await;
|
||||||
|
let (st, b) = app
|
||||||
|
.post(
|
||||||
|
&format!("/periods/{nov}/close"),
|
||||||
|
s,
|
||||||
|
json!({ "to_savings": 500_000 }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::OK, "{b}");
|
||||||
|
let dec = open(app, s, "2026-12-01", "2027-01-01", 100_000).await;
|
||||||
|
income(app, s, &dec, 2_000_000).await;
|
||||||
|
(nov, dec)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn daily_spending_by_group() {
|
||||||
|
let app = TestApp::new().await;
|
||||||
|
let s = app.register("Test", None).await;
|
||||||
|
let (_, dec) = two_periods(&app, &s).await;
|
||||||
|
let needs = category(&app, &s, "needs").await;
|
||||||
|
let wants = category(&app, &s, "wants").await;
|
||||||
|
spend(&app, &s, &dec, "2026-12-01", 100_000, &needs).await;
|
||||||
|
spend(&app, &s, &dec, "2026-12-01", 50_000, &wants).await;
|
||||||
|
spend(&app, &s, &dec, "2026-12-07", 20_000, &needs).await;
|
||||||
|
spend(&app, &s, &dec, "2026-12-31", 7_000, &needs).await;
|
||||||
|
let gone = spend(&app, &s, &dec, "2026-12-02", 99_000, &needs).await;
|
||||||
|
let (st, _) = app
|
||||||
|
.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s)
|
||||||
|
.await;
|
||||||
|
assert_eq!(st, StatusCode::NO_CONTENT);
|
||||||
|
|
||||||
|
let (st, days) = app.get(&format!("/periods/{dec}/daily"), &s).await;
|
||||||
|
assert_eq!(st, StatusCode::OK, "{days}");
|
||||||
|
let days = days.as_array().unwrap();
|
||||||
|
assert_eq!(days.len(), 31, "her gün, harcama olmasa da");
|
||||||
|
assert_eq!(days[0]["date"], "2026-12-01");
|
||||||
|
assert_eq!(
|
||||||
|
days[0]["spent"],
|
||||||
|
json!({ "needs": 100_000, "wants": 50_000, "culture": 0, "unexpected": 0 })
|
||||||
|
);
|
||||||
|
assert_eq!(days[1]["spent"]["needs"], 0, "silinen kayıt sayılmaz");
|
||||||
|
assert_eq!(days[6]["spent"]["needs"], 20_000);
|
||||||
|
assert_eq!(days[30]["date"], "2026-12-31");
|
||||||
|
assert_eq!(days[30]["spent"]["needs"], 7_000, "son gün dahil");
|
||||||
|
// Kasım'ın 30.11 harcaması Aralık'a karışmaz
|
||||||
|
let total: i64 = days
|
||||||
|
.iter()
|
||||||
|
.map(|d| d["spent"]["needs"].as_i64().unwrap())
|
||||||
|
.sum();
|
||||||
|
assert_eq!(total, 127_000);
|
||||||
|
|
||||||
|
// Başka hane göremez
|
||||||
|
let other = app.register("Diger", None).await;
|
||||||
|
let (st, _) = app.get(&format!("/periods/{dec}/daily"), &other).await;
|
||||||
|
assert_eq!(st, StatusCode::NOT_FOUND);
|
||||||
|
app.cleanup().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn period_stats_and_total_savings() {
|
||||||
|
let app = TestApp::new().await;
|
||||||
|
let s = app.register("Test", None).await;
|
||||||
|
let (nov, dec) = two_periods(&app, &s).await;
|
||||||
|
let needs = category(&app, &s, "needs").await;
|
||||||
|
spend(&app, &s, &dec, "2026-12-05", 40_000, &needs).await;
|
||||||
|
|
||||||
|
let (st, b) = app.get("/stats/periods", &s).await;
|
||||||
|
assert_eq!(st, StatusCode::OK, "{b}");
|
||||||
|
assert_eq!(b["total_saved"], 500_000);
|
||||||
|
let list = b["periods"].as_array().unwrap();
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
// Eskiden yeniye
|
||||||
|
assert_eq!(list[0]["id"], nov.as_str());
|
||||||
|
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]["to_savings"], 500_000);
|
||||||
|
assert_eq!(list[0]["last_date"], "2026-11-30");
|
||||||
|
assert_eq!(list[1]["id"], dec.as_str());
|
||||||
|
assert_eq!(
|
||||||
|
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]["to_savings"], Value::Null);
|
||||||
|
|
||||||
|
// limit: en yeni N dönem
|
||||||
|
let (_, b) = app.get("/stats/periods?limit=1", &s).await;
|
||||||
|
let list = b["periods"].as_array().unwrap();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert_eq!(list[0]["id"], dec.as_str());
|
||||||
|
assert_eq!(
|
||||||
|
b["total_saved"], 500_000,
|
||||||
|
"toplam birikim limitten bağımsız"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Başka hane: boş
|
||||||
|
let other = app.register("Diger", None).await;
|
||||||
|
let (_, b) = app.get("/stats/periods", &other).await;
|
||||||
|
assert_eq!(b["total_saved"], 0);
|
||||||
|
assert_eq!(b["periods"], json!([]));
|
||||||
|
app.cleanup().await;
|
||||||
|
}
|
||||||
+5
-1
@@ -1,7 +1,7 @@
|
|||||||
# 🧠 Proje Hafızası
|
# 🧠 Proje Hafızası
|
||||||
|
|
||||||
## ▶️ Sıradaki Adım
|
## ▶️ Sıradaki Adım
|
||||||
**Faz 7 – Adım 1:** Dashboard planı — önce `KAKEIBO_PROMPT.md` Dashboard bölümünü oku, adımları `docs/PHASES.md`'ye yaz ve kullanıcıya onaylat (özet kartları, grafikler; Recharts).
|
**Faz 7 – Adım 2:** Panel özet kartları (`features/dashboard/`): gelir, sabit gider, toplam birikim (`/stats/periods` → `total_saved`; "varlık değerleri Faz 8'de" notu), devreden borç (varsa), grup planlanan/gerçekleşen (Haftalık/Aylık geçişi, `useBudget`), **anlık durum** = kalan harcanabilir (en belirgin). Mobilde yüzen "➕ Harcama Ekle" → Defter (tutara odak). Skeleton + boş durumlar. Açık dönem yoksa son dönem salt okunur + "Yeni dönemi aç". **Ardından sorulacak:** Ayarlar'da kategori/ödeme türü yönetimi ekranı hiçbir fazda planlı değil (kullanıcı Defter'de kategorileri kısıtlı buldu; 3. kat alt kategori istiyor mu?).
|
||||||
**Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅.
|
**Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅.
|
||||||
|
|
||||||
## 📌 Alınan Kararlar
|
## 📌 Alınan Kararlar
|
||||||
@@ -112,3 +112,7 @@
|
|||||||
- **Adım 3:** Değerlendirme sayfası (`features/review/`): `Questions` (4 kart; 1. kart hesap açıklaması, 3. bütçe yüzdesi, 4. hedefe göre durum `lib/review.ts` → `savingsStatus`: above/below/deficit), `breakdowns.tsx` (`GroupComparison`, `TopCategories` ilk 5 + "Tümünü göster", `WeeklyDistribution`; ortak `Bar`, `ratio`), `Reflection` (görüntüle/düzenle, `useUpdatePeriod` + `version`, 409 formda; şema `features/review/schema.ts`), `NotesSection collapsed` (yalnız başlık, `aria-expanded` ile açılır). Ortak `components/markdown-tabs.tsx` (Yaz/Önizle, `useId` kimlikler; `NoteEditor` da kullanıyor). `useReview` + `queryKeys.review` (dönem önekinde → kayıt değişince yenilenir). Dönem sürüyorsa bilgi satırı. Frontend 84 test; e2e 24 kontrol (Ekim kapatılıp Kasım'a artı devri dahil). Mobil haftalık satır sıkıştırıldı.
|
- **Adım 3:** Değerlendirme sayfası (`features/review/`): `Questions` (4 kart; 1. kart hesap açıklaması, 3. bütçe yüzdesi, 4. hedefe göre durum `lib/review.ts` → `savingsStatus`: above/below/deficit), `breakdowns.tsx` (`GroupComparison`, `TopCategories` ilk 5 + "Tümünü göster", `WeeklyDistribution`; ortak `Bar`, `ratio`), `Reflection` (görüntüle/düzenle, `useUpdatePeriod` + `version`, 409 formda; şema `features/review/schema.ts`), `NotesSection collapsed` (yalnız başlık, `aria-expanded` ile açılır). Ortak `components/markdown-tabs.tsx` (Yaz/Önizle, `useId` kimlikler; `NoteEditor` da kullanıyor). `useReview` + `queryKeys.review` (dönem önekinde → kayıt değişince yenilenir). Dönem sürüyorsa bilgi satırı. Frontend 84 test; e2e 24 kontrol (Ekim kapatılıp Kasım'a artı devri dahil). Mobil haftalık satır sıkıştırıldı.
|
||||||
- **Adım 4:** Kapatma sihirbazı `/degerlendirme/kapat` (`features/review/close-page.tsx`): Özet (4 soru, erken kapatma uyarısı, "Nasıl daha iyi" boşsa uyarı + bağlantı) → Birikim (artıda `MoneyInput` varsayılan tamamı, Tamamı/Hiçbiri, canlı önizleme; açıkta/sıfırda yalnız bilgi) → Onay (`useClosePeriod`, `version`; 409 formda, `onSettled` tüm `['period']` + liste yenilenir → ikinci deneme güncel sürümle) → Sonuç (sayfa durumunda; kapanınca `current` null). `lib/review.ts` → `settlePreview` (backend `settle` ile aynı tutulmalı), şema `close-schema.ts` (0..gerçekleşen). `Stepper` artık `steps`/`label` alıyor. Dönem açma sihirbazı tarih önerisi: `suggestRanges(today, prevEnd)` / `rangeAfter` — son dönemin bitişinden başlar, çakışan ay önerilmez (erken kapatılan ay tekrar önerilmezdi hatası önlendi). Not: `moneyToText(0)` → `''` (plan taslağı için); 0 göstermek gerekiyorsa `formatNumber`. Frontend 89 test; e2e 33 kontrol (DB silindi, 8091 kapatıldı).
|
- **Adım 4:** Kapatma sihirbazı `/degerlendirme/kapat` (`features/review/close-page.tsx`): Özet (4 soru, erken kapatma uyarısı, "Nasıl daha iyi" boşsa uyarı + bağlantı) → Birikim (artıda `MoneyInput` varsayılan tamamı, Tamamı/Hiçbiri, canlı önizleme; açıkta/sıfırda yalnız bilgi) → Onay (`useClosePeriod`, `version`; 409 formda, `onSettled` tüm `['period']` + liste yenilenir → ikinci deneme güncel sürümle) → Sonuç (sayfa durumunda; kapanınca `current` null). `lib/review.ts` → `settlePreview` (backend `settle` ile aynı tutulmalı), şema `close-schema.ts` (0..gerçekleşen). `Stepper` artık `steps`/`label` alıyor. Dönem açma sihirbazı tarih önerisi: `suggestRanges(today, prevEnd)` / `rangeAfter` — son dönemin bitişinden başlar, çakışan ay önerilmez (erken kapatılan ay tekrar önerilmezdi hatası önlendi). Not: `moneyToText(0)` → `''` (plan taslağı için); 0 göstermek gerekiyorsa `formatNumber`. Frontend 89 test; e2e 33 kontrol (DB silindi, 8091 kapatıldı).
|
||||||
- **Adım 5:** Dönem seçimi: `lib/period-select.ts` (`useSelectedPeriod`: `?donem=` → listeden; yoksa açık dönem, o da yoksa düzeltmedeki; `pickPeriod`, `isWritable`, `canReopen` = backend kuralı, `periodPath`), `features/periods/` (`PeriodPicker` başlıkta `<select>`, açık döneme dönünce parametre kalkar; `SelectedPeriod` yükleniyor/hata/bulunamadı/yok kapısı). Defter, Plan, Değerlendirme, kapatma sihirbazı buna geçti (Panel ve dönem sihirbazı hâlâ `useCurrentPeriod`). Defter kapalı dönemde salt okunur (form yok, `EntryList readOnly`); hafta değişimi `donem`'i korur. Plan: düzenleme `isWritable`, kopyalama yalnız açık. `PeriodHeading` durum rozeti. Değerlendirme sonu `ClosingSection` (açık → Kapat · düzeltmede → Defter'de/Plan'da düzelt + Yeniden Kapat · kapalı → kapanış bilgisi + **sahip** için satır içi onaylı "Düzeltme için yeniden aç") + `PeriodHistory`. `useReopenPeriod`; `useUpdatePeriod` liste önbelleğini de günceller (seçili dönem listeden → art arda kayıtta 409 olmasın). **Ders:** başarıda bileşen kalkıyorsa `mutate(…, {onSuccess})` çalışmaz → `mutateAsync` kullan. Frontend 92 test; e2e 36 + kapatma 33 yeniden (DB silindi). Faz 6 ✅.
|
- **Adım 5:** Dönem seçimi: `lib/period-select.ts` (`useSelectedPeriod`: `?donem=` → listeden; yoksa açık dönem, o da yoksa düzeltmedeki; `pickPeriod`, `isWritable`, `canReopen` = backend kuralı, `periodPath`), `features/periods/` (`PeriodPicker` başlıkta `<select>`, açık döneme dönünce parametre kalkar; `SelectedPeriod` yükleniyor/hata/bulunamadı/yok kapısı). Defter, Plan, Değerlendirme, kapatma sihirbazı buna geçti (Panel ve dönem sihirbazı hâlâ `useCurrentPeriod`). Defter kapalı dönemde salt okunur (form yok, `EntryList readOnly`); hafta değişimi `donem`'i korur. Plan: düzenleme `isWritable`, kopyalama yalnız açık. `PeriodHeading` durum rozeti. Değerlendirme sonu `ClosingSection` (açık → Kapat · düzeltmede → Defter'de/Plan'da düzelt + Yeniden Kapat · kapalı → kapanış bilgisi + **sahip** için satır içi onaylı "Düzeltme için yeniden aç") + `PeriodHistory`. `useReopenPeriod`; `useUpdatePeriod` liste önbelleğini de günceller (seçili dönem listeden → art arda kayıtta 409 olmasın). **Ders:** başarıda bileşen kalkıyorsa `mutate(…, {onSuccess})` çalışmaz → `mutateAsync` kullan. Frontend 92 test; e2e 36 + kapatma 33 yeniden (DB silindi). Faz 6 ✅.
|
||||||
|
|
||||||
|
### 2026-11-12 — Faz 7
|
||||||
|
- Kararlar (kullanıcı onayı): toplam birikim Faz 8'e kadar = kapanışlarda birikime aktarılanların toplamı; sekmeler Günlük/Haftalık = bu dönem (kümülatif, harcanabilir günlere eşit dağıtılır), Aylık = son 12 dönem; açık dönem yoksa son dönem salt okunur.
|
||||||
|
- **Adım 1:** `services/stats.rs` + `routes/stats.rs`: `GET /periods/{id}/daily` (`DaySpent[]`: dönemin her günü, `spent` = `GroupAmounts`, harcama yoksa 0; `budget::spent_by_group_and_date` artık `pub(crate)`), `GET /stats/periods?limit=12` (`PeriodStats`: `total_saved` tüm dönemlerin `to_savings` toplamı — limitten bağımsız, yeniden açılan dönemin eski tutarı sayılır; `periods` eskiden yeniye, her biri `budget::summary` ile → dönem başına ~5 sorgu, en fazla 24). `tests/stats.rs` (2). Mutasyon 2/2 yakalandı. Backend 118 test.
|
||||||
|
|||||||
+6
-1
@@ -11,7 +11,7 @@
|
|||||||
| **Faz 4** | Dönem açma + aylık planlama + notlar | Plan ekranı | ✅ |
|
| **Faz 4** | Dönem açma + aylık planlama + notlar | Plan ekranı | ✅ |
|
||||||
| **Faz 5** | Günlük defter | Harcama girişi | 🔄 |
|
| **Faz 5** | Günlük defter | Harcama girişi | 🔄 |
|
||||||
| **Faz 6** | Ay sonu değerlendirmesi + dönem kapatma/devir sihirbazı | Tam dönem döngüsü | ✅ |
|
| **Faz 6** | Ay sonu değerlendirmesi + dönem kapatma/devir sihirbazı | Tam dönem döngüsü | ✅ |
|
||||||
| **Faz 7** | Dashboard: özet kartları ve grafikler | Ana ekran | ⏳ |
|
| **Faz 7** | Dashboard: özet kartları ve grafikler | Ana ekran | 🔄 |
|
||||||
| **Faz 8** | Birikimler/varlıklar, kur entegrasyonu, kâr/zarar | Birikim modülü | ⏳ |
|
| **Faz 8** | Birikimler/varlıklar, kur entegrasyonu, kâr/zarar | Birikim modülü | ⏳ |
|
||||||
| **Faz 9** | Raporlama + JSON/CSV içe–dışa aktarma + aylık değerlendirme çıktısı | Raporlar | ⏳ |
|
| **Faz 9** | Raporlama + JSON/CSV içe–dışa aktarma + aylık değerlendirme çıktısı | Raporlar | ⏳ |
|
||||||
| **Faz 10** | PWA, performans, erişilebilirlik, UI cilası | Telefona kurulabilir web uygulaması | ⏳ |
|
| **Faz 10** | PWA, performans, erişilebilirlik, UI cilası | Telefona kurulabilir web uygulaması | ⏳ |
|
||||||
@@ -66,3 +66,8 @@
|
|||||||
- ✅ 3. Frontend: Değerlendirme sayfası (4 soru, planlanan/gerçekleşen, en çok harcanan kategoriler, haftalık dağılım, "Nasıl daha iyi yapabilirim?", notlar)
|
- ✅ 3. Frontend: Değerlendirme sayfası (4 soru, planlanan/gerçekleşen, en çok harcanan kategoriler, haftalık dağılım, "Nasıl daha iyi yapabilirim?", notlar)
|
||||||
- ✅ 4. Frontend: dönem kapatma sihirbazı (+ yeni dönemi kopyalayarak açma)
|
- ✅ 4. Frontend: dönem kapatma sihirbazı (+ yeni dönemi kopyalayarak açma)
|
||||||
- ✅ 5. Frontend: kapalı dönemler listesi + yeniden açma (sahip) + düzeltmedeki dönemi Defter/Plan/Değerlendirme'de seçme
|
- ✅ 5. Frontend: kapalı dönemler listesi + yeniden açma (sahip) + düzeltmedeki dönemi Defter/Plan/Değerlendirme'de seçme
|
||||||
|
|
||||||
|
## Faz 7 Adımları
|
||||||
|
- ✅ 1. Backend: `GET /periods/{id}/daily` (gün × grup harcama; günlük/haftalık grafik verisi) + `GET /stats/periods` (son 12 dönem: harcanabilir, harcanan, birikime aktarılan; aylık grafik ve toplam birikim)
|
||||||
|
- ⏳ 2. Frontend: özet kartları (gelir, sabit gider, toplam birikim, devreden borç, grup planlanan/gerçekleşen haftalık–aylık, **anlık durum** en belirgin) + "➕ Harcama Ekle" (mobilde yüzen düğme) + skeleton ve boş durumlar
|
||||||
|
- ⏳ 3. Frontend: grafikler (Recharts) — kümülatif harcanabilir vs. gerçekleşen; 4 grup + harcanabilir; Günlük / Haftalık / Aylık sekmeleri, koyu tema, erişilebilir özet
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { GroupAmounts } from "./GroupAmounts";
|
||||||
|
|
||||||
|
export type DaySpent = {
|
||||||
|
/**
|
||||||
|
* `"YYYY-MM-DD"`
|
||||||
|
*/
|
||||||
|
date: string,
|
||||||
|
/**
|
||||||
|
* O gün gruplara göre harcanan (kuruş)
|
||||||
|
*/
|
||||||
|
spent: GroupAmounts, };
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { PeriodStatus } from "./PeriodStatus";
|
||||||
|
|
||||||
|
export type PeriodStat = { id: string, name: string, start_date: string,
|
||||||
|
/**
|
||||||
|
* Son gün, dahil
|
||||||
|
*/
|
||||||
|
last_date: string, status: PeriodStatus, spendable: number, spent: number,
|
||||||
|
/**
|
||||||
|
* Kapanmamışsa `null`
|
||||||
|
*/
|
||||||
|
to_savings: number | null, };
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { PeriodStat } from "./PeriodStat";
|
||||||
|
|
||||||
|
export type PeriodStats = {
|
||||||
|
/**
|
||||||
|
* Kapanışlarda birikime aktarılanların toplamı (kuruş)
|
||||||
|
*/
|
||||||
|
total_saved: number,
|
||||||
|
/**
|
||||||
|
* Eskiden yeniye
|
||||||
|
*/
|
||||||
|
periods: Array<PeriodStat>, };
|
||||||
Reference in New Issue
Block a user