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:
@@ -11,6 +11,7 @@ mod period;
|
||||
mod plan;
|
||||
mod review;
|
||||
mod spa;
|
||||
mod stats;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
@@ -82,6 +83,8 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/periods/{id}/notes", get(note::list).post(note::create))
|
||||
.route("/periods/{id}/summary", get(budget::summary))
|
||||
.route("/periods/{id}/review", get(review::get))
|
||||
.route("/periods/{id}/daily", get(stats::daily))
|
||||
.route("/stats/periods", get(stats::periods))
|
||||
.route(
|
||||
"/periods/{id}/budget",
|
||||
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.
|
||||
async fn spent_by_group_and_date(
|
||||
pub(crate) async fn spent_by_group_and_date(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
period_id: Uuid,
|
||||
|
||||
@@ -13,3 +13,4 @@ pub mod period;
|
||||
pub mod plan;
|
||||
pub mod review;
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user