"use client";
import { getDocumentIntlLocale } from "@/lib/i18n/browser";
import { useTranslations } from "@/components/i18n/i18n-provider";
import {
completeProjectRecord,
createProjectRecord,
updateProjectRecord,
} from "@/app/(dashboard)/projects/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 {
CalendarDays,
CheckCircle2,
Eye,
FolderKanban,
ImageIcon,
LayoutGrid,
List,
Pencil,
Plus,
Target,
Wallet,
Brain,
Loader2,
} from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useState, useTransition, type ChangeEvent } from "react";
import { StatCard } from "@/components/system/stat-card";
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;
translations?: LocalizedFieldValues;
};
const typeLabels = (t: any) => ({
client_project: t("projects.types.client"),
side_project: t("projects.types.side"),
});
const statusLabels = (t: any) => ({
planning: t("projects.status.planning"),
active: t("projects.status.active"),
paused: t("projects.status.paused"),
completed: t("projects.status.completed"),
cancelled: t("projects.status.cancelled"),
});
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[];
localization: {
defaultLocale: string;
locales: LocalizedFieldLocale[];
};
};
export function ProjectsClient({ projects, clients, localization }: ProjectsClientProps) {
const t = useTranslations();
const [query, setQuery] = useState("");
const [view, setView] = useState<"grid" | "list">("grid");
const normalizedQuery = query.trim().toLowerCase();
const types = typeLabels(t);
const filteredProjects = normalizedQuery
? projects.filter((project) =>
[project.name, project.description, project.clientName, types[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 (
{t("projects.list.title")}
{t("projects.list.count", { count: filteredProjects.length })}
{filteredProjects.length > 0 ? (
view === "grid" ? (
{filteredProjects.map((project) => (
))}
) : (
{t("projects.list.columns.project")}
{t("projects.list.columns.type")}
{t("projects.list.columns.status")}
{t("projects.list.columns.budgetDeadline")}
İşlemler
{filteredProjects.map((project) => (
))}
)
) : (
)}
);
}
function ProjectCard({
project,
clients,
localization,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
localization: ProjectsClientProps["localization"];
}) {
const t = useTranslations();
const router = useRouter();
const [isNavigating, startNavigation] = useTransition();
const detailHref = `/projects/${project.id}`;
function goToProjectDetail() {
startNavigation(() => {
router.push(detailHref);
});
}
function prefetchProjectDetail() {
router.prefetch(detailHref);
}
return (
{
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
goToProjectDetail();
}
}}
>
{isNavigating ? (
) : null}
{project.name}
{project.description || t("projects.card.noDescription")}
{statusLabels(t)[project.status]}
{t("projects.card.taskProgress", { done: project.doneTaskCount, total: project.taskCount })}
);
}
function ProjectCover({ project }: { project: ProjectListItem }) {
const t = useTranslations();
if (project.coverImageUrl) {
return (
);
}
return (
{t("projects.card.noCover")}
);
}
function ProjectRow({
project,
clients,
localization,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
localization: ProjectsClientProps["localization"];
}) {
const t = useTranslations();
return (
{project.name}
{project.clientName || t("projects.card.noClient")}
{typeLabels(t)[project.type]}
{statusLabels(t)[project.status]}
);
}
function ProjectMeta({ project }: { project: ProjectListItem }) {
const t = useTranslations();
return (
{typeLabels(t)[project.type]}
{project.clientName || t("projects.card.noClient")}
{project.due_date ? formatDate(project.due_date) : t("projects.card.noDeadline")}
{project.budget_amount ? formatCurrency(project.budget_amount) : t("projects.card.noBudget")}
);
}
function ProjectActions({
project,
clients,
localization,
showDetail,
}: {
project: ProjectListItem;
clients: ProjectClientOption[];
localization: ProjectsClientProps["localization"];
showDetail: boolean;
}) {
const t = useTranslations();
return (
event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
{showDetail ? (
) : null}
{project.status !== "completed" ? (
) : null}
);
}
function ProjectDialog({
mode,
project,
clients,
localization,
iconOnly = false,
}: {
mode: "create" | "edit";
project?: ProjectListItem;
clients: ProjectClientOption[];
localization: ProjectsClientProps["localization"];
iconOnly?: boolean;
}) {
const t = useTranslations();
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" ? t("projects.messages.created") : t("projects.messages.updated"));
} catch (error) {
toast.error(
error instanceof Error
? error.message
: t("projects.errors.saveFailed"),
);
} finally {
setIsSubmitting(false);
}
}
return (
);
}
function CoverImageInput({ project }: { project?: ProjectListItem }) {
const t = useTranslations();
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,
localization,
projectType,
onProjectTypeChange,
}: {
project?: ProjectListItem;
clients: ProjectClientOption[];
localization: ProjectsClientProps["localization"];
projectType: ProjectListItem["type"];
onProjectTypeChange: (value: ProjectListItem["type"]) => void;
}) {
const t = useTranslations();
return (
({ ...f, label: (t as any)(`projects.fields.${f.name}`) || f.label, placeholder: f.placeholder ? (t as any)(`projects.placeholders.${f.name}`) || f.placeholder : undefined }))}
values={project?.translations}
fallbackValues={{
name: project?.name,
description: project?.description,
coverImageAlt: project?.cover_image_alt,
}}
/>
);
}
function ProgressBar({ progress, compact = false }: { progress: number; compact?: boolean }) {
const t = useTranslations();
return (
{!compact ? (
{t("projects.card.progress")}
{progress}%
) : null}
);
}
function EmptyState({ hasQuery }: { hasQuery: boolean }) {
const t = useTranslations();
return (
{t("projects.empty.title")}
{t("projects.empty.description")}
);
}
function formatDate(value: string) {
return new Intl.DateTimeFormat(getDocumentIntlLocale(), {
day: "2-digit",
month: "short",
year: "numeric",
}).format(new Date(`${value}T00:00:00`));
}
function formatCurrency(value: number) {
return new Intl.NumberFormat(getDocumentIntlLocale(), {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value);
}
function AIProjectRiskDialog({ projectId }: { projectId?: string }) {
const t = useTranslations();
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 (
);
}