"use client"; import { getDocumentIntlLocale } from "@/lib/i18n/browser"; import { useTranslations } from "@/components/i18n/i18n-provider"; import { LocalizedFields, type LocalizedFieldLocale, type LocalizedFieldValues } from "@/components/i18n/localized-fields"; import { contentTranslationRegistry } from "@/lib/i18n/content"; import { createFinanceTransactionRecord, deleteFinanceTransactionRecord, updateFinanceTransactionRecord, } from "@/app/(dashboard)/finance/actions"; import { Badge, Button, Card, CardContent, Input, Label } from "poyraz-ui/atoms"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "poyraz-ui/molecules"; import { ArrowDownRight, ArrowUpRight, ChevronLeft, ChevronRight, Pencil, Plus, Trash2, Wallet, Brain, Loader2, } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { StatCard } from "@/components/system/stat-card"; export type FinanceRelationOption = { id: string; name: string; client_id?: string | null; }; export type FinanceTransactionItem = { id: string; type: "income" | "expense"; amount: number; currency: string; transaction_date: string; category: string | null; payment_status: "planned" | "pending" | "paid" | "cancelled"; client_id: string | null; project_id: string | null; clientName: string | null; projectName: string | null; description: string | null; translations?: LocalizedFieldValues; }; const paymentStatusClasses = { planned: "border-blue-200 bg-blue-50 text-blue-700", pending: "border-amber-200 bg-amber-50 text-amber-700", paid: "border-emerald-200 bg-emerald-50 text-emerald-700", cancelled: "border-zinc-200 bg-zinc-50 text-zinc-700", }; const currencyOptions = [ { value: "USD", labelKey: "finance.currency.usd" }, { value: "EUR", labelKey: "finance.currency.eur" }, { value: "TRY", labelKey: "finance.currency.try" }, { value: "GBP", labelKey: "finance.currency.gbp" }, { value: "CAD", labelKey: "finance.currency.cad" }, { value: "AUD", labelKey: "finance.currency.aud" }, ]; const financeSummaryCardConfig = [ { key: "afterTax", tone: "green", icon: Wallet, featured: true }, { key: "net", tone: "primary", icon: Wallet, featured: true }, { key: "income", tone: "green", icon: ArrowUpRight, featured: false }, { key: "expense", tone: "rose", icon: ArrowDownRight, featured: false }, { key: "pending", tone: "amber", icon: Wallet, featured: false }, { key: "tax", tone: "amber", icon: Wallet, featured: false }, ] as const; type FinanceClientProps = { transactions: FinanceTransactionItem[]; clients: FinanceRelationOption[]; projects: FinanceRelationOption[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }; export function FinanceClient({ transactions, clients, projects, localization }: FinanceClientProps) { const t = useTranslations(); const [query, setQuery] = useState(""); const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7)); const summaryTrackRef = useRef(null); const normalizedQuery = query.trim().toLowerCase(); const filteredByMonth = transactions.filter((transaction) => transaction.transaction_date.startsWith(monthFilter), ); const filteredTransactions = normalizedQuery ? filteredByMonth.filter((transaction) => [ transaction.description, transaction.category, transaction.clientName, transaction.projectName, t(`finance.types.${transaction.type}`), ] .filter(Boolean) .some((value) => value!.toLowerCase().includes(normalizedQuery)), ) : filteredByMonth; const summary = useMemo(() => calculateSummary(filteredByMonth), [filteredByMonth]); const categoryBreakdown = useMemo(() => calculateExpenseCategories(filteredByMonth), [filteredByMonth]); const summaryCards = financeSummaryCardConfig.map((card) => ({ ...card, label: t(`finance.summary.${card.key}`), value: formatCurrency(summary[card.key]), })); const scrollSummary = (direction: -1 | 1) => { const track = summaryTrackRef.current; if (!track) return; track.scrollBy({ left: direction * Math.max(track.clientWidth * 0.72, 260), behavior: "smooth", }); }; return (

{t("finance.title")}

{t("finance.summary.title")}

{t("finance.summary.description")}

{ if (event.key === "ArrowLeft") { event.preventDefault(); scrollSummary(-1); } if (event.key === "ArrowRight") { event.preventDefault(); scrollSummary(1); } }} className="flex snap-x snap-mandatory gap-3 overflow-x-auto scroll-smooth pb-3 [scrollbar-width:none] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 [&::-webkit-scrollbar]:hidden" > {summaryCards.map((card) => ( ))}

{t("finance.list.title")}

{t("finance.list.description", { count: filteredTransactions.length })}

setQuery(event.target.value)} placeholder={t("finance.list.searchPlaceholder")} className="sm:w-80" /> setMonthFilter(event.target.value)} className="sm:w-44" />
{filteredTransactions.length > 0 ? (
{t("finance.list.headers.transaction")} {t("finance.list.headers.date")} {t("finance.list.headers.amount")} {t("finance.list.headers.status")} {t("finance.list.headers.action")}
{filteredTransactions.map((transaction) => ( ))}
) : ( )}

{t("finance.expenseCategories.title")}

{t("finance.expenseCategories.description")}

{categoryBreakdown.length > 0 ? (
{categoryBreakdown.map((item) => (
{item.category === "__uncategorized" ? t("finance.expenseCategories.noCategory") : item.category} {formatCurrency(item.amount)}
))}
) : (

{t("finance.expenseCategories.noExpense")}

)}
); } function TransactionRow({ transaction, clients, projects, localization, }: { transaction: FinanceTransactionItem; clients: FinanceRelationOption[]; projects: FinanceRelationOption[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }) { const t = useTranslations(); const isIncome = transaction.type === "income"; return (
{isIncome ? : }
{transaction.description || t(`finance.types.${transaction.type}`)}
{transaction.category || t("finance.expenseCategories.noCategory")} ยท {transaction.projectName || transaction.clientName || t("finance.form.noClient")}
{formatDate(transaction.transaction_date)}
{isIncome ? "+" : "-"} {formatCurrency(transaction.amount, transaction.currency)}
{t(`finance.paymentStatus.${transaction.payment_status}`)}
); } function FinanceDialog({ mode, transaction, clients, projects, localization, }: { mode: "create" | "edit"; transaction?: FinanceTransactionItem; clients: FinanceRelationOption[]; projects: FinanceRelationOption[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }) { const t = useTranslations(); const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const action = mode === "create" ? createFinanceTransactionRecord : updateFinanceTransactionRecord; async function handleSubmit(formData: FormData) { setIsSubmitting(true); try { await action(formData); setOpen(false); toast.success(mode === "create" ? t("finance.form.messages.createSuccess") : t("finance.form.messages.updateSuccess")); } catch (error) { toast.error(resolveTranslatedError(t, error, "finance.form.messages.error")); } finally { setIsSubmitting(false); } } return (
{transaction ? : null} {mode === "create" ? t("finance.form.createTitle") : t("finance.form.editTitle")} {t("finance.form.description")}
); } function FinanceFormFields({ transaction, clients, projects, localization, }: { transaction?: FinanceTransactionItem; clients: FinanceRelationOption[]; projects: FinanceRelationOption[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }) { const t = useTranslations(); const [clientId, setClientId] = useState(transaction?.client_id || "__none"); const [projectId, setProjectId] = useState(transaction?.project_id || "__none"); const selectedProject = projectId === "__none" ? null : projects.find((project) => project.id === projectId) || null; const shouldLockClient = Boolean(selectedProject); const filteredProjects = clientId === "__none" || shouldLockClient ? projects : projects.filter((project) => project.client_id === clientId); const currencyValue = transaction?.currency || "USD"; const hasCustomCurrency = !currencyOptions.some((currency) => currency.value === currencyValue); function handleClientChange(nextClientId: string) { setClientId(nextClientId); if ( projectId !== "__none" && nextClientId !== "__none" && !projects.some((project) => project.id === projectId && project.client_id === nextClientId) ) { setProjectId("__none"); } } function handleProjectChange(nextProjectId: string) { setProjectId(nextProjectId); if (nextProjectId === "__none") { return; } const nextProject = projects.find((project) => project.id === nextProjectId); setClientId(nextProject?.client_id || "__none"); } return (
{t("finance.types.income")} {t("finance.types.expense")} {t("finance.paymentStatus.planned")} {t("finance.paymentStatus.pending")} {t("finance.paymentStatus.paid")} {t("finance.paymentStatus.cancelled")}
f.name === "category") .map((f) => ({ ...f, label: t(`finance.fields.${f.name}`) || f.label, placeholder: "placeholder" in f && typeof f.placeholder === "string" ? t(`finance.placeholders.${f.name}`) || f.placeholder : undefined, }))} values={transaction?.translations} fallbackValues={{ category: transaction?.category, }} />
{shouldLockClient ? : null}
f.name === "description") .map((f) => ({ ...f, label: t(`finance.fields.${f.name}`) || f.label, placeholder: "placeholder" in f && typeof f.placeholder === "string" ? t(`finance.placeholders.${f.name}`) || f.placeholder : undefined, }))} values={transaction?.translations} fallbackValues={{ description: transaction?.description, }} />
); } function SelectField({ name, label, defaultValue, children }: { name: string; label: string; defaultValue: string; children: React.ReactNode }) { return (
); } function EmptyState({ hasQuery }: { hasQuery: boolean }) { const t = useTranslations(); return (

{hasQuery ? t("finance.empty.noMatchTitle") : t("finance.empty.noTransactionTitle")}

{hasQuery ? t("finance.empty.noMatchDesc") : t("finance.empty.noTransactionDesc")}

); } function calculateSummary(transactions: FinanceTransactionItem[]) { return transactions.reduce( (summary, transaction) => { if (transaction.type === "income" && transaction.payment_status === "paid") { summary.income += transaction.amount; } if (transaction.type === "expense" && transaction.payment_status === "paid") { summary.expense += transaction.amount; } if (transaction.payment_status === "pending" || transaction.payment_status === "planned") { summary.pending += transaction.amount; } summary.net = summary.income - summary.expense; summary.tax = summary.income * 0.20; // 20% KDV/Vergi tahmini summary.afterTax = summary.net - summary.tax; return summary; }, { income: 0, expense: 0, net: 0, tax: 0, afterTax: 0, pending: 0 }, ); } function formatMessageContent(text: string) { if (!text) return null; const lines = text.split("\n"); return lines.map((line, i) => ( {line.split(/(\*\*.*?\*\*|\*.*?\*)/g).map((part, j) => { if (part.startsWith("**") && part.endsWith("**")) { return ( {part.slice(2, -2)} ); } if (part.startsWith("*") && part.endsWith("*")) { return {part.slice(1, -1)}; } return {part}; })} {i !== lines.length - 1 &&
}
)); } function AIFinanceDialog() { const t = useTranslations(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const handleAnalyze = async () => { setLoading(true); setResult(null); try { const res = await fetch("/api/finance-analysis", { method: "POST" }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || t("finance.ai.error")); } setResult(data.text); } catch (error) { setResult( t("finance.ai.errorWithReason", { reason: resolveTranslatedError(t, error, "finance.ai.error"), }), ); } finally { setLoading(false); } }; return ( {t("finance.ai.title")} {t("finance.ai.description")}
{!result && !loading && (
)} {loading && (

{t("finance.ai.analyzing")}

)} {result && (
{formatMessageContent(result)}
)}
{result && ( )}
); } function calculateExpenseCategories(transactions: FinanceTransactionItem[]) { const totals = new Map(); for (const transaction of transactions) { if (transaction.type !== "expense") continue; const category = transaction.category || "__uncategorized"; totals.set(category, (totals.get(category) || 0) + transaction.amount); } const total = Array.from(totals.values()).reduce((sum, value) => sum + value, 0); return Array.from(totals.entries()) .map(([category, amount]) => ({ category, amount, percent: total ? Math.round((amount / total) * 100) : 0, })) .sort((a, b) => b.amount - a.amount) .slice(0, 6); } function resolveTranslatedError( t: ReturnType, error: unknown, fallbackKey: string, ) { if (!(error instanceof Error)) return t(fallbackKey); if (/^(finance|api|validation)\./.test(error.message)) return t(error.message); return error.message || t(fallbackKey); } function formatCurrency(value: number, currency = "USD") { return new Intl.NumberFormat(getDocumentIntlLocale(), { style: "currency", currency, maximumFractionDigits: 0, }).format(value); } function formatDate(value: string) { return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "short", year: "numeric", }).format(new Date(`${value}T00:00:00`)); }