feat(değerlendirme): geçmiş dönemler ve düzeltme için yeniden açma

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 23:11:04 +03:00
parent 2c16cf4163
commit 861bfba85a
16 changed files with 500 additions and 127 deletions
+26 -21
View File
@@ -22,6 +22,7 @@ export function EntryList({
categories,
methods,
editingId,
readOnly = false,
onEdit,
onDeleted,
}: {
@@ -30,6 +31,8 @@ export function EntryList({
categories: CatalogItem[]
methods: CatalogItem[]
editingId?: string
/** Kapalı dönem: düzenle/sil yok */
readOnly?: boolean
onEdit: (e: Entry) => void
onDeleted: (id: string) => void
}) {
@@ -115,27 +118,29 @@ export function EntryList({
</p>
</div>
<span className="money shrink-0 font-medium">{formatMoney(e.amount)}</span>
<div className="flex shrink-0">
<Button
variant="ghost"
size="icon"
aria-label={s.edit(name)}
aria-pressed={editing}
onClick={() => onEdit(e)}
>
<Pencil aria-hidden />
</Button>
<Button
variant="ghost"
size="icon"
aria-label={s.remove(name)}
className="text-muted-foreground hover:text-expense"
disabled={del.isPending && del.variables === e.id}
onClick={() => remove(e, name)}
>
<Trash2 aria-hidden />
</Button>
</div>
{!readOnly && (
<div className="flex shrink-0">
<Button
variant="ghost"
size="icon"
aria-label={s.edit(name)}
aria-pressed={editing}
onClick={() => onEdit(e)}
>
<Pencil aria-hidden />
</Button>
<Button
variant="ghost"
size="icon"
aria-label={s.remove(name)}
className="text-muted-foreground hover:text-expense"
disabled={del.isPending && del.variables === e.id}
onClick={() => remove(e, name)}
>
<Trash2 aria-hidden />
</Button>
</div>
)}
</li>
)
})}
+52 -36
View File
@@ -1,13 +1,16 @@
import { useRef, useState } from 'react'
import { useSearchParams } from 'react-router'
import { Info } from 'lucide-react'
import { PageError, PageHeader, PageLoading } from '@/components/page'
import { NoPeriodCard } from '@/features/period-wizard/no-period-card'
import { PeriodPicker } from '@/features/periods/period-picker'
import { SelectedPeriod } from '@/features/periods/selected-period'
import { Section } from '@/features/planning/plan-read'
import { useCategories, usePaymentMethods } from '@/lib/catalog'
import { useEntries } from '@/lib/entries'
import { toIsoDate } from '@/lib/format'
import { parseWeekParam, weekIndexFor } from '@/lib/ledger'
import { useBudget, useCurrentPeriod } from '@/lib/periods'
import { isWritable } from '@/lib/period-select'
import { useBudget } from '@/lib/periods'
import { t } from '@/lib/strings'
import type { Entry } from '@/types/Entry'
import type { Period } from '@/types/Period'
@@ -17,19 +20,14 @@ import { WeekPicker } from './week-picker'
import { WeekSummary } from './week-summary'
export function LedgerPage() {
const current = useCurrentPeriod()
return (
<>
<PageHeader title={t.pages.ledger.title} description={t.pages.ledger.description} />
{current.isPending ? (
<PageLoading />
) : current.isError ? (
<PageError message={current.error.message} onRetry={current.refetch} />
) : current.data ? (
<LedgerView period={current.data} />
) : (
<NoPeriodCard />
)}
<PageHeader
title={t.pages.ledger.title}
description={t.pages.ledger.description}
actions={<PeriodPicker />}
/>
<SelectedPeriod>{(p) => <LedgerView key={p.id} period={p} />}</SelectedPeriod>
</>
)
}
@@ -48,6 +46,7 @@ function LedgerView({ period }: { period: Period }) {
const index =
parseWeekParam(params.get('hafta'), period.weeks.length) ?? weekIndexFor(period.weeks, today)
const week = period.weeks[index - 1]
const readOnly = !isWritable(period)
const budget = useBudget(period.id)
const categories = useCategories(true)
@@ -56,7 +55,15 @@ function LedgerView({ period }: { period: Period }) {
const goTo = (n: number) => {
setEditing(null)
setParams(n === todayIndex ? {} : { hafta: String(n) }, { replace: true })
setParams(
(prev) => {
const next = new URLSearchParams(prev) // seçili dönem (`donem`) korunur
if (n === todayIndex) next.delete('hafta')
else next.set('hafta', String(n))
return next
},
{ replace: true },
)
}
function edit(e: Entry) {
@@ -78,29 +85,37 @@ function LedgerView({ period }: { period: Period }) {
onChange={goTo}
/>
</div>
{readOnly && (
<p className="flex items-start gap-2 rounded-xl bg-muted px-3 py-2 text-sm text-muted-foreground">
<Info className="mt-0.5 size-4 shrink-0" aria-hidden />
{t.ledger.readOnly}
</p>
)}
<div className="grid items-start gap-4 lg:grid-cols-2">
<div ref={formRef} className="scroll-mt-4 lg:sticky lg:top-4">
<Section title={editing ? t.ledger.editTitle : t.ledger.formTitle}>
{catalogFailed?.error ? (
<PageError message={catalogFailed.error.message} onRetry={catalogFailed.refetch} />
) : !catalog ? (
<PageLoading />
) : (
<EntryForm
key={editing?.id ?? 'new'}
period={period}
week={week}
today={today}
categories={catalog.c}
methods={catalog.m}
entry={editing ?? undefined}
onSaved={(date) => goTo(weekIndexFor(period.weeks, date))}
onCancel={() => setEditing(null)}
/>
)}
</Section>
</div>
<div className="space-y-4">
{!readOnly && (
<div ref={formRef} className="scroll-mt-4 lg:sticky lg:top-4">
<Section title={editing ? t.ledger.editTitle : t.ledger.formTitle}>
{catalogFailed?.error ? (
<PageError message={catalogFailed.error.message} onRetry={catalogFailed.refetch} />
) : !catalog ? (
<PageLoading />
) : (
<EntryForm
key={editing?.id ?? 'new'}
period={period}
week={week}
today={today}
categories={catalog.c}
methods={catalog.m}
entry={editing ?? undefined}
onSaved={(date) => goTo(weekIndexFor(period.weeks, date))}
onCancel={() => setEditing(null)}
/>
)}
</Section>
</div>
)}
<div className={readOnly ? 'space-y-4 lg:col-span-2' : 'space-y-4'}>
<Section title={t.ledger.summaryTitle}>
{budget.isError ? (
<PageError message={budget.error.message} onRetry={budget.refetch} />
@@ -120,6 +135,7 @@ function LedgerView({ period }: { period: Period }) {
categories={catalog.c}
methods={catalog.m}
editingId={editing?.id}
readOnly={readOnly}
onEdit={edit}
onDeleted={(id) => editing?.id === id && setEditing(null)}
/>