feat(raporlar): harcama kırılımı uç noktası ve dönem istatistiklerinde birikim oranı verileri
- GET /reports/breakdown: grup, tüm kategoriler, ödeme türü ve ay kırılımı; dönem/tarih/grup/kategori/ödeme türü filtreleri - /stats/periods: gelir, sabit gider, devirler, gerçekleşen birikim - Faz 9 planı kullanıcı kararlarıyla güncellendi (ekstre içe aktarma, JSON geri dönüş noktası, aylık rapor) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ mod note;
|
||||
mod period;
|
||||
mod plan;
|
||||
mod rates;
|
||||
mod reports;
|
||||
mod review;
|
||||
mod spa;
|
||||
mod stats;
|
||||
@@ -87,6 +88,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/periods/{id}/review", get(review::get))
|
||||
.route("/periods/{id}/daily", get(stats::daily))
|
||||
.route("/stats/periods", get(stats::periods))
|
||||
.route("/reports/breakdown", get(reports::breakdown))
|
||||
.route(
|
||||
"/periods/{id}/budget",
|
||||
get(budget::get).put(budget::set_monthly),
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//! Rapor uç noktaları (hanenin tüm üyeleri):
|
||||
//!
|
||||
//! | Yöntem | Yol | İş |
|
||||
//! |---|---|---|
|
||||
//! | GET | `/api/reports/breakdown?period_id=&from=&to=&group=&category_id=&payment_method_id=` | harcama: toplam, grup, tüm kategoriler, ödeme türü, ay |
|
||||
//!
|
||||
//! `payment_method_id=none` → ödeme türü girilmemiş kayıtlar. Dönemler arası özet: `/api/stats/periods`.
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
};
|
||||
use mongodb::bson::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
error::{AppError, AppResult},
|
||||
models::group::Group,
|
||||
services::reports::{self, Filter},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BreakdownQuery {
|
||||
period_id: Option<String>,
|
||||
from: Option<String>,
|
||||
to: Option<String>,
|
||||
group: Option<String>,
|
||||
category_id: Option<String>,
|
||||
payment_method_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReportBreakdown")]
|
||||
pub struct BreakdownDto {
|
||||
/// Kuruş
|
||||
#[ts(type = "number")]
|
||||
total: i64,
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
/// 4 grup, sabit sırada
|
||||
groups: Vec<GroupRow>,
|
||||
/// Harcama olan **tüm** kategoriler, çoktan aza
|
||||
categories: Vec<CategoryRow>,
|
||||
/// Ödeme türleri; `null` = girilmemiş
|
||||
payment_methods: Vec<MethodRow>,
|
||||
/// Aylar (`"YYYY-MM"`), eskiden yeniye
|
||||
months: Vec<MonthRow>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReportGroupRow")]
|
||||
struct GroupRow {
|
||||
group: Group,
|
||||
#[ts(type = "number")]
|
||||
total: i64,
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReportCategoryRow")]
|
||||
struct CategoryRow {
|
||||
category_id: String,
|
||||
group: Group,
|
||||
#[ts(type = "number")]
|
||||
total: i64,
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReportMethodRow")]
|
||||
struct MethodRow {
|
||||
payment_method_id: Option<String>,
|
||||
#[ts(type = "number")]
|
||||
total: i64,
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReportMonthRow")]
|
||||
struct MonthRow {
|
||||
/// `"YYYY-MM"`
|
||||
month: String,
|
||||
#[ts(type = "number")]
|
||||
total: i64,
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
}
|
||||
|
||||
fn opt_id(v: Option<&str>, what: &str) -> AppResult<Option<Uuid>> {
|
||||
match v.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
None => Ok(None),
|
||||
Some(s) => Uuid::parse_str(s)
|
||||
.map(Some)
|
||||
.map_err(|_| AppError::BadRequest(format!("{what} geçersiz"))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn breakdown(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Query(q): Query<BreakdownQuery>,
|
||||
) -> AppResult<Json<BreakdownDto>> {
|
||||
let group = match q.group.as_deref().map(str::trim).filter(|g| !g.is_empty()) {
|
||||
None => None,
|
||||
Some(g) => Some(
|
||||
Group::ALL
|
||||
.into_iter()
|
||||
.find(|x| x.as_str() == g)
|
||||
.ok_or_else(|| AppError::BadRequest("Grup geçersiz".into()))?,
|
||||
),
|
||||
};
|
||||
let payment_method = match q.payment_method_id.as_deref().map(str::trim) {
|
||||
None | Some("") => None,
|
||||
Some("none") => Some(None),
|
||||
Some(id) => Some(opt_id(Some(id), "Ödeme türü")?),
|
||||
};
|
||||
let filter = Filter {
|
||||
period_id: opt_id(q.period_id.as_deref(), "Dönem")?,
|
||||
from: q.from.filter(|s| !s.trim().is_empty()),
|
||||
to: q.to.filter(|s| !s.trim().is_empty()),
|
||||
group,
|
||||
category_id: opt_id(q.category_id.as_deref(), "Kategori")?,
|
||||
payment_method,
|
||||
};
|
||||
let b = reports::breakdown(&s.db, auth.household_id, filter).await?;
|
||||
Ok(Json(BreakdownDto {
|
||||
total: b.total,
|
||||
count: b.count,
|
||||
groups: b
|
||||
.groups
|
||||
.into_iter()
|
||||
.map(|r| GroupRow {
|
||||
group: r.key,
|
||||
total: r.total,
|
||||
count: r.count,
|
||||
})
|
||||
.collect(),
|
||||
categories: b
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|r| CategoryRow {
|
||||
category_id: r.key.category_id.to_string(),
|
||||
group: r.key.group,
|
||||
total: r.total,
|
||||
count: r.count,
|
||||
})
|
||||
.collect(),
|
||||
payment_methods: b
|
||||
.methods
|
||||
.into_iter()
|
||||
.map(|r| MethodRow {
|
||||
payment_method_id: r.key.map(|u| u.to_string()),
|
||||
total: r.total,
|
||||
count: r.count,
|
||||
})
|
||||
.collect(),
|
||||
months: b
|
||||
.months
|
||||
.into_iter()
|
||||
.map(|r| MonthRow {
|
||||
month: r.key,
|
||||
total: r.total,
|
||||
count: r.count,
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//! | 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 |
|
||||
//! | GET | `/api/stats/periods?limit=12` | son dönemler (eskiden yeniye): gelir, sabit gider, devirler, harcanabilir, harcanan, gerçekleşen birikim, birikime aktarılan + toplam birikim |
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -76,11 +76,24 @@ struct PeriodStatDto {
|
||||
last_date: String,
|
||||
status: PeriodStatus,
|
||||
#[ts(type = "number")]
|
||||
income_total: i64,
|
||||
#[ts(type = "number")]
|
||||
fixed_expense_total: i64,
|
||||
#[ts(type = "number")]
|
||||
savings_goal: i64,
|
||||
#[ts(type = "number")]
|
||||
carryover_debt: i64,
|
||||
#[ts(type = "number")]
|
||||
carryover_surplus: i64,
|
||||
#[ts(type = "number")]
|
||||
spendable: i64,
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
/// Harcanan, gruplara göre
|
||||
spent_by_group: GroupAmounts,
|
||||
/// Gelir − sabit − borç + artı − harcanan (negatifse açık). Birikim oranı = bu ÷ gelir
|
||||
#[ts(type = "number")]
|
||||
realized_savings: i64,
|
||||
/// Kapanmamışsa `null`
|
||||
#[ts(type = "number | null")]
|
||||
to_savings: Option<i64>,
|
||||
@@ -100,14 +113,20 @@ pub async fn periods(
|
||||
.map(|x| {
|
||||
let end = parse_date(&x.period.end_date).expect("geçerli bitiş tarihi");
|
||||
PeriodStatDto {
|
||||
realized_savings: x.realized_savings(),
|
||||
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,
|
||||
spent_by_group: x.spent_by_group.into(),
|
||||
income_total: x.summary.income_total,
|
||||
fixed_expense_total: x.summary.fixed_expense_total,
|
||||
savings_goal: x.summary.savings_goal,
|
||||
carryover_debt: x.summary.carryover_debt,
|
||||
carryover_surplus: x.summary.carryover_surplus,
|
||||
spendable: x.summary.spendable,
|
||||
spent: x.summary.spent_total,
|
||||
spent_by_group: x.summary.spent_by_group.into(),
|
||||
to_savings: x.period.to_savings,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user