"use client";
import {
createFinanceTransactionRecord,
deleteFinanceTransactionRecord,
updateFinanceTransactionRecord,
} from "@/app/(dashboard)/finance/actions";
import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "poyraz-ui/molecules";
import {
ArrowDownRight,
ArrowUpRight,
Pencil,
Plus,
Trash2,
Wallet,
Brain,
Loader2,
} from "lucide-react";
import { useMemo, useState } from "react";
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;
};
const typeLabels = {
income: "Gelir",
expense: "Gider",
};
const paymentStatusLabels = {
planned: "Planlandı",
pending: "Bekliyor",
paid: "Ödendi",
cancelled: "İptal edildi",
};
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", label: "Dolar (USD)" },
{ value: "EUR", label: "Euro (EUR)" },
{ value: "TRY", label: "Türk lirası (TRY)" },
{ value: "GBP", label: "Sterlin (GBP)" },
{ value: "CAD", label: "Kanada doları (CAD)" },
{ value: "AUD", label: "Avustralya doları (AUD)" },
];
type FinanceClientProps = {
transactions: FinanceTransactionItem[];
clients: FinanceRelationOption[];
projects: FinanceRelationOption[];
};
export function FinanceClient({ transactions, clients, projects }: FinanceClientProps) {
const [query, setQuery] = useState("");
const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
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,
typeLabels[transaction.type],
]
.filter(Boolean)
.some((value) => value!.toLowerCase().includes(normalizedQuery)),
)
: filteredByMonth;
const summary = useMemo(() => calculateSummary(filteredByMonth), [filteredByMonth]);
const categoryBreakdown = useMemo(() => calculateExpenseCategories(filteredByMonth), [filteredByMonth]);
return (
Finans
Finans işlemleri
Gelir, gider, ödeme durumu ve proje/müşteri bağlantılarını takip et.
{filteredTransactions.length > 0 ? (
İşlem
Tarih
Tutar
Durum
İşlem
{filteredTransactions.map((transaction) => (
))}
) : (
)}
Gider kategorileri
Aylık gider dağılımı
{categoryBreakdown.length > 0 ? (
{categoryBreakdown.map((item) => (
{item.category}
{formatCurrency(item.amount)}
))}
) : (
Bu ay gider kaydı yok.
)}
);
}
function TransactionRow({
transaction,
clients,
projects,
}: {
transaction: FinanceTransactionItem;
clients: FinanceRelationOption[];
projects: FinanceRelationOption[];
}) {
const isIncome = transaction.type === "income";
return (
{transaction.description || typeLabels[transaction.type]}
{transaction.category || "Kategori yok"} · {transaction.projectName || transaction.clientName || "Bağlantı yok"}
{formatDate(transaction.transaction_date)}
{isIncome ? "+" : "-"}
{formatCurrency(transaction.amount, transaction.currency)}
{paymentStatusLabels[transaction.payment_status]}
);
}
function FinanceDialog({
mode,
transaction,
clients,
projects,
}: {
mode: "create" | "edit";
transaction?: FinanceTransactionItem;
clients: FinanceRelationOption[];
projects: FinanceRelationOption[];
}) {
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);
} finally {
setIsSubmitting(false);
}
}
return (
{mode === "create" ? : }
{mode === "create" ? "İşlem ekle" : "Düzenle"}
);
}
function FinanceFormFields({
transaction,
clients,
projects,
}: {
transaction?: FinanceTransactionItem;
clients: FinanceRelationOption[];
projects: FinanceRelationOption[];
}) {
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 (
Gelir
Gider
Planlandı
Bekliyor
Ödendi
İptal edildi
Kategori
Müşteri
{shouldLockClient ? : null}
Müşteri yok
{clients.map((client) => (
{client.name}
))}
Proje
Proje yok
{filteredProjects.map((project) => (
{project.name}
))}
Açıklama
);
}
function SelectField({ name, label, defaultValue, children }: { name: string; label: string; defaultValue: string; children: React.ReactNode }) {
return (
{label}
{children}
);
}
function StatCard({ label, value, tone }: { label: string; value: string; tone: "green" | "rose" | "primary" | "amber" }) {
const toneClass = {
green: "bg-emerald-50 text-emerald-700",
rose: "bg-rose-50 text-rose-700",
primary: "bg-primary/10 text-primary",
amber: "bg-amber-50 text-amber-700",
}[tone];
return (
);
}
function EmptyState({ hasQuery }: { hasQuery: boolean }) {
return (
{hasQuery ? "Aramana uygun işlem yok" : "Henüz finans işlemi eklenmedi"}
{hasQuery
? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
: "İlk gelir veya gider kaydını ekleyerek aylık finans özetini oluşturmaya başlayabilirsin."}
);
}
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 AIFinanceDialog() {
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 || "Bilinmeyen bir hata oluştu.");
}
setResult(data.text);
} catch (err: any) {
setResult("Hata: " + err.message);
} finally {
setLoading(false);
}
};
return (
AI Analizi
Yapay Zeka Finansal Yorumlama
Son 30 günlük finansal kayıtlarınızı analiz edip size önerilerde bulunuyorum.
{!result && !loading && (
Raporu Oluştur
)}
{loading && (
Verileriniz analiz ediliyor...
)}
{result && (
{result}
)}
{result && (
setOpen(false)}>Kapat
Yeniden Oluştur
)}
);
}
function calculateExpenseCategories(transactions: FinanceTransactionItem[]) {
const totals = new Map();
for (const transaction of transactions) {
if (transaction.type !== "expense") continue;
const category = transaction.category || "Kategori yok";
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 formatCurrency(value: number, currency = "USD") {
return new Intl.NumberFormat("tr-TR", {
style: "currency",
currency,
maximumFractionDigits: 0,
}).format(value);
}
function formatDate(value: string) {
return new Intl.DateTimeFormat("tr-TR", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(new Date(`${value}T00:00:00`));
}