feat(plan): ilk dönem sihirbazı (dönem, gelir, gider, birikim, bütçe)

- /plan/donem-ac?adim=1..5: her adım hemen kaydedilir, adım adreste
- Panel ve Plan'da açık dönem yoksa "Dönemi başlat" kartı
- MoneyInput (sayısal klavye, Türkçe biçim), moneyText/toKurus, period-dates
- lib/periods.ts: dönem, plan kalemi, özet, bütçe ve öneri kancaları
- SummaryTiles, PageLoading/PageError; 13 yeni vitest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 08:16:32 +03:00
parent 95231cd4d6
commit ca0d3a505c
26 changed files with 1594 additions and 7 deletions
@@ -0,0 +1,55 @@
import { z } from 'zod'
import { moneyText } from '@/lib/money-schema'
import { daysInclusive, isIsoDate, MAX_PERIOD_DAYS } from '@/lib/period-dates'
import { t } from '@/lib/strings'
const s = t.wizard
/** 1. adım: dönem tarihleri. `last` = son gün (dahil). */
export const datesSchema = z
.object({
start: z.string(),
last: z.string(),
name: z.string().trim().max(60, s.dates.nameTooLong),
})
.superRefine((v, ctx) => {
if (!isIsoDate(v.start)) {
ctx.addIssue({ code: 'custom', path: ['start'], message: s.dates.startRequired })
}
if (!isIsoDate(v.last)) {
ctx.addIssue({ code: 'custom', path: ['last'], message: s.dates.lastRequired })
return
}
if (!isIsoDate(v.start)) return
if (v.last < v.start) {
ctx.addIssue({ code: 'custom', path: ['last'], message: s.dates.lastBeforeStart })
} else if (daysInclusive(v.start, v.last) > MAX_PERIOD_DAYS) {
ctx.addIssue({ code: 'custom', path: ['last'], message: s.dates.tooLong(MAX_PERIOD_DAYS) })
}
})
export type DatesForm = z.infer<typeof datesSchema>
/** 2–3. adım: gelir / gider kalemi. Tarih boş olabilir; doluysa dönem içinde olmalı. */
export function itemSchema(start: string, last: string) {
return z.object({
name: z.string().trim().min(1, s.items.nameRequired).max(60, s.items.nameTooLong),
amount: moneyText('positive'),
date: z
.string()
.refine((d) => d === '' || (isIsoDate(d) && d >= start && d <= last), s.items.dateOutside),
})
}
export type ItemForm = z.infer<ReturnType<typeof itemSchema>>
/** 4. adım */
export const savingsSchema = z.object({ goal: moneyText('optional') })
export type SavingsForm = z.infer<typeof savingsSchema>
/** 5. adım: 4 grubun aylık bütçesi */
export const budgetSchema = z.object({
needs: moneyText('optional'),
wants: moneyText('optional'),
culture: moneyText('optional'),
unexpected: moneyText('optional'),
})
export type BudgetForm = z.infer<typeof budgetSchema>