fix: clean dashboard client type issues
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 "Görevler" sekmesindeki görevlerin tamamlanma durumuna göre otomatik hesaplanacaktır.</p>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
|
||||
@@ -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);
|
||||
router.push(detailHref);
|
||||
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">
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user