From 861bfba85aeb2cd965beda3b72b98fe56b8bdd89 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 12 Nov 2026 23:11:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(de=C4=9Ferlendirme):=20ge=C3=A7mi=C5=9F=20?= =?UTF-8?q?d=C3=B6nemler=20ve=20d=C3=BCzeltme=20i=C3=A7in=20yeniden=20a?= =?UTF-8?q?=C3=A7ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Defter, Plan ve Değerlendirme'de dönem seçici (?donem=) - Kapalı dönem salt okunur; düzeltmedeki dönem düzenlenebilir ve yeniden kapatılır - Hane sahibi için onaylı "Düzeltme için yeniden aç" - Geçmiş dönemler listesi, dönem durum rozeti Co-Authored-By: Claude Opus 5.5 --- docs/MEMORY.md | 3 +- docs/PHASES.md | 4 +- frontend/src/features/ledger/entry-list.tsx | 47 +++--- frontend/src/features/ledger/page.tsx | 88 ++++++----- .../src/features/periods/period-picker.tsx | 38 +++++ .../src/features/periods/selected-period.tsx | 30 ++++ frontend/src/features/planning/page.tsx | 27 ++-- frontend/src/features/planning/plan-read.tsx | 14 +- frontend/src/features/review/close-page.tsx | 49 +++--- frontend/src/features/review/page.tsx | 47 +++--- .../src/features/review/period-status.tsx | 141 ++++++++++++++++++ frontend/src/features/review/reflection.tsx | 4 +- frontend/src/lib/period-select.test.ts | 35 +++++ frontend/src/lib/period-select.ts | 56 +++++++ frontend/src/lib/periods.ts | 17 +++ frontend/src/lib/strings.ts | 27 ++++ 16 files changed, 500 insertions(+), 127 deletions(-) create mode 100644 frontend/src/features/periods/period-picker.tsx create mode 100644 frontend/src/features/periods/selected-period.tsx create mode 100644 frontend/src/features/review/period-status.tsx create mode 100644 frontend/src/lib/period-select.test.ts create mode 100644 frontend/src/lib/period-select.ts diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 9d3dcd4..d55f4ce 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,7 +1,7 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**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. +**Faz 7 – Adım 1:** Dashboard planı — önce `KAKEIBO_PROMPT.md` Dashboard bölümünü oku, adımları `docs/PHASES.md`'ye yaz ve kullanıcıya onaylat (özet kartları, grafikler; Recharts). **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 @@ -111,3 +111,4 @@ - **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ı). +- **Adım 5:** Dönem seçimi: `lib/period-select.ts` (`useSelectedPeriod`: `?donem=` → listeden; yoksa açık dönem, o da yoksa düzeltmedeki; `pickPeriod`, `isWritable`, `canReopen` = backend kuralı, `periodPath`), `features/periods/` (`PeriodPicker` başlıkta ` { + const p = periods.find((x) => x.id === e.target.value) + if (p) setParams(p.status === 'open' ? {} : { [PERIOD_PARAM]: p.id }) + }} + className="h-11 max-w-72 rounded-lg border bg-background px-3 text-sm" + > + {!period && } + {periods.map((p) => ( + + ))} + + + ) +} diff --git a/frontend/src/features/periods/selected-period.tsx b/frontend/src/features/periods/selected-period.tsx new file mode 100644 index 0000000..ff46818 --- /dev/null +++ b/frontend/src/features/periods/selected-period.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react' +import { PageError, PageLoading } from '@/components/page' +import { NoPeriodCard } from '@/features/period-wizard/no-period-card' +import { useSelectedPeriod } from '@/lib/period-select' +import { t } from '@/lib/strings' +import type { Period } from '@/types/Period' + +/** + * Seçili dönemi (`?donem=` ya da açık dönem) yükler; yükleniyor / hata / bulunamadı / + * dönem yok durumlarını tek yerde gösterir. Dönem varsa `children(dönem, tüm dönemler)`. + */ +export function SelectedPeriod({ + empty = , + children, +}: { + empty?: ReactNode + children: (period: Period, periods: Period[]) => ReactNode +}) { + const sel = useSelectedPeriod() + if (sel.error) return + if (sel.isPending) return + if (sel.period === undefined) + return ( +

+ {t.periods.notFound} +

+ ) + if (sel.period === null) return empty + return children(sel.period, sel.periods) +} diff --git a/frontend/src/features/planning/page.tsx b/frontend/src/features/planning/page.tsx index 4d45618..6d8de97 100644 --- a/frontend/src/features/planning/page.tsx +++ b/frontend/src/features/planning/page.tsx @@ -4,8 +4,10 @@ import { PageError, PageHeader, PageLoading } from '@/components/page' import { Button } from '@/components/ui/button' import { NotesSection } from '@/features/notes/notes-section' import { CopyPrevious } from '@/features/period-wizard/step-items' -import { NoPeriodCard } from '@/features/period-wizard/no-period-card' -import { useBudget, useCurrentPeriod, usePlanItems, useSummary } from '@/lib/periods' +import { PeriodPicker } from '@/features/periods/period-picker' +import { SelectedPeriod } from '@/features/periods/selected-period' +import { isWritable } from '@/lib/period-select' +import { useBudget, usePlanItems, useSummary } from '@/lib/periods' import { t } from '@/lib/strings' import type { Period } from '@/types/Period' import { PlanEdit } from './plan-edit' @@ -13,19 +15,14 @@ import { BudgetRead, ItemsList, PeriodHeading } from './plan-read' import { SummaryTiles } from './summary-tiles' export function PlanningPage() { - const current = useCurrentPeriod() return ( <> - - {current.isPending ? ( - - ) : current.isError ? ( - - ) : current.data ? ( - - ) : ( - - )} + } + /> + {(p) => } ) } @@ -56,7 +53,7 @@ function PlanView({ period }: { period: Period }) { ) } - const editable = period.status === 'open' + const editable = isWritable(period) // açık ya da düzeltmede return (
i.kind === 'income')} /> i.kind === 'expense')} />
- {editable && } + {period.status === 'open' && } diff --git a/frontend/src/features/planning/plan-read.tsx b/frontend/src/features/planning/plan-read.tsx index 691c85b..33651f9 100644 --- a/frontend/src/features/planning/plan-read.tsx +++ b/frontend/src/features/planning/plan-read.tsx @@ -18,7 +18,19 @@ export function PeriodHeading({ period, actions }: { period: Period; actions?: R return (
-

{period.name}

+

+ {period.name} + {period.status !== 'open' && ( + + {t.periods.status[period.status]} + + )} +

{formatDate(period.start_date)} – {formatDate(period.last_date)} ·{' '} {s.periodInfo(period.days, period.weeks.length)} diff --git a/frontend/src/features/review/close-page.tsx b/frontend/src/features/review/close-page.tsx index 607f5c3..6d3bf07 100644 --- a/frontend/src/features/review/close-page.tsx +++ b/frontend/src/features/review/close-page.tsx @@ -9,10 +9,12 @@ import { MoneyInput } from '@/components/money-input' import { PageError, PageHeader, PageLoading } from '@/components/page' import { Button } from '@/components/ui/button' import { NoPeriodCard } from '@/features/period-wizard/no-period-card' +import { SelectedPeriod } from '@/features/periods/selected-period' import { StepActions, StepIntro, Stepper } from '@/features/period-wizard/wizard-ui' import { PeriodHeading } from '@/features/planning/plan-read' import { formatDate, formatMoney, formatNumber, parseMoney, toIsoDate } from '@/lib/format' -import { useClosePeriod, useCurrentPeriod, useReview } from '@/lib/periods' +import { periodPath } from '@/lib/period-select' +import { useClosePeriod, useReview } from '@/lib/periods' import { settlePreview } from '@/lib/review' import { t } from '@/lib/strings' import { cn } from '@/lib/utils' @@ -27,32 +29,36 @@ const s = t.closing /** * Dönem kapatma sihirbazı: Özet → Birikim → Onay → Sonuç. * - * Kapanınca açık dönem kalmaz (`current` → `null`); sonuç bu yüzden sayfanın kendi - * durumunda tutulur. Adımlar adreste değil: yarıda yenilenirse baştan başlar (hiçbir şey + * Kapanınca dönem artık seçilemez (açık dönem kalmaz); sonuç bu yüzden sayfanın kendi + * durumunda tutulur. `?donem=` ile düzeltmedeki dönem yeniden kapatılır. Adımlar adreste değil: yarıda yenilenirse baştan başlar (hiçbir şey * kaydedilmemiştir, kapatma yalnızca son adımda tek istekle yapılır). */ export function ClosePeriodPage() { - const current = useCurrentPeriod() const [result, setResult] = useState(null) - - let body: ReactNode - if (result) body = - else if (current.isPending) body = - else if (current.isError) - body = - else if (!current.data) - body = ( -

-

{s.noOpen}

- -
- ) - else body = + const empty = ( +
+

{s.noOpen}

+ +
+ ) return (
- {body} + {result ? ( + + ) : ( + // Açık dönem ya da `?donem=` ile düzeltmedeki dönem (yeniden kapatma) + + {(p) => + p.status === 'closed' ? ( +

{s.alreadyClosed}

+ ) : ( + + ) + } +
+ )}
) } @@ -128,7 +134,10 @@ function StepSummary({ {!period.reflection_md && ( {s.noReflection}{' '} - + {s.writeReflection} diff --git a/frontend/src/features/review/page.tsx b/frontend/src/features/review/page.tsx index d261994..b6dd06f 100644 --- a/frontend/src/features/review/page.tsx +++ b/frontend/src/features/review/page.tsx @@ -1,44 +1,40 @@ import { useState } from 'react' import { Info } from 'lucide-react' -import { Link } from 'react-router' -import { paths } from '@/components/layout/nav-items' import { PageError, PageHeader, PageLoading } from '@/components/page' -import { Button } from '@/components/ui/button' import { NotesSection } from '@/features/notes/notes-section' -import { NoPeriodCard } from '@/features/period-wizard/no-period-card' +import { PeriodPicker } from '@/features/periods/period-picker' +import { SelectedPeriod } from '@/features/periods/selected-period' import { PeriodHeading } from '@/features/planning/plan-read' import { useCategories } from '@/lib/catalog' import { formatDate, toIsoDate } from '@/lib/format' -import { useCurrentPeriod, useReview } from '@/lib/periods' +import { useReview } from '@/lib/periods' import { t } from '@/lib/strings' import type { Period } from '@/types/Period' import { GroupComparison, TopCategories, WeeklyDistribution } from './breakdowns' +import { ClosingSection, PeriodHistory } from './period-status' import { Questions } from './questions' import { Reflection } from './reflection' export function ReviewPage() { - const current = useCurrentPeriod() return ( <> - - {current.isPending ? ( - - ) : current.isError ? ( - - ) : current.data ? ( - - ) : ( - - )} + } + /> + + {(p, all) => } + ) } /** * Hesaplar sorgu anında backend'de yapılır (`GET /periods/{id}/review`); defterde - * bir kayıt değişince bu sayfa da yenilenir. Kapalı dönem: salt okunur (Adım 5'te seçilebilir). + * bir kayıt değişince bu sayfa da yenilenir. Kapalı dönem salt okunur; `?donem=` ile seçilir. */ -function ReviewView({ period }: { period: Period }) { +function ReviewView({ period, periods }: { period: Period; periods: Period[] }) { const review = useReview(period.id) const categories = useCategories(true) const [today] = useState(() => toIsoDate(new Date())) @@ -50,7 +46,7 @@ function ReviewView({ period }: { period: Period }) { return (
- {!readOnly && today <= period.last_date && ( + {period.status === 'open' && today <= period.last_date && (

{t.review.ongoing(formatDate(period.last_date))} @@ -64,17 +60,8 @@ function ReviewView({ period }: { period: Period }) { - {!readOnly && ( -

-
-

{t.review.closeTitle}

-

{t.review.closeHint}

-
- -
- )} + +
) } diff --git a/frontend/src/features/review/period-status.tsx b/frontend/src/features/review/period-status.tsx new file mode 100644 index 0000000..1e79559 --- /dev/null +++ b/frontend/src/features/review/period-status.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react' +import { ChevronRight } from 'lucide-react' +import { Link } from 'react-router' +import { toast } from 'sonner' +import { paths } from '@/components/layout/nav-items' +import { Button } from '@/components/ui/button' +import { useMe } from '@/lib/auth' +import { formatDate, formatMoney, toIsoDate } from '@/lib/format' +import { canReopen, periodPath } from '@/lib/period-select' +import { useReopenPeriod } from '@/lib/periods' +import { t } from '@/lib/strings' +import { cn } from '@/lib/utils' +import type { Period } from '@/types/Period' + +const s = t.review + +/** + * Sayfa sonundaki kapanış bölümü, dönemin durumuna göre: + * açık → "Dönemi Kapat" · düzeltmede → "Yeniden Kapat" · kapalı → bilgi + (sahip) yeniden aç. + */ +export function ClosingSection({ period, periods }: { period: Period; periods: Period[] }) { + if (period.status === 'closed') return + const correcting = period.status === 'correcting' + return ( +
+
+

{correcting ? s.correctingTitle : s.closeTitle}

+

+ {correcting ? s.correctingHint : s.closeHint} +

+
+
+ {correcting && ( + <> + + + + )} + +
+
+ ) +} + +function ClosedInfo({ period, periods }: { period: Period; periods: Period[] }) { + const me = useMe() + const reopen = useReopenPeriod() + const [asking, setAsking] = useState(false) + const owner = me.data?.user.role === 'owner' + const allowed = canReopen(periods, period) + const closedAt = period.closed_at ? formatDate(toIsoDate(new Date(period.closed_at))) : '—' + + // `mutateAsync`: başarıda dönem "düzeltmede" olur ve bu bileşen kalkar; `mutate`'in + // bileşene bağlı geri çağrıları o zaman çalışmazdı (bildirim görünmezdi) + async function confirm() { + try { + await reopen.mutateAsync(period.id) + toast.success(s.reopened) + } catch (e) { + toast.error(e instanceof Error ? e.message : String(e)) + } + } + + return ( +
+

{s.closedInfo(closedAt, formatMoney(period.to_savings ?? 0))}

+ {owner && + (!allowed ? ( +

{s.reopenBlocked}

+ ) : asking ? ( +
+

{s.reopenConfirm}

+
+ + +
+
+ ) : ( + + ))} +
+ ) +} + +/** Kapalı ve düzeltmedeki dönemler; tıklayınca o dönemin değerlendirmesi */ +export function PeriodHistory({ periods, selected }: { periods: Period[]; selected: string }) { + const past = periods.filter((p) => p.status !== 'open') + if (past.length === 0) return null + return ( +
+

{t.periods.history}

+
    + {past.map((p) => ( +
  • + +
    +

    {p.name}

    +

    + {formatDate(p.start_date)} – {formatDate(p.last_date)} ·{' '} + {t.periods.status[p.status]} +

    +
    + {p.to_savings !== null && ( + + {t.periods.toSavings(formatMoney(p.to_savings))} + + )} + + +
  • + ))} +
+
+ ) +} diff --git a/frontend/src/features/review/reflection.tsx b/frontend/src/features/review/reflection.tsx index 123db92..786106b 100644 --- a/frontend/src/features/review/reflection.tsx +++ b/frontend/src/features/review/reflection.tsx @@ -39,7 +39,9 @@ export function Reflection({ period, readOnly }: { period: Period; readOnly: boo ) : text ? ( {text} ) : ( -

{s.reflectionEmpty}

+

+ {readOnly ? s.reflectionNone : s.reflectionEmpty} +

)} ) diff --git a/frontend/src/lib/period-select.test.ts b/frontend/src/lib/period-select.test.ts new file mode 100644 index 0000000..6ce7224 --- /dev/null +++ b/frontend/src/lib/period-select.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import type { Period } from '@/types/Period' +import { canReopen, periodPath, pickPeriod } from './period-select' + +const p = (id: string, start: string, status: Period['status']) => + ({ id, start_date: start, status }) as Period + +describe('period select', () => { + const sep = p('sep', '2026-09-01', 'closed') + const oct = p('oct', '2026-10-01', 'closed') + const nov = p('nov', '2026-11-01', 'open') + const all = [nov, oct, sep] // en yeni başta + + it('picks the requested, else open, else correcting', () => { + expect(pickPeriod(all, nov, 'sep')).toBe(sep) + expect(pickPeriod(all, nov, 'yok')).toBeUndefined() + expect(pickPeriod(all, nov, null)).toBe(nov) + const fixing = p('oct', '2026-10-01', 'correcting') + expect(pickPeriod([fixing, sep], null, null)).toBe(fixing) + expect(pickPeriod([sep], null, null)).toBeNull() + }) + + it('reopens only the last closed period', () => { + expect(canReopen(all, oct)).toBe(true) // sonrası yalnızca açık Kasım + expect(canReopen(all, sep)).toBe(false) // Ekim kapalı + expect(canReopen(all, nov)).toBe(false) // kapalı değil + const fixing = p('oct', '2026-10-01', 'correcting') + expect(canReopen([nov, fixing, sep], sep)).toBe(false) // Ekim düzeltmede + }) + + it('links keep the period unless it is the open one', () => { + expect(periodPath('/defter', nov)).toBe('/defter') + expect(periodPath('/defter', oct)).toBe('/defter?donem=oct') + }) +}) diff --git a/frontend/src/lib/period-select.ts b/frontend/src/lib/period-select.ts new file mode 100644 index 0000000..fabdce1 --- /dev/null +++ b/frontend/src/lib/period-select.ts @@ -0,0 +1,56 @@ +/** + * Hangi dönem gösteriliyor? Defter, Plan, Değerlendirme ve kapatma sihirbazı ortak kullanır. + * + * Adreste `?donem=` varsa o dönem (kapalı ya da düzeltmede olabilir); yoksa açık dönem, + * o da yoksa düzeltmedeki dönem. Dönem verisi listeden gelir (liste tüm alanları içerir). + */ +import { useSearchParams } from 'react-router' +import type { Period } from '@/types/Period' +import { useCurrentPeriod, usePeriods } from './periods' + +export const PERIOD_PARAM = 'donem' + +/** `undefined` → adresteki dönem bulunamadı; `null` → gösterilecek dönem yok */ +export function pickPeriod( + periods: Period[], + current: Period | null, + id: string | null, +): Period | null | undefined { + if (id) return periods.find((p) => p.id === id) + return current ?? periods.find((p) => p.status === 'correcting') ?? null +} + +/** Kayıt/plan değiştirilebilir mi? (backend `PeriodStatus::is_writable`) */ +export const isWritable = (p: Period) => p.status !== 'closed' + +/** + * Yeniden açılabilir mi? Yalnızca kapalı dönem ve ondan sonra kapalı/düzeltmede dönem yoksa + * (backend `closing::reopen` ile aynı kural; aksi hâlde sonraki devirler zincirleme bozulurdu). + */ +export function canReopen(periods: Period[], p: Period): boolean { + return ( + p.status === 'closed' && + !periods.some((q) => q.start_date > p.start_date && q.status !== 'open') + ) +} + +/** Sayfa adresi + dönem: açık dönem varsayılan olduğundan parametresiz */ +export function periodPath(path: string, p: Pick): string { + return p.status === 'open' ? path : `${path}?${PERIOD_PARAM}=${p.id}` +} + +export function useSelectedPeriod() { + const [params] = useSearchParams() + const id = params.get(PERIOD_PARAM) + const current = useCurrentPeriod() + const periods = usePeriods() + const failed = [current, periods].find((q) => q.isError) + const ready = current.isSuccess && periods.isSuccess + return { + isPending: !failed && !ready, + error: failed?.error ?? null, + refetch: () => Promise.all([current.refetch(), periods.refetch()]), + period: ready ? pickPeriod(periods.data, current.data, id) : undefined, + periods: periods.data ?? [], + } +} diff --git a/frontend/src/lib/periods.ts b/frontend/src/lib/periods.ts index 06f496c..6c3209d 100644 --- a/frontend/src/lib/periods.ts +++ b/frontend/src/lib/periods.ts @@ -62,6 +62,10 @@ export function useUpdatePeriod() { api.patch(`/periods/${id}`, body), onSuccess: (p) => { if (p.status === 'open') qc.setQueryData(queryKeys.currentPeriod, p) + // Seçili dönem listeden okunur → yeni sürüm hemen görünsün (art arda kayıtta 409 olmasın) + qc.setQueryData(queryKeys.periods, (list) => + list?.map((x) => (x.id === p.id ? p : x)), + ) void qc.invalidateQueries({ queryKey: queryKeys.periods, exact: true }) void refreshPeriod(qc, p.id) }, @@ -86,6 +90,19 @@ export function useClosePeriod() { }) } +/** **Sahip:** kapalı dönemi düzeltme için yeniden aç (`correcting`) */ +export function useReopenPeriod() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: string) => api.post(`/periods/${id}/reopen`), + onSettled: () => + Promise.all([ + qc.invalidateQueries({ queryKey: queryKeys.periods }), + qc.invalidateQueries({ queryKey: ['period'] }), + ]), + }) +} + // ---------- Plan kalemleri ---------- export function usePlanItems(periodId: string) { diff --git a/frontend/src/lib/strings.ts b/frontend/src/lib/strings.ts index 1e2ff38..f1a4933 100644 --- a/frontend/src/lib/strings.ts +++ b/frontend/src/lib/strings.ts @@ -250,6 +250,8 @@ export const t = { }, ledger: { + readOnly: + 'Bu dönem kapalı; kayıtlar yalnızca görüntülenebilir. Düzeltme için hane sahibi Değerlendirme’den yeniden açabilir.', week: (n: number, count: number) => `${n}. hafta / ${count}`, prevWeek: 'Önceki hafta', nextWeek: 'Sonraki hafta', @@ -322,6 +324,7 @@ export const t = { week: (n: number) => `${n}. hafta`, reflectionTitle: 'Nasıl daha iyi yapabilirim?', reflectionEmpty: 'Bu ay neler iyi gitti, neyi değiştirmek istersin? Kısa notlar yeterli.', + reflectionNone: 'Bu dönem için yazılmamış.', reflectionPlaceholder: '- Dışarıda yemeği haftada bire indir\n- Market alışverişini listeyle yap', reflectionSaved: 'Kaydedildi', @@ -332,6 +335,29 @@ export const t = { closeHint: 'Değerlendirme bittiyse dönemi kapat: birikimin ayrılır, kalan artı ya da açık sonraki döneme devreder.', closeAction: '🔒 Dönemi Kapat', + closedInfo: (date: string, saved: string) => + `Bu dönem ${date} tarihinde kapandı. Birikime aktarılan: ${saved}.`, + reopen: 'Düzeltme için yeniden aç', + reopenConfirm: + 'Dönem düzeltmeye açılır: kayıt ve plan değiştirilebilir. Bitince yeniden kapatman gerekir; devir o zaman yeniden hesaplanır.', + reopenYes: 'Evet, yeniden aç', + reopenNo: 'Vazgeç', + reopened: 'Dönem düzeltmeye açıldı', + reopenBlocked: 'Sonraki dönem kapalı olduğu için bu dönem yeniden açılamaz.', + correctingTitle: 'Bu dönem düzeltmede', + correctingHint: + 'Kayıtları ve planı düzelt, bitince yeniden kapat; sonraki döneme devir güncellenir.', + recloseAction: '🔒 Yeniden Kapat', + fixInLedger: 'Defter’de düzelt', + fixInPlan: 'Plan’da düzelt', + }, + + periods: { + picker: 'Dönem', + status: { open: 'Açık', closed: '🔒 Kapalı', correcting: '✏️ Düzeltmede' }, + notFound: 'Dönem bulunamadı. Listeden başka bir dönem seç.', + history: 'Geçmiş dönemler', + toSavings: (x: string) => `Birikime ${x}`, }, closing: { @@ -371,6 +397,7 @@ export const t = { toPlan: 'Plana git', toDashboard: 'Panele dön', noOpen: 'Kapatılacak açık dönem yok.', + alreadyClosed: 'Bu dönem zaten kapalı.', }, common: {