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 ( + + + + + +
+ {client ? : null} + + + {mode === "create" ? "Yeni müşteri" : "Müşteriyi düzenle"} + + + Müşteri bilgilerini sade tut; proje ve finans bağlantıları sonraki + modüllerden otomatik görünecek. + + + + + + + + + +
+
+ ); +} + +function ClientFormFields({ client }: { client?: ClientListItem }) { + return ( +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +