diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 5049c50..c9ad0f9 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,10 +1,12 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**Faz 7 – Adım 2:** Panel özet kartları (`features/dashboard/`): gelir, sabit gider, toplam birikim (`/stats/periods` → `total_saved`; "varlık değerleri Faz 8'de" notu), devreden borç (varsa), grup planlanan/gerçekleşen (Haftalık/Aylık geçişi, `useBudget`), **anlık durum** = kalan harcanabilir (en belirgin). Mobilde yüzen "➕ Harcama Ekle" → Defter (tutara odak). Skeleton + boş durumlar. Açık dönem yoksa son dönem salt okunur + "Yeni dönemi aç". **Ardından sorulacak:** Ayarlar'da kategori/ödeme türü yönetimi ekranı hiçbir fazda planlı değil (kullanıcı Defter'de kategorileri kısıtlı buldu; 3. kat alt kategori istiyor mu?). +**Faz 7 – Adım 3:** Grafikler (Recharts — yeni bağımlılık, gerekçe ver): 1) harcanabilir (günlere eşit dağıtılmış, kümülatif) vs gerçekleşen (kümülatif), 2) 4 grup kümülatif + harcanabilir çizgisi; sekmeler Günlük (`/periods/{id}/daily`) / Haftalık (aynı veriden `period.weeks` ile) / Aylık (`useStats` → son 12 dönem, harcanabilir vs harcanan). Saf seri hesapları `lib/` + test; koyu tema renkleri CSS token'larından; grafik altında kısa metin özeti (erişilebilirlik). **Bekleyen soru:** kategori yönetimi ekranı Faz 7'den hemen sonra mı (önerilen), Faz 10'da mı? **Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅. ## 📌 Alınan Kararlar +- **Kategoriler (kullanıcı, 2026-11-12):** 3. kat alt kategori **yok** (grup → kategori). Şartı: raporlarda **tüm** kategorilerin dağılımı görünmeli (yalnızca ilk 5 değil). Amaç "market ne için?" gibi kırılım → kategoriyi bölerek (ör. Market · Gıda / Temizlik); bunun için Ayarlar'da kategori yönetimi ekranı gerekli. +- **Canlı sürüm eski (2026-11-12 kontrol):** push'lar Gitea'da ama Dokploy yeniden derlemiyor (Faz 4+ yok, `/api/stats/periods` 404). Auto Deploy için Gitea webhook'u gerekiyor → kullanıcıya adım adım anlatıldı. - **Yerel demo hesapları** (`kakeibo_dev`): `demo@ornek.com` / `demo12345` (owner, "Demo Hanesi") · `es@ornek.com` / `demo12345` (member, aynı hane). `npm run db:reset` silerse API ile yeniden oluştur (ileride `db:seed`). - Sürüm kontrolü: Gitea (`https://gitea.ta1sph.info/suphicakir/kakeibo.git`), dal `main`. - Her adım sonunda Claude commit atar ve haber verir; push'u kullanıcı kendi uygulamasıyla yapar (Claude `git push` çalıştırmaz). @@ -116,3 +118,4 @@ ### 2026-11-12 — Faz 7 - Kararlar (kullanıcı onayı): toplam birikim Faz 8'e kadar = kapanışlarda birikime aktarılanların toplamı; sekmeler Günlük/Haftalık = bu dönem (kümülatif, harcanabilir günlere eşit dağıtılır), Aylık = son 12 dönem; açık dönem yoksa son dönem salt okunur. - **Adım 1:** `services/stats.rs` + `routes/stats.rs`: `GET /periods/{id}/daily` (`DaySpent[]`: dönemin her günü, `spent` = `GroupAmounts`, harcama yoksa 0; `budget::spent_by_group_and_date` artık `pub(crate)`), `GET /stats/periods?limit=12` (`PeriodStats`: `total_saved` tüm dönemlerin `to_savings` toplamı — limitten bağımsız, yeniden açılan dönemin eski tutarı sayılır; `periods` eskiden yeniye, her biri `budget::summary` ile → dönem başına ~5 sorgu, en fazla 24). `tests/stats.rs` (2). Mutasyon 2/2 yakalandı. Backend 118 test. +- **Adım 2:** Panel (`features/dashboard/`): `page.tsx` (açık dönem; yoksa `NoPeriodCard` + son dönem salt okunur, `lib/dashboard.ts` → `dashboardPeriod`), `overview.tsx` (kartlar: gelir, sabit, toplam birikim `useStats().total_saved`, borç yalnızca varsa — 3 kartta ızgara 3 sütun; **Anlık durum** DOM'da önce/mobilde üstte, masaüstünde sağda: kalan harcanabilir + `dailyAllowance` = kalan ÷ kalan gün (bugün dahil, kuruş aşağı); gruplar `WeekSummary` artık `index: number | 'month'`, Haftalık/Aylık geçişi; boş durumlar: bütçe dağıtılmamış → Plan, harcama yok → Defter). "Harcama Ekle": masaüstünde başlıkta, mobilde yüzen (alt menünün üstü) → `/defter` `state.focus` → `EntryForm autoFocus`. `components/skeleton.tsx`. `queryKeys.stats = ['periods','stats']` (kapatma/plan ile yenilenir), kayıt mutasyonları da stats'ı yeniler. Frontend 95 test; e2e 22 kontrol (DB silindi). Canlı yayın 2026-11-12 güncellendi (kullanıcı Deploy yaptı; `/api/stats/periods` 401). diff --git a/docs/PHASES.md b/docs/PHASES.md index 6a7a741..9a98066 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -69,5 +69,5 @@ ## Faz 7 Adımları - ✅ 1. Backend: `GET /periods/{id}/daily` (gün × grup harcama; günlük/haftalık grafik verisi) + `GET /stats/periods` (son 12 dönem: harcanabilir, harcanan, birikime aktarılan; aylık grafik ve toplam birikim) -- ⏳ 2. Frontend: özet kartları (gelir, sabit gider, toplam birikim, devreden borç, grup planlanan/gerçekleşen haftalık–aylık, **anlık durum** en belirgin) + "➕ Harcama Ekle" (mobilde yüzen düğme) + skeleton ve boş durumlar +- ✅ 2. Frontend: özet kartları (gelir, sabit gider, toplam birikim, devreden borç, grup planlanan/gerçekleşen haftalık–aylık, **anlık durum** en belirgin) + "➕ Harcama Ekle" (mobilde yüzen düğme) + skeleton ve boş durumlar - ⏳ 3. Frontend: grafikler (Recharts) — kümülatif harcanabilir vs. gerçekleşen; 4 grup + harcanabilir; Günlük / Haftalık / Aylık sekmeleri, koyu tema, erişilebilir özet diff --git a/frontend/src/components/skeleton.tsx b/frontend/src/components/skeleton.tsx new file mode 100644 index 0000000..695979a --- /dev/null +++ b/frontend/src/components/skeleton.tsx @@ -0,0 +1,6 @@ +import { cn } from '@/lib/utils' + +/** Yükleniyor yer tutucusu: içeriğin şeklinde gri, nabız gibi atan kutu */ +export function Skeleton({ className }: { className?: string }) { + return
+} diff --git a/frontend/src/features/dashboard/overview.tsx b/frontend/src/features/dashboard/overview.tsx new file mode 100644 index 0000000..5321383 --- /dev/null +++ b/frontend/src/features/dashboard/overview.tsx @@ -0,0 +1,255 @@ +import { useState } from 'react' +import { Link } from 'react-router' +import { paths } from '@/components/layout/nav-items' +import { PageError } from '@/components/page' +import { Skeleton } from '@/components/skeleton' +import { Button } from '@/components/ui/button' +import { WeekSummary } from '@/features/ledger/week-summary' +import { PeriodHeading } from '@/features/planning/plan-read' +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 { t } from '@/lib/strings' +import { cn } from '@/lib/utils' +import type { GroupBudget } from '@/types/GroupBudget' +import type { Period } from '@/types/Period' +import type { PeriodSummary } from '@/types/PeriodSummary' + +const s = t.dashboard + +/** Özet kartları: gelir, sabit gider, toplam birikim, devreden borç, anlık durum, gruplar */ +export function Overview({ period, writable }: { period: Period; writable: boolean }) { + const summary = useSummary(period.id) + const budget = useBudget(period.id) + const stats = useStats() + const [today] = useState(() => toIsoDate(new Date())) + + const failed = [summary, budget].find((q) => q.isError) + if (failed?.error) return + if (!summary.data || !budget.data) return + const sum = summary.data + + return ( +
+ + {/* Borç kartı yalnızca varsa: 3 kartta mobilde sonuncusu tam genişlik, masaüstünde 3 sütun */} +
0 + ? 'lg:grid-cols-4' + : 'lg:grid-cols-3 [&>*:last-child]:col-span-2 lg:[&>*:last-child]:col-span-1', + )} + > + + + + {sum.carryover_debt > 0 && ( + + )} +
+ {/* Anlık durum DOM'da önce (ekran okuyucu ve mobilde ilk), masaüstünde sağ sütunda */} +
+ + +
+
+ ) +} + +function Tile({ + emoji, + label, + amount, + hint, + tone, +}: { + emoji: string + label: string + /** `undefined` → yükleniyor */ + amount: number | undefined + hint?: string + tone?: string +}) { + return ( +
+
+ {emoji} {label} +
+
+ {amount === undefined ? : formatMoney(amount)} +
+ {hint &&
{hint}
} +
+ ) +} + +/** En belirgin kart: kalan harcanabilir + günlük öneri */ +function StatusCard({ + period, + summary, + today, + writable, +}: { + period: Period + summary: PeriodSummary + today: string + writable: boolean +}) { + const over = summary.remaining < 0 + const { daysLeft, perDay } = dailyAllowance(summary.remaining, period, today) + return ( +
+

+ 📍 {s.statusTitle} +

+

{over ? s.overspent : s.remaining}

+

+ {formatMoney(over ? -summary.remaining : summary.remaining)} +

+

+ {s.statusDetail(formatMoney(summary.spendable), formatMoney(summary.spent_total))} +

+ {writable && + (daysLeft === 0 ? ( +
+

{s.ended}

+ +
+ ) : ( +

+ {perDay !== null ? s.perDay(daysLeft, formatMoney(perDay)) : s.daysLeft(daysLeft)} +

+ ))} +
+ ) +} + +/** 4 grup: planlanan / gerçekleşen; Haftalık (bugünün haftası) ↔ Aylık (tüm dönem) */ +function GroupsCard({ + period, + budget, + summary, + today, + writable, +}: { + period: Period + budget: GroupBudget[] + summary: PeriodSummary + today: string + writable: boolean +}) { + const [mode, setMode] = useState<'week' | 'month'>('week') + const index = weekIndexFor(period.weeks, today) + const week = period.weeks[index - 1] + const range = (a: string, b: string) => `${formatDayMonth(a)}–${formatDayMonth(b)}` + + return ( +
+
+

+ 🎯 {s.groupsTitle} +

+
+ {(['week', 'month'] as const).map((m) => ( + + ))} +
+
+

+ {mode === 'week' && week + ? s.weekLabel(index, range(week.start_date, week.last_date)) + : s.monthLabel(range(period.start_date, period.last_date))} +

+ + {summary.budget_total === 0 ? ( + + ) : ( + + )} + {writable && summary.spent_total === 0 && summary.budget_total > 0 && ( +

+ + {s.noEntries} + +

+ )} +
+ ) +} + +function Empty({ text, to, action }: { text: string; to: string; action: string }) { + return ( +
+

{text}

+ +
+ ) +} + +/** Yükleniyor: kartların şeklinde yer tutucular */ +export function DashboardSkeleton() { + return ( +
+ +
+ {[0, 1, 2, 3].map((i) => ( + + ))} +
+
+ + +
+
+ ) +} diff --git a/frontend/src/features/dashboard/page.tsx b/frontend/src/features/dashboard/page.tsx index 53b1306..1f67512 100644 --- a/frontend/src/features/dashboard/page.tsx +++ b/frontend/src/features/dashboard/page.tsx @@ -1,14 +1,85 @@ -import { ComingSoon, PageHeader } from '@/components/page' +import { Plus } from 'lucide-react' +import { Link } from 'react-router' +import { paths } from '@/components/layout/nav-items' +import { PageError, PageHeader } from '@/components/page' +import { Button } from '@/components/ui/button' import { NoPeriodCard } from '@/features/period-wizard/no-period-card' -import { useCurrentPeriod } from '@/lib/periods' +import { dashboardPeriod } from '@/lib/dashboard' +import { useCurrentPeriod, usePeriods } from '@/lib/periods' import { t } from '@/lib/strings' +import { DashboardSkeleton, Overview } from './overview' +const s = t.dashboard + +/** Defter'e git ve tutar alanına odaklan */ +const addEntryLink = { to: paths.ledger, state: { focus: true } } + +/** + * Panel: açık dönemin özeti. Açık dönem yoksa "Dönemi başlat" çağrısı ve + * (varsa) son dönemin özeti salt okunur. "Harcama Ekle": masaüstünde başlıkta, + * mobilde sağ altta yüzen düğme. + */ export function DashboardPage() { const current = useCurrentPeriod() + const periods = usePeriods() + const failed = [current, periods].find((q) => q.isError) + const ready = current.isSuccess && periods.isSuccess + const period = ready ? dashboardPeriod(periods.data, current.data) : undefined + const canAdd = !!current.data + return ( <> - - {current.data === null ? : } + + + + {s.addEntry} + + + ) + } + /> + {failed?.error ? ( + void Promise.all([current.refetch(), periods.refetch()])} + /> + ) : period === undefined ? ( + + ) : period === null ? ( + + ) : ( +
+ {!current.data && ( + <> + +

{s.lastPeriod(period.name)}

+ + )} + +
+ )} + + {canAdd && ( + <> + {/* Yüzen düğme son içeriği örtmesin */} +
+ + + )} ) } diff --git a/frontend/src/features/ledger/entry-form.tsx b/frontend/src/features/ledger/entry-form.tsx index 01338cd..46df79e 100644 --- a/frontend/src/features/ledger/entry-form.tsx +++ b/frontend/src/features/ledger/entry-form.tsx @@ -71,6 +71,7 @@ export function EntryForm({ categories, methods, entry, + autoFocus = false, onSaved, onCancel, }: { @@ -82,6 +83,8 @@ export function EntryForm({ methods: CatalogItem[] /** Düzenlenen kayıt */ entry?: Entry + /** Yeni kayıtta açılışta tutar alanına odaklan */ + autoFocus?: boolean /** Kaydedilen kaydın tarihi (o haftaya geçmek için) */ onSaved: (date: string) => void onCancel?: () => void @@ -118,10 +121,10 @@ export function EntryForm({ } }, [entry, week, today, getValues, setValue]) - // Düzenlemeye geçince form görünür olsun ve tutara odaklansın + // Düzenlemeye geçince (ya da Panel'den gelince) tutara odaklan useEffect(() => { - if (entry) setFocus('amount') - }, [entry, setFocus]) + if (entry || autoFocus) setFocus('amount') + }, [entry, autoFocus, setFocus]) function pickGroup(code: GroupCode) { if (code === getValues('group')) return diff --git a/frontend/src/features/ledger/page.tsx b/frontend/src/features/ledger/page.tsx index 7f74382..7393279 100644 --- a/frontend/src/features/ledger/page.tsx +++ b/frontend/src/features/ledger/page.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react' -import { useSearchParams } from 'react-router' +import { useLocation, useSearchParams } from 'react-router' import { Info } from 'lucide-react' import { PageError, PageHeader, PageLoading } from '@/components/page' import { PeriodPicker } from '@/features/periods/period-picker' @@ -40,6 +40,8 @@ function LedgerView({ period }: { period: Period }) { const [params, setParams] = useSearchParams() const [today] = useState(() => toIsoDate(new Date())) const [editing, setEditing] = useState(null) + // Panel'deki "Harcama Ekle" → tutar alanına odaklan + const focusAmount = (useLocation().state as { focus?: boolean } | null)?.focus === true const formRef = useRef(null) const inPeriod = today >= period.start_date && today <= period.last_date const todayIndex = inPeriod ? weekIndexFor(period.weeks, today) : null @@ -108,6 +110,7 @@ function LedgerView({ period }: { period: Period }) { categories={catalog.c} methods={catalog.m} entry={editing ?? undefined} + autoFocus={focusAmount} onSaved={(date) => goTo(weekIndexFor(period.weeks, date))} onCancel={() => setEditing(null)} /> diff --git a/frontend/src/features/ledger/week-summary.tsx b/frontend/src/features/ledger/week-summary.tsx index 9522196..98e1b95 100644 --- a/frontend/src/features/ledger/week-summary.tsx +++ b/frontend/src/features/ledger/week-summary.tsx @@ -6,16 +6,19 @@ import type { GroupBudget } from '@/types/GroupBudget' const s = t.ledger -/** Seçili haftada grup bazlı bütçe / harcanan / kalan (+ ilerleme çubuğu) */ -export function WeekSummary({ budget, index }: { budget: GroupBudget[]; index: number }) { +/** + * Grup bazlı bütçe / harcanan / kalan (+ ilerleme çubuğu): seçili hafta ya da `'month'` → tüm dönem. + * Defter (haftalık) ve Panel (haftalık/aylık) kullanır. + */ +export function WeekSummary({ budget, index }: { budget: GroupBudget[]; index: number | 'month' }) { return (
    {budget.map((b) => { const g = group(b.group) - const w = b.weeks[index - 1] - const planned = w?.amount ?? 0 - const spent = w?.spent ?? 0 + const w = index === 'month' ? null : b.weeks[index - 1] + const planned = index === 'month' ? b.monthly : (w?.amount ?? 0) + const spent = index === 'month' ? b.spent : (w?.spent ?? 0) const left = planned - spent const over = left < 0 const ratio = planned > 0 ? Math.min(spent / planned, 1) : spent > 0 ? 1 : 0 diff --git a/frontend/src/lib/dashboard.test.ts b/frontend/src/lib/dashboard.test.ts new file mode 100644 index 0000000..226e270 --- /dev/null +++ b/frontend/src/lib/dashboard.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import type { Period } from '@/types/Period' +import { dailyAllowance, dashboardPeriod } from './dashboard' + +const nov = { start_date: '2026-11-01', last_date: '2026-11-30', days: 30 } + +describe('dashboard', () => { + it('picks the open period, else the latest one', () => { + const open = { id: 'o' } as Period + const last = { id: 'l' } as Period + expect(dashboardPeriod([last], open)).toBe(open) + expect(dashboardPeriod([last, { id: 'x' } as Period], null)).toBe(last) + expect(dashboardPeriod([], null)).toBeNull() + }) + + it('splits what is left over the remaining days', () => { + // 12.11 dahil 19 gün; 19.000 ₺ → günde 1.000 ₺ + expect(dailyAllowance(1_900_000, nov, '2026-11-12')).toEqual({ daysLeft: 19, perDay: 100_000 }) + expect(dailyAllowance(1_000, nov, '2026-11-30')).toEqual({ daysLeft: 1, perDay: 1_000 }) + // kuruş aşağı yuvarlanır (önerilen tutar kalanı aşmasın) + expect(dailyAllowance(1_000, nov, '2026-11-28')).toEqual({ daysLeft: 3, perDay: 333 }) + }) + + it('has no daily suggestion when over budget or the period has ended', () => { + expect(dailyAllowance(-5_000, nov, '2026-11-12').perDay).toBeNull() + expect(dailyAllowance(0, nov, '2026-11-12').perDay).toBeNull() + expect(dailyAllowance(5_000, nov, '2026-12-01')).toEqual({ daysLeft: 0, perDay: null }) + expect(dailyAllowance(3_000, nov, '2026-10-20')).toEqual({ daysLeft: 30, perDay: 100 }) + }) +}) diff --git a/frontend/src/lib/dashboard.ts b/frontend/src/lib/dashboard.ts new file mode 100644 index 0000000..7d676ec --- /dev/null +++ b/frontend/src/lib/dashboard.ts @@ -0,0 +1,29 @@ +/** + * Panel'in saf hesapları. Tutarlar backend'den gelir; burada yalnızca seçim ve yorum yapılır. + */ +import type { Period } from '@/types/Period' +import { daysInclusive } from './period-dates' + +/** Panel'de gösterilecek dönem: açık dönem; yoksa en son dönem (salt okunur) */ +export function dashboardPeriod(periods: Period[], current: Period | null): Period | null { + return current ?? periods[0] ?? null +} + +/** + * Anlık durum: kalan gün (bugün dahil) ve kalan tutarla günlük harcanabilir. + * Dönem başlamadıysa tüm günler, bittiyse 0 gün (günlük öneri yok). + */ +export function dailyAllowance( + remaining: number, + period: Pick, + today: string, +): { daysLeft: number; perDay: number | null } { + const daysLeft = + today > period.last_date + ? 0 + : today < period.start_date + ? period.days + : daysInclusive(today, period.last_date) + const perDay = daysLeft > 0 && remaining > 0 ? Math.floor(remaining / daysLeft) : null + return { daysLeft, perDay } +} diff --git a/frontend/src/lib/entries.ts b/frontend/src/lib/entries.ts index 8983187..f121b09 100644 --- a/frontend/src/lib/entries.ts +++ b/frontend/src/lib/entries.ts @@ -4,7 +4,13 @@ * Kayıt değişince dönemin tüm sorguları yenilenir (`queryKeys.period(id)` öneki): * liste, özet (`spent_total`, `remaining`) ve bütçe (`spent`) birlikte güncellenir. */ -import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, + type QueryClient, +} from '@tanstack/react-query' import type { Entry } from '@/types/Entry' import type { EntryCreateRequest } from '@/types/EntryCreateRequest' import type { EntryUpdateRequest } from '@/types/EntryUpdateRequest' @@ -12,6 +18,14 @@ import { api } from './api' import { queryKeys } from './query' /** Bir tarih aralığının kayıtları (ikisi de dahil); en yeni başta */ +/** Kayıt değişince: dönemin tüm verileri + Panel istatistikleri (harcanan) */ +function refresh(qc: QueryClient, periodId: string) { + return Promise.all([ + qc.invalidateQueries({ queryKey: queryKeys.period(periodId) }), + qc.invalidateQueries({ queryKey: queryKeys.stats }), + ]) +} + export function useEntries(periodId: string, range: { from: string; to: string }) { return useQuery({ queryKey: queryKeys.entries(periodId, range), @@ -26,7 +40,7 @@ export function useCreateEntry(periodId: string) { const qc = useQueryClient() return useMutation({ mutationFn: (body: EntryCreateRequest) => api.post(`/periods/${periodId}/entries`, body), - onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.period(periodId) }), + onSuccess: () => refresh(qc, periodId), }) } @@ -36,7 +50,7 @@ export function useUpdateEntry(periodId: string) { return useMutation({ mutationFn: ({ id, ...body }: EntryUpdateRequest & { id: string }) => api.patch(`/entries/${id}`, body), - onSettled: () => qc.invalidateQueries({ queryKey: queryKeys.period(periodId) }), + onSettled: () => refresh(qc, periodId), }) } @@ -45,7 +59,7 @@ export function useDeleteEntry(periodId: string) { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.delete(`/entries/${id}`), - onSettled: () => qc.invalidateQueries({ queryKey: queryKeys.period(periodId) }), + onSettled: () => refresh(qc, periodId), }) } @@ -53,6 +67,6 @@ export function useRestoreEntry(periodId: string) { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => api.post(`/entries/${id}/restore`), - onSettled: () => qc.invalidateQueries({ queryKey: queryKeys.period(periodId) }), + onSettled: () => refresh(qc, periodId), }) } diff --git a/frontend/src/lib/periods.ts b/frontend/src/lib/periods.ts index 6c3209d..8626a47 100644 --- a/frontend/src/lib/periods.ts +++ b/frontend/src/lib/periods.ts @@ -14,6 +14,7 @@ import type { PeriodCloseRequest } from '@/types/PeriodCloseRequest' import type { PeriodCloseResult } from '@/types/PeriodCloseResult' import type { PeriodCreateRequest } from '@/types/PeriodCreateRequest' import type { PeriodReview } from '@/types/PeriodReview' +import type { PeriodStats } from '@/types/PeriodStats' import type { PeriodSummary } from '@/types/PeriodSummary' import type { PeriodUpdateRequest } from '@/types/PeriodUpdateRequest' import type { PlanItem } from '@/types/PlanItem' @@ -137,6 +138,16 @@ export function useCopyPrevious(periodId: string) { }) } +// ---------- Panel ---------- + +/** Son dönemler (eskiden yeniye) + toplam birikim */ +export function useStats(limit = 12) { + return useQuery({ + queryKey: [...queryKeys.stats, limit], + queryFn: () => api.get(`/stats/periods?limit=${limit}`), + }) +} + // ---------- Özet ve bütçe ---------- export function useSummary(periodId: string) { diff --git a/frontend/src/lib/query.ts b/frontend/src/lib/query.ts index c11871e..adc21ba 100644 --- a/frontend/src/lib/query.ts +++ b/frontend/src/lib/query.ts @@ -15,6 +15,8 @@ export const queryKeys = { /** Dönem listesi; `currentPeriod` da bu önekle başlar (birlikte yenilenir) */ periods: ['periods'] as const, currentPeriod: ['periods', 'current'] as const, + /** Panel: son dönemler + toplam birikim (dönem listesiyle birlikte yenilenir) */ + stats: ['periods', 'stats'] as const, /** Bir döneme bağlı tüm veriler bu önekle başlar → `invalidateQueries({ queryKey: period(id) })` */ period: (id: string) => ['period', id] as const, planItems: (id: string) => ['period', id, 'plan-items'] as const, diff --git a/frontend/src/lib/strings.ts b/frontend/src/lib/strings.ts index f1a4933..dae1ab4 100644 --- a/frontend/src/lib/strings.ts +++ b/frontend/src/lib/strings.ts @@ -352,6 +352,35 @@ export const t = { fixInPlan: 'Plan’da düzelt', }, + dashboard: { + income: 'Toplam gelir', + fixed: 'Sabit giderler', + saved: 'Toplam birikim', + savedHint: 'Kapanan dönemlerde birikime ayrılanlar', + debt: 'Devreden borç', + debtHint: 'Önceki dönemden; harcanabilirden düşüldü', + groupsTitle: 'Planlanan / gerçekleşen', + weekly: 'Haftalık', + monthly: 'Aylık', + weekLabel: (n: number, range: string) => `${n}. hafta · ${range}`, + monthLabel: (range: string) => `Tüm dönem · ${range}`, + statusTitle: 'Anlık durum', + remaining: 'Kalan harcanabilir', + overspent: 'Harcanabilir aşıldı', + statusDetail: (spendable: string, spent: string) => + `Harcanabilir ${spendable} · harcanan ${spent}`, + perDay: (days: number, x: string) => `${days} gün kaldı · günde en fazla ${x}`, + daysLeft: (days: number) => `${days} gün kaldı`, + ended: 'Dönem bitti; değerlendirip kapatabilirsin.', + toReview: 'Değerlendirmeye git', + noBudget: 'Harcanabilir tutar henüz gruplara dağıtılmamış.', + toPlan: 'Plan’a git', + noEntries: 'Henüz harcama yok — ilkini ekle ✍️', + addEntry: 'Harcama Ekle', + lastPeriod: (name: string) => + `Açık dönem yok. Son dönem (${name}) gösteriliyor; değiştirilemez.`, + }, + periods: { picker: 'Dönem', status: { open: 'Açık', closed: '🔒 Kapalı', correcting: '✏️ Düzeltmede' },