From 9daeff3eb57cbc00179d676c4b2ca399c5b971c7 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Fri, 13 Nov 2026 22:04:22 +0300 Subject: [PATCH] =?UTF-8?q?feat(ayarlar):=20kategori=20ve=20=C3=B6deme=20t?= =?UTF-8?q?=C3=BCr=C3=BC=20y=C3=B6netimi,=20ayarlar=20men=C3=BCs=C3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 1 + docs/MEMORY.md | 4 +- docs/PHASES.md | 11 +- frontend/src/components/layout/nav-items.ts | 3 + .../src/features/settings/catalog-editor.tsx | 312 ++++++++++++++++++ .../src/features/settings/catalog-pages.tsx | 85 +++++ frontend/src/features/settings/data-page.tsx | 12 + frontend/src/features/settings/page.tsx | 44 ++- frontend/src/features/settings/subpage.tsx | 31 ++ frontend/src/features/statement/page.tsx | 2 +- frontend/src/lib/catalog.test.ts | 12 + frontend/src/lib/catalog.ts | 60 +++- frontend/src/lib/strings.ts | 58 +++- frontend/src/router.tsx | 5 + 14 files changed, 625 insertions(+), 15 deletions(-) create mode 100644 frontend/src/features/settings/catalog-editor.tsx create mode 100644 frontend/src/features/settings/catalog-pages.tsx create mode 100644 frontend/src/features/settings/data-page.tsx create mode 100644 frontend/src/features/settings/subpage.tsx create mode 100644 frontend/src/lib/catalog.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 6e73eae..2596f0b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,7 @@ npm run db:restore -- [--force] - Biçim: Prettier (`frontend/.prettierrc.json`: tek tırnak, `;` yok, 100 sütun); `npm --prefix frontend run format`. `src/types/` ve `components/ui/` hariç. - JSON yedeği: `services/backup.rs` (`collect` → `to_json`; `parse` doğrular; `restore` = geri dönüş noktası + tam değiştirme tek transaction; `retarget` kimlik çevirir). Yeni hane koleksiyonu eklenirse `backup::KINDS`'a da ekle. Biçim: DATA_MODEL §3.14b. Arayüz: `features/settings/backup-section.tsx`, `lib/backup.ts` (geri yükleme/geri alma sonrası tüm sorgular yenilenir). - Aylık rapor: `services/monthly_report.rs` (`gather` → `Facts` → saf `analyze` → `render`), şablon `templates/monthly_report.md` (`include_str!`; Dockerfile `templates/`'u kopyalar). Filtreler `tl`/`pct`/`cell`. Şablonda ham HTML yok (arayüz göstermez). Arayüz `features/review/report-page.tsx` (`/degerlendirme/rapor?donem=`), `lib/monthly-report.ts`; `api.getText`; `Markdown` tabloları `.table-wrap` içinde kayar; yazdırma `@page A4` + `PageHeader printHidden`. Cümlelerde sayıdan sonra ek kullanma ("%429'u" gibi ünlü uyumu gerekir) → "X / Y", "kullanımı %N". +- Ayarlar: `/ayarlar` menü (+ tema) → alt sayfalar `SettingsSubpage` ("‹ Ayarlar"): `/ayarlar/kategoriler`, `/odeme-turleri`, `/veri` (yedek, CSV, ekstre). Katalog yönetimi `features/settings/catalog-editor.tsx` + `lib/catalog.ts` (`useCreateCatalog`/`useUpdateCatalog`/`useReorderCatalog`, `moveId`); sıralama isteği grubun **tüm** kimlikleri (pasifler dahil); grup sonradan değişmez. - 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/MEMORY.md b/docs/MEMORY.md index 5fcf6dc..849af92 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,10 +1,11 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**Faz 10 planı:** PWA (vite-plugin-pwa, kurulabilir, çevrimdışı kabuk), performans, erişilebilirlik, UI cilası + **Ayarlar: kategori / ödeme türü yönetimi** (ekle, düzenle, emoji, sırala, pasifleştir; raporlar tüm kategorileri gösteriyor). Önce kapsamı ve adımları kullanıcıya sun. +**Faz 10 – Adım 2:** Hane ve hesap (Ayarlar'a yeni alt sayfalar). Backend'de var: `GET /household`, davetler `GET/POST /household/invites`, `DELETE /household/invites/{id}` (arayüz yok → davet şu an uygulamadan yapılamıyor!). Eksik backend: hane adı değiştir, üye çıkar (sahip), profil adı, şifre değiştir (eski şifre + argon2, diğer oturumlar?), varsayılan bütçe yüzdeleri + öneri yöntemi (`households.settings`). Önce mevcut household/auth route'larını oku. **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 +- **Tauri / mobil + masaüstü uygulamalar (kullanıcı, 2026-11-13):** Faz 11 (Windows, Linux, Android) **kullanıcının geniş kapsamlı testinden sonra, kullanıcı prompt verdiğinde** yapılır; kendiliğinden başlanmaz. Şimdi Faz 10: web uygulamasının eksikleri + arayüz cilası (PWA web kurulumu Faz 10'da kalır). - **Aylık rapor tasarımı (kullanıcı onayı, 2026-11-13):** 7 bölüm — Bir bakışta (4 soru + önceki dönem) · Ne yaptım (gruplar ✅≤%90/⚠️≤%100/❌, ilk 5 kategori + ortalamaya göre, haftalar, ödeme türü, birikim) · İyi gitti · Zorladı · Ne yapılabilirdi ("olsaydı" hesapları) · Kendi notların (plan notları + yansıtma, yorumsuz) · Gelecek dönem (önerilen bütçe = son ≤4 dönem ortalaması 50 ₺ yukarı, ≤3 somut adım, devir, boş hedef kutuları) + Ek: tüm kategoriler. Eşik: kategori %20 **ve** 250 ₺. Karşılaştırma: önceki ≤3 kapalı/arşiv dönem. "Sen" dili. Açık dönem → ara rapor (hedef yargısı yok, bitmemiş hafta yok). - **Hafta kuralı değişti (kullanıcı, 2026-11-13):** Pazartesi başlangıçlı haftalar yerine dönemin 1–8, 9–15, 16–22, 23–son günleri (ay = 4 hafta). Kasım 2026'da 6 hafta (01.11 tek gün) çıkması üzerine. Genel: `max(1, gün/7)` hafta, ilk 8 gün, ara 7, son kalan (6–12). Defter her açılışta bugünün haftası; hafta artık adrese yazılmaz (sekme geri yüklemede eski hafta açılıyordu). - **Ekstre kararları (kullanıcı, 2026-11-13):** biçim Excel (.xlsx/.xls) + CSV (kullanıcı tam bilmiyor; ilk gerçek ekstreyle denenecek), hesaba gelenler → **gelir** (plan kalemi), arşiv dönemi **takvim ayı**, iadeler **aynı kategoriden düşülür** (eksi tutarlı kayıt). @@ -147,3 +148,4 @@ - **Faz 9 – Adım 5b (2026-11-13):** `features/settings/backup-section.tsx` (Veri bölümünün en üstü): "Yedeği indir" (``, tıklayınca 1,5 sn sonra durum yenilenir) + hatırlatma (`daysSince`, hiç yok ya da ≥ 30 gün → uyarı rengi); sahip: dosya → önizleme (kaynak, aynı/başka hane, dönem aralığı, sayılar yedek/şimdi — farklı olan uyarı rengi, uyarılar) → onay kutusu (`#backup-confirm`; Radix `name`'i düğmeye taşımaz) → "Geri yükle" (destructive) → sonuç kutusu + "Geri al"; geri dönüş noktaları listesi (`window.confirm`). Üye: yalnız indirme + "yalnız sahip" notu. `lib/backup.ts` (geri yükleme/geri alma → `qc.invalidateQueries()` tümü), `api.postJsonText`, `queryKeys.backup`, `t.backup`. Frontend 134 test; e2e 27 kontrol (8091, DB silindi). - **Faz 9 – Adım 6a (2026-11-13):** `services/monthly_report.rs` + `routes/monthly_report.rs` (`GET /periods/{id}/report.md`, `text/markdown`), `templates/monthly_report.md` (minijinja 3.1, `serde` özelliği; trim/lstrip blocks; art arda boş satırlar sadeleştirilir). `minijinja` yeni bağımlılık (şablonda koşul/döngü). Dockerfile: `COPY templates/ /app/templates/` (`include_str!("../../../templates/…")`), `docker build --target api` geçti. Testler: 5 birim (biçim, eşikler, kapalı dönem yorumları, ara rapor + ilk dönem, tam şablon + hücre kaçışı) + 1 entegrasyon (Ekim kapalı → Kasım raporu: karşılaştırma, notlar, devir, öneri, 404). 5 bilinçli bozmadan 2'si önce kaçtı → eşik testleri eklendi, hepsi yakalandı. bson `DateTime::to_time_0_3` crate-içi → `from_unix_timestamp`. Backend 196 test. - **Faz 9 – Adım 6b (2026-11-13) → Faz 9 ✅:** `features/review/report-page.tsx` (`paths.monthlyReport = /degerlendirme/rapor`, dönem seçici, Yazdır/PDF `printSection()`, "Markdown indir" `downloadFile` (csv.ts'e genel yardımcı), `reportFileName` = backend slug). Bağlantılar: Değerlendirme başlığı, kapatma sonucu, dönem geçmişinde ayrı simge bağlantı (iç içe `` olmasın). Bulunan/düzeltilen: (1) şablondaki `` dipnot react-markdown'da hiç görünmüyordu → düz `_…_`; (2) telefonda tablolar sayfayı 541 px'e taşırıyordu (mobil emülasyonda `innerWidth` da büyür → kontrol 390 ile) → `Markdown` tablo sarmalayıcısı `.table-wrap`; (3) PDF ilk sayfada sayfa başlığı/dönem seçici → `PageHeader printHidden`; (4) ara raporda başlamamış haftalar "✅ %0" → "—"; (5) hedef kutuları sayfa sonunda bölünüyordu. PDF: Chrome `printToPDF` → 4 sayfa A4, `pdftoppm` ile gözle kontrol. Frontend 135, backend 196 test; e2e 22 kontrol (DB silindi). +- **Faz 10 – Adım 1 (2026-11-13):** Ayarlar menüsü (`features/settings/page.tsx`: Kategoriler, Ödeme türleri, Veri ve yedek + tema) ve alt sayfalar (`subpage.tsx`, `catalog-pages.tsx`, `data-page.tsx`); ekstre sayfası "‹ Veri ve yedek"e döner. `catalog-editor.tsx`: satırda ↑ ↓ ✎ (44 px, `aria-label` adlı), düzenleme formu ad + emoji (hızlı seçim paleti, `aria-pressed`), Pasifleştir/Yeniden etkinleştir + toast "Geri al" (sürümsüz PATCH), aynı ad hatası backend'den. `lib/catalog.ts` mutasyonları `['categories'|'payment-methods']` + rapor/dönem önbelleğini yeniler. Telefonda grup başlığı bölünüyordu → açıklama alta. Frontend 136 test; e2e 25 kontrol (DB silindi). diff --git a/docs/PHASES.md b/docs/PHASES.md index 3178ce6..d3ce228 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -14,8 +14,8 @@ | **Faz 7** | Dashboard: özet kartları ve grafikler | Ana ekran | ✅ | | **Faz 8** | Birikimler/varlıklar, kur entegrasyonu, kâr/zarar | Birikim modülü | ✅ | | **Faz 9** | Raporlama + JSON/CSV içe–dışa aktarma + aylık değerlendirme çıktısı | Raporlar | ✅ | -| **Faz 10** | PWA, performans, erişilebilirlik, UI cilası + **Ayarlar: kategori / ödeme türü yönetimi** | Telefona kurulabilir web uygulaması | ⏳ | -| **Faz 11+** | Tauri 2: Windows, Linux, Android (çevrimdışı + senkronizasyon) | Masaüstü ve mobil uygulamalar | ⏳ | +| **Faz 10** | PWA, performans, erişilebilirlik, UI cilası + **Ayarlar: kategori / ödeme türü yönetimi** | Telefona kurulabilir web uygulaması | 🔄 | +| **Faz 11+** | Tauri 2: Windows, Linux, Android (çevrimdışı + senkronizasyon) — **kullanıcının geniş kapsamlı testinden sonra, kullanıcı istediğinde** | Masaüstü ve mobil uygulamalar | ⏸️ | ## Faz 0 Adımları - ✅ 1. Ortam kontrolü ve kurulum yönergeleri @@ -85,3 +85,10 @@ - ✅ 4. **Banka / kredi kartı ekstresi içe aktarma** — 4a backend (arşiv dönemleri, iade, gelir, yoksay, tekrar, öğrenen kurallar) + 4b arayüz (`/ayarlar/ekstre`: Excel/CSV, sütun eşleştirme + profil, gözden geçirme) - ✅ 5. JSON tam hane yedeği — 5a backend (sürümlü dosya + SHA-256, önizleme, tam değiştirme tek transaction, geri dönüş noktaları + "geri al", başka haneye kimlik çevirerek taşıma) · 5b arayüz (Ayarlar → Veri: indir + "son yedek" hatırlatması, sahip için önizleme → onay → geri yükle → "Geri al", geri dönüş noktaları) - ✅ 6. Aylık değerlendirme çıktısı — 6a backend (`templates/monthly_report.md` + minijinja, `GET /periods/{id}/report.md`, kural tabanlı yorum ve öneriler) · 6b arayüz (`/degerlendirme/rapor`: uygulama içi görünüm, Yazdır/PDF A4, Markdown indir; Değerlendirme, kapatma sonucu ve dönem geçmişinden bağlantı) + +## Faz 10 Adımları (web uygulamasının eksikleri + cila) +- ✅ 1. Ayarlar yeniden düzeni (`/ayarlar` menü + tema) · **Kategori yönetimi** (grup grup ekle, ad/emoji düzenle, sırala, pasifleştir) · **Ödeme türü yönetimi** · Veri sayfası `/ayarlar/veri` +- 🔄 2. Hane ve hesap: hane adı, üyeler, **davet bağlantısı** (oluştur/kopyala/iptal), üye çıkarma; profil adı, **şifre değiştirme**; varsayılan bütçe yüzdeleri + öneri yöntemi +- ⏳ 3. Arayüz cilası ve erişilebilirlik: tüm sayfaların telefon/masaüstü/koyu tema taraması, boş durumlar, skeleton, %80 bütçe uyarısı, odak/klavye, kontrast, onay diyalogları +- ⏳ 4. Performans: sayfa bazlı kod bölme (lazy route), statik dosyalarda uzun önbellek, paket boyutu +- ⏳ 5. PWA: manifest + service worker (uygulama kabuğu; `/api` asla önbelleğe alınmaz), kurulum, güncelleme bildirimi diff --git a/frontend/src/components/layout/nav-items.ts b/frontend/src/components/layout/nav-items.ts index c5fad07..9def1b0 100644 --- a/frontend/src/components/layout/nav-items.ts +++ b/frontend/src/components/layout/nav-items.ts @@ -30,6 +30,9 @@ export const paths = { monthlyReport: '/degerlendirme/rapor', reports: '/raporlar', settings: '/ayarlar', + settingsCategories: '/ayarlar/kategoriler', + settingsPaymentMethods: '/ayarlar/odeme-turleri', + settingsData: '/ayarlar/veri', /** Banka / kart ekstresi içe aktarma */ statement: '/ayarlar/ekstre', login: '/giris', diff --git a/frontend/src/features/settings/catalog-editor.tsx b/frontend/src/features/settings/catalog-editor.tsx new file mode 100644 index 0000000..f4b02bb --- /dev/null +++ b/frontend/src/features/settings/catalog-editor.tsx @@ -0,0 +1,312 @@ +import { useId, useState, type FormEvent } from 'react' +import { ArrowDown, ArrowUp, Loader2, Pencil, Plus } from 'lucide-react' +import { toast } from 'sonner' +import { FormError } from '@/components/form-field' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + moveId, + useCreateCatalog, + useReorderCatalog, + useUpdateCatalog, + type CatalogKind, +} from '@/lib/catalog' +import { t } from '@/lib/strings' +import { cn } from '@/lib/utils' +import type { CatalogItem } from '@/types/CatalogItem' +import type { Group } from '@/types/Group' + +const s = t.settings.catalog +const message = (e: unknown) => (e instanceof Error ? e.message : String(e)) + +/** Hızlı seçim için sık kullanılan emojiler */ +const EMOJIS: Record = { + categories: [ + '🛒', + '🏠', + '💡', + '🚌', + '⛽', + '🚗', + '📱', + '🧾', + '🍽️', + '☕', + '👕', + '🎉', + '🎮', + '✈️', + '💇', + '🏋️', + '📖', + '🎓', + '🎬', + '🎨', + '💊', + '🩺', + '🔧', + '🎁', + '👶', + '🐶', + '🧹', + '💻', + ], + 'payment-methods': ['💵', '💳', '🏧', '🔄', '📱', '🏦', '🪙', '🧾'], +} + +/** + * Tek bir listenin yönetimi: kategorilerde bir grup, ödeme türlerinde tüm liste. + * `items` sıralı ve pasifler dahil (sıralama isteği listenin tamamını ister). + */ +export function CatalogEditor({ + kind, + group, + items, +}: { + kind: CatalogKind + group?: Group + items: CatalogItem[] +}) { + const [editing, setEditing] = useState(null) + const [adding, setAdding] = useState(false) + const create = useCreateCatalog(kind) + const update = useUpdateCatalog(kind) + const reorder = useReorderCatalog(kind) + const what = kind === 'categories' ? s.category : s.paymentMethod + + function move(item: CatalogItem, dir: -1 | 1) { + const ids = moveId( + items.map((i) => i.id), + item.id, + dir, + ) + if (!ids) return + reorder.mutate({ group, ids }, { onError: (e) => toast.error(message(e)) }) + } + + async function setActive(item: CatalogItem, active: boolean) { + await update.mutateAsync({ id: item.id, is_active: active, version: item.version }) + setEditing(null) + toast(active ? s.activated(item.name) : s.deactivated(item.name), { + action: { + label: s.undo, + // Geri al: sürüm değişti → sürümsüz gönder (yalnızca durum değişir) + onClick: () => update.mutate({ id: item.id, is_active: !active }), + }, + }) + } + + return ( +
+ {items.length === 0 && !adding && ( +

+ {kind === 'categories' ? s.empty : s.emptyMethods} +

+ )} +
    + {items.map((item, i) => + editing === item.id ? ( +
  • + setEditing(null)} + onSubmit={async (v) => { + await update.mutateAsync({ id: item.id, ...v, version: item.version }) + setEditing(null) + toast.success(s.saved) + }} + extra={ + + } + /> +
  • + ) : ( +
  • + + {item.emoji} + + {item.name} + {!item.is_active && ( + + {s.inactive} + + )} + + + +
  • + ), + )} + {adding && ( +
  • + setAdding(false)} + onSubmit={async (v) => { + const created = await create.mutateAsync({ group, ...v }) + setAdding(false) + toast.success(s.added(created.name)) + }} + /> +
  • + )} +
+ {!adding && ( + + )} +
+ ) +} + +/** Ad + emoji formu (ekleme ve düzenleme) */ +function ItemForm({ + kind, + initial, + pending, + onSubmit, + onCancel, + extra, +}: { + kind: CatalogKind + initial?: CatalogItem + pending: boolean + onSubmit: (v: { name: string; emoji: string }) => Promise + onCancel: () => void + extra?: React.ReactNode +}) { + const id = useId() + const [name, setName] = useState(initial?.name ?? '') + const [emoji, setEmoji] = useState(initial?.emoji ?? '') + const [error, setError] = useState() + + async function submit(e: FormEvent) { + e.preventDefault() + const n = name.trim() + if (!n) return setError(s.nameRequired) + if (n.length > 40) return setError(s.nameTooLong) + if ([...emoji.trim()].length > 4) return setError(s.emojiTooLong) + setError(undefined) + try { + await onSubmit({ name: n, emoji: emoji.trim() }) + } catch (err) { + setError(message(err)) + } + } + + return ( +
void submit(e)} className="space-y-3" noValidate> +
+
+ + setEmoji(e.target.value)} + className="text-center text-lg" + autoComplete="off" + /> +
+
+ + setName(e.target.value)} + maxLength={40} + autoFocus + autoComplete="off" + aria-invalid={!!error || undefined} + /> +
+
+
+

{s.emojiHint}

+
+ {EMOJIS[kind].map((e) => ( + + ))} +
+
+ +
+ + + {extra &&
{extra}
} +
+ + ) +} diff --git a/frontend/src/features/settings/catalog-pages.tsx b/frontend/src/features/settings/catalog-pages.tsx new file mode 100644 index 0000000..1114346 --- /dev/null +++ b/frontend/src/features/settings/catalog-pages.tsx @@ -0,0 +1,85 @@ +import { Info } from 'lucide-react' +import { PageError, PageLoading } from '@/components/page' +import { useCategories, usePaymentMethods } from '@/lib/catalog' +import { GROUPS } from '@/lib/groups' +import { t } from '@/lib/strings' +import { cn } from '@/lib/utils' +import { CatalogEditor } from './catalog-editor' +import { SettingsSubpage } from './subpage' + +const s = t.settings + +function Hint({ children }: { children: string }) { + return ( +

+ + {children} +

+ ) +} + +/** Ayarlar → Kategoriler: 4 grup, her birinde sıralı alt kategoriler (pasifler dahil) */ +export function CategoriesPage() { + const cats = useCategories(true) + return ( + + {cats.isError ? ( + + ) : !cats.data ? ( + + ) : ( +
+
+ {GROUPS.map((g) => ( +
+
+

+ + {g.emoji} {g.label} +

+

{g.description}

+
+ c.group === g.code)} + /> +
+ ))} +
+ {s.catalog.inactiveHint} + {s.catalog.groupFixed} +
+ )} +
+ ) +} + +/** Ayarlar → Ödeme türleri */ +export function PaymentMethodsPage() { + const methods = usePaymentMethods(true) + return ( + + {methods.isError ? ( + + ) : !methods.data ? ( + + ) : ( +
+ + {s.catalog.inactiveHint} +
+ )} +
+ ) +} diff --git a/frontend/src/features/settings/data-page.tsx b/frontend/src/features/settings/data-page.tsx new file mode 100644 index 0000000..e26e8d2 --- /dev/null +++ b/frontend/src/features/settings/data-page.tsx @@ -0,0 +1,12 @@ +import { t } from '@/lib/strings' +import { DataSection } from './data-section' +import { SettingsSubpage } from './subpage' + +/** Ayarlar → Veri ve yedek: JSON yedek, CSV, banka ekstresi */ +export function DataPage() { + return ( + + + + ) +} diff --git a/frontend/src/features/settings/page.tsx b/frontend/src/features/settings/page.tsx index c2e5f17..d7d2ee3 100644 --- a/frontend/src/features/settings/page.tsx +++ b/frontend/src/features/settings/page.tsx @@ -1,19 +1,47 @@ -import { ComingSoon, PageHeader } from '@/components/page' +import { ChevronRight, Database, Tags, Wallet, type LucideIcon } from 'lucide-react' +import { Link } from 'react-router' +import { paths } from '@/components/layout/nav-items' +import { PageHeader } from '@/components/page' +import { ThemeToggle } from '@/components/theme-toggle' import { t } from '@/lib/strings' -import { DataSection } from './data-section' +const s = t.settings + +const items: { to: string; icon: LucideIcon; title: string; hint: string }[] = [ + { to: paths.settingsCategories, icon: Tags, ...s.menu.categories }, + { to: paths.settingsPaymentMethods, icon: Wallet, ...s.menu.paymentMethods }, + { to: paths.settingsData, icon: Database, ...s.menu.data }, +] + +/** Ayarlar menüsü: her bölüm kendi sayfasında (telefonda kısa, odaklı ekranlar) */ export function SettingsPage() { return ( <>
-
-

- {t.data.title} -

- + +
+

{s.appearance}

+
-
) diff --git a/frontend/src/features/settings/subpage.tsx b/frontend/src/features/settings/subpage.tsx new file mode 100644 index 0000000..242db0a --- /dev/null +++ b/frontend/src/features/settings/subpage.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from 'react' +import { ChevronLeft } from 'lucide-react' +import { Link } from 'react-router' +import { paths } from '@/components/layout/nav-items' +import { PageHeader } from '@/components/page' +import { t } from '@/lib/strings' + +/** Ayarlar alt sayfası: "‹ Ayarlar" geri bağlantısı + başlık */ +export function SettingsSubpage({ + title, + description, + children, +}: { + title: string + description?: string + children: ReactNode +}) { + return ( + <> + + + {t.settings.back} + + + {children} + + ) +} diff --git a/frontend/src/features/statement/page.tsx b/frontend/src/features/statement/page.tsx index 979de76..9025f21 100644 --- a/frontend/src/features/statement/page.tsx +++ b/frontend/src/features/statement/page.tsx @@ -56,7 +56,7 @@ export function StatementPage() { return (
diff --git a/frontend/src/lib/catalog.test.ts b/frontend/src/lib/catalog.test.ts new file mode 100644 index 0000000..a2a6d91 --- /dev/null +++ b/frontend/src/lib/catalog.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { moveId } from './catalog' + +describe('catalog ordering', () => { + it('moves an item one step and refuses at the edges', () => { + expect(moveId(['a', 'b', 'c'], 'b', -1)).toEqual(['b', 'a', 'c']) + expect(moveId(['a', 'b', 'c'], 'b', 1)).toEqual(['a', 'c', 'b']) + expect(moveId(['a', 'b', 'c'], 'a', -1)).toBeNull() + expect(moveId(['a', 'b', 'c'], 'c', 1)).toBeNull() + expect(moveId(['a'], 'x', 1)).toBeNull() + }) +}) diff --git a/frontend/src/lib/catalog.ts b/frontend/src/lib/catalog.ts index 0f8596e..f15eaa7 100644 --- a/frontend/src/lib/catalog.ts +++ b/frontend/src/lib/catalog.ts @@ -1,9 +1,12 @@ /** - * Kategoriler ve ödeme türleri (sunucu verisi). Yönetim ekranı Ayarlar'da gelecek; + * Kategoriler ve ödeme türleri (sunucu verisi) + Ayarlar'daki yönetim işlemleri. * Defter pasifler dahil okur: eski kayıtların adı gösterilir, yeni seçimde yalnızca aktifler sunulur. */ -import { useQuery } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import type { CatalogCreateRequest } from '@/types/CatalogCreateRequest' import type { CatalogItem } from '@/types/CatalogItem' +import type { CatalogUpdateRequest } from '@/types/CatalogUpdateRequest' +import type { Group } from '@/types/Group' import { api } from './api' import { queryKeys } from './query' @@ -25,3 +28,56 @@ export function usePaymentMethods(includeInactive = false) { staleTime: STALE, }) } + +// ---------- Yönetim (Ayarlar) ---------- + +export type CatalogKind = 'categories' | 'payment-methods' + +/** Bir değişiklikten sonra listenin tüm biçimleri (aktif / pasif dahil) ve raporlar yenilenir */ +function useInvalidate(kind: CatalogKind) { + const qc = useQueryClient() + return () => + Promise.all([ + qc.invalidateQueries({ queryKey: [kind] }), + // Rapor ve değerlendirmelerde kategori adları görünür + qc.invalidateQueries({ queryKey: queryKeys.reports }), + qc.invalidateQueries({ queryKey: ['period'] }), + ]) +} + +export function useCreateCatalog(kind: CatalogKind) { + const invalidate = useInvalidate(kind) + return useMutation({ + mutationFn: (body: CatalogCreateRequest) => api.post(`/${kind}`, body), + onSuccess: invalidate, + }) +} + +export function useUpdateCatalog(kind: CatalogKind) { + const invalidate = useInvalidate(kind) + return useMutation({ + mutationFn: ({ id, ...body }: CatalogUpdateRequest & { id: string }) => + api.patch(`/${kind}/${id}`, body), + // Hata (ör. başkası değiştirdi) olsa da güncel liste gelsin + onSettled: invalidate, + }) +} + +export function useReorderCatalog(kind: CatalogKind) { + const invalidate = useInvalidate(kind) + return useMutation({ + mutationFn: (body: { group?: Group; ids: string[] }) => + api.put(`/${kind}/order`, body), + onSettled: invalidate, + }) +} + +/** Sıralamada bir öğeyi bir yukarı (-1) ya da aşağı (+1) taşı; uçta ise `null` */ +export function moveId(ids: readonly string[], id: string, dir: -1 | 1): string[] | null { + const i = ids.indexOf(id) + const j = i + dir + if (i < 0 || j < 0 || j >= ids.length) return null + const next = [...ids] + ;[next[i], next[j]] = [next[j], next[i]] + return next +} diff --git a/frontend/src/lib/strings.ts b/frontend/src/lib/strings.ts index 744d821..2b4278a 100644 --- a/frontend/src/lib/strings.ts +++ b/frontend/src/lib/strings.ts @@ -298,6 +298,62 @@ export const t = { over: 'Aşıldı', }, + settings: { + back: 'Ayarlar', + appearance: 'Görünüm', + menu: { + categories: { + title: 'Kategoriler', + hint: 'Grup grup alt kategoriler: ekle, düzenle, sırala', + }, + paymentMethods: { title: 'Ödeme türleri', hint: 'Nakit, kart, havale…' }, + data: { title: 'Veri ve yedek', hint: 'JSON yedek, CSV, banka ekstresi' }, + }, + categories: { + title: 'Kategoriler', + description: + 'Harcamalar 4 Kakeibo grubunda toplanır; her grubun altındaki kategorileri sen belirlersin.', + }, + paymentMethods: { + title: 'Ödeme türleri', + description: 'Harcamayı nasıl ödediğin: raporlarda ve ekstre aktarımında kullanılır.', + }, + data: { + title: 'Veri ve yedek', + description: 'Yedek, CSV dışa / içe aktarma ve banka ekstresi.', + }, + catalog: { + add: (what: string) => `${what} ekle`, + category: 'Kategori', + paymentMethod: 'Ödeme türü', + name: 'Ad', + emoji: 'Emoji', + emojiHint: 'İsteğe bağlı; aşağıdan seçebilir ya da klavyeden yazabilirsin.', + pick: (e: string) => `${e} seç`, + save: 'Kaydet', + cancel: 'Vazgeç', + edit: (name: string) => `${name}: düzenle`, + up: (name: string) => `${name}: yukarı taşı`, + down: (name: string) => `${name}: aşağı taşı`, + deactivate: 'Pasifleştir', + activate: 'Yeniden etkinleştir', + inactive: 'Pasif', + inactiveHint: + 'Pasif kayıt yeni harcamada seçilemez; eski kayıtlar, raporlar ve aktarımlar etkilenmez. Kalıcı silme yoktur, istediğinde geri açabilirsin.', + nameRequired: 'Ad girin', + nameTooLong: 'Ad en fazla 40 karakter olabilir', + emojiTooLong: 'Tek bir emoji girin', + added: (name: string) => `“${name}” eklendi`, + saved: 'Kaydedildi', + deactivated: (name: string) => `“${name}” pasifleştirildi`, + activated: (name: string) => `“${name}” yeniden etkin`, + undo: 'Geri al', + empty: 'Bu grupta henüz kategori yok.', + emptyMethods: 'Henüz ödeme türü yok.', + groupFixed: + 'Kategorinin grubu sonradan değiştirilemez (eski harcamaların grubu bozulmasın); gerekirse doğru grupta yeni kategori aç, eskisini pasifleştir.', + }, + }, monthlyReport: { title: 'Aylık Rapor', description: 'Dönemin değerlendirmesi: ne yaptın, ne iyi gitti, gelecek ay için öneriler.', @@ -675,7 +731,7 @@ export const t = { entryHint: 'Excel ya da CSV ekstreyi yükle; satırlar harcama, iade, gelir ya da yok say olarak ayrılır. Dönemi olmayan aylar için arşiv dönemi açılır.', open: 'Ekstre içe aktar', - back: 'Ayarlar', + back: 'Veri ve yedek', steps: ['Dosya', 'Eşleştir', 'Gözden geçir'], fileHint: 'İnternet bankacılığından “Excel’e aktar” ile indirdiğin dosyayı seç (.xlsx, .xls ya da .csv). Dosya bu cihazda okunur; yalnızca tarih, açıklama ve tutar gönderilir.', diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index b0cb37b..bf5f46c 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -17,6 +17,8 @@ import { MonthlyReportPage } from '@/features/review/report-page' import { ReviewPage } from '@/features/review/page' import { AssetPage } from '@/features/savings/asset-page' import { SavingsPage } from '@/features/savings/page' +import { CategoriesPage, PaymentMethodsPage } from '@/features/settings/catalog-pages' +import { DataPage } from '@/features/settings/data-page' import { SettingsPage } from '@/features/settings/page' import { StatementPage } from '@/features/statement/page' @@ -47,6 +49,9 @@ const routes: RouteObject[] = [ { path: paths.monthlyReport, element: }, { path: paths.reports, element: }, { path: paths.settings, element: }, + { path: paths.settingsCategories, element: }, + { path: paths.settingsPaymentMethods, element: }, + { path: paths.settingsData, element: }, { path: paths.statement, element: }, { path: paths.more, element: }, { path: '*', element: },