feat(rapor): aylık değerlendirme raporu backend (şablon, yorumlar, öneriler)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 21:36:43 +03:00
parent b974525cf6
commit dc0d783e46
14 changed files with 1772 additions and 3 deletions
+1
View File
@@ -82,6 +82,7 @@ npm run db:restore -- <dosya|latest> <hedef_db> [--force]
- Excel okuma: SheetJS **yerel paket** `frontend/vendor/xlsx-0.20.3.tgz` (npm 12 uzak tarball'u engeller; npm'deki `xlsx` eski/açıklı). Güncellerken: cdn.sheetjs.com'dan indir, içeriği/sha256'yı kontrol et, `npm i ./vendor/…`; Dockerfile `vendor/`'u `npm ci`'den önce kopyalar. Lazy yüklenir.
- Biçim: Prettier (`frontend/.prettierrc.json`: tek tırnak, `;` yok, 100 sütun); `npm --prefix frontend run format`. `src/types/` ve `components/ui/` hariç.
- JSON yedeği: `services/backup.rs` (`collect` → `to_json`; `parse` doğrular; `restore` = geri dönüş noktası + tam değiştirme tek transaction; `retarget` kimlik çevirir). Yeni hane koleksiyonu eklenirse `backup::KINDS`'a da ekle. Biçim: DATA_MODEL §3.14b. Arayüz: `features/settings/backup-section.tsx`, `lib/backup.ts` (geri yükleme/geri alma sonrası tüm sorgular yenilenir).
- Aylık rapor: `services/monthly_report.rs` (`gather` → `Facts` → saf `analyze` → `render`), şablon `templates/monthly_report.md` (`include_str!`; Dockerfile `templates/`'u kopyalar). Filtreler `tl`/`pct`/`cell`. Cümlelerde sayıdan sonra ek kullanma ("%429'u" gibi ünlü uyumu gerekir) → "X / Y", "kullanımı %N".
- Hane izolasyonu: hane kimliği yalnızca `AuthUser.household_id`'den; sorgular `scope(hid)` ile başlar; başka hane → 404.
## Ortam
+2
View File
@@ -20,6 +20,8 @@ COPY backend/Cargo.toml backend/Cargo.lock ./
RUN mkdir src && echo 'fn main() {}' > src/main.rs && touch src/lib.rs \
&& cargo build --release --locked \
&& rm -rf src target/release/deps/*kakeibo* target/release/.fingerprint/kakeibo-* target/release/kakeibo*
# Aylık rapor şablonu derleme anında ikiliye gömülür (include_str!)
COPY templates/ /app/templates/
COPY backend/src ./src
# touch: sahte lib.rs/main.rs'ten daha yeni olsun → Cargo gerçek kodu kesinlikle yeniden derler
RUN touch src/lib.rs src/main.rs && cargo build --release --locked
+10
View File
@@ -1428,6 +1428,7 @@ dependencies = [
"flate2",
"http-body-util",
"jsonwebtoken",
"minijinja",
"mongodb",
"quick-xml",
"reqwest",
@@ -1589,6 +1590,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "minijinja"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509eeb2603a84eccabb4d75f6ec8ba23b46bbf441aaeb863d6b83a5887fd51e4"
dependencies = [
"serde",
]
[[package]]
name = "miniz_oxide"
version = "0.9.1"
+1
View File
@@ -10,6 +10,7 @@ axum-extra = { version = "0.12.6", features = ["cookie"] }
dotenvy = "0.15.7"
flate2 = "1"
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["rust_crypto"] }
minijinja = { version = "3.1.0", features = ["serde"] }
mongodb = "3.10.0"
quick-xml = { version = "0.42.0", features = ["serialize"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
+5
View File
@@ -420,3 +420,8 @@ Content-Type: application/json
### Geri al: geri dönüş noktasına dön (bu da yeni bir nokta bırakır)
POST {{baseUrl}}/backup/restore-points/{{pointId}}/restore
# ─────────────────────────── Aylık değerlendirme raporu ───────────────────────────
### Dönemin raporu (Markdown; açık dönem → ara rapor)
GET {{baseUrl}}/periods/{{periodId}}/report.md
+2
View File
@@ -9,6 +9,7 @@ mod catalog;
mod entry;
mod health;
mod household;
mod monthly_report;
mod note;
mod period;
mod plan;
@@ -90,6 +91,7 @@ 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}/report.md", get(monthly_report::get))
.route("/periods/{id}/daily", get(stats::daily))
.route("/stats/periods", get(stats::periods))
.route("/reports/breakdown", get(reports::breakdown))
+44
View File
@@ -0,0 +1,44 @@
//! Aylık değerlendirme raporu (`services::monthly_report`, şablon `templates/monthly_report.md`).
//!
//! | Yöntem | Yol | İş |
//! |---|---|---|
//! | GET | `/api/periods/{id}/report.md` | dönemin raporu (Markdown); açık dönem → ara rapor |
use axum::{
extract::{Path, State},
http::header,
response::IntoResponse,
};
use mongodb::bson::Uuid;
use crate::{
auth::AuthUser,
error::{AppError, AppResult},
services::{backup::slug, monthly_report, period},
state::AppState,
};
pub async fn get(
State(s): State<AppState>,
auth: AuthUser,
Path(id): Path<String>,
) -> AppResult<impl IntoResponse> {
let id = Uuid::parse_str(&id).map_err(|_| AppError::NotFound)?;
let p = period::get(&s.db, auth.household_id, id).await?;
let body = monthly_report::markdown(&s.db, auth.household_id, id).await?;
let file = format!("kakeibo-rapor-{}.md", slug(&p.name));
Ok((
[
(
header::CONTENT_TYPE,
"text/markdown; charset=utf-8".to_string(),
),
(
header::CONTENT_DISPOSITION,
format!("inline; filename=\"{file}\""),
),
(header::CACHE_CONTROL, "no-store".to_string()),
],
body,
))
}
+1
View File
@@ -13,6 +13,7 @@ pub mod entry;
pub mod holding;
pub mod invite;
pub mod money;
pub mod monthly_report;
pub mod note;
pub mod period;
pub mod plan;
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -358,7 +358,13 @@ async fn bad_files_and_permissions() {
let newer = file.replacen("\"schema_version\": ", "\"schema_version\": 99", 1);
let (st, b) = app.post_text("/backup/preview", &owner, &newer).await;
assert_eq!(st, StatusCode::BAD_REQUEST);
assert!(b["error"]["message"].as_str().unwrap().contains("daha yeni"), "{b}");
assert!(
b["error"]["message"]
.as_str()
.unwrap()
.contains("daha yeni"),
"{b}"
);
assert_eq!(view(&app, &owner, &nov).await, before);
assert!(points(&app, &owner).await.is_empty());
+156
View File
@@ -0,0 +1,156 @@
//! Aylık değerlendirme raporu: kapalı dönem (önceki dönemle karşılaştırma, notlar, devir),
//! ara rapor, hane yalıtımı.
mod common;
use axum::http::StatusCode;
use common::{Session, TestApp};
use serde_json::json;
async fn id_by(app: &TestApp, s: &Session, name: &str) -> String {
let (_, list) = app.get("/categories", s).await;
list.as_array()
.unwrap()
.iter()
.find(|c| c["name"] == name)
.unwrap()["id"]
.as_str()
.unwrap()
.to_string()
}
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}");
let id = p["id"].as_str().unwrap().to_string();
app.post(
&format!("/periods/{id}/plan-items"),
s,
json!({ "kind": "income", "name": "Maaş", "amount": 1_000_000 }),
)
.await;
id
}
async fn spend(app: &TestApp, s: &Session, pid: &str, date: &str, amount: i64, cat: &str) {
let (st, b) = app
.post(
&format!("/periods/{pid}/entries"),
s,
json!({ "date": date, "amount": amount, "category_id": cat }),
)
.await;
assert_eq!(st, StatusCode::CREATED, "{b}");
}
async fn report(app: &TestApp, s: &Session, pid: &str) -> (StatusCode, String, String) {
app.get_text(&format!("/periods/{pid}/report.md"), s).await
}
#[tokio::test]
async fn closed_period_report_with_comparison_notes_and_carry() {
let app = TestApp::new().await;
let s = app.register("Sahip", None).await;
let market = id_by(&app, &s, "Market").await;
let yemek = id_by(&app, &s, "Dışarıda yemek").await;
// Ekim: 10.000 gelir, 6.000 harcama (Market 3.000, yemek 3.000) → kapalı
let oct = open(&app, &s, "2026-10-01", "2026-11-01", 200_000).await;
spend(&app, &s, &oct, "2026-10-05", 300_000, &market).await;
spend(&app, &s, &oct, "2026-10-06", 300_000, &yemek).await;
app.post(&format!("/periods/{oct}/close"), &s, json!({}))
.await;
// Kasım: hedef 3.000; bütçe Zorunlu 4.000 / İstek 1.000; Market 5.000 (+%67), yemek 1.000
let nov = open(&app, &s, "2026-11-01", "2026-12-01", 300_000).await;
app.put(
&format!("/periods/{nov}/budget"),
&s,
json!({ "needs": 400_000, "wants": 100_000, "culture": 0, "unexpected": 0 }),
)
.await;
spend(&app, &s, &nov, "2026-11-17", 500_000, &market).await;
spend(&app, &s, &nov, "2026-11-03", 100_000, &yemek).await;
app.post(
&format!("/periods/{nov}/notes"),
&s,
json!({ "context": "planning", "title": "Bu ayın niyeti", "body_md": "Dışarıda **az** ye." }),
)
.await;
let (_, p) = app.get(&format!("/periods/{nov}"), &s).await;
app.patch(
&format!("/periods/{nov}"),
&s,
json!({ "reflection_md": "Market listesiyle çık.", "version": p["version"] }),
)
.await;
// Açıkken: ara rapor
let (st, ct, md) = report(&app, &s, &nov).await;
assert_eq!(st, StatusCode::OK, "{md}");
assert!(ct.starts_with("text/markdown"), "{ct}");
assert!(md.starts_with("# Kasım 2026 — Aylık Değerlendirme"), "{md}");
// Testin çalıştığı güne göre Kasım 2026 sürüyor ya da bitmiş olabilir
assert_eq!(md.contains("· Sürüyor ("), md.contains("> **Ara rapor:**"));
let (st, b) = app
.post(
&format!("/periods/{nov}/close"),
&s,
json!({ "to_savings": 0 }),
)
.await;
assert_eq!(st, StatusCode::OK, "{b}");
let (st, _, md) = report(&app, &s, &nov).await;
assert_eq!(st, StatusCode::OK);
println!("{md}");
// Başlık ve karşılaştırma
assert!(md.contains("· Kapandı "));
// Önceki dönemle aynı birikim (4.000) → karşılaştırma cümlesi eklenmez
assert!(md.contains(
"> Birikim hedefin 3.000,00 ₺ idi; 4.000,00 ₺ biriktirdin (%133). Hedefe ulaştın.\n"
));
assert!(md.contains("- İstek bütçenin içinde kaldı.\n"));
assert!(md.contains("- Harcama yapmadığın gün: 28 / 30; günlük ortalama 200,00 ₺."));
assert!(md.contains("haftalık bütçe kullanımı %429."));
assert!(md.contains("| Kakeibo'nun 4 sorusu | Bu dönem | Ekim 2026 |"));
assert!(md.contains("| Ne kadar harcadım? | 6.000,00 ₺ | 6.000,00 ₺ |"));
// Gruplar, kategoriler (ortalamaya göre), haftalar
assert!(md.contains("| 🏠 Zorunlu | 4.000,00 ₺ | 5.000,00 ₺ | -1.000,00 ₺ | ❌ %125 |"));
assert!(
md.contains("| 🛒 Market | 5.000,00 ₺ | 1 | %83 | +%67 (+2.000,00 ₺) |"),
"{md}"
);
assert!(md.contains("| 3. hafta | 16.11 – 22.11 |"));
// Yorumlar
assert!(md.contains("- Zorunlu grubu bütçeyi 1.000,00 ₺ aştı (%125)."));
assert!(md.contains("- Market, ortalamasının %67 üstünde (+2.000,00 ₺); 1 işlem."));
assert!(md.contains("- Dışarıda yemek harcaman ortalamanın %67 altında (-2.000,00 ₺)."));
assert!(md.contains("- Zorunlu bütçede kalsaydı birikimin 5.000,00 ₺ olurdu (+1.000,00 ₺)."));
// Notlar
assert!(md.contains("#### Bu ayın niyeti\n\nDışarıda **az** ye."));
assert!(md.contains("#### Nasıl daha iyi yapabilirim?\n\nMarket listesiyle çık."));
// Öneri: Zorunlu (5.000 + 3.000) / 2 = 4.000
assert!(md.contains("| 🏠 Zorunlu | 4.000,00 ₺ | son 2 dönemin ortalaması |"));
assert!(md.contains("1. Market için haftalık 750,00 ₺ sınır koy"));
// Devir: 4.000 birikim, 0'ı aktarıldı → artı devreder
assert!(md.contains("**Devir:** gelecek döneme 4.000,00 ₺ artı devrediyor"));
assert!(md.contains("## Ek: Tüm kategoriler"));
assert!(!md.contains("{{") && !md.contains("{%"));
// Başka hane göremez; geçersiz kimlik → 404
let other = app.register("Diger", None).await;
let (st, _, _) = report(&app, &other, &nov).await;
assert_eq!(st, StatusCode::NOT_FOUND);
let (st, _, _) = report(&app, &s, "yok").await;
assert_eq!(st, StatusCode::NOT_FOUND);
app.cleanup().await;
}
+3 -1
View File
@@ -1,10 +1,11 @@
# 🧠 Proje Hafızası
## ▶️ Sıradaki Adım
**Faz 9 – Adım 6:** Aylık değerlendirme çıktısı — `templates/monthly_report.md`, Claude tasarlar (müşteri gözüyle: ne yaptım / ne iyi-kötü gitti / ne yapılabilirdi / ne yapmalıydım / gelecek ay önerileri). Önce taslak yapıyı kullanıcıya sun (bölümler, hangi veriden), sonra Markdown üretimi + yazdır/PDF.
**Faz 9 – Adım 6b:** Rapor arayüzü — Değerlendirme → "Aylık rapor" (`/degerlendirme/rapor?donem=`): `GET /periods/{id}/report.md` metnini `react-markdown` + `remark-gfm` ile göster (tablolar, görev kutuları), Yazdır/PDF (A4, açık tema, `print:` sınıfları, tablolar sayfa arasında bölünmesin, `<small>` dipnot), "Markdown indir" (Blob, dosya adı `kakeibo-rapor-<dönem>.md`), dönem seçici; kapatma sihirbazı sonucu + dönem geçmişinden "Raporu aç". Sonra Faz 9 ✅ → Faz 10.
**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
- **Aylık rapor tasarımı (kullanıcı onayı, 2026-11-13):** 7 bölüm — Bir bakışta (4 soru + önceki dönem) · Ne yaptım (gruplar ✅≤%90/⚠️≤%100/❌, ilk 5 kategori + ortalamaya göre, haftalar, ödeme türü, birikim) · İyi gitti · Zorladı · Ne yapılabilirdi ("olsaydı" hesapları) · Kendi notların (plan notları + yansıtma, yorumsuz) · Gelecek dönem (önerilen bütçe = son ≤4 dönem ortalaması 50 ₺ yukarı, ≤3 somut adım, devir, boş hedef kutuları) + Ek: tüm kategoriler. Eşik: kategori %20 **ve** 250 ₺. Karşılaştırma: önceki ≤3 kapalı/arşiv dönem. "Sen" dili. Açık dönem → ara rapor (hedef yargısı yok, bitmemiş hafta yok).
- **Hafta kuralı değişti (kullanıcı, 2026-11-13):** Pazartesi başlangıçlı haftalar yerine dönemin 1–8, 9–15, 16–22, 23–son günleri (ay = 4 hafta). Kasım 2026'da 6 hafta (01.11 tek gün) çıkması üzerine. Genel: `max(1, gün/7)` hafta, ilk 8 gün, ara 7, son kalan (6–12). Defter her açılışta bugünün haftası; hafta artık adrese yazılmaz (sekme geri yüklemede eski hafta açılıyordu).
- **Ekstre kararları (kullanıcı, 2026-11-13):** biçim Excel (.xlsx/.xls) + CSV (kullanıcı tam bilmiyor; ilk gerçek ekstreyle denenecek), hesaba gelenler → **gelir** (plan kalemi), arşiv dönemi **takvim ayı**, iadeler **aynı kategoriden düşülür** (eksi tutarlı kayıt).
- **Dikkat (2026-11-13):** Claude'un hızlı ardışık `backend/src` değişiklikleri (bilerek bozup geri alma, toplu düzenleme) kullanıcının `npm run dev` içindeki `cargo watch`'ını backend kapalı halde bırakabiliyor (giriş: "Beklenmeyen hata", 5173 → 502). Böyle denemelerden sonra `curl localhost:8080/api/health`; kapalıysa `touch backend/src/main.rs` (cargo watch yeniden başlatır). Arayüz artık 502/503/504'te "Sunucu şu an yanıt vermiyor" der.
@@ -144,3 +145,4 @@
- **Hafta düzeltmesi (2026-11-13):** `calendar::weeks` yeni kural (testler: Kasım/Aralık/Şubat/artık yıl, ay ortası dönem, 13/14/62 gün). Bütçe/defter/değerlendirme entegrasyon testleri yeni sınırlara göre güncellendi (backend 178). Frontend: `LedgerView` haftayı `useState`'te tutar, `parseWeekParam` kaldırıldı, dönem kısalırsa son haftaya düşer; `budget-preview`/`ledger` testleri yeni Aralık (8,7,7,9) ile (133). E2E 9 kontrol (8091, DB silindi). Dev verisinde elle haftalık bütçe yoktu → taşıma gerekmedi; canlıda varsa eşleşmeyen eski hafta yok sayılır (aylık tutar korunur).
- **Faz 9 – Adım 5a (2026-11-13):** `services/backup.rs` + `routes/backup.rs` (`/backup/download|status|preview|restore`, `/backup/restore-points/{id}/restore`; preview/restore/undo **sahip**, 20 MB). Biçim ham BSON belgeleri üzerinden (modeller eksik alanlı olduğu için tür tabanlı değil): `$uuid`/`$date`, `INT32_KEYS` (version, sort_order) + ayarlar i32 (`small_ints`), SHA-256 kanonik (sıralı anahtar) → yeniden biçimlendirilmiş dosya geçerli, değiştirilmiş reddedilir. **Silinmiş kayıtlar dahil** (tasarımdan sapma: referans bütünlüğü + birebir geri al). Migration v3 `restore_points` (transaction içinde ilk yazım için) + indeks. `flate2`, `sha2` doğrudan eklendi (zaten bağımlılık ağacında). Testler: 4 birim + 3 entegrasyon (aynı hane + geri al zinciri + 3 nokta sınırı, başka haneye iki kez kopya, bozuk/yabancı/yeni sürüm dosya, üye 403, başka hane 404). 3 bilinçli bozma yakalandı. Backend 190 test.
- **Faz 9 – Adım 5b (2026-11-13):** `features/settings/backup-section.tsx` (Veri bölümünün en üstü): "Yedeği indir" (`<a download>`, tıklayınca 1,5 sn sonra durum yenilenir) + hatırlatma (`daysSince`, hiç yok ya da ≥ 30 gün → uyarı rengi); sahip: dosya → önizleme (kaynak, aynı/başka hane, dönem aralığı, sayılar yedek/şimdi — farklı olan uyarı rengi, uyarılar) → onay kutusu (`#backup-confirm`; Radix `name`'i düğmeye taşımaz) → "Geri yükle" (destructive) → sonuç kutusu + "Geri al"; geri dönüş noktaları listesi (`window.confirm`). Üye: yalnız indirme + "yalnız sahip" notu. `lib/backup.ts` (geri yükleme/geri alma → `qc.invalidateQueries()` tümü), `api.postJsonText`, `queryKeys.backup`, `t.backup`. Frontend 134 test; e2e 27 kontrol (8091, DB silindi).
- **Faz 9 – Adım 6a (2026-11-13):** `services/monthly_report.rs` + `routes/monthly_report.rs` (`GET /periods/{id}/report.md`, `text/markdown`), `templates/monthly_report.md` (minijinja 3.1, `serde` özelliği; trim/lstrip blocks; art arda boş satırlar sadeleştirilir). `minijinja` yeni bağımlılık (şablonda koşul/döngü). Dockerfile: `COPY templates/ /app/templates/` (`include_str!("../../../templates/…")`), `docker build --target api` geçti. Testler: 5 birim (biçim, eşikler, kapalı dönem yorumları, ara rapor + ilk dönem, tam şablon + hücre kaçışı) + 1 entegrasyon (Ekim kapalı → Kasım raporu: karşılaştırma, notlar, devir, öneri, 404). 5 bilinçli bozmadan 2'si önce kaçtı → eşik testleri eklendi, hepsi yakalandı. bson `DateTime::to_time_0_3` crate-içi → `from_unix_timestamp`. Backend 196 test.
+1 -1
View File
@@ -84,4 +84,4 @@
- ✅ 3. CSV dışa/içe aktarma (uygulamanın kendi biçimi): harcamalar, plan kalemleri, varlık işlemleri; içe aktarmada önizleme + satır satır hata raporu
- ✅ 4. **Banka / kredi kartı ekstresi içe aktarma** — 4a backend (arşiv dönemleri, iade, gelir, yoksay, tekrar, öğrenen kurallar) + 4b arayüz (`/ayarlar/ekstre`: Excel/CSV, sütun eşleştirme + profil, gözden geçirme)
- ✅ 5. JSON tam hane yedeği — 5a backend (sürümlü dosya + SHA-256, önizleme, tam değiştirme tek transaction, geri dönüş noktaları + "geri al", başka haneye kimlik çevirerek taşıma) · 5b arayüz (Ayarlar → Veri: indir + "son yedek" hatırlatması, sahip için önizleme → onay → geri yükle → "Geri al", geri dönüş noktaları)
- ⏳ 6. Aylık değerlendirme çıktısı: `templates/monthly_report.md` (Claude tasarlar — müşteri gözüyle: ne yaptım, ne iyi/kötü gitti, ne yapmalıydım, gelecek ay önerileri) → Markdown + yazdırılabilir/PDF
- 🔄 6. Aylık değerlendirme çıktısı — ✅ 6a backend (`templates/monthly_report.md` + minijinja, `GET /periods/{id}/report.md`, kural tabanlı yorum ve öneriler) · ⏳ 6b arayüz (rapor sayfası, yazdır/PDF, Markdown indir). Plan: `templates/monthly_report.md` (Claude tasarlar — müşteri gözüyle: ne yaptım, ne iyi/kötü gitti, ne yapmalıydım, gelecek ay önerileri) → Markdown + yazdırılabilir/PDF
+192
View File
@@ -0,0 +1,192 @@
{#
Aylık değerlendirme raporu (Kakeibo). Değerler `backend/src/services/monthly_report.rs`'ten gelir;
hesap ve cümleler orada, burada yalnızca düzen var. Filtreler: `tl` (kuruş → 1.250,00 ₺),
`pct` (oran → %84), `cell` (tablo hücresi için kaçış).
#}
# {{ title }} — Aylık Değerlendirme
{{ range }} · {{ status_line }} · {{ household }}
{% if interim %}
> **Ara rapor:** dönem sürüyor; rakamlar bugüne kadarki kayıtlardan.
{% endif %}
## 1. Bir bakışta
> {{ headline }}
{% if prev_name %}
| Kakeibo'nun 4 sorusu | Bu dönem | {{ prev_name | cell }} |
|---|---:|---:|
{% for q in questions %}
| {{ q.label }} | {{ q.now | tl }} | {{ q.prev | tl }} |
{% endfor %}
{% else %}
| Kakeibo'nun 4 sorusu | Bu dönem |
|---|---:|
{% for q in questions %}
| {{ q.label }} | {{ q.now | tl }} |
{% endfor %}
{% endif %}
## 2. Ne yaptım?
### Gruplar
| Grup | Bütçe | Harcanan | Kalan | Kullanım |
|---|---:|---:|---:|---:|
{% for g in groups %}
| {{ g.emoji }} {{ g.label }} | {{ g.planned | tl }} | {{ g.spent | tl }} | {{ g.left | tl }} | {{ g.mark }} {{ g.usage | pct }} |
{% endfor %}
| **Toplam** | **{{ totals.planned | tl }}** | **{{ totals.spent | tl }}** | **{{ totals.left | tl }}** | {{ totals.usage | pct }} |
✅ bütçenin %90'ına kadar · ⚠️ %90–100 · ❌ bütçe aşıldı
{% if top %}
### En çok harcadığın kategoriler
{% if avg_n %}
| Kategori | Tutar | İşlem | Pay | Ortalamaya göre ({{ avg_n }} dönem) |
|---|---:|---:|---:|---:|
{% for c in top %}
| {{ c.emoji }} {{ c.name | cell }} | {{ c.spent | tl }} | {{ c.count }} | {{ c.share | pct }} | {{ c.change }} |
{% endfor %}
{% else %}
| Kategori | Tutar | İşlem | Pay |
|---|---:|---:|---:|
{% for c in top %}
| {{ c.emoji }} {{ c.name | cell }} | {{ c.spent | tl }} | {{ c.count }} | {{ c.share | pct }} |
{% endfor %}
{% endif %}
{% endif %}
### Haftalar
| Hafta | Günler | Bütçe | Harcanan | Kullanım |
|---|---|---:|---:|---:|
{% for w in weeks %}
| {{ w.index }}. hafta | {{ w.range }} | {{ w.planned | tl }} | {{ w.spent | tl }} | {{ w.mark }} {{ w.usage | pct }} |
{% endfor %}
{% for line in activity %}
- {{ line }}
{% endfor %}
{% if methods %}
### Ödeme türleri
| Ödeme türü | Tutar | Pay |
|---|---:|---:|
{% for m in methods %}
| {{ m.name | cell }} | {{ m.spent | tl }} | {{ m.share | pct }} |
{% endfor %}
{% endif %}
{% if savings %}
### Birikim
{% for line in savings %}
- {{ line }}
{% endfor %}
{% endif %}
## 3. Neler iyi gitti?
{% for line in good %}
- {{ line }}
{% else %}
- Bu dönem öne çıkan bir olumlu fark yok; 7. bölümdeki küçük adımlarla başlayabilirsin.
{% endfor %}
## 4. Neler zorladı?
{% for line in hard %}
- {{ line }}
{% else %}
- Belirgin bir zorlanma yok: bütçe ve birikim planın içinde.
{% endfor %}
## 5. Ne yapılabilirdi?
{% for line in could %}
- {{ line }}
{% else %}
- Rakamlar zaten planın içinde; aynı düzeni korumak yeterli.
{% endfor %}
## 6. Ne yapmalıydım? — kendi notların
{% if plan_notes %}
### Ay başında yazdıkların
{% for n in plan_notes %}
#### {{ n.title }}
{{ n.body }}
{% endfor %}
{% endif %}
{% if reflection or review_notes %}
### Ay sonunda yazdıkların
{% if reflection %}
#### Nasıl daha iyi yapabilirim?
{{ reflection }}
{% endif %}
{% for n in review_notes %}
#### {{ n.title }}
{{ n.body }}
{% endfor %}
{% endif %}
{% if not plan_notes and not reflection and not review_notes %}
_Bu dönem için not yazılmamış. Kakeibo'nun özü bu kısım: Değerlendirme sayfasındaki “Nasıl daha iyi yapabilirim?” alanına birkaç cümle yazmak bir sonraki ayı en çok değiştiren adımdır._
{% endif %}
## 7. Gelecek dönem için
### Önerilen bütçe
| Grup | Öneri | Dayanak |
|---|---:|---|
{% for s in suggestions %}
| {{ s.emoji }} {{ s.label }} | {{ s.amount | tl }} | {{ s.basis }} |
{% endfor %}
| **Toplam** | **{{ suggestion_total | tl }}** | |
{% if suggestion_note %}
{{ suggestion_note }}
{% endif %}
### Somut adımlar
{% for line in steps %}
{{ loop.index }}. {{ line }}
{% endfor %}
{% if carry %}
**Devir:** {{ carry }}
{% endif %}
### Kendi hedeflerin
- [ ] ______________________________________
- [ ] ______________________________________
- [ ] ______________________________________
{% if categories %}
## Ek: Tüm kategoriler
| Grup | Kategori | Tutar | İşlem | Pay |
|---|---|---:|---:|---:|
{% for c in categories %}
| {{ c.group }} | {{ c.emoji }} {{ c.name | cell }} | {{ c.spent | tl }} | {{ c.count }} | {{ c.share | pct }} |
{% endfor %}
{% endif %}
---
<small>Nasıl hesaplandı: Kalan = bütçe − harcanan. Ortalamalar, bu dönemden önceki en fazla 3 kapalı ya da arşiv dönemden. “İyi gitti / zorladı” için grup bütçesinin %90 ve %100'ü, kategorilerde ortalamaya göre en az %20 ve 250 ₺ fark aranır. Önerilen bütçe: son en fazla 4 dönemin ortalama harcaması, 50 ₺'ye yuvarlanmış. Rapor tarihi: {{ generated_on }}.</small>