"use client";
import {
completeProjectRecord,
createProjectRecord,
updateProjectRecord,
} from "@/app/(dashboard)/projects/actions";
import { PendingLink } from "@/components/ui/pending-link";
import { PendingSubmitButton } from "@/components/ui/pending-submit-button";
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,
toast,
} from "poyraz-ui/molecules";
import {
CalendarDays,
CheckCircle2,
Eye,
FolderKanban,
ImageIcon,
LayoutGrid,
List,
Pencil,
Plus,
Target,
Wallet,
Brain,
Loader2,
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState, type ChangeEvent } from "react";
export type ProjectClientOption = {
id: string;
name: string;
};
export type ProjectListItem = {
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;
cover_image_path: string | null;
cover_image_alt: string | null;
coverImageUrl: string | null;
taskCount: number;
doneTaskCount: number;
};
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",
};
type ProjectsClientProps = {
projects: ProjectListItem[];
clients: ProjectClientOption[];
};
export function ProjectsClient({ projects, clients }: ProjectsClientProps) {
const [query, setQuery] = useState("");
const [view, setView] = useState<"grid" | "list">("grid");
const normalizedQuery = query.trim().toLowerCase();
const filteredProjects = normalizedQuery
? projects.filter((project) =>
[project.name, project.description, project.clientName, typeLabels[project.type]]
.filter(Boolean)
.some((value) => value!.toLowerCase().includes(normalizedQuery)),
)
: projects;
const activeCount = projects.filter((project) => project.status === "active").length;
const sideProjectCount = projects.filter((project) => project.type === "side_project").length;
const averageProgress = projects.length
? Math.round(projects.reduce((sum, project) => sum + project.progress, 0) / projects.length)
: 0;
const totalBudget = projects.reduce((sum, project) => sum + (project.budget_amount || 0), 0);
return (
İş ve side project yönetimi
Projeler
Müşteri işleri ve kişisel side projectleri aynı yerde takip et.
Proje listesi
{filteredProjects.length} kayıt görüntüleniyor.
{filteredProjects.length > 0 ? (
view === "grid" ? (
{filteredProjects.map((project) => (
))}
) : (
Proje
Tür
Durum
İlerleme
İşlem
{filteredProjects.map((project) => (
))}
)
) : (
)}
);
}
function ProjectCard({
project,
clients,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
}) {
const router = useRouter();
const pathname = usePathname();
const [isNavigating, setIsNavigating] = useState(false);
const detailHref = `/projects/${project.id}`;
useEffect(() => {
setIsNavigating(false);
}, [pathname]);
function goToProjectDetail() {
setIsNavigating(true);
router.push(detailHref);
}
function prefetchProjectDetail() {
router.prefetch(detailHref);
}
return (
{
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
goToProjectDetail();
}
}}
>
{isNavigating ? (
) : null}
{project.name}
{project.description || "Açıklama eklenmedi."}
{statusLabels[project.status]}
{project.doneTaskCount}/{project.taskCount} görev tamamlandı
);
}
function ProjectCover({ project }: { project: ProjectListItem }) {
if (project.coverImageUrl) {
return (
);
}
return (
Kapak görseli yok
);
}
function ProjectRow({
project,
clients,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
}) {
return (
{project.name}
{project.clientName || "Bağımsız side project"}
{typeLabels[project.type]}
{statusLabels[project.status]}
);
}
function ProjectMeta({ project }: { project: ProjectListItem }) {
return (
{typeLabels[project.type]}
{project.clientName || "Müşteri bağlantısı yok"}
{project.due_date ? formatDate(project.due_date) : "Deadline yok"}
{project.budget_amount ? formatCurrency(project.budget_amount) : "Bütçe yok"}
);
}
function ProjectActions({
project,
clients,
showDetail,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
showDetail: boolean;
}) {
return (
event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
{showDetail ? (
) : null}
{project.status !== "completed" ? (
) : null}
);
}
function ProjectDialog({
mode,
project,
clients,
iconOnly = false,
}: {
mode: "create" | "edit";
project?: ProjectListItem;
clients: ProjectClientOption[];
iconOnly?: boolean;
}) {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [projectType, setProjectType] = useState(project?.type || "client_project");
const action = mode === "create" ? createProjectRecord : updateProjectRecord;
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
await action(formData);
setOpen(false);
toast.success(mode === "create" ? "Proje eklendi." : "Proje güncellendi.");
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Proje kaydedilirken beklenmeyen bir hata oluştu.",
);
} finally {
setIsSubmitting(false);
}
}
return (
);
}
function CoverImageInput({ project }: { project?: ProjectListItem }) {
const inputId = `cover-${project?.id || "new"}`;
const [previewUrl, setPreviewUrl] = useState(project?.coverImageUrl || "");
useEffect(() => {
return () => {
if (previewUrl.startsWith("blob:")) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
function handleFileChange(event: ChangeEvent) {
const file = event.target.files?.[0];
if (!file) {
setPreviewUrl(project?.coverImageUrl || "");
return;
}
const nextPreviewUrl = URL.createObjectURL(file);
setPreviewUrl((currentPreviewUrl) => {
if (currentPreviewUrl.startsWith("blob:")) {
URL.revokeObjectURL(currentPreviewUrl);
}
return nextPreviewUrl;
});
}
return (
);
}
function ProjectFormFields({
project,
clients,
projectType,
onProjectTypeChange,
}: {
project?: ProjectListItem;
clients: ProjectClientOption[];
projectType: ProjectListItem["type"];
onProjectTypeChange: (value: ProjectListItem["type"]) => void;
}) {
return (
);
}
function ProgressBar({ progress, compact = false }: { progress: number; compact?: boolean }) {
return (
{!compact ? (
İlerleme
{progress}%
) : null}
);
}
function StatCard({
label,
value,
icon: Icon,
tone,
}: {
label: string;
value: string;
icon: typeof FolderKanban;
tone: "green" | "blue" | "amber" | "red";
}) {
const toneClass = {
green: "bg-emerald-50 text-emerald-700",
blue: "bg-blue-50 text-blue-700",
amber: "bg-amber-50 text-amber-700",
red: "bg-primary/10 text-primary",
}[tone];
return (
);
}
function EmptyState({ hasQuery }: { hasQuery: boolean }) {
return (
{hasQuery ? "Aramana uygun proje yok" : "Henüz proje eklenmedi"}
{hasQuery
? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
: "İlk müşteri projen veya side project kaydınla operasyon akışını kurmaya başlayabilirsin."}
);
}
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 formatCurrency(value: number) {
return new Intl.NumberFormat("tr-TR", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value);
}
function AIProjectRiskDialog({ projectId }: { projectId?: string }) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const handleAnalyze = async () => {
setLoading(true);
setResult(null);
try {
const res = await fetch("/api/project-risk", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ projectId }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
}
setResult(data.text);
} catch (err) {
setResult(
"Hata: " +
(err instanceof Error ? err.message : "Bilinmeyen bir hata oluştu."),
);
} finally {
setLoading(false);
}
};
return (
);
}