feat(birikim): dönem kapanışındaki birikimi varlıklara ekleme
- Kapatma sonucunda onayla "Birikime ekle" (TL alımı, dönemin son günü) - Birikimler'de "Bekleyen birikimler" listesi - Yeniden kapatmada bağlı kayıt yeni tutara güncellenir (0 → silinir), TL çıkışları aşılırsa reddedilir - Panel "Toplam birikim" artık portföyün güncel değeri Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@ import { dailyAllowance } from '@/lib/dashboard'
|
||||
import { formatDayMonth, formatMoney, toIsoDate } from '@/lib/format'
|
||||
import { weekIndexFor } from '@/lib/ledger'
|
||||
import { periodPath } from '@/lib/period-select'
|
||||
import { useBudget, useStats, useSummary } from '@/lib/periods'
|
||||
import { usePortfolio } from '@/lib/assets'
|
||||
import { useBudget, useSummary } from '@/lib/periods'
|
||||
import { t } from '@/lib/strings'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { GroupBudget } from '@/types/GroupBudget'
|
||||
@@ -27,7 +28,7 @@ const DashboardCharts = lazy(() => import('./charts'))
|
||||
export function Overview({ period, writable }: { period: Period; writable: boolean }) {
|
||||
const summary = useSummary(period.id)
|
||||
const budget = useBudget(period.id)
|
||||
const stats = useStats()
|
||||
const portfolio = usePortfolio()
|
||||
const [today] = useState(() => toIsoDate(new Date()))
|
||||
|
||||
const failed = [summary, budget].find((q) => q.isError)
|
||||
@@ -52,7 +53,7 @@ export function Overview({ period, writable }: { period: Period; writable: boole
|
||||
<Tile
|
||||
emoji="🐷"
|
||||
label={s.saved}
|
||||
amount={stats.data?.total_saved}
|
||||
amount={portfolio.data?.value_total}
|
||||
hint={s.savedHint}
|
||||
tone="text-income"
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 { useAddPeriodSavings } from '@/lib/assets'
|
||||
import { periodPath } from '@/lib/period-select'
|
||||
import { useClosePeriod, useReview } from '@/lib/periods'
|
||||
import { settlePreview } from '@/lib/review'
|
||||
@@ -308,6 +309,12 @@ function Result({ result: r }: { result: PeriodCloseResult }) {
|
||||
<p className="text-muted-foreground">
|
||||
{r.carryover ? (attached ? s.resultAttached : s.resultHint) : s.noCarry}
|
||||
</p>
|
||||
{r.to_savings > 0 &&
|
||||
(r.savings_linked ? (
|
||||
<p className="text-sm text-muted-foreground">{s.savingsUpdated}</p>
|
||||
) : (
|
||||
<AddToSavings periodId={r.period.id} amount={r.to_savings} />
|
||||
))}
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button asChild size="lg">
|
||||
<Link to={attached ? paths.planning : paths.periodWizard}>
|
||||
@@ -322,6 +329,39 @@ function Result({ result: r }: { result: PeriodCloseResult }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Kapanıştaki birikimi TL varlığına ekleme (onay = düğme) */
|
||||
function AddToSavings({ periodId, amount }: { periodId: string; amount: number }) {
|
||||
const add = useAddPeriodSavings()
|
||||
const [error, setError] = useState<string>()
|
||||
if (add.isSuccess) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-xl bg-income/10 p-3 text-sm">
|
||||
<CircleCheck className="size-5 text-income" aria-hidden />
|
||||
<span className="font-medium">{s.addedToSavings}</span>
|
||||
<Link to={paths.savings} className="underline">
|
||||
{s.toSavingsPage}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-2 rounded-xl border p-3">
|
||||
<p className="text-sm text-muted-foreground">{s.addToSavingsHint}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={add.isPending}
|
||||
onClick={() => {
|
||||
setError(undefined)
|
||||
add.mutate(periodId, { onError: (e) => setError(e.message) })
|
||||
}}
|
||||
>
|
||||
{s.addToSavings(formatMoney(amount))}
|
||||
</Button>
|
||||
<FormError message={error} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- ortak ----------
|
||||
|
||||
/** Gerçekleşen birikim → birikime aktarılan + devreden (önizleme ve sonuçta aynı görünüm) */
|
||||
|
||||
@@ -12,6 +12,7 @@ import { cn } from '@/lib/utils'
|
||||
import type { Portfolio } from '@/types/Portfolio'
|
||||
import type { PortfolioItem } from '@/types/PortfolioItem'
|
||||
import { AddCurrency } from './add-currency'
|
||||
import { PendingSavings } from './pending'
|
||||
import { Notice, Pl, RateLine } from './parts'
|
||||
|
||||
const s = t.assets
|
||||
@@ -46,6 +47,7 @@ function SavingsView({ p }: { p: Portfolio }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PendingSavings />
|
||||
<Summary p={p} />
|
||||
{p.last_fetch === null ? (
|
||||
<Notice tone="warning">{s.noFetch}</Notice>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAddPeriodSavings, usePendingSavings } from '@/lib/assets'
|
||||
import { formatDate, formatMoney } from '@/lib/format'
|
||||
import { t } from '@/lib/strings'
|
||||
|
||||
const s = t.assets
|
||||
|
||||
/** Kapanan dönemlerin henüz eklenmemiş birikimleri: tek dokunuşla TL alımı olarak ekle */
|
||||
export function PendingSavings() {
|
||||
const pending = usePendingSavings()
|
||||
const add = useAddPeriodSavings()
|
||||
if (!pending.data?.length) return null
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="pending-title"
|
||||
className="rounded-2xl border-2 border-income/40 bg-income/5 p-4 sm:p-5"
|
||||
>
|
||||
<h2 id="pending-title" className="font-semibold">
|
||||
<span aria-hidden>🐷</span> {s.pendingTitle}
|
||||
</h2>
|
||||
<p className="mb-2 text-sm text-muted-foreground">{s.pendingHint}</p>
|
||||
<ul className="divide-y">
|
||||
{pending.data.map((p) => (
|
||||
<li key={p.period_id} className="flex items-center gap-3 py-2.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
{p.name} · <span className="money">{formatMoney(p.to_savings)}</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{s.pendingLine(formatDate(p.last_date))}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
disabled={add.isPending}
|
||||
onClick={() =>
|
||||
add.mutate(p.period_id, {
|
||||
onSuccess: () => toast.success(s.added(p.name)),
|
||||
onError: (e) => toast.error(e.message),
|
||||
})
|
||||
}
|
||||
>
|
||||
{s.add}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type { AssetTransaction } from '@/types/AssetTransaction'
|
||||
import type { AssetTxCreateRequest } from '@/types/AssetTxCreateRequest'
|
||||
import type { AssetTxUpdateRequest } from '@/types/AssetTxUpdateRequest'
|
||||
import type { AssetUpdateRequest } from '@/types/AssetUpdateRequest'
|
||||
import type { PendingSavings } from '@/types/PendingSavings'
|
||||
import type { Portfolio } from '@/types/Portfolio'
|
||||
import type { Rate } from '@/types/Rate'
|
||||
import type { RateCreateRequest } from '@/types/RateCreateRequest'
|
||||
@@ -63,3 +64,17 @@ export const useSetRate = () =>
|
||||
useAssetMutation((body: RateCreateRequest) => api.post<Rate>('/rates', body))
|
||||
|
||||
export const useDeleteRate = () => useAssetMutation((id: string) => api.delete(`/rates/${id}`))
|
||||
|
||||
/** Kapanıp birikime henüz eklenmemiş dönemler */
|
||||
export function usePendingSavings() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.pendingSavings,
|
||||
queryFn: () => api.get<PendingSavings[]>('/savings/pending'),
|
||||
})
|
||||
}
|
||||
|
||||
/** Dönemin kapanıştaki birikimini TL varlığına ekle (dönem başına bir kez) */
|
||||
export const useAddPeriodSavings = () =>
|
||||
useAssetMutation((periodId: string) =>
|
||||
api.post<AssetTransaction>(`/periods/${periodId}/to-savings`),
|
||||
)
|
||||
|
||||
@@ -84,10 +84,12 @@ export function useClosePeriod() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...body }: PeriodCloseRequest & { id: string }) =>
|
||||
api.post<PeriodCloseResult>(`/periods/${id}/close`, body),
|
||||
// Yeniden kapatma birikimdeki bağlı kaydı da değiştirebilir → portföy de yenilenir
|
||||
onSettled: () =>
|
||||
Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.periods }),
|
||||
qc.invalidateQueries({ queryKey: ['period'] }),
|
||||
qc.invalidateQueries({ queryKey: queryKeys.portfolio }),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const queryKeys = {
|
||||
/** Birikimler: portföy ve varlık işlemleri bu önekle başlar (birlikte yenilenir) */
|
||||
portfolio: ['portfolio'] as const,
|
||||
assetTxs: (assetId: string) => ['portfolio', 'txs', assetId] as const,
|
||||
pendingSavings: ['portfolio', 'pending'] as const,
|
||||
period: (id: string) => ['period', id] as const,
|
||||
planItems: (id: string) => ['period', id, 'plan-items'] as const,
|
||||
summary: (id: string) => ['period', id, 'summary'] as const,
|
||||
|
||||
@@ -356,7 +356,7 @@ export const t = {
|
||||
income: 'Toplam gelir',
|
||||
fixed: 'Sabit giderler',
|
||||
saved: 'Toplam birikim',
|
||||
savedHint: 'Kapanan dönemlerde birikime ayrılanlar',
|
||||
savedHint: 'Birikimlerin güncel değeri',
|
||||
debt: 'Devreden borç',
|
||||
debtHint: 'Önceki dönemden; harcanabilirden düşüldü',
|
||||
groupsTitle: 'Planlanan / gerçekleşen',
|
||||
@@ -487,6 +487,11 @@ export const t = {
|
||||
activate: 'Etkinleştir',
|
||||
statusHint: 'Pasif varlık listede kalır ama yeni işlem alınmaz.',
|
||||
notFound: 'Varlık bulunamadı.',
|
||||
pendingTitle: 'Bekleyen birikimler',
|
||||
pendingHint: 'Kapanan dönemlerde birikime ayırdığın ama buraya henüz eklenmemiş tutarlar.',
|
||||
pendingLine: (date: string) => `${date} tarihli TL alımı olarak eklenir`,
|
||||
add: 'Ekle',
|
||||
added: (name: string) => `${name} birikimi eklendi`,
|
||||
},
|
||||
|
||||
periods: {
|
||||
@@ -535,6 +540,12 @@ export const t = {
|
||||
toDashboard: 'Panele dön',
|
||||
noOpen: 'Kapatılacak açık dönem yok.',
|
||||
alreadyClosed: 'Bu dönem zaten kapalı.',
|
||||
addToSavings: (x: string) => `🐷 Birikime ekle (${x})`,
|
||||
addToSavingsHint:
|
||||
'Onaylarsan bu tutar Birikimler’de, dönemin son günü tarihli Türk Lirası alımı olarak görünür.',
|
||||
addedToSavings: 'Birikime eklendi',
|
||||
toSavingsPage: 'Birikimlere git',
|
||||
savingsUpdated: 'Birikimlerdeki kayıt yeni tutarla güncellendi.',
|
||||
},
|
||||
|
||||
common: {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PendingSavings = { period_id: string, name: string,
|
||||
/**
|
||||
* Dönemin son günü (eklenecek alımın tarihi)
|
||||
*/
|
||||
last_date: string,
|
||||
/**
|
||||
* Kuruş
|
||||
*/
|
||||
to_savings: number, };
|
||||
@@ -10,4 +10,9 @@ realized_savings: number, to_savings: number,
|
||||
/**
|
||||
* Sonraki döneme devreden; yoksa `null`
|
||||
*/
|
||||
carryover: Carryover | null, };
|
||||
carryover: Carryover | null,
|
||||
/**
|
||||
* Birikimlerde bu döneme bağlı kayıt var (yeniden kapatmada yeni tutara güncellendi).
|
||||
* `false` ve `to_savings > 0` → arayüz "Birikime ekle" önerir.
|
||||
*/
|
||||
savings_linked: boolean, };
|
||||
|
||||
Reference in New Issue
Block a user