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:
@@ -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>
|
||||
Reference in New Issue
Block a user