feat(plan): plan ekranı — görüntüleme/düzenleme modu ve haftalık bütçe

- Salt okunur görünüm: özet kutucukları, gelir/gider, 4 grup + haftalık döküm
- Düzenle → taslak; Kaydet yalnızca değişenleri gönderir, Vazgeç geri alır
- Kaydedilmemiş değişiklikle çıkışta onay
- Hafta elle ayarlama / otomatiğe döndürme, canlı önizleme (backend ile aynı hesap)
- DistributionBar ve SuggestionPicker ortak bileşen; 9 yeni vitest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 08:47:00 +03:00
parent ca0d3a505c
commit 9004a7cc42
18 changed files with 1276 additions and 110 deletions
+33
View File
@@ -17,6 +17,7 @@ import type { PlanItem } from '@/types/PlanItem'
import type { PlanItemCreateRequest } from '@/types/PlanItemCreateRequest'
import type { SuggestMethod } from '@/types/SuggestMethod'
import { api } from './api'
import type { PlanOps } from './plan-draft'
import { queryKeys } from './query'
function refreshPeriod(qc: QueryClient, id: string) {
@@ -132,3 +133,35 @@ export function useSetBudget(periodId: string) {
},
})
}
// ---------- Plan ekranı: toplu kaydet ----------
/**
* Düzenleme modundaki değişiklikleri sırayla gönderir (`diffPlan` çıktısı).
* Tek transaction değildir: biri hata verirse kalanlar gönderilmez, hata fırlatılır;
* her durumda dönem verisi yeniden yüklenir → ekran sunucudaki gerçek durumu gösterir.
*/
export function useSavePlan(period: Period) {
const qc = useQueryClient()
return useMutation({
mutationFn: async (ops: PlanOps) => {
const pid = period.id
if (ops.savings !== null) {
await api.patch<Period>(`/periods/${pid}`, {
savings_goal: ops.savings,
version: period.version,
})
}
for (const id of ops.deletes) await api.delete(`/plan-items/${id}`)
for (const { id, ...body } of ops.updates) await api.patch(`/plan-items/${id}`, body)
for (const body of ops.creates) await api.post(`/periods/${pid}/plan-items`, body)
if (ops.monthly) await api.put(`/periods/${pid}/budget`, ops.monthly)
for (const body of ops.weeks) await api.put(`/periods/${pid}/budget/weeks`, body)
},
onSettled: () =>
Promise.all([
qc.invalidateQueries({ queryKey: queryKeys.periods }),
refreshPeriod(qc, period.id),
]),
})
}