fix: clean dashboard client type issues

This commit is contained in:
poyrazavsever
2026-07-18 20:18:39 +03:00
parent 100b624061
commit 1d318e1ea0
11 changed files with 131 additions and 92 deletions
@@ -120,7 +120,7 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<div className="bg-background border border-border rounded-xl p-3 shadow-lg shadow-black/5">
<p className="font-medium text-foreground mb-2 text-sm">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
{payload.map((entry, index) => (
<div key={index} className="flex items-center justify-between gap-6 text-xs">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
@@ -3,7 +3,7 @@
import { useState } from "react";
import { format } from "date-fns";
import { tr } from "date-fns/locale";
import { Receipt, Plus, MoreHorizontal, FileEdit, Trash2, Send, Download, CheckCircle2 } from "lucide-react";
import { Plus, MoreHorizontal, FileEdit, Trash2, Send, Download, CheckCircle2 } from "lucide-react";
import { Button, Card, CardContent, Badge } from "poyraz-ui/atoms";
import {
DropdownMenu,
@@ -3,7 +3,7 @@
import { useState } from "react";
import { format } from "date-fns";
import { tr } from "date-fns/locale";
import { FileText, Plus, MoreHorizontal, FileEdit, Trash2, Mail, CheckCircle2, XCircle } from "lucide-react";
import { Plus, MoreHorizontal, FileEdit, Trash2, Mail, CheckCircle2, XCircle } from "lucide-react";
import { Button, Card, CardContent, Badge } from "poyraz-ui/atoms";
import {
DropdownMenu,
+21 -32
View File
@@ -1,7 +1,6 @@
"use client";
import {
archiveClientRecord,
createClientRecord,
updateClientRecord,
updateClientPipelineStage,
@@ -28,10 +27,7 @@ import {
toast,
} from "poyraz-ui/molecules";
import {
Archive,
ExternalLink,
Mail,
PauseCircle,
Pencil,
Phone,
Plus,
@@ -45,7 +41,6 @@ import Link from "next/link";
import { useState } from "react";
import { format, isPast, isToday } from "date-fns";
import { tr } from "date-fns/locale";
import { useEffect } from "react";
import { cn } from "@/lib/utils";
import { StatCard } from "@/components/system/stat-card";
@@ -68,19 +63,13 @@ export type ClientListItem = {
client_value_score: number;
};
const statusLabels = {
active: "Aktif",
paused: "Duraklatıldı",
archived: "Arşivlendi",
};
type ClientPipelineStage = ClientListItem["pipeline_stage"];
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",
};
const pipelineStages = [
const pipelineStages: Array<{
id: ClientPipelineStage;
label: string;
color: string;
}> = [
{ id: "lead", label: "Potansiyel (Lead)", color: "border-slate-200 bg-slate-50 text-slate-700" },
{ id: "contacted", label: "İletişime Geçildi", color: "border-blue-200 bg-blue-50 text-blue-700" },
{ id: "proposal_sent", label: "Teklif İletildi", color: "border-amber-200 bg-amber-50 text-amber-700" },
@@ -92,26 +81,24 @@ 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 [draggedClientId, setDraggedClientId] = useState<string | null>(null);
const [localClients, setLocalClients] = useState(clients);
useEffect(() => {
setLocalClients(clients);
}, [clients]);
const [pipelineOverrides, setPipelineOverrides] = useState<
Partial<Record<string, ClientPipelineStage>>
>({});
const localClients = clients.map((client) => ({
...client,
pipeline_stage: pipelineOverrides[client.id] ?? client.pipeline_stage,
}));
function handleDragStart(event: React.DragEvent<HTMLDivElement>, clientId: string) {
setDraggedClientId(clientId);
@@ -119,7 +106,7 @@ export function ClientsClient({
event.dataTransfer.setData("text/plain", clientId);
}
async function handleDrop(newStage: string) {
async function handleDrop(newStage: ClientPipelineStage) {
if (!draggedClientId) return;
const clientId = draggedClientId;
@@ -128,15 +115,17 @@ export function ClientsClient({
const client = localClients.find(c => c.id === clientId);
if (!client || client.pipeline_stage === newStage) return;
setLocalClients(prev =>
prev.map(c => c.id === clientId ? { ...c, pipeline_stage: newStage as any } : c)
);
const previousStage = client.pipeline_stage;
setPipelineOverrides((current) => ({ ...current, [clientId]: newStage }));
try {
await updateClientPipelineStage(clientId, newStage as any);
await updateClientPipelineStage(clientId, newStage);
toast.success("Müşteri aşaması güncellendi.");
} catch (error) {
setLocalClients(clients);
setPipelineOverrides((current) => ({
...current,
[clientId]: previousStage,
}));
toast.error(
error instanceof Error
? error.message
-2
View File
@@ -57,8 +57,6 @@ export default async function ClientsPage() {
clients={clients}
totalRevenue={clients.reduce((sum, client) => sum + client.revenueTotal, 0)}
activeCount={clients.filter((client) => client.status === "active").length}
pausedCount={clients.filter((client) => client.status === "paused").length}
archivedCount={clients.filter((client) => client.status === "archived").length}
/>
);
}
+2 -2
View File
@@ -125,14 +125,14 @@ export function DashboardClient({ data }: DashboardClientProps) {
<div className="bg-background border border-border rounded-xl p-3 shadow-lg shadow-black/5">
<p className="font-medium text-foreground mb-2 text-sm">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
{payload.map((entry, index) => (
<div key={index} className="flex items-center justify-between gap-6 text-xs">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-muted-foreground">{entry.name === 'income' ? 'Gelir' : 'Gider'}</span>
</div>
<span className="font-semibold text-foreground">
{formatCurrency(entry.value)}
{formatCurrency(Number(entry.value ?? 0))}
</span>
</div>
))}
+4 -2
View File
@@ -644,8 +644,10 @@ function AIFinanceDialog() {
throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
}
setResult(data.text);
} catch (err: any) {
setResult("Hata: " + err.message);
} catch (error) {
setResult(
`Hata: ${error instanceof Error ? error.message : "Bilinmeyen bir hata oluştu."}`,
);
} finally {
setLoading(false);
}
+2 -1
View File
@@ -5,6 +5,7 @@ import {
type ProjectDetailTaskItem,
type ProjectFinanceItem,
type ProjectPlanningSectionItem,
type ProjectRevisionItem,
} from "@/app/(dashboard)/projects/[id]/project-detail-client";
import { DomainError } from "@/server/domain/errors";
import { requireFreelancerBackend } from "@/server/web/freelancer";
@@ -18,7 +19,7 @@ export default async function ProjectDetailPage({ params }: { params: Promise<{
sections: ProjectPlanningSectionItem[];
tasks: ProjectDetailTaskItem[];
financeTransactions: ProjectFinanceItem[];
revisions: Array<Record<string, unknown>>;
revisions: ProjectRevisionItem[];
};
try {
const row = service.getProject(actor, id);
@@ -46,7 +46,8 @@ import {
Trash2,
Wallet,
} from "lucide-react";
import { useEffect, useState, useTransition, type DragEvent } from "react";
import Image from "next/image";
import { useState, useTransition, type DragEvent } from "react";
export type ProjectDetail = {
id: string;
@@ -105,12 +106,20 @@ export type ProjectFinanceItem = {
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: any[];
revisions: ProjectRevisionItem[];
};
const typeLabels = {
@@ -239,11 +248,14 @@ export function ProjectDetailClient({
<Card>
<CardContent className="p-0">
{project.coverImageUrl ? (
<div className="aspect-[16/7] overflow-hidden rounded-t-sm border-b border-border bg-muted">
<img
<div className="relative aspect-[16/7] overflow-hidden rounded-t-sm border-b border-border bg-muted">
<Image
src={project.coverImageUrl}
alt={project.cover_image_alt || project.name}
className="h-full w-full object-cover"
fill
sizes="(min-width: 1024px) 60vw, 100vw"
unoptimized
className="object-cover"
/>
</div>
) : (
@@ -339,16 +351,29 @@ export function ProjectDetailClient({
);
}
function RevisionsPanel({ projectId, revisions }: { projectId: string; revisions: any[] }) {
function RevisionsPanel({
projectId,
revisions,
}: {
projectId: string;
revisions: ProjectRevisionItem[];
}) {
const [isUpdating, setIsUpdating] = useState(false);
async function handleStatusChange(id: string, status: string) {
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 (err: any) {
console.error(err);
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Revizyon durumu güncellenemedi.",
);
} finally {
setIsUpdating(false);
}
@@ -370,7 +395,12 @@ function RevisionsPanel({ projectId, revisions }: { projectId: string; revisions
</div>
<Select
defaultValue={rev.status}
onValueChange={(val) => handleStatusChange(rev.id, val)}
onValueChange={(value) =>
handleStatusChange(
rev.id,
value as ProjectRevisionItem["status"],
)
}
disabled={isUpdating}
>
<SelectTrigger className="w-40 h-8 text-xs">
@@ -591,26 +621,31 @@ function TaskPanel({
tasks: ProjectDetailTaskItem[];
}) {
const [view, setView] = useState<"list" | "kanban">("list");
const [localTasks, setLocalTasks] = useState(tasks);
const [statusOverrides, setStatusOverrides] = useState<
Partial<Record<string, ProjectDetailTaskItem["status"]>>
>({});
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
const [, startTransition] = useTransition();
useEffect(() => {
setLocalTasks(tasks);
}, [tasks]);
const localTasks = tasks.map((task) => ({
...task,
status: statusOverrides[task.id] ?? task.status,
}));
function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) {
const previousTasks = localTasks;
const previousStatus = localTasks.find((task) => task.id === taskId)?.status;
setPendingTask(taskId, true);
setLocalTasks((currentTasks) =>
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
);
setStatusOverrides((current) => ({ ...current, [taskId]: status }));
startTransition(() => {
void updateTaskStatusRecord(taskId, status, projectId)
.catch((error) => {
setLocalTasks(previousTasks);
setStatusOverrides((current) => {
const next = { ...current };
if (previousStatus) next[taskId] = previousStatus;
else delete next[taskId];
return next;
});
toast.error(
error instanceof Error
? error.message
@@ -924,7 +959,7 @@ function ProjectSettingsDialog({ project }: { project: ProjectDetail }) {
</div>
)}
{progressType === "auto" && (
<p className="text-xs text-muted-foreground">İlerleme yüzdesi "Görevler" sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.</p>
<p className="text-xs text-muted-foreground">İlerleme yüzdesi &quot;Görevler&quot; sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.</p>
)}
<div className="grid gap-2">
+17 -14
View File
@@ -38,8 +38,9 @@ import {
Brain,
Loader2,
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState, type ChangeEvent } from "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 = {
@@ -215,17 +216,13 @@ function ProjectCard({
clients: ProjectClientOption[];
}) {
const router = useRouter();
const pathname = usePathname();
const [isNavigating, setIsNavigating] = useState(false);
const [isNavigating, startNavigation] = useTransition();
const detailHref = `/projects/${project.id}`;
useEffect(() => {
setIsNavigating(false);
}, [pathname]);
function goToProjectDetail() {
setIsNavigating(true);
startNavigation(() => {
router.push(detailHref);
});
}
function prefetchProjectDetail() {
@@ -287,11 +284,14 @@ function ProjectCard({
function ProjectCover({ project }: { project: ProjectListItem }) {
if (project.coverImageUrl) {
return (
<div className="aspect-video overflow-hidden rounded-sm border border-border bg-muted">
<img
<div className="relative aspect-video overflow-hidden rounded-sm border border-border bg-muted">
<Image
src={project.coverImageUrl}
alt={project.cover_image_alt || project.name}
className="h-full w-full object-cover"
fill
sizes="(min-width: 1280px) 30vw, (min-width: 768px) 45vw, 100vw"
unoptimized
className="object-cover"
/>
</div>
);
@@ -515,10 +515,13 @@ function CoverImageInput({ project }: { project?: ProjectListItem }) {
className="group relative flex aspect-16/7 cursor-pointer items-center justify-center overflow-hidden rounded-sm border border-dashed border-border bg-muted/20 transition-colors hover:border-primary/50 hover:bg-primary/5"
>
{previewUrl ? (
<img
<Image
src={previewUrl}
alt={project?.cover_image_alt || project?.name || "Proje kapak görseli önizlemesi"}
className="h-full w-full object-cover"
fill
sizes="(min-width: 640px) 640px, 100vw"
unoptimized
className="object-cover"
/>
) : (
<div className="flex flex-col items-center gap-3 text-muted-foreground transition-colors group-hover:text-primary">
+25 -14
View File
@@ -31,7 +31,7 @@ import {
Plus,
Trash2,
} from "lucide-react";
import { useEffect, useState, useTransition, type DragEvent } from "react";
import { useState, useTransition, type DragEvent } from "react";
export type TaskRelationOption = {
id: string;
@@ -82,29 +82,37 @@ type TasksClientProps = {
};
export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
const [localTasks, setLocalTasks] = useState(tasks);
const [statusOverrides, setStatusOverrides] = useState<
Partial<Record<string, TaskListItem["status"]>>
>({});
const [deletedTaskIds, setDeletedTaskIds] = useState<Set<string>>(new Set());
const [query, setQuery] = useState("");
const [projectFilter, setProjectFilter] = useState("__all");
const [view, setView] = useState<"list" | "kanban">("list");
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
const [, startTransition] = useTransition();
useEffect(() => {
setLocalTasks(tasks);
}, [tasks]);
const localTasks = tasks
.filter((task) => !deletedTaskIds.has(task.id))
.map((task) => ({
...task,
status: statusOverrides[task.id] ?? task.status,
}));
function handleTaskStatusChange(taskId: string, status: TaskListItem["status"]) {
const previousTasks = localTasks;
const previousStatus = localTasks.find((task) => task.id === taskId)?.status;
setPendingTask(taskId, true);
setLocalTasks((currentTasks) =>
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
);
setStatusOverrides((current) => ({ ...current, [taskId]: status }));
startTransition(() => {
void updateTaskStatusRecord(taskId, status)
.catch((error) => {
setLocalTasks(previousTasks);
setStatusOverrides((current) => {
const next = { ...current };
if (previousStatus) next[taskId] = previousStatus;
else delete next[taskId];
return next;
});
toast.error(
error instanceof Error
? error.message
@@ -118,7 +126,6 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
}
function handleTaskDelete(taskId: string) {
const previousTasks = localTasks;
const task = localTasks.find((item) => item.id === taskId);
const formData = new FormData();
formData.set("id", taskId);
@@ -128,12 +135,16 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
}
setPendingTask(taskId, true);
setLocalTasks((currentTasks) => currentTasks.filter((item) => item.id !== taskId));
setDeletedTaskIds((current) => new Set(current).add(taskId));
startTransition(() => {
void deleteTaskRecord(formData)
.catch((error) => {
setLocalTasks(previousTasks);
setDeletedTaskIds((current) => {
const next = new Set(current);
next.delete(taskId);
return next;
});
toast.error(
error instanceof Error ? error.message : "Görev silinemedi.",
);