feat(değerlendirme): ay sonu değerlendirmesi — 4 soru ve döküm (backend)
- GET /periods/{id}/review: param, hedef, harcanan, biriken; grup, kategori ve hafta bazında planlanan/gerçekleşen
- Dönemde "Nasıl daha iyi yapabilirim?" metni (reflection_md)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ mod household;
|
||||
mod note;
|
||||
mod period;
|
||||
mod plan;
|
||||
mod review;
|
||||
mod spa;
|
||||
|
||||
use axum::{
|
||||
@@ -78,6 +79,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/plan-items/{id}", patch(plan::update).delete(plan::delete))
|
||||
.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}/budget",
|
||||
get(budget::get).put(budget::set_monthly),
|
||||
|
||||
@@ -54,6 +54,8 @@ pub struct UpdateRequest {
|
||||
end_date: Option<String>,
|
||||
#[ts(type = "number", optional)]
|
||||
savings_goal: Option<i64>,
|
||||
/// "Nasıl daha iyi yapabilirim?" (Markdown); boş metin → kaldır
|
||||
reflection_md: Option<String>,
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -73,6 +75,8 @@ pub struct PeriodDto {
|
||||
/// Kuruş
|
||||
#[ts(type = "number")]
|
||||
savings_goal: i64,
|
||||
/// "Nasıl daha iyi yapabilirim?" (Markdown)
|
||||
reflection_md: Option<String>,
|
||||
version: i32,
|
||||
weeks: Vec<WeekDto>,
|
||||
}
|
||||
@@ -102,6 +106,7 @@ impl From<Period> for PeriodDto {
|
||||
end_date: p.end_date,
|
||||
status: p.status,
|
||||
savings_goal: p.savings_goal,
|
||||
reflection_md: p.reflection_md,
|
||||
version: p.version,
|
||||
weeks: weeks(start, end)
|
||||
.into_iter()
|
||||
@@ -178,6 +183,7 @@ pub async fn update(
|
||||
start_date: req.start_date,
|
||||
end_date: req.end_date,
|
||||
savings_goal: req.savings_goal,
|
||||
reflection_md: req.reflection_md,
|
||||
version: req.version,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Ay sonu değerlendirmesi:
|
||||
//!
|
||||
//! | Yöntem | Yol | İş |
|
||||
//! |---|---|---|
|
||||
//! | GET | `/api/periods/{id}/review` | 4 soru, grup/kategori/hafta bazında planlanan–gerçekleşen |
|
||||
//!
|
||||
//! "Nasıl daha iyi yapabilirim?" metni: `PATCH /api/periods/{id}` → `reflection_md`.
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use mongodb::bson::Uuid;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
error::{AppError, AppResult},
|
||||
models::group::Group,
|
||||
services::review,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "PeriodReview")]
|
||||
pub struct ReviewDto {
|
||||
#[ts(type = "number")]
|
||||
income_total: i64,
|
||||
#[ts(type = "number")]
|
||||
fixed_expense_total: i64,
|
||||
#[ts(type = "number")]
|
||||
carryover_debt: i64,
|
||||
#[ts(type = "number")]
|
||||
carryover_surplus: i64,
|
||||
/// 1. Ne kadar param vardı? (gelir − sabit gider − borç + artı)
|
||||
#[ts(type = "number")]
|
||||
available: i64,
|
||||
/// 2. Ne kadar biriktirmek istedim?
|
||||
#[ts(type = "number")]
|
||||
savings_goal: i64,
|
||||
/// 3. Ne kadar harcadım?
|
||||
#[ts(type = "number")]
|
||||
spent_total: i64,
|
||||
/// 4. Ne kadar biriktirdim? (negatifse açık var)
|
||||
#[ts(type = "number")]
|
||||
realized_savings: i64,
|
||||
/// Gerçekleşen − hedef birikim (negatifse hedefin altında)
|
||||
#[ts(type = "number")]
|
||||
savings_gap: i64,
|
||||
/// 4 grup, sabit sırada
|
||||
groups: Vec<ReviewGroupDto>,
|
||||
/// Harcama olan kategoriler, çoktan aza
|
||||
categories: Vec<ReviewCategoryDto>,
|
||||
/// Haftalar: 4 grubun toplamı
|
||||
weeks: Vec<ReviewWeekDto>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReviewGroup")]
|
||||
struct ReviewGroupDto {
|
||||
group: Group,
|
||||
/// Aylık bütçe
|
||||
#[ts(type = "number")]
|
||||
planned: i64,
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReviewCategory")]
|
||||
struct ReviewCategoryDto {
|
||||
category_id: String,
|
||||
group: Group,
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
/// Kayıt sayısı
|
||||
#[ts(type = "number")]
|
||||
count: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "ReviewWeek")]
|
||||
struct ReviewWeekDto {
|
||||
index: u32,
|
||||
start_date: String,
|
||||
last_date: String,
|
||||
#[ts(type = "number")]
|
||||
planned: i64,
|
||||
#[ts(type = "number")]
|
||||
spent: i64,
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<Json<ReviewDto>> {
|
||||
let id = Uuid::parse_str(&id).map_err(|_| AppError::NotFound)?;
|
||||
let r = review::review(&s.db, auth.household_id, id).await?;
|
||||
|
||||
// Haftalar: her grubun aynı sıradaki haftası toplanır (hepsi aynı takvimden gelir)
|
||||
let weeks = r
|
||||
.groups
|
||||
.first()
|
||||
.map(|g| {
|
||||
g.weeks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, w)| ReviewWeekDto {
|
||||
index: w.index,
|
||||
start_date: w.start_date.clone(),
|
||||
last_date: w.last_date.clone(),
|
||||
planned: r.groups.iter().map(|g| g.weeks[i].amount).sum(),
|
||||
spent: r.groups.iter().map(|g| g.weeks[i].spent).sum(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Json(ReviewDto {
|
||||
income_total: r.summary.income_total,
|
||||
fixed_expense_total: r.summary.fixed_expense_total,
|
||||
carryover_debt: r.summary.carryover_debt,
|
||||
carryover_surplus: r.summary.carryover_surplus,
|
||||
available: r.available,
|
||||
savings_goal: r.summary.savings_goal,
|
||||
spent_total: r.summary.spent_total,
|
||||
realized_savings: r.realized_savings,
|
||||
savings_gap: r.realized_savings - r.summary.savings_goal,
|
||||
groups: r
|
||||
.groups
|
||||
.iter()
|
||||
.map(|g| ReviewGroupDto {
|
||||
group: g.group,
|
||||
planned: g.monthly,
|
||||
spent: g.spent,
|
||||
})
|
||||
.collect(),
|
||||
categories: r
|
||||
.categories
|
||||
.into_iter()
|
||||
.map(|c| ReviewCategoryDto {
|
||||
category_id: c.category_id.to_string(),
|
||||
group: c.group,
|
||||
spent: c.spent,
|
||||
count: c.count,
|
||||
})
|
||||
.collect(),
|
||||
weeks,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user