From b6c6be5c979c84d4ba6360267c7cd9e4d8d33615 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 11:58:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(ui):=20cari=20hesaplar=20sayfalar=C4=B1,?= =?UTF-8?q?=20hareket=20formu=20ve=20vade=20uyar=C4=B1s=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/app/(app)/cari/[id]/page.tsx | 17 + src/app/(app)/cari/page.tsx | 22 + src/app/(app)/dashboard/page.tsx | 4 + src/components/layout/nav-config.ts | 4 +- .../contacts/components/contact-dialogs.tsx | 434 ++++++++++++++++++ .../components/contact-statement-view.tsx | 155 +++++++ .../contacts/components/contacts-view.tsx | 186 ++++++++ .../contacts/components/due-banner.tsx | 29 ++ .../components/transaction-form.tsx | 2 +- .../components/transaction-parts.tsx | 57 ++- src/i18n/tr.ts | 77 ++++ 11 files changed, 975 insertions(+), 12 deletions(-) create mode 100644 src/app/(app)/cari/[id]/page.tsx create mode 100644 src/app/(app)/cari/page.tsx create mode 100644 src/features/contacts/components/contact-dialogs.tsx create mode 100644 src/features/contacts/components/contact-statement-view.tsx create mode 100644 src/features/contacts/components/contacts-view.tsx create mode 100644 src/features/contacts/components/due-banner.tsx diff --git a/src/app/(app)/cari/[id]/page.tsx b/src/app/(app)/cari/[id]/page.tsx new file mode 100644 index 0000000..0db1f1c --- /dev/null +++ b/src/app/(app)/cari/[id]/page.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { ContactStatementView } from "@/features/contacts/components/contact-statement-view"; +import { getContactStatement } from "@/features/contacts/service"; +import { tr } from "@/i18n/tr"; +import { requireActiveHousehold } from "@/lib/dal"; + +export const metadata: Metadata = { title: tr.contacts.statement }; + +/** /cari/:id — cari ekstresi (gizli cari yalnızca sahibine; aksi 404). */ +export default async function ContactPage({ params }: PageProps<"/cari/[id]">) { + const { user, household } = await requireActiveHousehold("viewer"); + const { id } = await params; + const data = await getContactStatement(user.id, household.id, id); + if (!data) notFound(); + return ; +} diff --git a/src/app/(app)/cari/page.tsx b/src/app/(app)/cari/page.tsx new file mode 100644 index 0000000..5552f78 --- /dev/null +++ b/src/app/(app)/cari/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { PageHeader } from "@/components/page-header"; +import { ContactsView } from "@/features/contacts/components/contacts-view"; +import { listContacts } from "@/features/contacts/service"; +import { hasHouseholdRole } from "@/features/households/roles"; +import { tr } from "@/i18n/tr"; +import { requireActiveHousehold } from "@/lib/dal"; + +export const metadata: Metadata = { title: tr.contacts.title }; + +export default async function ContactsPage() { + const { user, household } = await requireActiveHousehold("viewer"); + const data = await listContacts(user.id, household.id); + if (!data) notFound(); + return ( + <> + + + + ); +} diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 7ea3637..4f6aae1 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -3,6 +3,7 @@ import { Suspense } from "react"; import { PageHeader } from "@/components/page-header"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { SummaryCardsSkeleton } from "@/features/dashboard/components/summary-cards-skeleton"; +import { ContactsDueBanner } from "@/features/contacts/components/due-banner"; import { OverdueBanner } from "@/features/recurring/components/overdue-banner"; import { tr } from "@/i18n/tr"; import { requireActiveHousehold } from "@/lib/dal"; @@ -63,6 +64,9 @@ export default async function DashboardPage() { + + + }> diff --git a/src/components/layout/nav-config.ts b/src/components/layout/nav-config.ts index 239e67f..d21cd79 100644 --- a/src/components/layout/nav-config.ts +++ b/src/components/layout/nav-config.ts @@ -27,7 +27,7 @@ export type NavItem = { /** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */ /** Alt fazlar ondalıkla: 6.1 = Faz 6a. */ -export const CURRENT_PHASE = 7.2; +export const CURRENT_PHASE = 8.1; export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE; @@ -37,7 +37,7 @@ export const MAIN_NAV: NavItem[] = [ { 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.1 }, - { href: "/cari", label: tr.nav.contacts, icon: Users, phase: 8 }, + { href: "/cari", label: tr.nav.contacts, icon: Users, phase: 8.1 }, { href: "/birikim", label: tr.nav.savings, icon: Coins, phase: 9 }, { href: "/butce", label: tr.nav.budget, icon: Target, phase: 10 }, { href: "/stok", label: tr.nav.stock, icon: Package, phase: 11 }, diff --git a/src/features/contacts/components/contact-dialogs.tsx b/src/features/contacts/components/contact-dialogs.tsx new file mode 100644 index 0000000..52ea6e6 --- /dev/null +++ b/src/features/contacts/components/contact-dialogs.tsx @@ -0,0 +1,434 @@ +"use client"; + +import { Loader2, Trash2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { MoneyInput } from "@/components/money-input"; +import { ResponsiveDialog } from "@/components/responsive-dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { + getTransactionFormOptionsAction, + setTransactionDeletedAction, +} from "@/features/transactions/actions"; +import { AccountSelect } from "@/features/transactions/components/transaction-form"; +import type { TransactionFormOptions, TransactionListItem } from "@/features/transactions/service"; +import { tr } from "@/i18n/tr"; +import { + CONTACT_ENTRY_KINDS, + CONTACT_RELATIONS, + isOutgoing, + type ContactEntryKind, + type ContactRelation, +} from "@/shared/contacts"; +import { todayIstanbul } from "@/shared/dates"; +import { formatMoneyInput } from "@/shared/money"; +import { saveContactAction, saveContactEntryAction } from "../actions"; +import type { ContactView } from "../service"; + +const t = tr.contacts; +type Errors = Record; + +/** Cari oluştur / düzenle. */ +export function ContactFormDialog({ + open, + contact, + onOpenChange, +}: { + open: boolean; + contact: ContactView | null; + onOpenChange: (open: boolean) => void; +}) { + return open ? ( + onOpenChange(false)} /> + ) : null; +} + +function ContactForm({ contact, onClose }: { contact: ContactView | null; onClose: () => void }) { + const [v, setV] = useState({ + name: contact?.name ?? "", + relation: (contact?.relation ?? "friend") as ContactRelation, + phone: contact?.phone ?? "", + note: contact?.note ?? "", + openingBalance: contact?.openingBalanceMinor + ? formatMoneyInput(contact.openingBalanceMinor) + : "", + openingDate: contact?.openingDate ?? todayIstanbul(), + dueDate: contact?.dueDate ?? "", + reminderDays: String(contact?.reminderDays ?? 3), + isPrivate: contact?.isPrivate ?? false, + }); + const [errors, setErrors] = useState({}); + const [busy, setBusy] = useState(false); + const set = (k: K, value: (typeof v)[K]) => + setV((x) => ({ ...x, [k]: value })); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + const r = await saveContactAction(contact?.id ?? null, v); + setBusy(false); + if (r.ok) { + toast.success(contact ? t.toast.updated : t.toast.created); + onClose(); + } else setErrors({ ...r.fieldErrors, root: r.fieldErrors ? undefined : r.error }); + } + + const f = t.fields; + return ( + !o && onClose()} + title={contact ? t.editTitle : t.createTitle} + footer={ + <> + + + + } + > +
+ + + {f.name} + set("name", e.target.value)} + aria-invalid={Boolean(errors.name)} + autoFocus + /> + {errors.name ? {errors.name} : null} + +
+ + {f.relation} + + + + {f.phone} + set("phone", e.target.value)} + aria-invalid={Boolean(errors.phone)} + /> + {errors.phone ? {errors.phone} : null} + +
+
+ + {f.openingBalance} + set("openingBalance", x)} + allowNegative + /> + {errors.openingBalance ? {errors.openingBalance} : null} + + + {tr.transactions.fields.date} + set("openingDate", e.target.value)} + /> + +
+ {f.openingHint} + setV((x) => ({ ...x, dueDate: d, reminderDays: r }))} + errors={errors} + /> + + {f.note} +