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
+134
View File
@@ -0,0 +1,134 @@
/**
* Dönem, plan kalemleri, özet ve bütçe: sunucu verisi kancaları (TanStack Query).
*
* Kural: bir dönemde bir şey değişince o döneme bağlı tüm sorgular
* (`queryKeys.period(id)` önekiyle) yenilenir → özet ve öneri hep güncel kalır.
*/
import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
import type { BudgetSuggestion } from '@/types/BudgetSuggestion'
import type { CopyPreviousResult } from '@/types/CopyPreviousResult'
import type { GroupAmounts } from '@/types/GroupAmounts'
import type { GroupBudget } from '@/types/GroupBudget'
import type { Period } from '@/types/Period'
import type { PeriodCreateRequest } from '@/types/PeriodCreateRequest'
import type { PeriodSummary } from '@/types/PeriodSummary'
import type { PeriodUpdateRequest } from '@/types/PeriodUpdateRequest'
import type { PlanItem } from '@/types/PlanItem'
import type { PlanItemCreateRequest } from '@/types/PlanItemCreateRequest'
import type { SuggestMethod } from '@/types/SuggestMethod'
import { api } from './api'
import { queryKeys } from './query'
function refreshPeriod(qc: QueryClient, id: string) {
return qc.invalidateQueries({ queryKey: queryKeys.period(id) })
}
// ---------- Dönem ----------
/** Açık dönem: `undefined` yükleniyor, `null` açık dönem yok */
export function useCurrentPeriod() {
return useQuery({
queryKey: queryKeys.currentPeriod,
queryFn: () => api.get<Period | null>('/periods/current'),
})
}
export function usePeriods() {
return useQuery({
queryKey: queryKeys.periods,
queryFn: () => api.get<Period[]>('/periods'),
})
}
export function useCreatePeriod() {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: PeriodCreateRequest) => api.post<Period>('/periods', body),
onSuccess: (p) => {
qc.setQueryData(queryKeys.currentPeriod, p)
void qc.invalidateQueries({ queryKey: queryKeys.periods, exact: true })
},
})
}
export function useUpdatePeriod() {
const qc = useQueryClient()
return useMutation({
mutationFn: ({ id, ...body }: PeriodUpdateRequest & { id: string }) =>
api.patch<Period>(`/periods/${id}`, body),
onSuccess: (p) => {
if (p.status === 'open') qc.setQueryData(queryKeys.currentPeriod, p)
void qc.invalidateQueries({ queryKey: queryKeys.periods, exact: true })
void refreshPeriod(qc, p.id)
},
})
}
// ---------- Plan kalemleri ----------
export function usePlanItems(periodId: string) {
return useQuery({
queryKey: queryKeys.planItems(periodId),
queryFn: () => api.get<PlanItem[]>(`/periods/${periodId}/plan-items`),
})
}
export function useCreatePlanItem(periodId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: PlanItemCreateRequest) =>
api.post<PlanItem>(`/periods/${periodId}/plan-items`, body),
onSuccess: () => refreshPeriod(qc, periodId),
})
}
export function useDeletePlanItem(periodId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (itemId: string) => api.delete(`/plan-items/${itemId}`),
onSuccess: () => refreshPeriod(qc, periodId),
})
}
export function useCopyPrevious(periodId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: () => api.post<CopyPreviousResult>(`/periods/${periodId}/plan-items/copy-previous`),
onSuccess: () => refreshPeriod(qc, periodId),
})
}
// ---------- Özet ve bütçe ----------
export function useSummary(periodId: string) {
return useQuery({
queryKey: queryKeys.summary(periodId),
queryFn: () => api.get<PeriodSummary>(`/periods/${periodId}/summary`),
})
}
export function useBudget(periodId: string) {
return useQuery({
queryKey: queryKeys.budget(periodId),
queryFn: () => api.get<GroupBudget[]>(`/periods/${periodId}/budget`),
})
}
export function useSuggestion(periodId: string, method: SuggestMethod) {
return useQuery({
queryKey: queryKeys.suggestion(periodId, method),
queryFn: () =>
api.get<BudgetSuggestion>(`/periods/${periodId}/budget/suggestion?method=${method}`),
})
}
export function useSetBudget(periodId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: GroupAmounts) => api.put<GroupBudget[]>(`/periods/${periodId}/budget`, body),
onSuccess: (groups) => {
qc.setQueryData(queryKeys.budget(periodId), groups)
void qc.invalidateQueries({ queryKey: queryKeys.summary(periodId) })
},
})
}