diff --git a/CLAUDE.md b/CLAUDE.md index 9cf6488..4dc9eb6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,7 @@ npm run db:restore -- [--force] - Erişilebilirlik kuralları: `--muted-foreground` açık temada 0.52 (gri zeminde AA); kart bileşenleri (`Section`, rapor bölümü) `min-w-0` — ızgara öğesi içeriğine uzayıp telefonda taşmasın; `Button` `sm`/`icon-sm` dokunmatikte (`pointer-coarse`) 44 px; tutarlar `formatMoney` ile **bölünmez boşluklu** (`1.250,00\u00a0₺`; testlerde `\u00a0`); bütçe çubuğu `budgetLevel` (≥ %80 turuncu + "Bütçe kullanımı %N", aşım kırmızı); `Markdown shiftHeadings` (sayfada tek h1). Cümlelerde sayıdan sonra ek yok. - PWA: `vite.config.ts` `VitePWA` (registerType `prompt`, `/api` asla önbelleğe alınmaz, `navigateFallbackDenylist /api`, xlsx + yabancı font alt kümeleri ön önbellekte yok); `lib/pwa.tsx` (`PwaUpdater` "Yeni sürüm hazır → Yenile", saatlik yoklama; `OfflineBanner`), `lib/online.ts`. `main.tsx` `onlineManager.setOnline(navigator.onLine)`. Oturum sorgusu ağ hatası/duraklama → `RequireAuth` giriş sayfasına **atmaz**, "Tekrar dene" gösterir. Geliştirmede SW yok. DEPLOY §7. - Giriş güvenliği: `auth/guard.rs` (IP başına üst üste 3 hata → giriş+kayıt 15 dk kilit, bellek içi; e-postaya göre kilit YOK), `auth/captcha.rs` (Cloudflare Turnstile; anahtar yoksa kapalı; testte `Captcha::Fixed`), `auth/client_ip.rs` (`TRUST_PROXY` → en sağdaki X-Forwarded-For). Hata kodları `locked` (429 + Retry-After), `captcha` (400), `invalid_credentials` (kalan hak mesajda). `GET /auth/guard`. Arayüz `features/auth/guard.tsx` (`useAuthSecurity`), `lock-notice.tsx`, `components/turnstile.tsx`. Testler `TestApp::with_captcha`, `send_with` (X-Forwarded-For). DEPLOY §8. -- Borç takibi (docs/BORC_TAKIBI.md): `services/debt.rs` (saf: `total`, `installment_load`, `remaining_after`, `summarize`, `period_debt`; içe aktarma `parse_import` kaynak belge JSON + `asOf`, TL → kuruş `tl_to_kurus` kayan noktasız), `routes/debt.rs` (`/debt*`, `/periods/{id}/debt`, `/periods/{id}/opening-debt`). Koleksiyonlar `debt_snapshots`, `installment_plans` (`first_month`), `debt_targets`; toplam saklanmaz. Kakeibo `carryovers` ile karıştırma. Kişisel seed verisi repoya girmez. Arayüz `features/debt/` (`debt-card.tsx` Panel en üstü + /borc, `debt-chart.tsx` lazy Recharts, `snapshot-form.tsx` kalan taksit önerisi `remainingAfter`, `history.tsx`, `targets-editor.tsx`, `import-section.tsx`, `installments-page.tsx`), `lib/debt.ts` (saf hesaplar backend ile aynı). Adresler `/borc`, `/taksitler`; menüde "Borç" (masaüstü + Diğer). +- Borç takibi (docs/BORC_TAKIBI.md): `services/debt.rs` (saf: `total`, `installment_load`, `remaining_after`, `summarize`, `period_debt`; içe aktarma `parse_import` kaynak belge JSON + `asOf`, TL → kuruş `tl_to_kurus` kayan noktasız), `routes/debt.rs` (`/debt*`, `/periods/{id}/debt`, `/periods/{id}/opening-debt`). Koleksiyonlar `debt_snapshots`, `installment_plans` (`first_month`), `debt_targets`; toplam saklanmaz. Kakeibo `carryovers` ile karıştırma. Kişisel seed verisi repoya girmez. Arayüz `features/debt/` (`debt-card.tsx` Panel en üstü + /borc, `debt-chart.tsx` lazy Recharts, `snapshot-form.tsx` kalan taksit önerisi `remainingAfter`, `history.tsx`, `targets-editor.tsx`, `import-section.tsx`, `installments-page.tsx`), `lib/debt.ts` (saf hesaplar backend ile aynı). Adresler `/borc`, `/taksitler`; menüde "Borç" (masaüstü + Diğer). Plan: `features/debt/period-debt.tsx` (`PeriodDebtSection`: ay başı [elle düzelt], devreden banka borcu, borç ödemesi, taksit, hedef ay sonu + yol haritası, dönem içi ilerleme, "Taksitler" sabit gideri önerisi `findInstallmentItem`); borç > 0 iken `SummaryTiles debtPayment` → "Borç ödemesi" (Plan, düzenleme, sihirbaz). Kakeibo'nun "Devreden borç"u ile karışmasın diye banka borcu "Devreden banka borcu". - Hane izolasyonu: hane kimliği yalnızca `AuthUser.household_id`'den; sorgular `scope(hid)` ile başlar; başka hane → 404. ## Ortam diff --git a/docs/BORC_TAKIBI.md b/docs/BORC_TAKIBI.md index 241f1fb..b7fce6d 100644 --- a/docs/BORC_TAKIBI.md +++ b/docs/BORC_TAKIBI.md @@ -57,4 +57,6 @@ dizisi reddedilir. ## Adımlar - B1 backend (modeller, hesaplar, uç noktalar, içe aktarma, testler) - B2 arayüz: Panel kartı, `/borc`, `/taksitler` -- B3 aylık plan entegrasyonu + içe aktarma dosyasının hazırlanması (kullanıcıya, repo dışı) +- B3 aylık plan entegrasyonu + içe aktarma dosyasının hazırlanması (kullanıcıya, repo dışı: `samples/kakeibo-borc-baslangic.json`, git ve Docker dışında) + +**Durum (2026-11-14): B1–B3 tamamlandı.** diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 23ea35e..2487320 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,7 +1,7 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**Borç takibi – B3:** Aylık plan entegrasyonu: Plan sayfasında borç bölümü (`usePeriodDebt`: ay başı toplam [son ölçüm, elle düzelt → `PUT /periods/{id}/opening-debt`], devreden borç, "Borç ödemesi" = birikim hedefi etiketi (borç > 0 iken plan özetinde "Birikim" yerine), taksit yükü + "Taksitler" sabit gideri önerisi (yoksa tek tıkla ekle), hedef ay sonu borç (salt okunur) + yol haritası hedefi; dönem içi son ölçümle karşılaştırma). Sonra içe aktarma dosyası: scratchpad `borc/kakeibo-borc-baslangic.json` (kaynak belgeden + `asOf: 2026-11`) kullanıcıya verilecek (repo dışı); canlıda /borc → İçe aktar. +**Sıradaki:** Kullanıcının canlıda (kakeibo.ta1sph.website) testi. Borç başlangıç verisi: `samples/kakeibo-borc-baslangic.json` (git/Docker dışı) → canlıda /borc → İçe aktar. Kullanıcının bulduğu hatalar/istekler önce. Faz 11 (Tauri) yalnız kullanıcı isteyince. **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 @@ -158,3 +158,4 @@ - **Giriş güvenliği (2026-11-14):** Backend `auth/guard.rs` (AttemptGuard, ardışık sayaç, başarıda sıfır, süre bitince yeni tur, 1 sa dokunulmayan kayıt silinir), `auth/captcha.rs` (siteverify, 8 sn zaman aşımı, ağ hatası → `Unavailable` sayılmaz ama reddedilir, `internal-error` de öyle), `auth/client_ip.rs` (en sağ XFF, yoksa ConnectInfo; `main.rs` `into_make_service_with_connect_info`). Login/register `precheck` (kilit → captcha) + sayılan hatalar (yanlış şifre `InvalidCredentialsLeft(n)`, RegistrationClosed / BadRequest davet / Conflict e-posta). E-postaya göre giriş limiter'ı kaldırıldı (şifre değiştirmede kullanıcı başına kaldı). Config `TURNSTILE_SITE_KEY`/`SECRET_KEY` (ikisi birden), `TRUST_PROXY` (varsayılan true: Traefik arkasında herkes aynı IP olmasın). Testler `tests/security.rs` (4) + guard/XFF birim; 2 bilinçli bozma yakalandı; backend 214. Frontend `useAuthSecurity` (jeton, her hatada widget reset, kilit → `useAuthGuard` refetch, `dataUpdatedAt`+saniye geri sayım), Turnstile explicit render (tr, tema, flexible), betik yüklenemezse açıklama + düğme kapalı. E2E: Cloudflare test anahtarlarıyla gerçek widget + siteverify; kalan hak 2/1, kilit 14:57 sayaç, yenileme/kayıt/API kilitli; Cloudflare engelli → düğme kapalı, jetonsuz API 400 captcha. **Kullanıcıda:** Cloudflare'den anahtar alıp Dokploy'a girmek (DEPLOY §8). - **Borç takibi B1 (2026-11-14):** Migration v4 (3 koleksiyon) + indeksler + `backup::KINDS`. Taksit planı `first_month` ile (içe aktarmada `asOf − paidCount + 1`); kaynak belgedeki tüm kabul rakamları birim + entegrasyon testinde birebir (78.530,48 / 79.078,85 / devreden 64.352,79 / yükler 4.311,82…182,02 / kalan 14.177,69 ve 9.865,87). Ay sonu hedefi toplam borç cinsinden (= ay başı − C − taksit; yol haritası da toplam). Kart/IBAN: 12+ haneli sayı reddedilir. Testler `tests/debt.rs` (3) + 6 birim; 2 bilinçli bozma yakalandı. - **Borç takibi B2 (2026-11-14):** Panel en üstünde `DebtCard` (yalnız ölçüm varsa; değişim yeşil/kırmızı ok, başlangıç→0 ilerleme, tahmini bitiş, döküm), `/borc` (grafik: gerçek düz + hedef kesikli, ilk ölçümden başlar, eksen ay başları; ölçüm tablosu tam genişlik; form [toplam canlı, kalan taksit önerisi; form anahtarı taksit planlarına bağlı → içe aktarmadan sonra öneri dolar]; yol haritası düzenleyici; içe aktar ön izleme→onay), `/taksitler` (CSS çubuk 6 ay, planlar ödenen/kalan, yeni taksitte borç varken uyarı). Menüde "Borç". E2E 33 kontrol (belge JSON'u içe aktarılarak; kabul rakamları ekranda). Bulunan: öneri boş kalıyordu (form içe aktarmadan önce kurulmuştu); eksende aynı ay iki kez; tablo sıkışması. +- **Borç takibi B3 (2026-11-14) → B1–B3 ✅:** Plan'da "Bu ayın borcu" (`PeriodDebtSection`, yalnız borç verisi varsa): ay başı (ölçümden / elle, `useSetOpeningDebt`, "Son ölçüme dön"), devreden banka borcu, borç ödemesi (C), bu ayın taksiti, hedef ay sonu + yol haritası farkı (üstünde kırmızı / altında yeşil), dönem içi son ölçüme göre azalma vs C+taksit çubuğu, "Taksitler" sabit gideri tek tık (adı `findInstallmentItem`: taksit/taksitler, tr küçük harf; tutar farklıysa uyarı). `SummaryTiles debtPayment`. E2E 18 kontrol (hedef 67.218,66 = 78.530,48 − 7.000 − 4.311,82; harcanabilir 53.000 → 48.688,18). İçe aktarma dosyası `samples/` (gitignore) — 12+ haneli sayı yok. diff --git a/docs/PHASES.md b/docs/PHASES.md index aa86699..3bc2b98 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -96,4 +96,4 @@ ## Borç Takibi Adımları (docs/BORC_TAKIBI.md, kullanıcı isteği 2026-11-14) - ✅ B1. Backend: ölçümler, taksit planları (ilk ay + adet → aylık yük/kalan kendiliğinden), yol haritası, özet, dönem borcu (ay başı, devreden, hedef), elle ay başı, kaynak belge JSON'unu içe aktarma (ön izleme/onay), JSON yedeğine dahil - ✅ B2. Arayüz: Panel borç kartı, `/borc` (gerçek + hedef grafik, tablo, yeni ölçüm, hedefler, içe aktar), `/taksitler` -- 🔄 B3. Aylık plan entegrasyonu ("Borç ödemesi", ay başı/devreden/hedef, taksit önerisi) + içe aktarma dosyası (repo dışı) +- ✅ B3. Aylık plan entegrasyonu ("Borç ödemesi", ay başı/devreden/hedef, taksit önerisi) + içe aktarma dosyası (repo dışı) diff --git a/frontend/src/features/debt/period-debt.tsx b/frontend/src/features/debt/period-debt.tsx new file mode 100644 index 0000000..a0bd777 --- /dev/null +++ b/frontend/src/features/debt/period-debt.tsx @@ -0,0 +1,283 @@ +import { useState } from 'react' +import { CircleCheck, Landmark, Loader2, Pencil, Plus } from 'lucide-react' +import { Link } from 'react-router' +import { toast } from 'sonner' +import { FormError } from '@/components/form-field' +import { paths } from '@/components/layout/nav-items' +import { MoneyInput } from '@/components/money-input' +import { Button } from '@/components/ui/button' +import { findInstallmentItem, usePeriodDebt, useSetOpeningDebt } from '@/lib/debt' +import { formatDate, formatMoney, formatNumber, parseMoney } from '@/lib/format' +import { useCreatePlanItem } from '@/lib/periods' +import { t } from '@/lib/strings' +import { cn } from '@/lib/utils' +import type { Period } from '@/types/Period' +import type { PeriodDebt } from '@/types/PeriodDebt' +import type { PlanItem } from '@/types/PlanItem' + +const s = t.debt.period + +/** + * Aylık plan: bu ayın banka borcu (docs/BORC_TAKIBI.md §3). Ay başı borç son ölçümden + * (elle düzeltilebilir), devreden, borç ödemesi (= birikim hedefi), taksit yükü, hedef ay sonu. + * Borç takibinde hiç veri yoksa görünmez. + */ +export function PeriodDebtSection({ + period, + items, + writable, +}: { + period: Period + items: PlanItem[] + writable: boolean +}) { + const q = usePeriodDebt(period.id) + const d = q.data + if (!d || (d.opening_total === null && d.installment_load === 0)) return null + + return ( +
+
+

+ + {s.title} +

+ + {s.toDebt} + +
+

{s.hint}

+ +
+ + + + + +
+ + + {writable && ( + + )} +
+ ) +} + +function Tile({ + label, + amount, + sign, + hint, +}: { + label: string + amount: number | null + sign?: string + hint?: string +}) { + return ( +
+
+ {sign && {sign} } + {label} +
+
{amount === null ? '—' : formatMoney(amount)}
+ {hint &&

{hint}

} +
+ ) +} + +/** Ay başı borç: kaynağı yazılı; düzeltilebilir (dönem açıkken) */ +function Opening({ period, d, writable }: { period: Period; d: PeriodDebt; writable: boolean }) { + const set = useSetOpeningDebt(period.id) + const [editing, setEditing] = useState(false) + const [value, setValue] = useState('') + const [error, setError] = useState() + const source = + d.opening_source === 'manual' + ? s.manual + : d.opening_date + ? s.fromSnapshot(formatDate(d.opening_date)) + : s.noOpening + + async function save(amount: number | null) { + setError(undefined) + try { + await set.mutateAsync(amount) + setEditing(false) + toast.success(s.openingSaved) + } catch (e) { + setError(e instanceof Error ? e.message : String(e)) + } + } + + return ( +
+
+ {s.opening} + {writable && !editing && ( + + )} +
+ {editing ? ( +
+ + +
+ + {d.opening_source === 'manual' && ( + + )} + +
+
+ ) : ( + <> +
+ {d.opening_total === null ? '—' : formatMoney(d.opening_total)} +
+

{source}

+ + )} +
+ ) +} + +/** Hedef ay sonu borç (+ yol haritasıyla karşılaştırma) */ +function Target({ d }: { d: PeriodDebt }) { + const diff = + d.closing_target !== null && d.roadmap_target !== null + ? d.closing_target - d.roadmap_target + : null + return ( +
+
+ = + {s.target} +
+
+ {d.closing_target === null ? '—' : formatMoney(d.closing_target)} +
+ {d.roadmap_target !== null && ( +

+ {s.roadmap(formatMoney(d.roadmap_target))} + {diff !== null && diff !== 0 && ( + 0 ? 'text-expense' : 'text-income'}> + {' · '} + {diff > 0 + ? s.vsRoadmapAbove(formatMoney(diff)) + : s.vsRoadmapBelow(formatMoney(-diff))} + + )} +

+ )} +
+ ) +} + +/** Aylık başarı: ay başı − son ölçüm ≥ borç ödemesi + taksit */ +function Progress({ d }: { d: PeriodDebt }) { + if (d.opening_total === null || d.latest_total === null || !d.latest_date) return null + const down = d.opening_total - d.latest_total + const needed = d.payment + d.installment_load + const ok = down >= needed && needed > 0 + const ratio = needed > 0 ? Math.min(Math.max(down / needed, 0), 1) : 0 + return ( +
+

{s.progressTitle(formatDate(d.latest_date))}

+

+ {ok && } + {down >= 0 ? s.paidDown(formatMoney(down)) : s.grew(formatMoney(-down))} + + {' '} + · {s.needed(formatMoney(needed))} + +

+
+
+
+
+ ) +} + +/** Taksit yükü planda "Taksitler" sabit gideri olarak yoksa: tek tıkla ekle */ +function InstallmentsSuggestion({ + period, + items, + load, +}: { + period: Period + items: PlanItem[] + load: number +}) { + const create = useCreatePlanItem(period.id) + if (load <= 0) return null + const existing = findInstallmentItem(items) + if (existing) { + if (existing.amount === load) return null + return ( +

+ {s.installmentsMismatch(formatMoney(existing.amount), formatMoney(load))} +

+ ) + } + return ( + + ) +} diff --git a/frontend/src/features/period-wizard/step-budget.tsx b/frontend/src/features/period-wizard/step-budget.tsx index aa708c4..9998585 100644 --- a/frontend/src/features/period-wizard/step-budget.tsx +++ b/frontend/src/features/period-wizard/step-budget.tsx @@ -13,6 +13,7 @@ import { useHousehold } from '@/lib/household' import { toKurus } from '@/lib/money-schema' import { useBudget, useSetBudget, useSuggestion, useSummary } from '@/lib/periods' import { moneyToText } from '@/lib/plan-draft' +import { useDebt } from '@/lib/debt' import { t } from '@/lib/strings' import { cn } from '@/lib/utils' import type { GroupAmounts } from '@/types/GroupAmounts' @@ -87,6 +88,7 @@ function BudgetStepForm({ onBack: () => void onDone: () => void }) { + const debt = useDebt() const setBudget = useSetBudget(period.id) const { control, @@ -122,7 +124,7 @@ function BudgetStepForm({ return (
- + 0} />
reset(toForm(a))} /> diff --git a/frontend/src/features/planning/page.tsx b/frontend/src/features/planning/page.tsx index 6d8de97..adaa42a 100644 --- a/frontend/src/features/planning/page.tsx +++ b/frontend/src/features/planning/page.tsx @@ -4,7 +4,9 @@ import { PageError, PageHeader, PageLoading } from '@/components/page' import { Button } from '@/components/ui/button' import { NotesSection } from '@/features/notes/notes-section' import { CopyPrevious } from '@/features/period-wizard/step-items' +import { PeriodDebtSection } from '@/features/debt/period-debt' import { PeriodPicker } from '@/features/periods/period-picker' +import { useDebt } from '@/lib/debt' import { SelectedPeriod } from '@/features/periods/selected-period' import { isWritable } from '@/lib/period-select' import { useBudget, usePlanItems, useSummary } from '@/lib/periods' @@ -32,6 +34,8 @@ function PlanView({ period }: { period: Period }) { const items = usePlanItems(period.id) const summary = useSummary(period.id) const budget = useBudget(period.id) + const debt = useDebt() + const inDebt = (debt.data?.latest?.total ?? 0) > 0 const [editing, setEditing] = useState(false) const failed = [items, summary, budget].find((q) => q.isError) @@ -67,7 +71,8 @@ function PlanView({ period }: { period: Period }) { ) } /> - + +
i.kind === 'income')} /> i.kind === 'expense')} /> diff --git a/frontend/src/features/planning/plan-edit.tsx b/frontend/src/features/planning/plan-edit.tsx index d2fd593..368a42f 100644 --- a/frontend/src/features/planning/plan-edit.tsx +++ b/frontend/src/features/planning/plan-edit.tsx @@ -23,6 +23,7 @@ import { formatDayMonth, formatMoney, formatNumber, parseMoney } from '@/lib/for import { group, GROUPS } from '@/lib/groups' import { useSavePlan } from '@/lib/periods' import { diffPlan, GROUP_CODES, hasChanges, moneyToText, toDraft } from '@/lib/plan-draft' +import { useDebt } from '@/lib/debt' import { t } from '@/lib/strings' import { cn } from '@/lib/utils' import type { GroupBudget } from '@/types/GroupBudget' @@ -59,6 +60,7 @@ export function PlanEdit({ summary: PeriodSummary onClose: () => void }) { + const debt = useDebt() const save = useSavePlan(period) const { control, @@ -146,7 +148,7 @@ export function PlanEdit({
- + 0} />
0) { tiles.push({ label: s.debt, amount: summary.carryover_debt, sign: '−', tone: 'text-expense' }) diff --git a/frontend/src/lib/debt.test.ts b/frontend/src/lib/debt.test.ts index cc74671..78e4c25 100644 --- a/frontend/src/lib/debt.test.ts +++ b/frontend/src/lib/debt.test.ts @@ -79,3 +79,17 @@ describe('debt (backend ile aynı, kabul rakamları)', () => { ]) }) }) + +describe('plan integration', () => { + it('finds the installments expense item by name', async () => { + const { findInstallmentItem } = await import('./debt') + const items = [ + { kind: 'income', name: 'Taksitler' }, + { kind: 'expense', name: 'Kira' }, + { kind: 'expense', name: ' TAKSİTLER ' }, + ] + expect(findInstallmentItem(items)?.name).toBe(' TAKSİTLER ') + expect(findInstallmentItem([{ kind: 'expense', name: 'Taksit' }])).toBeTruthy() + expect(findInstallmentItem([{ kind: 'expense', name: 'Taksitli alışveriş' }])).toBeUndefined() + }) +}) diff --git a/frontend/src/lib/debt.ts b/frontend/src/lib/debt.ts index 30900b4..b3a322a 100644 --- a/frontend/src/lib/debt.ts +++ b/frontend/src/lib/debt.ts @@ -159,6 +159,24 @@ export function useSetTargets() { }) } +export function useSetOpeningDebt(periodId: string) { + const invalidate = useInvalidateDebt() + return useMutation({ + mutationFn: (amount: number | null) => + api.put(`/periods/${periodId}/opening-debt`, { amount }), + onSuccess: invalidate, + }) +} + +/** Plan kalemlerinde "Taksit(ler)" adlı sabit gider (büyük/küçük harf ve Türkçe harf duyarsız) */ +export function findInstallmentItem( + items: readonly T[], +): T | undefined { + return items.find( + (i) => i.kind === 'expense' && /^taks[iı]t(ler|lar)?$/.test(i.name.trim().toLocaleLowerCase('tr')), + ) +} + export function useDebtImport() { const invalidate = useInvalidateDebt() return useMutation({ diff --git a/frontend/src/lib/strings.ts b/frontend/src/lib/strings.ts index eef8f87..955771d 100644 --- a/frontend/src/lib/strings.ts +++ b/frontend/src/lib/strings.ts @@ -253,6 +253,8 @@ export const t = { income: 'Gelir', fixed: 'Sabit giderler', savings: 'Birikim', + /** Borç takibinde borç > 0 iken birikim hedefinin adı */ + debtPayment: 'Borç ödemesi', debt: 'Devreden borç', surplus: 'Devreden artı', spendable: 'Harcanabilir', @@ -525,6 +527,37 @@ export const t = { removeInstallmentConfirm: (m: string) => `${m} taksit planı silinsin mi?`, noInstallments: 'Kayıtlı taksit planı yok.', toInstallments: 'Taksitler', + period: { + title: 'Bu ayın borcu', + hint: 'Banka borcu (KMH, kart, taksitler). Harcanabilir tutarı düşürmez; birikim hedefin borç bitene kadar borç ödemesi sayılır.', + opening: 'Ay başı borç', + fromSnapshot: (date: string) => `${date} ölçümünden`, + manual: 'Elle girildi', + noOpening: 'Ölçüm yok', + editOpening: 'Ay başı borcu düzelt', + resetOpening: 'Son ölçüme dön', + openingSaved: 'Ay başı borç kaydedildi', + carried: 'Devreden banka borcu', + carriedHint: 'Ay başı borç − ekstreye gelmemiş taksitler', + payment: 'Borç ödemesi', + load: 'Bu ayın taksiti', + target: 'Hedef ay sonu borç', + targetHint: 'Ay başı − borç ödemesi − bu ayın taksiti', + roadmap: (amount: string) => `Yol haritası: ${amount}`, + vsRoadmapAbove: (amount: string) => `yol haritasının ${amount} üstünde`, + vsRoadmapBelow: (amount: string) => `yol haritasının ${amount} altında`, + progressTitle: (date: string) => `${date} ölçümüne göre`, + paidDown: (amount: string) => `Borç ${amount} azaldı`, + grew: (amount: string) => `Borç ${amount} arttı`, + needed: (amount: string) => `ayın hedefi: ${amount} azalma (borç ödemesi + taksit)`, + onTrack: 'Hedefe ulaşıldı', + addInstallments: (amount: string) => `"Taksitler" sabit giderini ekle (${amount})`, + installmentsAdded: 'Taksitler sabit giderlere eklendi', + installmentsMismatch: (plan: string, load: string) => + `Plandaki "Taksitler" ${plan}; bu ayın taksit yükü ${load}. Düzenle'den güncelleyebilirsin.`, + installmentsName: 'Taksitler', + toDebt: 'Borç takibi', + }, }, monthlyReport: { title: 'Aylık Rapor',