diff --git a/src/app/(app)/planner/[category]/page.tsx b/src/app/(app)/planner/[category]/page.tsx
new file mode 100644
index 0000000..27ff359
--- /dev/null
+++ b/src/app/(app)/planner/[category]/page.tsx
@@ -0,0 +1,10 @@
+import { CategoryTransactions } from "@/features/planner/CategoryTransactions";
+
+export default async function CategoryTransactionsPage({
+ params,
+}: {
+ params: Promise<{ category: string }>;
+}) {
+ const { category } = await params;
+ return ;
+}
diff --git a/src/app/(app)/planner/page.tsx b/src/app/(app)/planner/page.tsx
new file mode 100644
index 0000000..1b75157
--- /dev/null
+++ b/src/app/(app)/planner/page.tsx
@@ -0,0 +1,5 @@
+import { PlannerView } from "@/features/planner/PlannerView";
+
+export default function PlannerPage() {
+ return ;
+}
diff --git a/src/features/planner/CategoryTransactions.tsx b/src/features/planner/CategoryTransactions.tsx
new file mode 100644
index 0000000..d624a23
--- /dev/null
+++ b/src/features/planner/CategoryTransactions.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import * as React from "react";
+import Link from "next/link";
+import { TextFieldRoot, TextFieldInput } from "@seed-design/react";
+import { Card } from "@/ds";
+import { tugrik } from "@/ds/money";
+import { useTransactions } from "@/api/hooks/reads";
+import type { Txn } from "@/api/schemas";
+import { plannerStrings as s } from "./strings";
+
+export interface CategoryTransactionsProps {
+ category: string;
+}
+
+/** MM.dd from an RFC3339/ISO timestamp — mirrors CategoryTransactionsView's
+ * `shortDate` on iOS. Falls back to an empty string on unparsable input. */
+function shortDate(rfc: string): string {
+ const d = new Date(rfc);
+ if (Number.isNaN(d.getTime())) return "";
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
+ const dd = String(d.getDate()).padStart(2, "0");
+ return `${mm}.${dd}`;
+}
+
+/** Every transaction in one category (server expands to sub-categories too) —
+ * opened by tapping a category-limit row on the planner hub. Ports
+ * `CategoryTransactionsView.swift`: header + filter + list, tap-free (the web
+ * port has no transaction detail cover yet). */
+export function CategoryTransactions({ category }: CategoryTransactionsProps) {
+ const { data, isLoading } = useTransactions({
+ category,
+ from: "2020-01-01",
+ to: "2027-12-31",
+ limit: 300,
+ });
+ const [search, setSearch] = React.useState("");
+
+ const rows: Txn[] = React.useMemo(() => {
+ const all = data ?? [];
+ const needle = search.trim().toLowerCase();
+ if (!needle) return all;
+ return all.filter((t) => t.title.toLowerCase().includes(needle));
+ }, [data, search]);
+
+ return (
+
+
+
+ ←
+
+
{category}
+
+
+
+
+
+
+
+ {isLoading && …
}
+ {!isLoading && rows.length === 0 && (
+ {s.categoryTransactions.empty}
+ )}
+ {!isLoading && rows.length > 0 && (
+
+ )}
+
+
+ );
+}
diff --git a/src/features/planner/PlannerView.test.tsx b/src/features/planner/PlannerView.test.tsx
new file mode 100644
index 0000000..0aa6916
--- /dev/null
+++ b/src/features/planner/PlannerView.test.tsx
@@ -0,0 +1,55 @@
+import { render, screen } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import type { Budget } from "@/api/schemas";
+
+// jsdom has no CSS.supports(); Seed's SegmentedControl/TextField call it via
+// @seed-design/react-supports to detect :focus-visible support.
+if (typeof (globalThis as any).CSS === "undefined") {
+ (globalThis as any).CSS = { supports: () => false };
+} else if (typeof (globalThis as any).CSS.supports !== "function") {
+ (globalThis as any).CSS.supports = () => false;
+}
+
+const budgetFixture: Budget = {
+ dayLimit: "50000",
+ weekLimit: "300000",
+ monthLimit: "1200000",
+ plannedIncome: "2000000",
+ plannedIncomeManual: "0",
+ loanObligations: "0",
+ savingsContributions: "0",
+ subscriptionContributions: "0",
+ availableIncome: "2000000",
+ categories: [{ name: "хоол", day: "10000", week: "60000", month: "240000" }],
+ savingsGoals: [],
+ report: {
+ day: { overallSpent: "15000", overallLimit: "50000", rows: [{ category: "хоол", spent: "5000", limit: "20000" }] },
+ week: { overallSpent: "0", overallLimit: "300000", rows: [] },
+ month: { overallSpent: "0", overallLimit: "1200000", rows: [] },
+ },
+};
+
+vi.mock("@/api/hooks/reads", () => ({
+ useBudget: () => ({ data: budgetFixture, isLoading: false }),
+ useNetWorth: () => ({ data: { assets: "0", liabilities: "0", total: "0", accounts: [] }, isLoading: false }),
+}));
+
+vi.mock("@/api/hooks/mutations", () => ({
+ usePutBudget: () => ({ mutate: vi.fn() }),
+ useSavingsGoalMutations: () => ({ post: { mutate: vi.fn() }, delete: { mutate: vi.fn() } }),
+}));
+
+import { PlannerView } from "./PlannerView";
+
+describe("PlannerView", () => {
+ it("renders the day-horizon overall total and a category limit", () => {
+ render();
+
+ // Overall (day) card: spent / limit from budget.report.day.
+ expect(screen.getByText("15,000₮ / 50,000₮")).toBeInTheDocument();
+
+ // Category limit row: name + spent / limit from budget.report.day.rows.
+ expect(screen.getByText("хоол")).toBeInTheDocument();
+ expect(screen.getByText("5,000₮ / 20,000₮")).toBeInTheDocument();
+ });
+});
diff --git a/src/features/planner/PlannerView.tsx b/src/features/planner/PlannerView.tsx
new file mode 100644
index 0000000..d8f0608
--- /dev/null
+++ b/src/features/planner/PlannerView.tsx
@@ -0,0 +1,821 @@
+"use client";
+
+import * as React from "react";
+import Link from "next/link";
+import {
+ TextFieldRoot,
+ TextFieldInput,
+ SegmentedControlRoot,
+ SegmentedControlItem,
+ SegmentedControlItemHiddenInput,
+ ProgressCircleRoot,
+ ProgressCircleTrack,
+ ProgressCircleRange,
+ ContentDialogRoot,
+ ContentDialogBackdrop,
+ ContentDialogPositioner,
+ ContentDialogContent,
+ ContentDialogHeader,
+ ContentDialogTitle,
+ ContentDialogBody,
+ ContentDialogFooter,
+ Skeleton,
+} from "@seed-design/react";
+import { Card, MercuryButton, HideAmountsToggle, NameEdit } from "@/ds";
+import { tugrik, tugrikShort } from "@/ds/money";
+import { useBudget, useNetWorth } from "@/api/hooks/reads";
+import { usePutBudget, useSavingsGoalMutations } from "@/api/hooks/mutations";
+import type { Budget, SavingsGoal, Account } from "@/api/schemas";
+import { plannerStrings as s } from "./strings";
+
+type Horizon = "day" | "week" | "month";
+type CategoryLimit = Budget["categories"][number];
+type HorizonReport = Budget["report"]["day"];
+type LimitRow = HorizonReport["rows"][number];
+
+function dec(v: string | undefined | null): number {
+ return parseFloat(v ?? "0") || 0;
+}
+
+function onlyDigits(v: string): string {
+ return v.replace(/[^0-9]/g, "");
+}
+
+const HORIZON_LABEL: Record = {
+ day: s.horizon.day,
+ week: s.horizon.week,
+ month: s.horizon.month,
+};
+
+const OVERALL_LABEL: Record = {
+ day: s.overall.label.day,
+ week: s.overall.label.week,
+ month: s.overall.label.month,
+};
+
+/** The Төлөвлөгөө hub — ports `PlannerView.swift` + `LimitsHubModel.swift`.
+ * Shows the editable planned income, a day/week/month horizon toggle, the
+ * overall spend-vs-limit for that horizon, per-category limits, and savings
+ * goals. Every edit persists via `PUT /budget`, echoing back the full
+ * `categories` array (the DTO contract — nothing else may be dropped). */
+export function PlannerView() {
+ const { data: budget, isLoading } = useBudget();
+ const { data: netWorth } = useNetWorth();
+ const putBudget = usePutBudget();
+ const goalMutations = useSavingsGoalMutations();
+
+ const [horizon, setHorizon] = React.useState("day");
+
+ const accounts: Account[] = netWorth?.accounts ?? [];
+
+ function reportFor(h: Horizon): HorizonReport | undefined {
+ return budget?.report[h];
+ }
+
+ function categoryLimitFor(name: string): CategoryLimit {
+ return budget?.categories.find((c) => c.name === name) ?? { name, day: "0", week: "0", month: "0" };
+ }
+
+ /** Save a partial change, echoing the current budget for everything else
+ * (mirrors `LimitsHubModel.save` — nothing, including the planned-income
+ * override, may be silently lost). */
+ function save(partial: {
+ dayLimit?: string;
+ weekLimit?: string;
+ monthLimit?: string;
+ plannedIncomeManual?: string;
+ categories?: CategoryLimit[];
+ }) {
+ if (!budget) return;
+ putBudget.mutate({
+ dayLimit: partial.dayLimit ?? budget.dayLimit,
+ weekLimit: partial.weekLimit ?? budget.weekLimit,
+ monthLimit: partial.monthLimit ?? budget.monthLimit,
+ plannedIncomeManual: partial.plannedIncomeManual ?? budget.plannedIncomeManual,
+ categories: partial.categories ?? budget.categories,
+ });
+ }
+
+ function saveOverall(h: Horizon, value: number) {
+ if (!budget) return;
+ save({
+ dayLimit: h === "day" ? String(value) : budget.dayLimit,
+ weekLimit: h === "week" ? String(value) : budget.weekLimit,
+ monthLimit: h === "month" ? String(value) : budget.monthLimit,
+ });
+ }
+
+ function saveCategoryLimit(name: string, day: number, week: number, month: number) {
+ if (!budget) return;
+ const cats = [...budget.categories];
+ const updated: CategoryLimit = { name, day: String(day), week: String(week), month: String(month) };
+ const idx = cats.findIndex((c) => c.name === name);
+ if (idx >= 0) cats[idx] = updated;
+ else cats.push(updated);
+ save({ categories: cats });
+ }
+
+ function removeCategoryLimit(name: string) {
+ if (!budget) return;
+ save({ categories: budget.categories.filter((c) => c.name !== name) });
+ }
+
+ const report = reportFor(horizon);
+ const overallSpent = dec(report?.overallSpent);
+ const overallLimit = dec(report?.overallLimit);
+ const rows: LimitRow[] = report?.rows ?? [];
+ const goals: SavingsGoal[] = budget?.savingsGoals ?? [];
+
+ return (
+
+
+
+
save({ plannedIncomeManual: String(v) })} />
+
+ setHorizon(v as Horizon)}>
+ {(["day", "week", "month"] as const).map((h) => (
+
+
+ {HORIZON_LABEL[h]}
+
+ ))}
+
+
+ saveOverall(horizon, v)}
+ />
+
+
+
+
+ goalMutations.post.mutate({
+ originalName: goal.originalName,
+ name: goal.name,
+ target: String(goal.target),
+ monthlyContribution: String(goal.monthly),
+ accountId: goal.accountId,
+ targetDate: goal.targetDate,
+ })
+ }
+ onDelete={(name) => goalMutations.delete.mutate(name)}
+ />
+
+ );
+}
+
+// --- Planned income ---------------------------------------------------------
+
+function PlannedIncomeCard({
+ budget,
+ loading,
+ onSave,
+}: {
+ budget: Budget | undefined;
+ loading: boolean;
+ onSave: (value: number) => void;
+}) {
+ const [editing, setEditing] = React.useState(false);
+ const [draft, setDraft] = React.useState("");
+
+ const plannedIncome = dec(budget?.plannedIncome);
+ const loanObligations = dec(budget?.loanObligations);
+ const savingsContributions = dec(budget?.savingsContributions);
+ const availableIncome = dec(budget?.availableIncome);
+ const showBreakdown = loanObligations > 0 || savingsContributions > 0;
+
+ return (
+
+ {loading ? (
+
+ ) : editing ? (
+
+
{s.plannedIncome.editTitle}
+
setDraft(onlyDigits(v))}>
+
+
+
+ {s.plannedIncome.autoHint(tugrik(plannedIncome))}
+
+
+ setEditing(false)} style={{ flex: 1 }}>
+ {s.amountEntry.cancel}
+
+ {
+ onSave(parseFloat(draft) || 0);
+ setEditing(false);
+ }}
+ >
+ {s.amountEntry.save}
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+function BreakdownRow({ label, value, bold }: { label: string; value: string; bold?: boolean }) {
+ return (
+
+
+ {label}
+
+ {value}
+
+ );
+}
+
+// --- Overall (horizon) card --------------------------------------------------
+
+function OverallCard({
+ horizon,
+ spent,
+ limit,
+ loading,
+ onSave,
+}: {
+ horizon: Horizon;
+ spent: number;
+ limit: number;
+ loading: boolean;
+ onSave: (value: number) => void;
+}) {
+ const [editing, setEditing] = React.useState(false);
+ const [draft, setDraft] = React.useState("");
+ const percent = limit > 0 ? Math.min(100, (spent / limit) * 100) : 0;
+
+ return (
+
+ {loading ? (
+
+ ) : editing ? (
+
+
{s.overall.editTitle(HORIZON_LABEL[horizon])}
+
setDraft(onlyDigits(v))}>
+
+
+
+ setEditing(false)} style={{ flex: 1 }}>
+ {s.amountEntry.cancel}
+
+ {
+ onSave(parseFloat(draft) || 0);
+ setEditing(false);
+ }}
+ >
+ {s.amountEntry.save}
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+// --- Category limits ---------------------------------------------------------
+
+function CategoryList({
+ loading,
+ rows,
+ horizon,
+ categoryLimitFor,
+ onSaveLimit,
+ onRemoveLimit,
+}: {
+ loading: boolean;
+ rows: LimitRow[];
+ horizon: Horizon;
+ categoryLimitFor: (name: string) => CategoryLimit;
+ onSaveLimit: (name: string, day: number, week: number, month: number) => void;
+ onRemoveLimit: (name: string) => void;
+}) {
+ const [editingName, setEditingName] = React.useState(null);
+ const [drafts, setDrafts] = React.useState({ day: "", week: "", month: "" });
+ const [adding, setAdding] = React.useState(false);
+ const [newName, setNewName] = React.useState("");
+ const [confirmRemove, setConfirmRemove] = React.useState(null);
+
+ function openEditor(name: string) {
+ const limit = categoryLimitFor(name);
+ setDrafts({ day: dec(limit.day) > 0 ? limit.day : "", week: dec(limit.week) > 0 ? limit.week : "", month: dec(limit.month) > 0 ? limit.month : "" });
+ setEditingName(name);
+ }
+
+ return (
+
+
+
{s.categories.title}
+ { setNewName(""); setAdding(true); }}>
+ {s.categories.add}
+
+
+
+ {adding && (
+
+
+
+
+
+ setAdding(false)}>
+ {s.amountEntry.cancel}
+
+ {
+ const name = newName.trim();
+ setAdding(false);
+ openEditor(name);
+ }}
+ >
+ {s.amountEntry.save}
+
+
+
+ )}
+
+ {loading && (
+
+
+
+
+ )}
+
+ {!loading && rows.length === 0 && (
+
+ {s.categories.empty}
+
+ )}
+
+ {!loading && rows.length > 0 && (
+
+ {rows.map((row) => {
+ const spent = dec(row.spent);
+ const limit = dec(row.limit);
+ const over = limit > 0 && spent > limit;
+ const percent = limit > 0 ? Math.min(100, (spent / limit) * 100) : 0;
+
+ if (editingName === row.category) {
+ return (
+ -
+ {s.categories.editTitle(row.category, HORIZON_LABEL[horizon])}
+ setDrafts((d) => ({ ...d, day: v }))} />
+ setDrafts((d) => ({ ...d, week: v }))} />
+ setDrafts((d) => ({ ...d, month: v }))} />
+
+ setEditingName(null)}>
+ {s.amountEntry.cancel}
+
+ {
+ onSaveLimit(row.category, parseFloat(drafts.day) || 0, parseFloat(drafts.week) || 0, parseFloat(drafts.month) || 0);
+ setEditingName(null);
+ }}
+ >
+ {s.amountEntry.save}
+
+
+
+
+ );
+ }
+
+ return (
+ -
+
+
+
+ {row.category}
+
+ {tugrik(spent)}
+ {limit > 0 ? ` / ${tugrik(limit)}` : " / —"}
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ !open && setConfirmRemove(null)}
+ title={s.categories.removeConfirmTitle}
+ body={confirmRemove ? s.categories.removeConfirmBody(confirmRemove) : ""}
+ confirmLabel={s.categories.remove}
+ onConfirm={() => {
+ if (confirmRemove) {
+ onRemoveLimit(confirmRemove);
+ setEditingName(null);
+ }
+ }}
+ />
+
+ );
+}
+
+function AmountField({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
+ return (
+
+ {label}
+ onChange(onlyDigits(v))} style={{ flex: 1 }}>
+
+
+
+ );
+}
+
+function ProgressBar({ percent, tone }: { percent: number; tone: "brand" | "critical" }) {
+ return (
+
+ );
+}
+
+// --- Savings goals ------------------------------------------------------------
+
+interface GoalDraft {
+ originalName?: string;
+ name: string;
+ target: number;
+ monthly: number;
+ accountId: number;
+ targetDate: string;
+}
+
+function SavingsSection({
+ goals,
+ accounts,
+ onSave,
+ onDelete,
+}: {
+ goals: SavingsGoal[];
+ accounts: Account[];
+ onSave: (goal: GoalDraft) => void;
+ onDelete: (name: string) => void;
+}) {
+ const [editing, setEditing] = React.useState<{ mode: "add" | "edit"; goal?: SavingsGoal } | null>(null);
+ const [confirmDelete, setConfirmDelete] = React.useState(null);
+
+ return (
+
+
+
{s.savings.title}
+ setEditing({ mode: "add" })}>
+ {s.savings.add}
+
+
+
+ {goals.length === 0 && !editing && (
+
+ {s.savings.empty}
+
+ )}
+
+ {goals.length > 0 && (
+
+ {goals.map((goal) => {
+ const saved = dec(goal.saved);
+ const target = dec(goal.target);
+ const monthly = dec(goal.monthlyContribution);
+ const remaining = Math.max(0, target - saved);
+ const done = target > 0 && saved >= target;
+ const percent = target > 0 ? Math.min(100, (saved / target) * 100) : 0;
+ return (
+ -
+
+
+ );
+ })}
+
+ )}
+
+ {editing && (
+ setEditing(null)}
+ onSave={(draft) => {
+ onSave(draft);
+ setEditing(null);
+ }}
+ onRequestDelete={() => editing.goal && setConfirmDelete(editing.goal.name)}
+ />
+ )}
+
+ !open && setConfirmDelete(null)}
+ title={s.savings.deleteConfirmTitle}
+ body={confirmDelete ? s.savings.deleteConfirmBody(confirmDelete) : ""}
+ confirmLabel={s.savings.delete}
+ onConfirm={() => {
+ if (confirmDelete) {
+ onDelete(confirmDelete);
+ setConfirmDelete(null);
+ setEditing(null);
+ }
+ }}
+ />
+
+ );
+}
+
+function GoalEditor({
+ existing,
+ accounts,
+ onSave,
+ onCancel,
+ onRequestDelete,
+}: {
+ existing?: SavingsGoal;
+ accounts: Account[];
+ onSave: (draft: GoalDraft) => void;
+ onCancel: () => void;
+ onRequestDelete: () => void;
+}) {
+ const [name, setName] = React.useState(existing?.name ?? "");
+ const [editingName, setEditingName] = React.useState(false);
+ const [target, setTarget] = React.useState(existing?.target ?? "");
+ const [monthly, setMonthly] = React.useState(existing?.monthlyContribution ?? "");
+ const [accountId, setAccountId] = React.useState(existing?.accountId ?? 0);
+ const [targetDate, setTargetDate] = React.useState(existing?.targetDate ?? "");
+
+ const canSave = name.trim().length > 0 && (parseFloat(target) || 0) > 0;
+
+ return (
+
+
{existing ? s.savings.editTitle : s.savings.addTitle}
+
+ {editingName ? (
+
{
+ setName(n);
+ setEditingName(false);
+ }}
+ onCancel={() => setEditingName(false)}
+ />
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {s.amountEntry.cancel}
+
+
+ onSave({
+ originalName: existing?.name,
+ name: name.trim(),
+ target: parseFloat(target) || 0,
+ monthly: parseFloat(monthly) || 0,
+ accountId,
+ targetDate,
+ })
+ }
+ >
+ {s.amountEntry.save}
+
+
+
+ {existing && (
+
+ )}
+
+ );
+}
+
+// --- Shared confirmation dialog (never a native alert) -----------------------
+
+function ConfirmDialog({
+ open,
+ onOpenChange,
+ title,
+ body,
+ confirmLabel,
+ onConfirm,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ body: string;
+ confirmLabel: string;
+ onConfirm: () => void;
+}) {
+ return (
+
+
+
+
+
+ {title}
+
+
+ {body}
+
+
+ onOpenChange(false)}>
+ {s.amountEntry.cancel}
+
+ {
+ onConfirm();
+ onOpenChange(false);
+ }}
+ >
+ {confirmLabel}
+
+
+
+
+
+ );
+}
diff --git a/src/features/planner/strings.ts b/src/features/planner/strings.ts
new file mode 100644
index 0000000..d9b1530
--- /dev/null
+++ b/src/features/planner/strings.ts
@@ -0,0 +1,66 @@
+// Planner feature copy, ported verbatim from
+// ios/Mercury/Features/Planner/{PlannerView,LimitsHubModel,PlannerEditViews,
+// SavingsGoalEditView,CategoryTransactionsView}.swift.
+export const plannerStrings = {
+ header: { title: "Төлөвлөгөө" },
+ horizon: {
+ day: "Өдөр",
+ week: "7 хоног",
+ month: "Сар",
+ },
+ plannedIncome: {
+ label: "Төлөвлөгдсөн орлого",
+ loanObligations: "Зээлийн төлбөр",
+ savings: "Хадгаламж",
+ available: "Зарцуулах боломжтой",
+ editTitle: "Төлөвлөгдсөн орлого",
+ autoHint: (detected: string) => `Цалингаар илрүүлсэн: ${detected}`,
+ autoAction: "Автоматаар тооцох (цалингаар)",
+ },
+ overall: {
+ label: {
+ day: "Өнөөдрийн лимит",
+ week: "7 хоногийн лимит",
+ month: "Энэ сарын лимит",
+ },
+ editTitle: (horizonLabel: string) => `${horizonLabel} — нийт лимит`,
+ },
+ categories: {
+ title: "Ангиллын лимит",
+ empty: "Лимит алга — ангилал нэмнэ үү",
+ add: "Ангилал нэмэх",
+ addNamePlaceholder: "Ангиллын нэр",
+ editTitle: (name: string, horizonLabel: string) => `${name} · ${horizonLabel}`,
+ remove: "Лимит хасах",
+ removeConfirmTitle: "Ангиллын лимит хасах уу?",
+ removeConfirmBody: (name: string) => `«${name}» ангиллын лимит хасагдана.`,
+ },
+ savings: {
+ title: "Хадгаламж",
+ empty: "Зорилго алга — хадгаламжийн зорилго нэмнэ үү",
+ add: "Зорилго нэмэх",
+ linkAccount: "Данс холбох",
+ noAccount: "Холбохгүй",
+ perMonth: "Сар бүр",
+ remaining: "Үлдсэн",
+ addTitle: "Шинэ зорилго",
+ editTitle: "Зорилго засах",
+ name: "Нэр",
+ namePlaceholder: "Жишээ: Машины балон сан",
+ target: "Зорилтот дүн",
+ account: "Холбосон данс",
+ monthly: "Сар бүрийн хуримтлал",
+ date: "Зорилтот огноо",
+ delete: "Зорилго устгах",
+ deleteConfirmTitle: "Зорилго устгах уу?",
+ deleteConfirmBody: (name: string) => `«${name}» хадгаламжийн зорилго устана.`,
+ },
+ amountEntry: {
+ save: "Хадгалах",
+ cancel: "Болих",
+ },
+ categoryTransactions: {
+ empty: "Гүйлгээ алга",
+ filterPlaceholder: "филтер",
+ },
+} as const;