diff --git a/app/(dashboard)/clients/actions.ts b/app/(dashboard)/clients/actions.ts
new file mode 100644
index 0000000..3701c22
--- /dev/null
+++ b/app/(dashboard)/clients/actions.ts
@@ -0,0 +1,119 @@
+"use server";
+
+import { createClient } from "@/lib/supabase/server";
+import { revalidatePath } from "next/cache";
+
+const CLIENT_STATUSES = ["active", "paused", "archived"] as const;
+
+function cleanText(value: FormDataEntryValue | null) {
+ const text = typeof value === "string" ? value.trim() : "";
+ return text.length > 0 ? text : null;
+}
+
+function readStatus(value: FormDataEntryValue | null) {
+ const status = typeof value === "string" ? value : "active";
+ return CLIENT_STATUSES.includes(status as (typeof CLIENT_STATUSES)[number])
+ ? status
+ : "active";
+}
+
+function cleanWebsite(value: FormDataEntryValue | null) {
+ const website = cleanText(value)?.replace(/\s/g, "") || null;
+
+ if (!website) {
+ return null;
+ }
+
+ return /^https?:\/\//i.test(website) ? website : `https://${website}`;
+}
+
+async function getCurrentUserId() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
+
+ if (error || !user) {
+ throw new Error("Müşteri işlemi için giriş yapmış kullanıcı bulunamadı.");
+ }
+
+ return { supabase, userId: user.id };
+}
+
+export async function createClientRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const name = cleanText(formData.get("name"));
+
+ if (!name) {
+ throw new Error("Müşteri adı zorunludur.");
+ }
+
+ const { error } = await supabase.from("clients").insert({
+ user_id: userId,
+ name,
+ company_name: cleanText(formData.get("company_name")),
+ email: cleanText(formData.get("email")),
+ phone: cleanText(formData.get("phone")),
+ website: cleanWebsite(formData.get("website")),
+ status: readStatus(formData.get("status")),
+ notes: cleanText(formData.get("notes")),
+ });
+
+ if (error) {
+ throw new Error(`Müşteri eklenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/clients");
+}
+
+export async function updateClientRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+ const name = cleanText(formData.get("name"));
+
+ if (!id || !name) {
+ throw new Error("Müşteri güncellemek için müşteri adı ve kayıt kimliği zorunludur.");
+ }
+
+ const { error } = await supabase
+ .from("clients")
+ .update({
+ name,
+ company_name: cleanText(formData.get("company_name")),
+ email: cleanText(formData.get("email")),
+ phone: cleanText(formData.get("phone")),
+ website: cleanWebsite(formData.get("website")),
+ status: readStatus(formData.get("status")),
+ notes: cleanText(formData.get("notes")),
+ })
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Müşteri güncellenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/clients");
+}
+
+export async function archiveClientRecord(formData: FormData) {
+ const { supabase, userId } = await getCurrentUserId();
+ const id = cleanText(formData.get("id"));
+
+ if (!id) {
+ throw new Error("Arşivlenecek müşteri bulunamadı.");
+ }
+
+ const { error } = await supabase
+ .from("clients")
+ .update({ status: "archived" })
+ .eq("id", id)
+ .eq("user_id", userId);
+
+ if (error) {
+ throw new Error(`Müşteri arşivlenemedi: ${error.message}`);
+ }
+
+ revalidatePath("/clients");
+}
diff --git a/app/(dashboard)/clients/clients-client.tsx b/app/(dashboard)/clients/clients-client.tsx
new file mode 100644
index 0000000..2cf865b
--- /dev/null
+++ b/app/(dashboard)/clients/clients-client.tsx
@@ -0,0 +1,566 @@
+"use client";
+
+import {
+ archiveClientRecord,
+ createClientRecord,
+ updateClientRecord,
+} from "@/app/(dashboard)/clients/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 {
+ Archive,
+ ExternalLink,
+ Mail,
+ PauseCircle,
+ Pencil,
+ Phone,
+ Plus,
+ UserCheck,
+ Users,
+ Wallet,
+ type LucideIcon,
+} from "lucide-react";
+import Link from "next/link";
+import { useState } from "react";
+
+export type ClientListItem = {
+ id: string;
+ name: string;
+ company_name: string | null;
+ email: string | null;
+ phone: string | null;
+ website: string | null;
+ status: "active" | "paused" | "archived";
+ notes: string | null;
+ created_at: string;
+ projectCount: number;
+ revenueTotal: number;
+};
+
+const statusLabels = {
+ active: "Aktif",
+ paused: "Duraklatıldı",
+ archived: "Arşivlendi",
+};
+
+const statusClasses = {
+ active: "border-emerald-200 bg-emerald-50 text-emerald-700",
+ paused: "border-amber-200 bg-amber-50 text-amber-700",
+ archived: "border-zinc-200 bg-zinc-50 text-zinc-600",
+};
+
+type ClientsClientProps = {
+ clients: ClientListItem[];
+ totalRevenue: number;
+ activeCount: number;
+ pausedCount: number;
+ archivedCount: number;
+};
+
+export function ClientsClient({
+ clients,
+ totalRevenue,
+ activeCount,
+ pausedCount,
+ archivedCount,
+}: ClientsClientProps) {
+ const [query, setQuery] = useState("");
+ const normalizedQuery = query.trim().toLowerCase();
+ const filteredClients = normalizedQuery
+ ? clients.filter((client) =>
+ [
+ client.name,
+ client.company_name,
+ client.email,
+ client.phone,
+ client.website,
+ client.notes,
+ ]
+ .filter(Boolean)
+ .some((value) => value!.toLowerCase().includes(normalizedQuery)),
+ )
+ : clients;
+
+ return (
+
+
+
+
+
+ Freelancer operasyonu
+
+
+
+ Müşteriler
+
+
+ Çalıştığın müşterileri, iletişim bilgilerini ve temel iş durumunu tek
+ ekrandan yönet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Müşteri listesi
+
+
+ {filteredClients.length} kayıt görüntüleniyor.
+
+
+
setQuery(event.target.value)}
+ placeholder="Müşteri, firma, e-posta veya not ara"
+ className="md:max-w-sm"
+ />
+
+
+ {filteredClients.length > 0 ? (
+
+
+ Müşteri
+ İletişim
+ Durum
+ Projeler
+ İşlem
+
+
+ {filteredClients.map((client) => (
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+function ClientRow({ client }: { client: ClientListItem }) {
+ return (
+
+
+
+
+ {getInitials(client.name)}
+
+
+
{client.name}
+
+ {client.company_name || "Firma bilgisi yok"}
+
+
+
+
+
+
+ {client.email ? (
+
+
+
{client.email}
+
+ ) : null}
+ {client.phone ? (
+
+
+
{client.phone}
+
+ ) : null}
+ {client.website ? (
+
+
+
{client.website.replace(/^https?:\/\//, "")}
+
+ ) : null}
+ {!client.email && !client.phone && !client.website ? (
+
İletişim bilgisi yok
+ ) : null}
+
+
+
+
+ {statusLabels[client.status]}
+
+
+
+
+
{client.projectCount}
+
{formatCurrency(client.revenueTotal)}
+
+
+
+
+ {client.status !== "archived" ? (
+
+ ) : null}
+
+
+ );
+}
+
+function ClientDialog({
+ mode,
+ client,
+}: {
+ mode: "create" | "edit";
+ client?: ClientListItem;
+}) {
+ const [open, setOpen] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const action = mode === "create" ? createClientRecord : updateClientRecord;
+
+ async function handleSubmit(formData: FormData) {
+ setIsSubmitting(true);
+
+ try {
+ await action(formData);
+ setOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
+
+function ClientFormFields({ client }: { client?: ClientListItem }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function PhoneInput({
+ id,
+ name,
+ defaultValue,
+}: {
+ id: string;
+ name: string;
+ defaultValue: string;
+}) {
+ const [value, setValue] = useState(defaultValue);
+
+ return (
+ setValue(formatPhone(event.target.value))}
+ />
+ );
+}
+
+function WebsiteInput({
+ id,
+ name,
+ defaultValue,
+}: {
+ id: string;
+ name: string;
+ defaultValue: string;
+}) {
+ const [value, setValue] = useState(defaultValue);
+
+ return (
+ setValue(event.target.value.replace(/\s/g, ""))}
+ onBlur={() => setValue(normalizeWebsite(value))}
+ />
+ );
+}
+
+function StatCard({
+ label,
+ value,
+ description,
+ icon: Icon,
+ iconClassName,
+}: {
+ label: string;
+ value: string;
+ description?: string;
+ icon: LucideIcon;
+ iconClassName: string;
+}) {
+ return (
+
+
+
+
+
{label}
+
{value}
+ {description ? (
+
{description}
+ ) : null}
+
+
+
+
+
+
+
+ );
+}
+
+function EmptyState({ hasQuery }: { hasQuery: boolean }) {
+ return (
+
+
+
+ {hasQuery ? "Aramana uygun müşteri yok" : "Henüz müşteri eklenmedi"}
+
+
+ {hasQuery
+ ? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
+ : "İlk müşterini ekleyerek proje, görev ve finans kayıtlarını müşteriyle ilişkilendirmeye başlayabilirsin."}
+
+
+ );
+}
+
+function getInitials(name: string) {
+ return name
+ .split(" ")
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((part) => part[0]?.toUpperCase())
+ .join("");
+}
+
+function formatPhone(input: string) {
+ const digits = input.replace(/\D/g, "");
+
+ if (!digits) {
+ return "";
+ }
+
+ const local = digits.startsWith("90")
+ ? digits.slice(2, 12)
+ : digits.startsWith("0")
+ ? digits.slice(1, 11)
+ : digits.slice(0, 10);
+
+ const area = local.slice(0, 3);
+ const first = local.slice(3, 6);
+ const second = local.slice(6, 8);
+ const third = local.slice(8, 10);
+
+ let formatted = "+90";
+ if (area) formatted += ` (${area}`;
+ if (area.length === 3) formatted += ")";
+ if (first) formatted += ` ${first}`;
+ if (second) formatted += ` ${second}`;
+ if (third) formatted += ` ${third}`;
+
+ return formatted;
+}
+
+function normalizeWebsite(input: string) {
+ const value = input.trim().replace(/\s/g, "");
+
+ if (!value) {
+ return "";
+ }
+
+ if (/^https?:\/\//i.test(value)) {
+ return value;
+ }
+
+ return `https://${value}`;
+}
+
+function getWebsiteHref(input: string) {
+ return /^https?:\/\//i.test(input) ? input : `https://${input}`;
+}
+
+function formatCurrency(value: number) {
+ return new Intl.NumberFormat("tr-TR", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(value);
+}
diff --git a/app/(dashboard)/clients/page.tsx b/app/(dashboard)/clients/page.tsx
index ac7929e..f2b972d 100644
--- a/app/(dashboard)/clients/page.tsx
+++ b/app/(dashboard)/clients/page.tsx
@@ -1,282 +1,106 @@
-"use client";
+import { ClientsClient, type ClientListItem } from "@/app/(dashboard)/clients/clients-client";
+import { createClient } from "@/lib/supabase/server";
-import { useState } from "react";
-import {
- Plus, Search, User, Users, Mail, Phone, Globe,
- MoreHorizontal, MessageSquare, Briefcase,
- TrendingUp, Star, Clock, X, ChevronRight,
- ArrowUpRight, DollarSign, Brain, Shield,
- Activity, CheckCircle2, AlertCircle
-} from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
+type ClientRow = {
+ id: string;
+ name: string;
+ company_name: string | null;
+ email: string | null;
+ phone: string | null;
+ website: string | null;
+ status: "active" | "paused" | "archived";
+ notes: string | null;
+ created_at: string;
+};
-// Mock Data
-const clients = [
- {
- id: 1,
- name: "Acme Corp",
- contact: "John Doe",
- email: "john@acme.com",
- status: "Active",
- value: "$12,400",
- projects: 2,
- health: "Stable",
- lastContact: "2 days ago",
- aiInsight: "Excellent relationship. High potential for upsell into the Q3 Marketing Package.",
- history: [
- { type: "Meeting", date: "May 12", note: "Q3 Strategy Review" },
- { type: "Payment", date: "May 08", note: "$4,200 received" },
- ]
- },
- {
- id: 2,
- name: "Global Tech",
- contact: "Jane Smith",
- email: "jane@global.io",
- status: "Onboarding",
- value: "$8,500",
- projects: 1,
- health: "Critical",
- lastContact: "1 week ago",
- aiInsight: "Risk of churn detected. Last communication was 7 days ago. Immediate outreach suggested.",
- history: [
- { type: "Proposal", date: "May 01", note: "Infrastructure Scale" },
- ]
- },
- { id: 3, name: "Nexus Design", contact: "Mike Ross", email: "mike@nexus.com", status: "Active", value: "$42,000", projects: 4, health: "Growth", lastContact: "Today", aiInsight: "Client is expanding rapidly. Consider offering a dedicated project manager role.", history: [] },
- { id: 4, name: "Stark Ind.", contact: "Pepper P.", email: "pepper@stark.com", status: "Lead", value: "$0", projects: 0, health: "Neutral", lastContact: "May 14", aiInsight: "Warm lead from the Webflow conference. Interested in AI integration.", history: [] },
-];
+type ProjectRow = {
+ client_id: string | null;
+};
-export default function ClientsPage() {
- const [selectedClient, setSelectedClient] = useState(null);
+type FinanceRow = {
+ client_id: string | null;
+ amount: number | string;
+ type: "income" | "expense";
+ payment_status: "planned" | "pending" | "paid" | "cancelled";
+};
+
+export default async function ClientsPage() {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (!user) {
+ return null;
+ }
+
+ const [{ data: clientRows }, { data: projectRows }, { data: financeRows }] =
+ await Promise.all([
+ supabase
+ .from("clients")
+ .select("id, name, company_name, email, phone, website, status, notes, created_at")
+ .eq("user_id", user.id)
+ .order("created_at", { ascending: false }),
+ supabase.from("projects").select("client_id").eq("user_id", user.id),
+ supabase
+ .from("finance_transactions")
+ .select("client_id, amount, type, payment_status")
+ .eq("user_id", user.id),
+ ]);
+
+ const projectCountByClient = countProjectsByClient((projectRows || []) as ProjectRow[]);
+ const revenueByClient = sumRevenueByClient((financeRows || []) as FinanceRow[]);
+
+ const clients: ClientListItem[] = ((clientRows || []) as ClientRow[]).map((client) => ({
+ ...client,
+ projectCount: projectCountByClient.get(client.id) || 0,
+ revenueTotal: revenueByClient.get(client.id) || 0,
+ }));
+
+ const activeCount = clients.filter((client) => client.status === "active").length;
+ const pausedCount = clients.filter((client) => client.status === "paused").length;
+ const archivedCount = clients.filter((client) => client.status === "archived").length;
+ const totalRevenue = clients.reduce((sum, client) => sum + client.revenueTotal, 0);
return (
-
-
- {/* Top Header */}
-
-
-
- Network / Strategic Clients
-
-
-
- 12 ACTIVE PARTNERSHIPS
-
-
-
-
-
-
-
-
-
-
-
- {/* CRM Stats */}
-
-
-
-
-
-
-
- AI Insight
-
-
- "High churn risk for Global Tech. Immediate outreach suggested."
-
-
-
-
- {/* Main Clients List */}
-
-
-
-
Partner Entity
-
Relationship
-
Strategic Value
-
Engagement
-
Actions
-
-
-
- {clients.map((client) => (
-
setSelectedClient(client)}
- 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"
- >
-
-
- {client.name.split(' ').map(n => n[0]).join('')}
-
-
-
{client.name}
-
{client.contact}
-
-
-
-
-
- {client.health}
-
-
-
-
- {client.value}
- {client.projects} Active Projects
-
-
-
- {client.lastContact}
- Last Touchpoint
-
-
-
-
-
-
-
- ))}
-
-
-
-
- {/* Client Detail Sheet */}
-
- {selectedClient && (
- <>
- setSelectedClient(null)} className="fixed inset-0 bg-black/80 backdrop-blur-md z-[100]" />
-
-
-
-
-
- {selectedClient.name.split(' ').map((n: string) => n[0]).join('')}
-
-
-
{selectedClient.name}
-
-
{selectedClient.contact}
-
-
{selectedClient.email}
-
-
-
-
-
-
-
-
- {/* AI Client Pulse */}
-
-
-
-
-
- Client Health Pulse
-
-
- "{selectedClient.aiInsight}"
-
-
-
- {/* Key Financials */}
-
-
-
LIFETIME VALUE
-
{selectedClient.value}
-
-
-
ACTIVE DELIVERABLES
-
{selectedClient.projects}
-
-
-
- {/* Relationship History */}
-
-
Relationship Log
-
- {selectedClient.history?.length > 0 ? selectedClient.history.map((log: any, i: number) => (
-
-
-
-
-
{log.note}
-
{log.date} • {log.type}
-
-
-
- )) :
No historical log entries found for this partner.
}
-
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
-
-
- >
- )}
-
-
-
+
);
}
-function ActionButton({ icon: Icon, label }: { icon: any, label: string }) {
- return (
-
- );
+function countProjectsByClient(projects: ProjectRow[]) {
+ const countByClient = new Map();
+
+ for (const project of projects) {
+ if (!project.client_id) continue;
+ countByClient.set(project.client_id, (countByClient.get(project.client_id) || 0) + 1);
+ }
+
+ return countByClient;
}
-function ClientStatCard({ label, value, subtext, icon: Icon }: any) {
- return (
-
-
-
-
-
-
-
-
-
{label}
-
- {value}
- {subtext}
-
-
-
- );
+function sumRevenueByClient(transactions: FinanceRow[]) {
+ const revenueByClient = new Map();
+
+ for (const transaction of transactions) {
+ if (
+ !transaction.client_id ||
+ transaction.type !== "income" ||
+ transaction.payment_status !== "paid"
+ ) {
+ continue;
+ }
+
+ revenueByClient.set(
+ transaction.client_id,
+ (revenueByClient.get(transaction.client_id) || 0) + Number(transaction.amount || 0),
+ );
+ }
+
+ return revenueByClient;
}
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index e97ffa3..3594287 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -19,7 +19,7 @@ export default async function DashboardLayout({
.maybeSingle()
: { data: null };
- const fallbackName = user?.email?.split("@")[0] ?? "MindSpace Kullanıcısı";
+ const fallbackName = user?.email?.split("@")[0] ?? "Cognis Kullanıcısı";
const displayName =
[profile?.first_name, profile?.last_name].filter(Boolean).join(" ") ||
fallbackName;