diff --git a/app/(dashboard)/finance/actions.ts b/app/(dashboard)/finance/actions.ts
new file mode 100644
index 0000000..196e5c2
--- /dev/null
+++ b/app/(dashboard)/finance/actions.ts
@@ -0,0 +1,122 @@
+"use server";
+
+import { createClient } from "@/lib/supabase/server";
+import { revalidatePath } from "next/cache";
+
+const TRANSACTION_TYPES = ["income", "expense"] as const;
+const PAYMENT_STATUSES = ["planned", "pending", "paid", "cancelled"] as const;
+
+function cleanText(value: FormDataEntryValue | null) {
+ const text = typeof value === "string" ? value.trim() : "";
+ return text.length > 0 && text !== "__none" ? text : null;
+}
+
+function readType(value: FormDataEntryValue | null) {
+ const type = typeof value === "string" ? value : "expense";
+ return TRANSACTION_TYPES.includes(type as (typeof TRANSACTION_TYPES)[number])
+ ? type
+ : "expense";
+}
+
+function readPaymentStatus(value: FormDataEntryValue | null) {
+ const status = typeof value === "string" ? value : "planned";
+ return PAYMENT_STATUSES.includes(status as (typeof PAYMENT_STATUSES)[number])
+ ? status
+ : "planned";
+}
+
+function readAmount(value: FormDataEntryValue | null) {
+ const amount = Number(typeof value === "string" ? value.replace(",", ".") : value);
+ return Number.isFinite(amount) && amount >= 0 ? amount : null;
+}
+
+async function getCurrentUserId() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
+
+ if (error || !user) {
+ throw new Error("Finans işlemi için giriş yapmış kullanıcı bulunamadı.");
+ }
+
+ return { supabase, userId: user.id };
+}
+
+function readPayload(formData: FormData) {
+ return {
+ type: readType(formData.get("type")),
+ amount: readAmount(formData.get("amount")),
+ currency: cleanText(formData.get("currency")) || "USD",
+ transaction_date: cleanText(formData.get("transaction_date")) || new Date().toISOString().slice(0, 10),
+ category: cleanText(formData.get("category")),
+ payment_status: readPaymentStatus(formData.get("payment_status")),
+ client_id: cleanText(formData.get("client_id")),
+ project_id: cleanText(formData.get("project_id")),
+ description: cleanText(formData.get("description")),
+ };
+}
+
+export async function createFinanceTransactionRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const payload = readPayload(formData);
+
+ if (payload.amount === null) {
+ throw new Error("Tutar zorunludur.");
+ }
+
+ const { error } = await supabase.from("finance_transactions").insert({
+ user_id: userId,
+ ...payload,
+ });
+
+ if (error) {
+ throw new Error(`Finans işlemi eklenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/finance");
+}
+
+export async function updateFinanceTransactionRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+ const payload = readPayload(formData);
+
+ if (!id || payload.amount === null) {
+ throw new Error("Finans işlemini güncellemek için kayıt kimliği ve tutar zorunludur.");
+ }
+
+ const { error } = await supabase
+ .from("finance_transactions")
+ .update(payload)
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Finans işlemi güncellenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/finance");
+}
+
+export async function deleteFinanceTransactionRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+
+ if (!id) {
+ throw new Error("Silinecek finans işlemi bulunamadı.");
+ }
+
+ const { error } = await supabase
+ .from("finance_transactions")
+ .delete()
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Finans işlemi silinemedi: ${error.message}`);
+ }
+
+ revalidatePath("/finance");
+}
diff --git a/app/(dashboard)/finance/finance-client.tsx b/app/(dashboard)/finance/finance-client.tsx
new file mode 100644
index 0000000..2865aa5
--- /dev/null
+++ b/app/(dashboard)/finance/finance-client.tsx
@@ -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 (
+
+
+
+
+
+ Finans
+
+
+
+ Finans işlemleri
+
+
+ Gelir, gider, ödeme durumu ve proje/müşteri bağlantılarını takip et.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
İşlem listesi
+
+ {filteredTransactions.length} kayıt görüntüleniyor.
+
+
+
+ setQuery(event.target.value)}
+ placeholder="Kategori, müşteri, proje veya açıklama ara"
+ className="sm:w-80"
+ />
+ setMonthFilter(event.target.value)}
+ className="sm:w-44"
+ />
+
+
+
+ {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
+
+
+
+
+ Tutar
+
+
+
+ Para birimi
+
+
+
+
+
+ {currencyOptions.map((currency) => (
+
+ {currency.label}
+
+ ))}
+ {hasCustomCurrency ? (
+ {currencyValue}
+ ) : null}
+
+
+
+
+ Tarih
+
+
+
+
+ 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;
+ return summary;
+ },
+ { income: 0, expense: 0, net: 0, pending: 0 },
+ );
+}
+
+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`));
+}
diff --git a/app/(dashboard)/finance/page.tsx b/app/(dashboard)/finance/page.tsx
index 145c3a8..ff354a8 100644
--- a/app/(dashboard)/finance/page.tsx
+++ b/app/(dashboard)/finance/page.tsx
@@ -1,404 +1,93 @@
-"use client";
-
-import { useState, useMemo } from "react";
-import {
- Plus, Search, TrendingUp, TrendingDown, DollarSign,
- CreditCard, ArrowUpRight, ArrowDownRight, MoreHorizontal,
- Calendar, FileText, Download, PieChart as PieChartIcon,
- Wallet, Brain, ArrowRight, X, Check, Activity,
- Briefcase, Landmark, Receipt, Percent, Target, Filter
-} from "lucide-react";
import {
- Area, AreaChart, Bar, BarChart, CartesianGrid,
- ResponsiveContainer, Tooltip, XAxis, YAxis, Cell,
- Pie, PieChart
-} from "recharts";
-import { motion, AnimatePresence } from "framer-motion";
+ FinanceClient,
+ type FinanceRelationOption,
+ type FinanceTransactionItem,
+} from "@/app/(dashboard)/finance/finance-client";
+import { createClient } from "@/lib/supabase/server";
-// Mock Data
-const cashflowData = [
- { month: "Jan", income: 12500, expense: 8400, predicted: 13000 },
- { month: "Feb", income: 14200, expense: 9100, predicted: 14500 },
- { month: "Mar", income: 13800, expense: 8800, predicted: 14000 },
- { month: "Apr", income: 15600, expense: 10200, predicted: 16000 },
- { month: "May", income: 18400, expense: 11500, predicted: 19500 },
- { month: "Jun", income: 21000, expense: 12000, predicted: 22000 },
-];
+type FinanceRow = {
+ id: string;
+ type: "income" | "expense";
+ amount: number | string;
+ currency: string;
+ transaction_date: string;
+ category: string | null;
+ payment_status: "planned" | "pending" | "paid" | "cancelled";
+ client_id: string | null;
+ project_id: string | null;
+ description: string | null;
+ clients: { name: string } | { name: string }[] | null;
+ projects: { name: string } | { name: string }[] | null;
+};
-const expensesByCategory = [
- { name: "Software", value: 3400, color: "#6C5BB0" },
- { name: "Rent", value: 2500, color: "#a798e8" },
- { name: "Marketing", value: 1800, color: "#10b981" },
- { name: "Taxes", value: 4200, color: "#3b82f6" },
- { name: "Travel", value: 1200, color: "#f59e0b" },
-];
+export default async function FinancePage() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
-const transactions = [
- { id: 1, name: "Stripe Subscription", date: "May 15", amount: "+$4,200", status: "Success", type: "Income", category: "Direct Sales", project: "Cognis Mobile" },
- { id: 2, name: "Amazon Web Services", date: "May 12", amount: "-$840", status: "Pending", type: "Expense", category: "Cloud Infra", project: "Infrastructure" },
- { id: 3, name: "Office Rent Q3", date: "May 10", amount: "-$2,500", status: "Success", type: "Expense", category: "Overhead", project: "General" },
- { id: 4, name: "Client: Marketing Site", date: "May 08", amount: "+$2,800", status: "Success", type: "Income", category: "Freelance", project: "Marketing Site" },
- { id: 5, name: "GitHub Enterprise", date: "May 05", amount: "-$120", status: "Success", type: "Expense", category: "Software", project: "Infrastructure" },
-];
+ if (!user) {
+ return null;
+ }
-export default function FinancePage() {
- const [showInvoiceModal, setShowInvoiceModal] = useState(false);
- const [selectedTransaction, setSelectedTransaction] = useState(null);
+ const [{ data: financeRows }, { data: clientRows }, { data: projectRows }] =
+ await Promise.all([
+ supabase
+ .from("finance_transactions")
+ .select("id, type, amount, currency, transaction_date, category, payment_status, client_id, project_id, description, clients(name), projects(name)")
+ .eq("user_id", user.id)
+ .order("transaction_date", { ascending: false }),
+ supabase
+ .from("clients")
+ .select("id, name")
+ .eq("user_id", user.id)
+ .neq("status", "archived")
+ .order("name", { ascending: true }),
+ supabase
+ .from("projects")
+ .select("id, name, client_id")
+ .eq("user_id", user.id)
+ .neq("status", "cancelled")
+ .order("name", { ascending: true }),
+ ]);
+
+ const transactions: FinanceTransactionItem[] = ((financeRows || []) as unknown as FinanceRow[]).map((transaction) => ({
+ id: transaction.id,
+ type: normalizeType(transaction.type),
+ amount: Number(transaction.amount),
+ currency: transaction.currency,
+ transaction_date: transaction.transaction_date,
+ category: transaction.category,
+ payment_status: normalizePaymentStatus(transaction.payment_status),
+ client_id: transaction.client_id,
+ project_id: transaction.project_id,
+ clientName: getRelationName(transaction.clients),
+ projectName: getRelationName(transaction.projects),
+ description: transaction.description,
+ }));
return (
-
-
- {/* Top Header */}
-
-
-
- Business / Financials
-
-
-
- ALL SYSTEMS HEALTHY
-
-
-
- setShowInvoiceModal(true)}
- className="bg-primary hover:bg-primary/90 text-primary-foreground border border-primary/20 px-5 py-2 rounded-sm text-xs font-black tracking-widest flex items-center gap-2 transition-all shadow-lg shadow-primary/30 active:scale-95"
- >
-
- CREATE INVOICE
-
-
-
-
- {/* KPI Row */}
-
-
-
-
-
-
-
- {/* Main Analysis Area */}
-
-
- {/* Cashflow Chart */}
-
-
-
-
-
-
-
- Intelligent Cashflow
- AI Forecasting
-
-
Real-time revenue monitoring compared with deep-learning projections for the next quarter.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Expense Breakdown & AI Insights */}
-
-
-
Category Allocation
-
-
-
-
- {expensesByCategory.map((entry, index) => (
- |
- ))}
-
-
-
-
-
-
- {expensesByCategory.slice(0, 3).map(ex => (
-
-
-
${ex.value.toLocaleString()}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
Strategic Advantage
-
-
- "Your project-specific ROI is 18% higher when tasks are completed in the morning focus block. Strategic reinvestment of $5k suggested for Q4."
-
-
-
- OPTIMIZE SPENDING
-
-
-
-
-
- {/* Transactions Ledger */}
-
-
-
-
Strategic Ledger
-
Showing last 24 transactions across 5 projects
-
-
-
-
- FILTER
-
-
Full Report
-
-
-
-
- {transactions.map(t => (
-
setSelectedTransaction(t)} className="grid grid-cols-12 gap-4 p-6 items-center hover:bg-white/[0.02] transition-all group cursor-pointer border-l-2 border-transparent hover:border-primary">
-
-
- {t.type === 'Income' ?
:
}
-
-
-
-
{t.name}
-
- {t.category}
- {t.project}
-
-
-
{t.date}
-
-
- {t.amount}
-
-
-
-
-
-
- ))}
-
-
-
-
- {/* Invoice Editor Modal */}
-
- {showInvoiceModal && (
- <>
- setShowInvoiceModal(false)} className="fixed inset-0 bg-black/90 backdrop-blur-xl z-[100]" />
-
-
-
-
-
-
-
Strategic Invoice
-
AI-Optimized Billing Engine
-
-
-
setShowInvoiceModal(false)} className="p-2 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors relative z-10">
-
-
-
-
-
-
Select Client
-
-
-
-
-
-
- Project Association
-
- Marketing Site Redesign
- Infrastructure Scale
- Cognis Mobile Dev
-
-
-
-
-
-
-
Billable Items
-
- ADD CUSTOM LINE
-
-
-
-
-
-
- AI SUGGESTION: ADD "QA & TESTING" (8H)
-
-
-
-
-
-
- Subtotal
- $4,680.00
-
-
- Tax (18%)
- $842.40
-
-
- Grand Total
- $5,522.40
-
-
-
-
-
-
- GENERATE & SEND
-
-
- SAVE AS DRAFT
-
-
-
- >
- )}
-
-
- {/* Transaction Detail Sheet */}
-
- {selectedTransaction && (
- <>
- setSelectedTransaction(null)} className="fixed inset-0 bg-black/80 backdrop-blur-md z-[100]" />
-
-
-
- {selectedTransaction.type === 'Income' ?
:
}
-
-
setSelectedTransaction(null)} className="p-2 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors">
-
-
-
-
{selectedTransaction.name}
-
- {selectedTransaction.amount}
- {selectedTransaction.status}
-
-
-
-
-
-
-
-
-
-
-
- Financial Insight
-
-
- This {selectedTransaction.type.toLowerCase()} was processed via Stripe and linked to your **{selectedTransaction.project}** deliverables. Tax obligations have been pre-calculated.
-
-
-
-
- DOWNLOAD RECEIPT
-
-
- >
- )}
-
-
-
+
);
}
-function InvoiceLineItem({ title, rate, hours, total }: any) {
- return (
-
-
- {title}
- Strategic Development Block
-
-
{rate}
-
{hours}
-
{total}
-
- );
+function getRelationName(relation: FinanceRow["clients"] | FinanceRow["projects"]) {
+ if (!relation) return null;
+ return Array.isArray(relation) ? relation[0]?.name || null : relation.name;
}
-function DetailMeta({ label, value, isPrimary = false }: any) {
- return (
-
- {label}
- {value}
-
- );
+function normalizeType(type: string): FinanceTransactionItem["type"] {
+ return type === "income" ? "income" : "expense";
}
-function FinanceKpiCard({ label, value, change, trend, icon: Icon, color = "default" }: any) {
- const isUp = trend === "up";
- const trendColor = isUp ? "text-emerald-500" : "text-rose-500";
-
- return (
-
- );
+function normalizePaymentStatus(status: string): FinanceTransactionItem["payment_status"] {
+ if (status === "pending" || status === "paid" || status === "cancelled") {
+ return status;
+ }
+
+ return "planned";
}
diff --git a/app/(dashboard)/journal/actions.ts b/app/(dashboard)/journal/actions.ts
new file mode 100644
index 0000000..d9e512d
--- /dev/null
+++ b/app/(dashboard)/journal/actions.ts
@@ -0,0 +1,106 @@
+"use server";
+
+import { createClient } from "@/lib/supabase/server";
+import { revalidatePath } from "next/cache";
+
+function cleanText(value: FormDataEntryValue | null) {
+ const text = typeof value === "string" ? value.trim() : "";
+ return text.length > 0 ? text : null;
+}
+
+function readScore(value: FormDataEntryValue | null) {
+ const score = Number(typeof value === "string" ? value : value?.toString());
+ return Number.isInteger(score) && score >= 1 && score <= 5 ? score : null;
+}
+
+async function getCurrentUserId() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
+
+ if (error || !user) {
+ throw new Error("Günlük kaydı için giriş yapmış kullanıcı bulunamadı.");
+ }
+
+ return { supabase, userId: user.id };
+}
+
+function readPayload(formData: FormData) {
+ return {
+ log_date: cleanText(formData.get("log_date")) || new Date().toISOString().slice(0, 10),
+ mood_score: readScore(formData.get("mood_score")),
+ energy_score: readScore(formData.get("energy_score")),
+ work_satisfaction_score: readScore(formData.get("work_satisfaction_score")),
+ note: cleanText(formData.get("note")),
+ };
+}
+
+export async function createDailyLogRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const payload = readPayload(formData);
+
+ if (!payload.mood_score || !payload.energy_score) {
+ throw new Error("Mood ve enerji skorları zorunludur.");
+ }
+
+ const { error } = await supabase
+ .from("daily_logs")
+ .upsert(
+ {
+ user_id: userId,
+ ...payload,
+ },
+ { onConflict: "user_id,log_date" },
+ );
+
+ if (error) {
+ throw new Error(`Günlük kaydı eklenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/journal");
+}
+
+export async function updateDailyLogRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+ const payload = readPayload(formData);
+
+ if (!id || !payload.mood_score || !payload.energy_score) {
+ throw new Error("Günlük kaydını güncellemek için kayıt kimliği, mood ve enerji skorları zorunludur.");
+ }
+
+ const { error } = await supabase
+ .from("daily_logs")
+ .update(payload)
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Günlük kaydı güncellenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/journal");
+}
+
+export async function deleteDailyLogRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+
+ if (!id) {
+ throw new Error("Silinecek günlük kaydı bulunamadı.");
+ }
+
+ const { error } = await supabase
+ .from("daily_logs")
+ .delete()
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Günlük kaydı silinemedi: ${error.message}`);
+ }
+
+ revalidatePath("/journal");
+}
diff --git a/app/(dashboard)/journal/journal-client.tsx b/app/(dashboard)/journal/journal-client.tsx
new file mode 100644
index 0000000..052db28
--- /dev/null
+++ b/app/(dashboard)/journal/journal-client.tsx
@@ -0,0 +1,511 @@
+"use client";
+
+import {
+ createDailyLogRecord,
+ deleteDailyLogRecord,
+ updateDailyLogRecord,
+} from "@/app/(dashboard)/journal/actions";
+import { Badge, Button, Card, CardContent, Input, Label, Textarea } from "poyraz-ui/atoms";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "poyraz-ui/molecules";
+import {
+ Activity,
+ Battery,
+ CalendarDays,
+ LineChart as LineChartIcon,
+ Pencil,
+ Plus,
+ Smile,
+ Trash2,
+} from "lucide-react";
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import type { ReactNode } from "react";
+import { useMemo, useState } from "react";
+
+export type DailyLogItem = {
+ id: string;
+ log_date: string;
+ mood_score: number;
+ energy_score: number;
+ work_satisfaction_score: number | null;
+ note: string | null;
+};
+
+type JournalClientProps = {
+ logs: DailyLogItem[];
+};
+
+const scoreLabels: Record = {
+ 1: "Çok düşük",
+ 2: "Düşük",
+ 3: "Orta",
+ 4: "İyi",
+ 5: "Çok iyi",
+};
+
+export function JournalClient({ logs }: JournalClientProps) {
+ const [monthFilter, setMonthFilter] = useState(() => new Date().toISOString().slice(0, 7));
+ const filteredLogs = logs.filter((log) => log.log_date.startsWith(monthFilter));
+ const summary = useMemo(() => calculateSummary(filteredLogs), [filteredLogs]);
+ const chartData = useMemo(
+ () =>
+ [...filteredLogs]
+ .sort((a, b) => a.log_date.localeCompare(b.log_date))
+ .map((log) => ({
+ date: formatShortDate(log.log_date),
+ mood: log.mood_score,
+ energy: log.energy_score,
+ satisfaction: log.work_satisfaction_score,
+ })),
+ [filteredLogs],
+ );
+
+ return (
+
+
+
+
+
+
+ Mood ve enerji
+
+
+ Günlük ruh hali, enerji ve çalışma memnuniyetini takip ederek kişisel kapasite trendini gör.
+
+
+
+
+
+ setMonthFilter(event.target.value)}
+ className="sm:w-44"
+ />
+
+
+
+
+
+ }
+ tone="primary"
+ />
+ }
+ tone="green"
+ />
+ }
+ tone="blue"
+ />
+ }
+ tone="amber"
+ />
+
+
+
+
+
+
+
Aylık trend
+
+ Mood, enerji ve çalışma memnuniyetinin günlük değişimi.
+
+
+
+ {chartData.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
Kapasite sinyali
+
Bu ayki günlük kayıtlardan kısa okuma.
+
+
+ {summary.insights.map((insight) => (
+
+ {insight}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
Günlük kayıtlar
+
{filteredLogs.length} kayıt görüntüleniyor.
+
+
+
+ {filteredLogs.length > 0 ? (
+
+
+ Tarih
+ Mood
+ Enerji
+ Not
+ İşlem
+
+
+ {filteredLogs.map((log) => (
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+function DailyLogRow({ log }: { log: DailyLogItem }) {
+ return (
+
+
+
{formatDate(log.log_date)}
+
{formatWeekday(log.log_date)}
+
+
+
+
+
{log.note || "Not eklenmedi."}
+ {log.work_satisfaction_score ? (
+
Çalışma memnuniyeti: {log.work_satisfaction_score}/5
+ ) : null}
+
+
+
+
+
+
+ );
+}
+
+function DailyLogDialog({ mode, log }: { mode: "create" | "edit"; log?: DailyLogItem }) {
+ const [open, setOpen] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const action = mode === "create" ? createDailyLogRecord : updateDailyLogRecord;
+
+ async function handleSubmit(formData: FormData) {
+ setIsSubmitting(true);
+
+ try {
+ await action(formData);
+ setOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+ {mode === "create" ? : }
+ {mode === "create" ? "Günlük ekle" : "Düzenle"}
+
+
+
+
+
+
+ );
+}
+
+function DailyLogFormFields({ log }: { log?: DailyLogItem }) {
+ const [moodScore, setMoodScore] = useState(log?.mood_score || 3);
+ const [energyScore, setEnergyScore] = useState(log?.energy_score || 3);
+ const [satisfactionScore, setSatisfactionScore] = useState(log?.work_satisfaction_score || 3);
+
+ return (
+
+
+ Tarih
+
+
+
+
+
+
+
+
+ Not
+
+
+
+ );
+}
+
+function ScorePicker({
+ name,
+ label,
+ value,
+ onChange,
+ tone,
+}: {
+ name: string;
+ label: string;
+ value: number;
+ onChange: (value: number) => void;
+ tone: "primary" | "green" | "blue";
+}) {
+ return (
+
+
+ {label}
+ {scoreLabels[value]}
+
+
+
+ {[1, 2, 3, 4, 5].map((score) => (
+ onChange(score)}
+ className={`h-10 rounded-sm border text-sm font-semibold transition-colors ${
+ value === score
+ ? getScoreActiveClass(tone)
+ : "border-border bg-background text-muted-foreground hover:border-primary/40"
+ }`}
+ >
+ {score}
+
+ ))}
+
+
+ );
+}
+
+function ScoreBadge({ score, tone }: { score: number; tone: "primary" | "green" }) {
+ const className =
+ tone === "green"
+ ? "border-emerald-200 bg-emerald-50 text-emerald-700"
+ : "border-primary/20 bg-primary/10 text-primary";
+
+ return {score}/5 · {scoreLabels[score]} ;
+}
+
+function StatCard({
+ label,
+ value,
+ icon,
+ tone,
+}: {
+ label: string;
+ value: string;
+ icon: ReactNode;
+ tone: "primary" | "green" | "blue" | "amber";
+}) {
+ const toneClass = {
+ primary: "bg-primary/10 text-primary",
+ green: "bg-emerald-50 text-emerald-700",
+ blue: "bg-blue-50 text-blue-700",
+ amber: "bg-amber-50 text-amber-700",
+ }[tone];
+
+ return (
+
+
+
+
+ {icon}
+
+
+
+ );
+}
+
+function EmptyState() {
+ return (
+
+
+
Bu ay günlük kayıt yok
+
+ Mood ve enerji trendini görmek için ilk günlük kaydını ekle.
+
+
+ );
+}
+
+function calculateSummary(logs: DailyLogItem[]) {
+ const moodAverage = average(logs.map((log) => log.mood_score));
+ const energyAverage = average(logs.map((log) => log.energy_score));
+ const satisfactionAverage = average(
+ logs
+ .map((log) => log.work_satisfaction_score)
+ .filter((score): score is number => typeof score === "number"),
+ );
+
+ const insights = [];
+
+ if (logs.length === 0) {
+ insights.push("Bu ay için henüz okunabilir bir trend yok.");
+ } else {
+ insights.push(`Bu ay ${logs.length} günlük kayıt var.`);
+ insights.push(
+ energyAverage && energyAverage < 3
+ ? "Enerji ortalaması düşük. Dashboard raporlarında geciken işler ile birlikte okunmalı."
+ : "Enerji ortalaması dengeli görünüyor.",
+ );
+ insights.push(
+ moodAverage && moodAverage >= 4
+ ? "Mood seviyesi güçlü. Yüksek odak isteyen işler için iyi bir dönem olabilir."
+ : "Mood trendi izlenmeli. Not alanı hangi günlerin zor geçtiğini anlamak için önemli.",
+ );
+ }
+
+ return { moodAverage, energyAverage, satisfactionAverage, insights };
+}
+
+function average(values: number[]) {
+ if (values.length === 0) return 0;
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
+}
+
+function getScoreActiveClass(tone: "primary" | "green" | "blue") {
+ if (tone === "green") return "border-emerald-600 bg-emerald-600 text-white";
+ if (tone === "blue") return "border-blue-600 bg-blue-600 text-white";
+ return "border-primary bg-primary text-primary-foreground";
+}
+
+function formatDate(value: string) {
+ return new Intl.DateTimeFormat("tr-TR", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ }).format(new Date(`${value}T00:00:00`));
+}
+
+function formatShortDate(value: string) {
+ return new Intl.DateTimeFormat("tr-TR", {
+ day: "2-digit",
+ month: "short",
+ }).format(new Date(`${value}T00:00:00`));
+}
+
+function formatWeekday(value: string) {
+ return new Intl.DateTimeFormat("tr-TR", {
+ weekday: "long",
+ }).format(new Date(`${value}T00:00:00`));
+}
diff --git a/app/(dashboard)/journal/page.tsx b/app/(dashboard)/journal/page.tsx
index 29f2b05..483be4e 100644
--- a/app/(dashboard)/journal/page.tsx
+++ b/app/(dashboard)/journal/page.tsx
@@ -1,294 +1,41 @@
-"use client";
+import { JournalClient, type DailyLogItem } from "@/app/(dashboard)/journal/journal-client";
+import { createClient } from "@/lib/supabase/server";
-import { useState } from "react";
-import {
- Plus, Search, Calendar, Clock, Brain, MessageSquare,
- Smile, Frown, Meh, Star, MoreHorizontal, X,
- Zap, Save, Trash2, Edit3, Image as ImageIcon, Link as LinkIcon,
- ChevronLeft, ChevronRight, Activity, Filter, AlignLeft, Hash, ArrowRight
-} from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
+type DailyLogRow = {
+ id: string;
+ log_date: string;
+ mood_score: number;
+ energy_score: number;
+ work_satisfaction_score: number | null;
+ note: string | null;
+};
-// Mock Data
-const journalEntries = [
- {
- id: 1,
- date: "May 15, 2026",
- title: "Deep Work Breakthrough",
- excerpt: "Today I finally cracked the multi-tenant architecture logic for the Cognis core. Energy was high after the morning focus block.",
- sentiment: "Great",
- moodScore: 92,
- tags: ["Productivity", "Coding"],
- content: "The morning started with a 4-hour deep work block. I avoided all notifications and focused purely on the database schema. The multi-tenant logic is now solid. I feel a huge weight off my shoulders. Physical energy was 9/10 thanks to the 7am gym session."
- },
- {
- id: 2,
- date: "May 14, 2026",
- title: "Project Risk Discussion",
- excerpt: "Met with Alex regarding the Infrastructure delays. Felt a bit anxious about the timeline but the AI risk report helped us focus.",
- sentiment: "Neutral",
- moodScore: 65,
- tags: ["Meeting", "Stress"],
- content: "Alex and I went through the infrastructure roadmap. We are indeed behind on the database migration. The stress is real, but we have a plan now. AI suggests prioritizing the migration scripts. Note for tomorrow: focus on script optimization."
- },
- { id: 3, date: "May 12, 2026", title: "Creative Flow", excerpt: "Spent the afternoon in Figma. The new 'Cyber-Lavender' palette is looking stunning in the dark mode previews.", sentiment: "Great", moodScore: 88, tags: ["Design", "Creative"] },
-];
+export default async function JournalPage() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
-export default function JournalPage() {
- const [selectedEntry, setSelectedEntry] = useState(null);
- const [isCreating, setIsCreating] = useState(false);
+ if (!user) {
+ return null;
+ }
- return (
-
-
- {/* Top Header */}
-
-
-
- Mindset / Strategic Journal
-
-
-
- 128 ENTRIES RECORDED
-
-
-
-
-
-
-
-
setIsCreating(true)}
- className="bg-primary hover:bg-primary/90 text-primary-foreground border border-primary/20 px-5 py-2 rounded-sm text-xs font-black tracking-widest flex items-center gap-2 transition-all shadow-lg shadow-primary/30 active:scale-95"
- >
-
- NEW ENTRY
-
-
-
+ const { data: logRows } = await supabase
+ .from("daily_logs")
+ .select("id, log_date, mood_score, energy_score, work_satisfaction_score, note")
+ .eq("user_id", user.id)
+ .order("log_date", { ascending: false })
+ .limit(180);
-
-
- {/* Left Sidebar: Entries List */}
-
-
-
Recent Reflections
-
- FILTER
-
-
-
-
- {journalEntries.map(entry => (
-
setSelectedEntry(entry)}
- className={`p-5 rounded-sm border cursor-pointer transition-all ${selectedEntry?.id === entry.id ? 'bg-[#1F172B] border-primary/40 shadow-xl' : 'bg-[#0A0710] border-white/5 hover:border-white/10'}`}
- >
-
- {entry.date}
-
-
- {entry.title}
- "{entry.excerpt}"
-
- {entry.tags.map(tag => (
- #{tag}
- ))}
-
-
- ))}
-
-
+ const logs: DailyLogItem[] = ((logRows || []) as DailyLogRow[]).map((log) => ({
+ id: log.id,
+ log_date: log.log_date,
+ mood_score: Number(log.mood_score),
+ energy_score: Number(log.energy_score),
+ work_satisfaction_score:
+ typeof log.work_satisfaction_score === "number" ? Number(log.work_satisfaction_score) : null,
+ note: log.note,
+ }));
- {/* Main Content: Entry Viewer/Editor */}
-
-
- {selectedEntry ? (
-
-
-
-
-
{selectedEntry.date}
-
-
-
MOOD SCORE: {selectedEntry.moodScore}%
-
-
-
{selectedEntry.title}
-
-
-
-
-
-
-
-
-
- {/* AI Psychological Insight */}
-
-
-
-
-
- Cognitive Analysis
-
-
- "I've noticed that your mood scores peak when you mention 'Deep Work' blocks in the morning. However, meeting-heavy days seem to correlate with anxious sentiment. Consider restructuring 'Infrastructure' discussions for early PM."
-
-
-
- {/* Body Content */}
-
-
- {selectedEntry.content || "No detailed content available for this entry."}
-
-
-
- {/* Strategic Connections */}
-
-
Connected Strategics
-
-
-
-
-
-
Goal: Scaling Backend
-
Directly Referenced
-
-
-
-
-
-
-
-
-
Habit: 7AM Gym
-
Impact Observed
-
-
-
-
-
-
-
-
-
-
- {[1, 2, 3].map(i =>
A{i}
)}
-
+2
-
-
- 12 COMMENTS
-
-
-
- ) : (
-
-
-
-
No Entry Selected
-
Select a reflection from the sidebar or create a new strategic entry to begin.
-
-
setIsCreating(true)}
- className="bg-primary/10 text-primary border border-primary/20 px-6 py-2.5 rounded-sm text-xs font-black tracking-widest uppercase hover:bg-primary/20 transition-all"
- >
- Create Your First Entry
-
-
- )}
-
-
-
-
- {/* New Entry Modal */}
-
- {isCreating && (
- <>
- setIsCreating(false)} className="fixed inset-0 bg-black/90 backdrop-blur-xl z-[100]" />
-
-
-
-
-
-
New Reflection
-
Documenting Strategic Growth
-
-
-
setIsCreating(false)} className="p-2 hover:bg-white/10 rounded-sm text-muted-foreground transition-colors">
-
-
-
-
- Title of Reflection
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Auto-saving to Cloud...
-
-
-
-
-
-
- PUBLISH REFLECTION
-
- setIsCreating(false)} className="px-10 border border-white/10 rounded-sm text-[11px] font-black uppercase tracking-[0.2em] text-muted-foreground hover:bg-white/5 transition-all">
- DISCARD
-
-
-
- >
- )}
-
-
-
- );
-}
-
-function SentimentIcon({ sentiment }: { sentiment: string }) {
- if (sentiment === "Great") return ;
- if (sentiment === "Neutral") return ;
- return ;
+ return ;
}