feat(değerlendirme): dönem kapatma sihirbazı

- /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 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 22:59:38 +03:00
parent 311cfc8c60
commit 2c16cf4163
16 changed files with 639 additions and 18 deletions
@@ -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 <PageLoading />
if (periods.isError)
return <PageError message={periods.error.message} onRetry={periods.refetch} />
// 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 <DatesStepForm period={period} prevEnd={previous?.end_date} onDone={onDone} />
}
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 {
@@ -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 (
<nav aria-label={s.title} className="mb-6">
<nav aria-label={label} className="mb-6">
<p className="mb-2 text-sm font-medium sm:hidden">
{s.stepOf(current, total)} · {s.steps[current - 1]}
{s.stepOf(current, total)} · {steps[current - 1]}
</p>
<div className="h-1.5 overflow-hidden rounded-full bg-muted sm:hidden" aria-hidden>
<div
@@ -21,13 +29,13 @@ export function Stepper({ current }: { current: number }) {
/>
</div>
<ol className="hidden gap-2 sm:flex">
{s.steps.map((label, i) => {
{steps.map((name, i) => {
const n = i + 1
const done = n < current
const active = n === current
return (
<li
key={label}
key={name}
aria-current={active ? 'step' : undefined}
className={cn(
'flex flex-1 items-center gap-2 rounded-lg border px-3 py-2 text-sm',
@@ -43,7 +51,7 @@ export function Stepper({ current }: { current: number }) {
>
{done ? <Check className="size-3.5" aria-hidden /> : n}
</span>
{label}
{name}
</li>
)
})}