feat(ui): renk paleti, ikonlar, MoneyInput ve navigasyon
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { CategoriesManager } from "@/features/categories/components/categories-manager";
|
||||
import { listCategories } from "@/features/categories/service";
|
||||
import { hasHouseholdRole } from "@/features/households/roles";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { requireActiveHousehold } from "@/lib/dal";
|
||||
|
||||
export const metadata: Metadata = { title: tr.categories.title };
|
||||
|
||||
export default async function CategoriesPage() {
|
||||
const { user, household } = await requireActiveHousehold("viewer");
|
||||
const categories = await listCategories(user.id, household.id, { includeArchived: true });
|
||||
if (!categories) notFound();
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={tr.categories.title} description={tr.categories.description} />
|
||||
<CategoriesManager
|
||||
categories={categories}
|
||||
canEdit={hasHouseholdRole(household.role, "member")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -96,6 +96,21 @@
|
||||
--chart-4: var(--warning);
|
||||
--chart-5: oklch(0.65 0.08 230);
|
||||
--radius: 0.625rem;
|
||||
|
||||
/* Kullanıcı verisi renkleri (kategori/hesap): anahtar → değişken (shared/appearance.ts) */
|
||||
--swatch-blue: oklch(0.6 0.15 258);
|
||||
--swatch-sky: oklch(0.6 0.15 230);
|
||||
--swatch-teal: oklch(0.6 0.15 185);
|
||||
--swatch-green: oklch(0.6 0.15 150);
|
||||
--swatch-lime: oklch(0.6 0.15 125);
|
||||
--swatch-amber: oklch(0.6 0.15 80);
|
||||
--swatch-orange: oklch(0.6 0.15 55);
|
||||
--swatch-red: oklch(0.6 0.15 25);
|
||||
--swatch-rose: oklch(0.6 0.15 10);
|
||||
--swatch-pink: oklch(0.6 0.15 350);
|
||||
--swatch-purple: oklch(0.6 0.15 305);
|
||||
--swatch-indigo: oklch(0.6 0.15 275);
|
||||
--swatch-slate: oklch(0.55 0.03 255);
|
||||
--sidebar: oklch(0.975 0.008 250);
|
||||
--sidebar-foreground: oklch(0.22 0.04 262);
|
||||
--sidebar-primary: oklch(0.49 0.17 258);
|
||||
@@ -140,6 +155,20 @@
|
||||
--chart-3: var(--expense);
|
||||
--chart-4: var(--warning);
|
||||
--chart-5: oklch(0.7 0.07 230);
|
||||
|
||||
--swatch-blue: oklch(0.74 0.13 258);
|
||||
--swatch-sky: oklch(0.74 0.13 230);
|
||||
--swatch-teal: oklch(0.74 0.13 185);
|
||||
--swatch-green: oklch(0.74 0.13 150);
|
||||
--swatch-lime: oklch(0.74 0.13 125);
|
||||
--swatch-amber: oklch(0.74 0.13 80);
|
||||
--swatch-orange: oklch(0.74 0.13 55);
|
||||
--swatch-red: oklch(0.74 0.13 25);
|
||||
--swatch-rose: oklch(0.74 0.13 10);
|
||||
--swatch-pink: oklch(0.74 0.13 350);
|
||||
--swatch-purple: oklch(0.74 0.13 305);
|
||||
--swatch-indigo: oklch(0.74 0.13 275);
|
||||
--swatch-slate: oklch(0.72 0.03 255);
|
||||
--sidebar: oklch(0.2 0.045 264);
|
||||
--sidebar-foreground: oklch(0.96 0.01 250);
|
||||
--sidebar-primary: oklch(0.76 0.12 245);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
Baby,
|
||||
Banknote,
|
||||
Book,
|
||||
Briefcase,
|
||||
Building,
|
||||
Bus,
|
||||
Car,
|
||||
CircleEllipsis,
|
||||
Coffee,
|
||||
Coins,
|
||||
CreditCard,
|
||||
Droplet,
|
||||
Dumbbell,
|
||||
Film,
|
||||
Flame,
|
||||
Fuel,
|
||||
Gamepad2,
|
||||
Gift,
|
||||
GraduationCap,
|
||||
HeartPulse,
|
||||
House,
|
||||
Landmark,
|
||||
Music,
|
||||
PawPrint,
|
||||
Percent,
|
||||
PiggyBank,
|
||||
Pill,
|
||||
Plane,
|
||||
Receipt,
|
||||
Repeat,
|
||||
Scissors,
|
||||
Shirt,
|
||||
ShoppingBasket,
|
||||
ShoppingCart,
|
||||
Smartphone,
|
||||
Sofa,
|
||||
Sparkles,
|
||||
SprayCan,
|
||||
Tag,
|
||||
Train,
|
||||
TrendingUp,
|
||||
Undo2,
|
||||
Utensils,
|
||||
Wallet,
|
||||
Wifi,
|
||||
Wrench,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
type LucideProps,
|
||||
} from "lucide-react";
|
||||
import type { IconKey } from "@/shared/appearance";
|
||||
|
||||
/** Saklanan ikon anahtarı → lucide bileşeni (shared/appearance.ts ICON_KEYS ile birebir). */
|
||||
export const ICONS: Record<IconKey, LucideIcon> = {
|
||||
wallet: Wallet,
|
||||
banknote: Banknote,
|
||||
landmark: Landmark,
|
||||
"piggy-bank": PiggyBank,
|
||||
"credit-card": CreditCard,
|
||||
utensils: Utensils,
|
||||
"shopping-cart": ShoppingCart,
|
||||
"shopping-basket": ShoppingBasket,
|
||||
receipt: Receipt,
|
||||
zap: Zap,
|
||||
droplet: Droplet,
|
||||
flame: Flame,
|
||||
wifi: Wifi,
|
||||
smartphone: Smartphone,
|
||||
house: House,
|
||||
building: Building,
|
||||
car: Car,
|
||||
fuel: Fuel,
|
||||
bus: Bus,
|
||||
train: Train,
|
||||
"heart-pulse": HeartPulse,
|
||||
pill: Pill,
|
||||
"graduation-cap": GraduationCap,
|
||||
book: Book,
|
||||
shirt: Shirt,
|
||||
gamepad: Gamepad2,
|
||||
film: Film,
|
||||
music: Music,
|
||||
coffee: Coffee,
|
||||
plane: Plane,
|
||||
gift: Gift,
|
||||
baby: Baby,
|
||||
"paw-print": PawPrint,
|
||||
wrench: Wrench,
|
||||
sofa: Sofa,
|
||||
repeat: Repeat,
|
||||
briefcase: Briefcase,
|
||||
"trending-up": TrendingUp,
|
||||
coins: Coins,
|
||||
percent: Percent,
|
||||
undo: Undo2,
|
||||
sparkles: Sparkles,
|
||||
"spray-can": SprayCan,
|
||||
scissors: Scissors,
|
||||
dumbbell: Dumbbell,
|
||||
tag: Tag,
|
||||
"circle-ellipsis": CircleEllipsis,
|
||||
};
|
||||
|
||||
export function AppIcon({ name, ...props }: { name: IconKey } & LucideProps) {
|
||||
const Icon = ICONS[name] ?? Tag;
|
||||
return <Icon aria-hidden {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ICON_KEYS, SWATCHES, type IconKey, type Swatch } from "@/shared/appearance";
|
||||
import { AppIcon } from "./app-icon";
|
||||
import { swatchStyle } from "./swatch";
|
||||
import { tr } from "@/i18n/tr";
|
||||
|
||||
/** Renk seçici (radio grubu, klavye ile gezilebilir). */
|
||||
export function SwatchPicker({
|
||||
value,
|
||||
onChange,
|
||||
labelledBy,
|
||||
}: {
|
||||
value: Swatch;
|
||||
onChange: (v: Swatch) => void;
|
||||
labelledBy: string;
|
||||
}) {
|
||||
return (
|
||||
<div role="radiogroup" aria-labelledby={labelledBy} className="flex flex-wrap gap-2">
|
||||
{SWATCHES.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === s}
|
||||
aria-label={tr.swatches[s]}
|
||||
title={tr.swatches[s]}
|
||||
onClick={() => onChange(s)}
|
||||
style={swatchStyle(s)}
|
||||
className="flex size-8 items-center justify-center rounded-full bg-(--swatch) text-background ring-offset-2 ring-offset-background outline-none focus-visible:ring-3 focus-visible:ring-ring/50 aria-checked:ring-2 aria-checked:ring-(--swatch)"
|
||||
>
|
||||
{value === s ? <Check className="size-4" /> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** İkon seçici; seçili renkle önizler. */
|
||||
export function IconPicker({
|
||||
value,
|
||||
color,
|
||||
onChange,
|
||||
labelledBy,
|
||||
}: {
|
||||
value: IconKey;
|
||||
color: Swatch;
|
||||
onChange: (v: IconKey) => void;
|
||||
labelledBy: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby={labelledBy}
|
||||
style={swatchStyle(color)}
|
||||
className="grid max-h-44 grid-cols-8 gap-1 overflow-y-auto rounded-lg border p-1.5 sm:grid-cols-10"
|
||||
>
|
||||
{ICON_KEYS.map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === key}
|
||||
aria-label={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cn(
|
||||
"flex aspect-square items-center justify-center rounded-md outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50",
|
||||
"aria-checked:bg-(--swatch)/15 aria-checked:text-(--swatch) aria-checked:ring-1 aria-checked:ring-(--swatch)",
|
||||
)}
|
||||
>
|
||||
<AppIcon name={key} className="size-4.5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { IconKey, Swatch } from "@/shared/appearance";
|
||||
import { AppIcon } from "./app-icon";
|
||||
|
||||
/** Renk anahtarını CSS değişkenine bağlar: --swatch-blue → --swatch (sınıflar bunu kullanır). */
|
||||
export const swatchStyle = (color: Swatch) =>
|
||||
({ "--swatch": `var(--swatch-${color})` }) as React.CSSProperties;
|
||||
|
||||
/** Renkli yuvarlak ikon rozeti (kategori/hesap). */
|
||||
export function IconBadge({
|
||||
icon,
|
||||
color,
|
||||
className,
|
||||
size = "md",
|
||||
}: {
|
||||
icon: IconKey;
|
||||
color: Swatch;
|
||||
className?: string;
|
||||
size?: "sm" | "md";
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
style={swatchStyle(color)}
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center justify-center rounded-full bg-(--swatch)/15 text-(--swatch)",
|
||||
size === "sm" ? "size-7 [&_svg]:size-3.5" : "size-9 [&_svg]:size-4.5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<AppIcon name={icon} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Package,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Tags,
|
||||
Target,
|
||||
UserCog,
|
||||
Users,
|
||||
@@ -25,14 +26,16 @@ export type NavItem = {
|
||||
};
|
||||
|
||||
/** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */
|
||||
export const CURRENT_PHASE = 5;
|
||||
/** Alt fazlar ondalıkla: 6.1 = Faz 6a. */
|
||||
export const CURRENT_PHASE = 6.1;
|
||||
|
||||
export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE;
|
||||
|
||||
export const MAIN_NAV: NavItem[] = [
|
||||
{ href: "/dashboard", label: tr.nav.dashboard, icon: LayoutDashboard, phase: 2 },
|
||||
{ href: "/islemler", label: tr.nav.transactions, icon: ArrowLeftRight, phase: 6 },
|
||||
{ href: "/hesaplar", label: tr.nav.accounts, icon: Wallet, phase: 6 },
|
||||
{ href: "/islemler", label: tr.nav.transactions, icon: ArrowLeftRight, phase: 6.2 },
|
||||
{ href: "/hesaplar", label: tr.nav.accounts, icon: Wallet, phase: 6.1 },
|
||||
{ href: "/kategoriler", label: tr.nav.categories, icon: Tags, phase: 6.1 },
|
||||
{ href: "/planli", label: tr.nav.planned, icon: CalendarClock, phase: 7 },
|
||||
{ href: "/cari", label: tr.nav.contacts, icon: Users, phase: 8 },
|
||||
{ href: "/birikim", label: tr.nav.savings, icon: Coins, phase: 9 },
|
||||
@@ -49,9 +52,11 @@ export const ADMIN_NAV: NavItem[] = [
|
||||
];
|
||||
|
||||
/** Mobil alt menü: ortada Hızlı ekle, sağda "Daha fazla" (yan menüyü açar). */
|
||||
const navByHref = (href: string) => MAIN_NAV.find((i) => i.href === href)!;
|
||||
|
||||
export const BOTTOM_NAV = {
|
||||
left: [MAIN_NAV[0]!, MAIN_NAV[1]!],
|
||||
right: [MAIN_NAV[6]!],
|
||||
left: [navByHref("/dashboard"), navByHref("/islemler")],
|
||||
right: [navByHref("/butce")],
|
||||
};
|
||||
|
||||
export function isActivePath(pathname: string, href: string) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/components/ui/input-group";
|
||||
import { formatMoneyInput, parseMoney } from "@/shared/money";
|
||||
|
||||
/**
|
||||
* Türkçe tutar girişi ("1.250,50"). Değer string olarak tutulur (zod sunucuda/istemcide ayrıştırır);
|
||||
* alandan çıkınca geçerliyse Türkçe biçime getirilir.
|
||||
*/
|
||||
export function MoneyInput({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
allowNegative = false,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof InputGroupInput>, "value" | "onChange" | "type"> & {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
allowNegative?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
{...props}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
className="text-right tabular-nums"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
onChange(e.target.value.replace(allowNegative ? /[^\d.,\s-−]/g : /[^\d.,\s]/g, ""))
|
||||
}
|
||||
onBlur={(e) => {
|
||||
const minor = parseMoney(value);
|
||||
if (minor !== null && value.trim() !== "") onChange(formatMoneyInput(minor));
|
||||
onBlur?.(e);
|
||||
}}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>₺</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { diffFields, writeAudit } from "@/lib/audit";
|
||||
import { getDb } from "@/lib/db/mongo";
|
||||
import { connectMongoose } from "@/lib/db/mongoose";
|
||||
import { toObjectId } from "@/lib/object-id";
|
||||
import { installDefaultCategories } from "@/features/categories/service";
|
||||
import { HouseholdModel } from "@/models/household";
|
||||
import { HouseholdMemberModel } from "@/models/household-member";
|
||||
import type { HouseholdSettings } from "@/shared/schemas/household";
|
||||
@@ -321,7 +322,8 @@ export async function adminCreateHousehold(
|
||||
},
|
||||
session,
|
||||
);
|
||||
// FAZ 6: varsayılan Türkçe kategori seti bu transaction içinde yüklenecek.
|
||||
// Varsayılan Türkçe kategori seti aynı transaction'da (Bölüm 6.2).
|
||||
await installDefaultCategories(household!._id, actor, session);
|
||||
});
|
||||
return ok({ id: createdId });
|
||||
}
|
||||
|
||||
+160
@@ -34,6 +34,21 @@ export const tr = {
|
||||
unauthorized: "Bu işlem için yetkiniz yok.",
|
||||
notFound: "Kayıt bulunamadı.",
|
||||
},
|
||||
swatches: {
|
||||
blue: "Mavi",
|
||||
sky: "Gök mavisi",
|
||||
teal: "Turkuaz",
|
||||
green: "Yeşil",
|
||||
lime: "Fıstık yeşili",
|
||||
amber: "Kehribar",
|
||||
orange: "Turuncu",
|
||||
red: "Kırmızı",
|
||||
rose: "Gül",
|
||||
pink: "Pembe",
|
||||
purple: "Mor",
|
||||
indigo: "Çivit",
|
||||
slate: "Gri",
|
||||
},
|
||||
theme: {
|
||||
label: "Tema",
|
||||
light: "Açık",
|
||||
@@ -45,6 +60,7 @@ export const tr = {
|
||||
dashboard: "Ana sayfa",
|
||||
transactions: "İşlemler",
|
||||
accounts: "Hesaplar",
|
||||
categories: "Kategoriler",
|
||||
budget: "Bütçe",
|
||||
planned: "Planlı işlemler",
|
||||
contacts: "Cari hesaplar",
|
||||
@@ -122,6 +138,143 @@ export const tr = {
|
||||
memberRemoved: (name: string) => `${name} haneden çıkarıldı.`,
|
||||
banned: "Askıda",
|
||||
},
|
||||
accounts: {
|
||||
title: "Hesaplar",
|
||||
description: "Nakit, banka, birikim, kredi kartı ve yemek kartı hesapları.",
|
||||
newAccount: "Yeni hesap",
|
||||
createTitle: "Yeni hesap",
|
||||
editTitle: "Hesabı düzenle",
|
||||
formDescription: "Güncel bakiye işlemlerden hesaplanır; açılış bakiyesi başlangıç noktasıdır.",
|
||||
types: {
|
||||
cash: "Nakit",
|
||||
bank: "Vadesiz banka",
|
||||
savings: "Birikim / vadeli",
|
||||
credit_card: "Kredi kartı",
|
||||
meal_card: "Yemek kartı",
|
||||
},
|
||||
typeGroups: {
|
||||
cash: "Nakit",
|
||||
bank: "Banka hesapları",
|
||||
savings: "Birikim hesapları",
|
||||
credit_card: "Kredi kartları",
|
||||
meal_card: "Yemek kartları",
|
||||
},
|
||||
providers: {
|
||||
pluxee: "Pluxee (Sodexo)",
|
||||
multinet: "Multinet",
|
||||
edenred: "Edenred (Ticket)",
|
||||
setcard: "Setcard",
|
||||
metropol: "Metropol",
|
||||
other: "Diğer",
|
||||
},
|
||||
fields: {
|
||||
name: "Hesap adı",
|
||||
type: "Tür",
|
||||
typeLockedHint: "Hesap türü oluşturulduktan sonra değiştirilemez.",
|
||||
institution: "Banka / kurum",
|
||||
institutionPlaceholder: "ör. Ziraat Bankası",
|
||||
provider: "Kart sağlayıcısı",
|
||||
openingBalance: "Açılış bakiyesi (TL)",
|
||||
openingDebt: "Açılış borcu (TL)",
|
||||
openingBalanceHint: "Banka hesabı KMH kullanıyorsa eksi girilebilir (ör. -2.500,00).",
|
||||
openingDebtHint: "Karttaki mevcut toplam borç; pozitif girin.",
|
||||
openingDate: "Açılış tarihi",
|
||||
owner: "Hesap sahibi",
|
||||
ownerNone: "— Ortak hesap —",
|
||||
isPrivate: "Yalnızca ben (bakiyeyi yalnızca hesap sahibi görür)",
|
||||
appearance: "Görünüm",
|
||||
advanced: "Gelişmiş ayarlar",
|
||||
overdraft: "Esnek hesap (KMH)",
|
||||
overdraftEnabled: "Bu hesapta KMH var",
|
||||
overdraftLimit: "KMH limiti (TL)",
|
||||
overdraftRate: "Aylık faiz oranı (%)",
|
||||
overdraftStatementDay: "Hesap kesim günü",
|
||||
card: "Kart bilgileri",
|
||||
cardLimit: "Kart limiti (TL)",
|
||||
cardStatementDay: "Hesap kesim günü",
|
||||
cardStatementDayHint: "Ayda o gün yoksa (ör. 31) ayın son günü kullanılır.",
|
||||
cardDueKind: "Son ödeme günü",
|
||||
dueKinds: { daysAfter: "Kesimden N gün sonra", fixedDay: "Ayın sabit günü" },
|
||||
cardDueValueDays: "Gün sayısı",
|
||||
cardDueValueDay: "Ayın günü",
|
||||
cardPaymentAccount: "Ödeme hesabı",
|
||||
cardPaymentAccountNone: "— Seçilmedi —",
|
||||
},
|
||||
summary: {
|
||||
assets: "Varlıklar",
|
||||
cardDebt: "Kart borçları",
|
||||
net: "Net",
|
||||
includesHidden: "Toplamlara gizli hesaplar dahil.",
|
||||
excludesHidden: "Gizli hesaplar toplamlara dahil değil (hane ayarı).",
|
||||
},
|
||||
balance: "Bakiye",
|
||||
debt: "Borç",
|
||||
hidden: "Gizli",
|
||||
hiddenHint: "Bu hesap “yalnızca ben” olarak işaretli; bakiyeyi yalnızca hesap sahibi görür.",
|
||||
privateBadge: "Yalnızca ben",
|
||||
overdraftBadge: (limit: string) => `KMH ${limit}`,
|
||||
limitLabel: (limit: string) => `Limit ${limit}`,
|
||||
statementLabel: (day: number) => `Kesim: ${day}`,
|
||||
empty: "Henüz hesap yok.",
|
||||
emptyOwnerHint: "Başlamak için ilk hesabınızı ekleyin (ör. nakit cüzdan, maaş hesabı).",
|
||||
archivedSection: (n: number) => `Arşivlenmiş hesaplar (${n})`,
|
||||
archive: "Arşivle",
|
||||
unarchive: "Arşivden çıkar",
|
||||
archiveTitle: "Hesap arşivlensin mi?",
|
||||
archiveDescription: (name: string) =>
|
||||
`“${name}” yeni işlemlerde seçilemez; geçmiş işlemler ve raporlar korunur. İstediğiniz zaman arşivden çıkarabilirsiniz.`,
|
||||
created: (name: string) => `${name} oluşturuldu.`,
|
||||
updated: (name: string) => `${name} güncellendi.`,
|
||||
archivedToast: (name: string) => `${name} arşivlendi.`,
|
||||
unarchivedToast: (name: string) => `${name} arşivden çıkarıldı.`,
|
||||
ownerNotMember: "Hesap sahibi bu hanenin üyesi olmalı.",
|
||||
invalidPaymentAccount:
|
||||
"Ödeme hesabı bu hanedeki aktif bir banka, nakit veya birikim hesabı olmalı.",
|
||||
typeLocked: "Hesap türü değiştirilemez.",
|
||||
ownerOnly: "Hesapları yalnızca hane sahipleri yönetebilir.",
|
||||
},
|
||||
categories: {
|
||||
title: "Kategoriler",
|
||||
description: "Gelir ve gider kategorileri. Alt kategoriler raporlarda üst kategoriye toplanır.",
|
||||
expense: "Gider",
|
||||
income: "Gelir",
|
||||
newCategory: "Yeni kategori",
|
||||
addChild: "Alt kategori ekle",
|
||||
edit: "Düzenle",
|
||||
moveUp: "Yukarı taşı",
|
||||
moveDown: "Aşağı taşı",
|
||||
archive: "Arşivle",
|
||||
unarchive: "Arşivden çıkar",
|
||||
showArchived: "Arşivi göster",
|
||||
archived: "Arşivli",
|
||||
defaultBadge: "Varsayılan",
|
||||
empty: "Bu türde kategori yok.",
|
||||
createTitle: "Yeni kategori",
|
||||
editTitle: "Kategoriyi düzenle",
|
||||
formDescription: "Alt kategori için bir üst kategori seçin; boş bırakılırsa ana kategori olur.",
|
||||
name: "Ad",
|
||||
parent: "Üst kategori",
|
||||
noParent: "— Ana kategori —",
|
||||
icon: "İkon",
|
||||
color: "Renk",
|
||||
created: (name: string) => `${name} oluşturuldu.`,
|
||||
updated: (name: string) => `${name} güncellendi.`,
|
||||
archivedToast: (name: string) => `${name} arşivlendi.`,
|
||||
unarchivedToast: (name: string) => `${name} arşivden çıkarıldı.`,
|
||||
archiveTitle: "Kategori arşivlensin mi?",
|
||||
archiveDescription: (name: string) =>
|
||||
`“${name}” ve alt kategorileri yeni işlemlerde seçilemez. Geçmiş işlemler ve raporlar etkilenmez.`,
|
||||
duplicate: "Bu düzeyde aynı adlı bir kategori zaten var.",
|
||||
violations: {
|
||||
self: "Kategori kendi altına taşınamaz.",
|
||||
descendant: "Kategori kendi alt kategorisinin altına taşınamaz.",
|
||||
kind: "Gelir ve gider kategorileri birbirinin altına konamaz.",
|
||||
depth: "En fazla 5 seviye derinliğe izin verilir.",
|
||||
parentNotFound: "Üst kategori bulunamadı.",
|
||||
parentArchived: "Üst kategori arşivli; önce onu arşivden çıkarın.",
|
||||
},
|
||||
readOnly: "İzleyici rolündesiniz; kategorileri değiştiremezsiniz.",
|
||||
},
|
||||
adminHouseholds: {
|
||||
title: "Haneler",
|
||||
description: "Hane oluşturma, kullanıcıları haneye atama ve hane içi roller.",
|
||||
@@ -281,6 +434,13 @@ export const tr = {
|
||||
householdNameMax: "Hane adı en fazla 80 karakter olabilir",
|
||||
selectUser: "Bir kullanıcı seçin",
|
||||
wholeNumber: "Tam sayı girin",
|
||||
money: "Geçerli bir tutar girin (ör. 1.250,50)",
|
||||
moneyNonNegative: "Tutar negatif olamaz",
|
||||
percent: "0 ile 100 arasında bir oran girin (ör. 4,25)",
|
||||
date: "Geçerli bir tarih seçin",
|
||||
selectProvider: "Kart sağlayıcısını seçin",
|
||||
privateNeedsOwner: "“Yalnızca ben” hesabı için hesap sahibini seçin",
|
||||
dueDays: "Kesimden sonra en fazla 30 gün olabilir",
|
||||
},
|
||||
errors: {
|
||||
notFoundTitle: "Sayfa bulunamadı",
|
||||
|
||||
@@ -17,6 +17,13 @@ export const AUDIT_ACTIONS = [
|
||||
"user.ban",
|
||||
"user.unban",
|
||||
"user.remove",
|
||||
"account.create",
|
||||
"account.update",
|
||||
"account.archive",
|
||||
"account.unarchive",
|
||||
"category.archive",
|
||||
"category.unarchive",
|
||||
"category.move",
|
||||
] as const;
|
||||
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Kullanıcı verisindeki renk ve ikonlar anahtar olarak saklanır (sabit renk kodu yok).
|
||||
* Renkler globals.css'teki --swatch-* değişkenlerine, ikonlar lucide bileşenlerine eşlenir.
|
||||
*/
|
||||
export const SWATCHES = [
|
||||
"blue",
|
||||
"sky",
|
||||
"teal",
|
||||
"green",
|
||||
"lime",
|
||||
"amber",
|
||||
"orange",
|
||||
"red",
|
||||
"rose",
|
||||
"pink",
|
||||
"purple",
|
||||
"indigo",
|
||||
"slate",
|
||||
] as const;
|
||||
export type Swatch = (typeof SWATCHES)[number];
|
||||
|
||||
export const ICON_KEYS = [
|
||||
"wallet",
|
||||
"banknote",
|
||||
"landmark",
|
||||
"piggy-bank",
|
||||
"credit-card",
|
||||
"utensils",
|
||||
"shopping-cart",
|
||||
"shopping-basket",
|
||||
"receipt",
|
||||
"zap",
|
||||
"droplet",
|
||||
"flame",
|
||||
"wifi",
|
||||
"smartphone",
|
||||
"house",
|
||||
"building",
|
||||
"car",
|
||||
"fuel",
|
||||
"bus",
|
||||
"train",
|
||||
"heart-pulse",
|
||||
"pill",
|
||||
"graduation-cap",
|
||||
"book",
|
||||
"shirt",
|
||||
"gamepad",
|
||||
"film",
|
||||
"music",
|
||||
"coffee",
|
||||
"plane",
|
||||
"gift",
|
||||
"baby",
|
||||
"paw-print",
|
||||
"wrench",
|
||||
"sofa",
|
||||
"repeat",
|
||||
"briefcase",
|
||||
"trending-up",
|
||||
"coins",
|
||||
"percent",
|
||||
"undo",
|
||||
"sparkles",
|
||||
"spray-can",
|
||||
"scissors",
|
||||
"dumbbell",
|
||||
"tag",
|
||||
"circle-ellipsis",
|
||||
] as const;
|
||||
export type IconKey = (typeof ICON_KEYS)[number];
|
||||
@@ -0,0 +1,27 @@
|
||||
import mongoose from "mongoose";
|
||||
import { expect } from "vitest";
|
||||
import { getDb, getMongoClient } from "@/lib/db/mongo";
|
||||
import { HouseholdModel } from "@/models/household";
|
||||
import { runSeed } from "../../scripts/seed/run";
|
||||
|
||||
/** Entegrasyon testleri için: test veritabanını temizler, seed'i çalıştırır, kimlikleri döner. */
|
||||
export async function setupSeededDb() {
|
||||
expect(getDb().databaseName).toBe("butcem_test");
|
||||
await getDb().dropDatabase();
|
||||
await runSeed({ log: () => {} });
|
||||
const users = new Map<string, string>();
|
||||
for (const u of await getDb().collection("user").find().toArray())
|
||||
users.set(String(u.username), String(u._id));
|
||||
const households = new Map<string, string>();
|
||||
for (const h of await HouseholdModel.find().lean()) households.set(h.name, String(h._id));
|
||||
return {
|
||||
user: (username: string) => users.get(username)!,
|
||||
household: (name: string) => households.get(name)!,
|
||||
};
|
||||
}
|
||||
|
||||
export async function teardownDb() {
|
||||
await getDb().dropDatabase();
|
||||
await mongoose.disconnect();
|
||||
await getMongoClient().close();
|
||||
}
|
||||
Reference in New Issue
Block a user