feat(dashboard): implement daily journal functionality with CRUD operations
- Added actions for creating, updating, and deleting daily log records in `actions.ts`. - Introduced `journal-client.tsx` for rendering the journal UI, including log entries and statistics. - Refactored `page.tsx` to fetch user-specific daily logs from Supabase and display them using the new client component. - Implemented form handling for daily log creation and updates, including mood and energy scoring.
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
"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,
|
||||
} 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 (
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||
<div className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Wallet className="h-4 w-4" />
|
||||
Finans
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||
Finans işlemleri
|
||||
</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
Gelir, gider, ödeme durumu ve proje/müşteri bağlantılarını takip et.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FinanceDialog mode="create" clients={clients} projects={projects} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<StatCard label="Aylık gelir" value={formatCurrency(summary.income)} tone="green" />
|
||||
<StatCard label="Aylık gider" value={formatCurrency(summary.expense)} tone="rose" />
|
||||
<StatCard label="Net kazanç" value={formatCurrency(summary.net)} tone="primary" />
|
||||
<StatCard label="Bekleyen" value={formatCurrency(summary.pending)} tone="amber" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
||||
<Card>
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">İşlem listesi</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filteredTransactions.length} kayıt görüntüleniyor.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Kategori, müşteri, proje veya açıklama ara"
|
||||
className="sm:w-80"
|
||||
/>
|
||||
<Input
|
||||
type="month"
|
||||
value={monthFilter}
|
||||
onChange={(event) => setMonthFilter(event.target.value)}
|
||||
className="sm:w-44"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredTransactions.length > 0 ? (
|
||||
<div className="overflow-hidden rounded-sm border border-border">
|
||||
<div className="hidden grid-cols-[1.4fr_0.8fr_0.8fr_0.8fr_1fr] gap-4 border-b border-border bg-muted/40 px-4 py-3 text-xs font-medium uppercase text-muted-foreground lg:grid">
|
||||
<span>İşlem</span>
|
||||
<span>Tarih</span>
|
||||
<span>Tutar</span>
|
||||
<span>Durum</span>
|
||||
<span className="text-right">İşlem</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{filteredTransactions.map((transaction) => (
|
||||
<TransactionRow
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
clients={clients}
|
||||
projects={projects}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState hasQuery={Boolean(normalizedQuery)} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">Gider kategorileri</h2>
|
||||
<p className="text-sm text-muted-foreground">Aylık gider dağılımı</p>
|
||||
</div>
|
||||
{categoryBreakdown.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{categoryBreakdown.map((item) => (
|
||||
<div key={item.category} className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">{item.category}</span>
|
||||
<span className="font-medium text-foreground">{formatCurrency(item.amount)}</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary"
|
||||
style={{ width: `${item.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Bu ay gider kaydı yok.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionRow({
|
||||
transaction,
|
||||
clients,
|
||||
projects,
|
||||
}: {
|
||||
transaction: FinanceTransactionItem;
|
||||
clients: FinanceRelationOption[];
|
||||
projects: FinanceRelationOption[];
|
||||
}) {
|
||||
const isIncome = transaction.type === "income";
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 px-4 py-4 lg:grid-cols-[1.4fr_0.8fr_0.8fr_0.8fr_1fr] lg:items-center">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<div className={isIncome ? "rounded-sm bg-emerald-50 p-2 text-emerald-700" : "rounded-sm bg-rose-50 p-2 text-rose-700"}>
|
||||
{isIncome ? <ArrowUpRight className="h-4 w-4" /> : <ArrowDownRight className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-foreground">
|
||||
{transaction.description || typeLabels[transaction.type]}
|
||||
</div>
|
||||
<div className="truncate text-sm text-muted-foreground">
|
||||
{transaction.category || "Kategori yok"} · {transaction.projectName || transaction.clientName || "Bağlantı yok"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{formatDate(transaction.transaction_date)}</div>
|
||||
<div className={isIncome ? "font-semibold text-emerald-700" : "font-semibold text-rose-700"}>
|
||||
{isIncome ? "+" : "-"}
|
||||
{formatCurrency(transaction.amount, transaction.currency)}
|
||||
</div>
|
||||
<div>
|
||||
<Badge className={paymentStatusClasses[transaction.payment_status]}>
|
||||
{paymentStatusLabels[transaction.payment_status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-start gap-2 lg:justify-end">
|
||||
<FinanceDialog mode="edit" transaction={transaction} clients={clients} projects={projects} />
|
||||
<form action={deleteFinanceTransactionRecord}>
|
||||
<input type="hidden" name="id" value={transaction.id} />
|
||||
<Button type="submit" variant="outline" className="h-9 gap-2 text-rose-600">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Sil
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={mode === "create" ? "default" : "outline"} className="h-9 gap-2">
|
||||
{mode === "create" ? <Plus className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
|
||||
{mode === "create" ? "İşlem ekle" : "Düzenle"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[min(680px,calc(100dvh-6rem))] overflow-hidden sm:max-w-xl">
|
||||
<form action={handleSubmit} className="flex max-h-[min(640px,calc(100dvh-9rem))] flex-col">
|
||||
{transaction ? <input type="hidden" name="id" value={transaction.id} /> : null}
|
||||
<DialogHeader className="shrink-0 pb-5">
|
||||
<DialogTitle>{mode === "create" ? "Yeni finans işlemi" : "Finans işlemini düzenle"}</DialogTitle>
|
||||
<DialogDescription>Gelir veya gider kaydını müşteri/proje bağlantısıyla kaydet.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto pr-2">
|
||||
<FinanceFormFields transaction={transaction} clients={clients} projects={projects} />
|
||||
</div>
|
||||
<DialogFooter className="shrink-0 border-t border-border pt-5">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Kaydediliyor" : mode === "create" ? "İşlemi ekle" : "Değişiklikleri kaydet"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<SelectField name="type" label="Tip" defaultValue={transaction?.type || "income"}>
|
||||
<SelectItem value="income">Gelir</SelectItem>
|
||||
<SelectItem value="expense">Gider</SelectItem>
|
||||
</SelectField>
|
||||
<SelectField name="payment_status" label="Ödeme durumu" defaultValue={transaction?.payment_status || "planned"}>
|
||||
<SelectItem value="planned">Planlandı</SelectItem>
|
||||
<SelectItem value="pending">Bekliyor</SelectItem>
|
||||
<SelectItem value="paid">Ödendi</SelectItem>
|
||||
<SelectItem value="cancelled">İptal edildi</SelectItem>
|
||||
</SelectField>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label>Tutar</Label>
|
||||
<Input name="amount" type="number" min="0" step="0.01" required defaultValue={transaction?.amount ?? ""} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Para birimi</Label>
|
||||
<Select name="currency" defaultValue={currencyValue}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Para birimi seç" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currencyOptions.map((currency) => (
|
||||
<SelectItem key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{hasCustomCurrency ? (
|
||||
<SelectItem value={currencyValue}>{currencyValue}</SelectItem>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Tarih</Label>
|
||||
<Input name="transaction_date" type="date" defaultValue={transaction?.transaction_date || new Date().toISOString().slice(0, 10)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Kategori</Label>
|
||||
<Input name="category" defaultValue={transaction?.category || ""} placeholder="Örn. Yazılım, müşteri ödemesi, vergi" />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label>Müşteri</Label>
|
||||
{shouldLockClient ? <input type="hidden" name="client_id" value={clientId} /> : null}
|
||||
<Select
|
||||
name="client_id"
|
||||
value={clientId}
|
||||
onValueChange={handleClientChange}
|
||||
disabled={shouldLockClient}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Müşteri seç" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">Müşteri yok</SelectItem>
|
||||
{clients.map((client) => (
|
||||
<SelectItem key={client.id} value={client.id}>
|
||||
{client.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Proje</Label>
|
||||
<Select name="project_id" value={projectId} onValueChange={handleProjectChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Proje seç" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none">Proje yok</SelectItem>
|
||||
{filteredProjects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Açıklama</Label>
|
||||
<Textarea name="description" defaultValue={transaction?.description || ""} rows={3} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectField({ name, label, defaultValue, children }: { name: string; label: string; defaultValue: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>{label}</Label>
|
||||
<Select name={name} defaultValue={defaultValue}>
|
||||
<SelectTrigger><SelectValue placeholder={`${label} seç`} /></SelectTrigger>
|
||||
<SelectContent>{children}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between gap-3 p-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-foreground">{value}</p>
|
||||
</div>
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-sm ${toneClass}`}>
|
||||
<Wallet className="h-5 w-5" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({ hasQuery }: { hasQuery: boolean }) {
|
||||
return (
|
||||
<div className="flex min-h-72 flex-col items-center justify-center rounded-sm border border-dashed border-border bg-muted/20 p-8 text-center">
|
||||
<Wallet className="h-10 w-10 text-muted-foreground" />
|
||||
<h3 className="mt-4 text-lg font-semibold text-foreground">
|
||||
{hasQuery ? "Aramana uygun işlem yok" : "Henüz finans işlemi eklenmedi"}
|
||||
</h3>
|
||||
<p className="mt-2 max-w-md text-sm text-muted-foreground">
|
||||
{hasQuery
|
||||
? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
|
||||
: "İlk gelir veya gider kaydını ekleyerek aylık finans özetini oluşturmaya başlayabilirsin."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
return summary;
|
||||
},
|
||||
{ income: 0, expense: 0, net: 0, pending: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
function calculateExpenseCategories(transactions: FinanceTransactionItem[]) {
|
||||
const totals = new Map<string, number>();
|
||||
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`));
|
||||
}
|
||||
Reference in New Issue
Block a user