"use client"; import { getDocumentIntlLocale } from "@/lib/i18n/browser"; import { completeProjectRecord, createProjectPlanningSectionRecord, deleteProjectPlanningSectionRecord, updateProjectPlanningSectionRecord, } from "@/app/(dashboard)/projects/actions"; import { createTaskRecord, updateTaskStatusRecord, } from "@/app/(dashboard)/tasks/actions"; import { LocalizedFields, type LocalizedFieldLocale, type LocalizedFieldValues } from "@/components/i18n/localized-fields"; import { PendingLink } from "@/components/ui/pending-link"; import { PendingSubmitButton } from "@/components/ui/pending-submit-button"; import { contentTranslationRegistry } from "@/lib/i18n/content"; import { Badge, Button, Card, CardContent, Input, Label } from "poyraz-ui/atoms"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "poyraz-ui/molecules"; import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, FileText, FolderKanban, KanbanSquare, LayoutList, Loader2, Palette, Pencil, Plus, Settings2, Target, Trash2, Wallet, } from "lucide-react"; import Image from "next/image"; import { useState, useTransition, type DragEvent } from "react"; export type ProjectDetail = { id: string; client_id: string | null; clientName: string | null; name: string; type: "client_project" | "side_project"; description: string | null; status: "planning" | "active" | "paused" | "completed" | "cancelled"; start_date: string | null; due_date: string | null; budget_amount: number | null; currency: string; progress: number; progress_type: "manual" | "auto"; revision_quota: number; cover_image_alt: string | null; coverImageUrl: string | null; translations?: LocalizedFieldValues; }; export type ProjectPlanningSectionItem = { id: string; project_id: string; category: | "overview" | "problem" | "goal" | "audience" | "scope" | "design_system" | "color_palette" | "typography" | "assets" | "notes"; title: string; content: string | null; sort_order: number; translations?: LocalizedFieldValues; }; export type ProjectDetailTaskItem = { id: string; title: string; status: "todo" | "in_progress" | "done"; priority: "low" | "medium" | "high" | "urgent"; due_at: string | null; is_public_to_client: boolean; translations?: LocalizedFieldValues; }; export type ProjectFinanceItem = { id: string; type: "income" | "expense"; amount: number; currency: string; payment_status: "planned" | "pending" | "paid" | "cancelled"; transaction_date: string; category: string | null; }; export type ProjectRevisionItem = { id: string; description: string; status: "pending" | "in_progress" | "completed" | "rejected"; created_at: string; requested_by: string; }; type ProjectDetailClientProps = { project: ProjectDetail; sections: ProjectPlanningSectionItem[]; tasks: ProjectDetailTaskItem[]; financeTransactions: ProjectFinanceItem[]; revisions: ProjectRevisionItem[]; localization: { defaultLocale: string; locales: LocalizedFieldLocale[]; }; }; const typeLabels = { client_project: "Müşteri projesi", side_project: "Side project", }; const statusLabels = { planning: "Planlama", active: "Aktif", paused: "Duraklatıldı", completed: "Tamamlandı", cancelled: "İptal edildi", }; const statusClasses = { planning: "border-blue-200 bg-blue-50 text-blue-700", active: "border-emerald-200 bg-emerald-50 text-emerald-700", paused: "border-amber-200 bg-amber-50 text-amber-700", completed: "border-zinc-200 bg-zinc-50 text-zinc-700", cancelled: "border-rose-200 bg-rose-50 text-rose-700", }; const priorityClasses = { low: "border-zinc-200 bg-zinc-50 text-zinc-700", medium: "border-blue-200 bg-blue-50 text-blue-700", high: "border-amber-200 bg-amber-50 text-amber-700", urgent: "border-rose-200 bg-rose-50 text-rose-700", }; const sectionLabels: Record = { overview: "Genel bakış", problem: "Çözdüğü problem", goal: "Amaç", audience: "Hedef kitle", scope: "Kapsam", design_system: "Design system", color_palette: "Renk paleti", typography: "Tipografi", assets: "Görsel varlıklar", notes: "Notlar", }; const planningCategories: ProjectPlanningSectionItem["category"][] = [ "overview", "problem", "goal", "audience", "scope", "notes", ]; const designCategories: ProjectPlanningSectionItem["category"][] = [ "design_system", "color_palette", "typography", "assets", ]; export function ProjectDetailClient({ project, sections, tasks, financeTransactions, revisions, localization, }: ProjectDetailClientProps) { const [activeTab, setActiveTab] = useState<"planning" | "design" | "tasks" | "finance" | "revisions">( "planning", ); const planningSections = sections.filter((section) => planningCategories.includes(section.category), ); const designSections = sections.filter((section) => designCategories.includes(section.category), ); const doneTaskCount = tasks.filter((task) => task.status === "done").length; const incomeTotal = financeTransactions .filter((transaction) => transaction.type === "income") .reduce((sum, transaction) => sum + transaction.amount, 0); const expenseTotal = financeTransactions .filter((transaction) => transaction.type === "expense") .reduce((sum, transaction) => sum + transaction.amount, 0); return (

{project.name}

{statusLabels[project.status]}
{project.status !== "completed" ? (
} pendingChildren="Tamamlanıyor" > Tamamla
) : null}
{project.coverImageUrl ? (
{project.cover_image_alt
) : (
Kapak görseli yok
)}
setActiveTab("planning")}> Planlama setActiveTab("design")}> Design system setActiveTab("tasks")}> Görevler setActiveTab("finance")}> Finans setActiveTab("revisions")}> Revizyonlar {revisions.filter(r => r.status === 'pending').length > 0 && ( {revisions.filter(r => r.status === 'pending').length} )}
{activeTab === "planning" ? ( ) : null} {activeTab === "design" ? ( ) : null} {activeTab === "tasks" ? ( ) : null} {activeTab === "finance" ? : null} {activeTab === "revisions" ? : null}
); } function RevisionsPanel({ projectId, revisions, }: { projectId: string; revisions: ProjectRevisionItem[]; }) { const [isUpdating, setIsUpdating] = useState(false); async function handleStatusChange( id: string, status: ProjectRevisionItem["status"], ) { setIsUpdating(true); try { const { updateRevisionStatus } = await import("@/app/(dashboard)/projects/actions"); await updateRevisionStatus(id, projectId, status); } catch (error) { toast.error( error instanceof Error ? error.message : "Revizyon durumu güncellenemedi.", ); } finally { setIsUpdating(false); } } return (

Müşteri Revizyon Talepleri

{revisions.length === 0 ? (

Bu proje için henüz bir revizyon talebi oluşturulmamış.

) : (
{revisions.map(rev => (
{new Date(rev.created_at).toLocaleString(getDocumentIntlLocale())}

{rev.description}

))}
)}
); } function SectionGrid({ projectId, title, description, sections, defaultCategory, localization, }: { projectId: string; title: string; description: string; sections: ProjectPlanningSectionItem[]; defaultCategory: ProjectPlanningSectionItem["category"]; localization: ProjectDetailClientProps["localization"]; }) { return (

{title}

{description}

{sections.length > 0 ? (
{sections.map((section) => ( ))}
) : (

Henüz kayıt yok

Bu proje için ilk planlama veya design system alanını ekleyerek proje bilgisini görevlerden bağımsız hale getir.

)}
); } function PlanningSectionCard({ section, localization, }: { section: ProjectPlanningSectionItem; localization: ProjectDetailClientProps["localization"]; }) { return (
{sectionLabels[section.category]}

{section.title}

} aria-label="Sil" />

{section.content || "İçerik eklenmedi."}

); } function SectionDialog({ projectId, mode, defaultCategory, section, localization, }: { projectId: string; mode: "create" | "edit"; defaultCategory?: ProjectPlanningSectionItem["category"]; section?: ProjectPlanningSectionItem; localization: ProjectDetailClientProps["localization"]; }) { const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const action = mode === "create" ? createProjectPlanningSectionRecord : updateProjectPlanningSectionRecord; async function handleSubmit(formData: FormData) { setIsSubmitting(true); try { await action(formData); setOpen(false); } finally { setIsSubmitting(false); } } return (
{section ? : null} {mode === "create" ? "Planlama alanı ekle" : "Planlama alanını düzenle"} Projenin görev dışı bilgisini yapılandırılmış alanlarda sakla.
); } function TaskPanel({ projectId, clientId, tasks, localization, }: { projectId: string; clientId: string | null; tasks: ProjectDetailTaskItem[]; localization: ProjectDetailClientProps["localization"]; }) { const [view, setView] = useState<"list" | "kanban">("list"); const [statusOverrides, setStatusOverrides] = useState< Partial> >({}); const [pendingTaskIds, setPendingTaskIds] = useState>(new Set()); const [, startTransition] = useTransition(); const localTasks = tasks.map((task) => ({ ...task, status: statusOverrides[task.id] ?? task.status, })); function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) { const previousStatus = localTasks.find((task) => task.id === taskId)?.status; setPendingTask(taskId, true); setStatusOverrides((current) => ({ ...current, [taskId]: status })); startTransition(() => { void updateTaskStatusRecord(taskId, status, projectId) .catch((error) => { setStatusOverrides((current) => { const next = { ...current }; if (previousStatus) next[taskId] = previousStatus; else delete next[taskId]; return next; }); toast.error( error instanceof Error ? error.message : "Görev durumu güncellenemedi.", ); }) .finally(() => { setPendingTask(taskId, false); }); }); } function setPendingTask(taskId: string, pending: boolean) { setPendingTaskIds((current) => { const next = new Set(current); if (pending) { next.add(taskId); } else { next.delete(taskId); } return next; }); } return (

Proje görevleri

Bu proje ile bağlantılı görevler aynı task modülünden beslenir.

{localTasks.length > 0 && view === "list" ? (
Görev Öncelik Son tarih İşlem
{localTasks.map((task) => (
{task.title}
{task.is_public_to_client && ( Müşteriye Açık )}
{task.status === "done" ? "Tamamlandı" : task.status === "in_progress" ? "Devam ediyor" : "Yapılacak"}
{task.priority}
{task.due_at ? formatDateTime(task.due_at) : "Yok"}
{task.status !== "done" ? ( ) : null}
))}
) : null} {localTasks.length > 0 && view === "kanban" ? ( ) : null} {localTasks.length === 0 ? ( ) : null}
); } function ProjectTaskKanban({ tasks, pendingTaskIds, onTaskStatusChange, }: { tasks: ProjectDetailTaskItem[]; pendingTaskIds: Set; onTaskStatusChange: (taskId: string, status: ProjectDetailTaskItem["status"]) => void; }) { const columns = ["todo", "in_progress", "done"] as const; const [draggedTaskId, setDraggedTaskId] = useState(null); function handleDragStart(event: DragEvent, taskId: string) { setDraggedTaskId(taskId); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", taskId); event.dataTransfer.setDragImage(event.currentTarget, 24, 24); } function handleDrop(status: ProjectDetailTaskItem["status"]) { if (!draggedTaskId) return; onTaskStatusChange(draggedTaskId, status); setDraggedTaskId(null); } return (
{columns.map((status) => { const columnTasks = tasks.filter((task) => task.status === status); return (
{ event.preventDefault(); event.dataTransfer.dropEffect = "move"; }} onDrop={() => handleDrop(status)} >

{getTaskStatusLabel(status)}

{columnTasks.length}
{columnTasks.map((task) => ( handleDragStart(event, task.id)} onDragEnd={() => setDraggedTaskId(null)} >
{task.title}
{task.due_at ? formatDateTime(task.due_at) : "Son tarih yok"}
{task.priority} {task.status !== "done" ? ( ) : null}
))}
); })}
); } function ProjectSettingsDialog({ project }: { project: ProjectDetail }) { const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [progressType, setProgressType] = useState<"manual" | "auto">(project.progress_type); const [progress, setProgress] = useState(project.progress); const [revisionQuota, setRevisionQuota] = useState(project.revision_quota); async function handleSubmit() { setIsSubmitting(true); try { const { updateProjectSettings } = await import("@/app/(dashboard)/projects/actions"); await updateProjectSettings(project.id, progressType, progress, revisionQuota); setOpen(false); } catch (error) { console.error(error); } finally { setIsSubmitting(false); } } return (
Proje ayarları İlerleme hesaplama yöntemi ve revizyon kotasını belirle.
{progressType === "manual" && (
setProgress(Number(e.target.value))} className="flex-1 accent-primary" /> {progress}%
)} {progressType === "auto" && (

İlerleme yüzdesi "Görevler" sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.

)}
setRevisionQuota(Number(e.target.value))} />

Müşterinin portal üzerinden talep edebileceği toplam revizyon hakkı.

); } function ProjectTaskDialog({ projectId, clientId, localization, }: { projectId: string; clientId: string | null; localization: ProjectDetailClientProps["localization"]; }) { const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); async function handleSubmit(formData: FormData) { setIsSubmitting(true); try { await createTaskRecord(formData); setOpen(false); } finally { setIsSubmitting(false); } } return (
{clientId ? : null} Projeye görev ekle Yeni görev bu proje ile ilişkilendirilerek görev modülüne kaydedilir.

Eğer müşteri hesabı varsa, bu görev müşteri portalındaki proje detayında da görünür olur.

); } function FinancePanel({ transactions }: { transactions: ProjectFinanceItem[] }) { return (

Finans bağlantıları

Bu projeye bağlanan gelir ve gider kayıtları.

{transactions.length > 0 ? (
{transactions.map((transaction) => (
{transaction.category || (transaction.type === "income" ? "Gelir" : "Gider")}
{formatDate(transaction.transaction_date)} · {transaction.payment_status}
{transaction.type === "income" ? "+" : "-"} {formatCurrency(transaction.amount, transaction.currency)}
))}
) : ( )}
); } function EmptyPanel({ icon: Icon, title, }: { icon: typeof ClipboardList; title: string; }) { return (

{title}

); } function InfoItem({ label, value, icon: Icon, }: { label: string; value: string; icon: typeof FolderKanban; }) { return (
{label}
{value}
); } function StatCard({ label, value, icon: Icon, }: { label: string; value: string; icon: typeof Palette; }) { return (

{label}

{value}

); } function TabButton({ active, onClick, children, }: { active: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); } function formatDate(value: string) { return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "short", year: "numeric", }).format(new Date(`${value}T00:00:00`)); } function formatDateTime(value: string) { return new Intl.DateTimeFormat(getDocumentIntlLocale(), { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }).format(new Date(value)); } function getTaskStatusLabel(status: ProjectDetailTaskItem["status"]) { if (status === "done") return "Tamamlandı"; if (status === "in_progress") return "Devam ediyor"; return "Yapılacak"; } function formatCurrency(value: number, currency: string) { return new Intl.NumberFormat(getDocumentIntlLocale(), { style: "currency", currency, maximumFractionDigits: 0, }).format(value); }