import { useState, type ReactNode } from 'react' import { zodResolver } from '@hookform/resolvers/zod' import { CircleCheck, Info, TriangleAlert } from 'lucide-react' import { Controller, useForm, useWatch } from 'react-hook-form' import { Link } from 'react-router' import { paths } from '@/components/layout/nav-items' import { FormError, FormField } from '@/components/form-field' 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 { 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' import type { Period } from '@/types/Period' import type { PeriodCloseResult } from '@/types/PeriodCloseResult' import type { PeriodReview } from '@/types/PeriodReview' import { closeSchema, type CloseForm } from './close-schema' import { Questions } from './questions' const s = t.closing /** * Dönem kapatma sihirbazı: Özet → Birikim → Onay → Sonuç. * * 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 [result, setResult] = useState(null) const empty = (

{s.noOpen}

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

{s.alreadyClosed}

) : ( ) }
)}
) } function Wizard({ period, onClosed, }: { period: Period onClosed: (r: PeriodCloseResult) => void }) { const review = useReview(period.id) const [step, setStep] = useState(1) /** Seçilen tutar (kuruş); seçilmediyse birikimin tamamı */ const [toSavings, setToSavings] = useState(null) if (review.isError) return if (!review.data) return const realized = review.data.realized_savings const go = (n: number) => { setStep(n) window.scrollTo({ top: 0 }) } return ( <> {step === 1 && go(2)} />} {step === 2 && ( go(1)} onNext={(v) => { setToSavings(v) go(3) }} /> )} {step === 3 && ( go(2)} onClosed={onClosed} /> )} ) } // ---------- 1. Özet ---------- function StepSummary({ period, review, onNext, }: { period: Period review: PeriodReview onNext: () => void }) { const [today] = useState(() => toIsoDate(new Date())) return (
{today <= period.last_date && ( {s.early(formatDate(period.last_date))} )} {!period.reflection_md && ( {s.noReflection}{' '} {s.writeReflection} )}
) } // ---------- 2. Birikim ---------- function StepSavings({ realized, initial, onBack, onNext, }: { realized: number initial: number onBack: () => void onNext: (toSavings: number) => void }) { if (realized <= 0) { return (
onNext(0)} />
) } return } function SavingsForm({ realized, initial, onBack, onNext, }: { realized: number initial: number onBack: () => void onNext: (toSavings: number) => void }) { const { control, handleSubmit, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(closeSchema(realized)), mode: 'onTouched', defaultValues: { to_savings: formatNumber(initial / 100) }, }) const value = useWatch({ control, name: 'to_savings' }) const preview = settlePreview(realized, parseMoney(value) ?? 0) return (
onNext(parseMoney(v.to_savings) ?? 0))} noValidate>
( {(f) => ( )} )} />
{[ { label: s.all, amount: realized }, { label: s.none, amount: 0 }, ].map((o) => ( ))}
) } // ---------- 3. Onay ---------- function StepConfirm({ period, realized, toSavings, onBack, onClosed, }: { period: Period realized: number toSavings: number onBack: () => void onClosed: (r: PeriodCloseResult) => void }) { const close = useClosePeriod() const [error, setError] = useState() const preview = settlePreview(realized, toSavings) async function submit() { setError(undefined) try { const r = await close.mutateAsync({ id: period.id, version: period.version, // Açıkta birikim istenmez (backend reddeder) to_savings: realized > 0 ? preview.toSavings : undefined, }) onClosed(r) window.scrollTo({ top: 0 }) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } } return (
{s.willClose(period.name)}

{s.reopenNote}

) } // ---------- Sonuç ---------- function Result({ result: r }: { result: PeriodCloseResult }) { const attached = !!r.carryover?.to_period_id return (

{s.done(r.period.name)}

{r.carryover ? (attached ? s.resultAttached : s.resultHint) : s.noCarry}

) } // ---------- ortak ---------- /** Gerçekleşen birikim → birikime aktarılan + devreden (önizleme ve sonuçta aynı görünüm) */ function Breakdown({ realized, toSavings, carry, }: { realized: number toSavings: number carry: { kind: 'debt' | 'surplus'; amount: number } | null }) { const rows = [ { label: s.realized, amount: realized, tone: realized < 0 ? 'text-expense' : '' }, ...(realized >= 0 ? [{ label: s.savingsLine, amount: toSavings, tone: 'text-income' }] : []), carry ? { label: carry.kind === 'debt' ? s.debtLine : s.surplusLine, amount: carry.amount, tone: carry.kind === 'debt' ? 'text-expense' : 'text-income', } : { label: s.carryLine, amount: 0, tone: 'text-muted-foreground' }, ] return (
{rows.map((row) => (
{row.label}
{formatMoney(row.amount)}
))}
) } function Notice({ tone = 'info', children }: { tone?: 'info' | 'warning'; children: ReactNode }) { const Icon = tone === 'warning' ? TriangleAlert : Info return (

{children}

) }