From 2c16cf41635b2227dfefc49443a6ac110103d799 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 12 Nov 2026 22:59:38 +0300 Subject: [PATCH] =?UTF-8?q?feat(de=C4=9Ferlendirme):=20d=C3=B6nem=20kapatm?= =?UTF-8?q?a=20sihirbaz=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /degerlendirme/kapat: Özet → Birikim → Onay → Sonuç - Birikime aktarılacak tutar (varsayılan tamamı), kalan artı/açık devri önizlemesi - Değerlendirme sayfasına "Dönemi Kapat" bölümü - Yeni dönem önerisi son dönemin bitişinden başlar (erken kapatılan ay önerilmez) Co-Authored-By: Claude Opus 5.5 --- docs/MEMORY.md | 3 +- docs/PHASES.md | 4 +- frontend/src/components/layout/nav-items.ts | 2 + .../src/features/period-wizard/step-dates.tsx | 44 ++- .../src/features/period-wizard/wizard-ui.tsx | 22 +- frontend/src/features/review/close-page.tsx | 367 ++++++++++++++++++ .../src/features/review/close-schema.test.ts | 20 + frontend/src/features/review/close-schema.ts | 17 + frontend/src/features/review/page.tsx | 14 + frontend/src/lib/period-dates.test.ts | 34 +- frontend/src/lib/period-dates.ts | 32 ++ frontend/src/lib/periods.ts | 20 + frontend/src/lib/review.test.ts | 16 +- frontend/src/lib/review.ts | 17 + frontend/src/lib/strings.ts | 43 ++ frontend/src/router.tsx | 2 + 16 files changed, 639 insertions(+), 18 deletions(-) create mode 100644 frontend/src/features/review/close-page.tsx create mode 100644 frontend/src/features/review/close-schema.test.ts create mode 100644 frontend/src/features/review/close-schema.ts diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 7074dac..9d3dcd4 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,7 +1,7 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**Faz 6 – Adım 4:** Kapatma sihirbazı (`/degerlendirme/kapat`, Değerlendirme'de "🔒 Dönemi Kapat"): 1) özet + planlanan/gerçekleşen (review verisi), 2) gerçekleşen birikim → artıdaysa "birikime aktar" tutarı (MoneyInput, varsayılan tamamı; kalan artı devreder), açıktaysa borç bilgisi, 3) onay (`POST /close`, `version`), 4) sonuç + "Yeni dönemi aç" (sihirbaz `/plan/donem-ac`, önceki aydan kopyala adımları zaten var). Kapanınca `currentPeriod` null → sorgular yenilenir. +**Faz 6 – Adım 5:** Kapalı dönemler listesi (Değerlendirme'de ya da ayrı sayfada; ad, tarih, gerçekleşen/birikime aktarılan, devir) + kapalı dönemin salt okunur değerlendirmesi + **sahip** için "Düzeltme için yeniden aç" (`POST /reopen`, 409 zincir uyarısı) + `correcting` dönemi Defter/Plan/Değerlendirme'de seçme (ör. `?donem=`) ve "Yeniden kapat" (aynı sihirbaz). Plan sayfası şu an yalnız `status === 'open'` için düzenlenebilir. **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 @@ -110,3 +110,4 @@ - **Adım 1:** Değerlendirme backend: `services/review.rs` (`budget::summary` + `budget::view` + kategori aggregation), `routes/review.rs` → `GET /periods/{id}/review` (`PeriodReview`: `available` = gelir − sabit − borç + artı, `savings_goal`, `spent_total`, `realized_savings` = available − harcanan, `savings_gap`; `groups` planned/spent, `categories` çoktan aza (+count), `weeks` 4 grubun toplamı). Kapalı dönemde okunur. `periods.reflection_md` (PATCH, ≤ 20.000 karakter, `""` → null, yalnızca açık dönem). `tests/review.rs` (3). Mutasyon: 4. soruya hedef düşmek / silinenleri saymak → yakalandı. Not: `TestApp::register` adı e-postaya çevirir → Türkçe karakterli ad kullanma. - **Adım 2:** Kapatma backend: `services/closing.rs` — `settle(realized, to_savings)` (< 0 → borç, birikim istenemez; ≥ 0 → varsayılan tamamı birikime, kalan artı), `close` (tek transaction: dönem `closed` + `closed_at/by` + `to_savings`, bu dönemden eski devirler soft delete, yeni devir; `to_period_id` = sonraki dönem ya da null), `reopen` (sahip; `closed` → **`correcting`**; ardından kapalı/düzeltmede dönem varsa 409). `PeriodStatus::Correcting` + `is_writable()` → `get_open`/`update` buna bakar; `current()` yalnız `open`. `period::create` artık transaction: önceki dönemin bağsız devrini bağlar. `POST /periods/{id}/close` (`PeriodCloseRequest`/`PeriodCloseResult`/`Carryover`), `POST /periods/{id}/reopen`. Period DTO: `to_savings`, `closed_at`. `tests/closing.rs` (4) + `settle` birim testi. Mutasyon 3/3 yakalandı; hata zorlanınca transaction geri aldı (dönem açık, sürüm aynı). Backend 113 test. Karar: yeniden açma = düzeltme modu (kullanıcı "a"). - **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ı). diff --git a/docs/PHASES.md b/docs/PHASES.md index eab82ca..176d101 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -64,5 +64,5 @@ - ✅ 1. Backend: değerlendirme (`GET /periods/{id}/review`: 4 soru, grup/kategori/hafta) + `reflection_md` - ✅ 2. Backend: dönem kapatma (transaction: durum, devir, birikime aktarılan tutar) + yeni dönemde devrin bağlanması + yeniden açma (sahip) - ✅ 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) -- ⏳ 5. Frontend: kapalı dönemler listesi + yeniden açma (sahip) +- ✅ 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 diff --git a/frontend/src/components/layout/nav-items.ts b/frontend/src/components/layout/nav-items.ts index f080065..8d6c62a 100644 --- a/frontend/src/components/layout/nav-items.ts +++ b/frontend/src/components/layout/nav-items.ts @@ -23,6 +23,8 @@ export const paths = { savings: '/birikim', more: '/diger', review: '/degerlendirme', + /** Dönem kapatma sihirbazı */ + closePeriod: '/degerlendirme/kapat', reports: '/raporlar', settings: '/ayarlar', login: '/giris', diff --git a/frontend/src/features/period-wizard/step-dates.tsx b/frontend/src/features/period-wizard/step-dates.tsx index 6170e6c..e5afca7 100644 --- a/frontend/src/features/period-wizard/step-dates.tsx +++ b/frontend/src/features/period-wizard/step-dates.tsx @@ -6,8 +6,10 @@ import { FormError, FormField } from '@/components/form-field' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ApiError } from '@/lib/api' -import { daysInclusive, isIsoDate, lastToEnd, monthRange } from '@/lib/period-dates' -import { useCreatePeriod, useUpdatePeriod } from '@/lib/periods' +import { PageError, PageLoading } from '@/components/page' +import { formatDate } from '@/lib/format' +import { daysInclusive, isIsoDate, lastToEnd, suggestRanges } from '@/lib/period-dates' +import { useCreatePeriod, usePeriods, useUpdatePeriod } from '@/lib/periods' import { t } from '@/lib/strings' import type { Period } from '@/types/Period' import { datesSchema, type DatesForm } from './schemas' @@ -17,15 +19,45 @@ const s = t.wizard.dates const monthName = new Intl.DateTimeFormat('tr-TR', { month: 'long', year: 'numeric' }) -/** 1. adım: dönem yoksa açar, varsa (geri gelindiyse) günceller. */ +/** Hızlı seçim etiketi: takvim ayıysa "Aralık 2026", değilse "15.11.2026 – 14.12.2026" */ +function rangeLabel(r: { start: string; last: string }) { + const [y, m, d] = r.start.split('-').map(Number) + const monthEnd = new Date(Date.UTC(y, m, 0)).toISOString().slice(0, 10) + return d === 1 && r.last === monthEnd + ? monthName.format(new Date(y, m - 1, 1)) + : `${formatDate(r.start)} – ${formatDate(r.last)}` +} + +/** + * 1. adım: dönem yoksa açar, varsa (geri gelindiyse) günceller. + * Öneriler son dönemin bitişinden başlar (erken kapatılan ay tekrar önerilmez). + */ export function StepDates({ period, onDone }: { period: Period | null; onDone: () => void }) { + const periods = usePeriods() + if (periods.isPending) return + if (periods.isError) + return + // Liste en yeni başta; sihirbazda açık dönem varsa (geri gelindi) o hariç tutulur + const previous = periods.data.find((p) => p.id !== period?.id) + return +} + +function DatesStepForm({ + period, + prevEnd, + onDone, +}: { + period: Period | null + prevEnd?: string + onDone: () => void +}) { const create = useCreatePeriod() const update = useUpdatePeriod() const [today] = useState(() => new Date()) // render başına değil, bir kez - const presets = [0, 1].map((offset) => ({ - label: monthName.format(new Date(today.getFullYear(), today.getMonth() + offset, 1)), - range: monthRange(today, offset), + const presets = suggestRanges(today, prevEnd).map((range) => ({ + label: rangeLabel(range), + range, })) const { diff --git a/frontend/src/features/period-wizard/wizard-ui.tsx b/frontend/src/features/period-wizard/wizard-ui.tsx index 8191bb8..223c9d3 100644 --- a/frontend/src/features/period-wizard/wizard-ui.tsx +++ b/frontend/src/features/period-wizard/wizard-ui.tsx @@ -7,12 +7,20 @@ import { cn } from '@/lib/utils' const s = t.wizard /** Adım göstergesi: mobilde "Adım 2/5 · Gelirler" + çubuk, geniş ekranda numaralı liste. */ -export function Stepper({ current }: { current: number }) { - const total = s.steps.length +export function Stepper({ + current, + steps = s.steps, + label = s.title, +}: { + current: number + steps?: readonly string[] + label?: string +}) { + const total = steps.length return ( -