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">
|
<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>
|
<p className="font-medium text-foreground mb-2 text-sm">{label}</p>
|
||||||
<div className="space-y-1.5">
|
<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 key={index} className="flex items-center justify-between gap-6 text-xs">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { tr } from "date-fns/locale";
|
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 { Button, Card, CardContent, Badge } from "poyraz-ui/atoms";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { tr } from "date-fns/locale";
|
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 { Button, Card, CardContent, Badge } from "poyraz-ui/atoms";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
archiveClientRecord,
|
|
||||||
createClientRecord,
|
createClientRecord,
|
||||||
updateClientRecord,
|
updateClientRecord,
|
||||||
updateClientPipelineStage,
|
updateClientPipelineStage,
|
||||||
@@ -28,10 +27,7 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
} from "poyraz-ui/molecules";
|
} from "poyraz-ui/molecules";
|
||||||
import {
|
import {
|
||||||
Archive,
|
|
||||||
ExternalLink,
|
|
||||||
Mail,
|
Mail,
|
||||||
PauseCircle,
|
|
||||||
Pencil,
|
Pencil,
|
||||||
Phone,
|
Phone,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -45,7 +41,6 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { format, isPast, isToday } from "date-fns";
|
import { format, isPast, isToday } from "date-fns";
|
||||||
import { tr } from "date-fns/locale";
|
import { tr } from "date-fns/locale";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { StatCard } from "@/components/system/stat-card";
|
import { StatCard } from "@/components/system/stat-card";
|
||||||
|
|
||||||
@@ -68,19 +63,13 @@ export type ClientListItem = {
|
|||||||
client_value_score: number;
|
client_value_score: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusLabels = {
|
type ClientPipelineStage = ClientListItem["pipeline_stage"];
|
||||||
active: "Aktif",
|
|
||||||
paused: "Duraklatıldı",
|
|
||||||
archived: "Arşivlendi",
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusClasses = {
|
const pipelineStages: Array<{
|
||||||
active: "border-emerald-200 bg-emerald-50 text-emerald-700",
|
id: ClientPipelineStage;
|
||||||
paused: "border-amber-200 bg-amber-50 text-amber-700",
|
label: string;
|
||||||
archived: "border-zinc-200 bg-zinc-50 text-zinc-600",
|
color: string;
|
||||||
};
|
}> = [
|
||||||
|
|
||||||
const pipelineStages = [
|
|
||||||
{ id: "lead", label: "Potansiyel (Lead)", color: "border-slate-200 bg-slate-50 text-slate-700" },
|
{ 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: "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" },
|
{ id: "proposal_sent", label: "Teklif İletildi", color: "border-amber-200 bg-amber-50 text-amber-700" },
|
||||||
@@ -92,26 +81,24 @@ type ClientsClientProps = {
|
|||||||
clients: ClientListItem[];
|
clients: ClientListItem[];
|
||||||
totalRevenue: number;
|
totalRevenue: number;
|
||||||
activeCount: number;
|
activeCount: number;
|
||||||
pausedCount: number;
|
|
||||||
archivedCount: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ClientsClient({
|
export function ClientsClient({
|
||||||
clients,
|
clients,
|
||||||
totalRevenue,
|
totalRevenue,
|
||||||
activeCount,
|
activeCount,
|
||||||
pausedCount,
|
|
||||||
archivedCount,
|
|
||||||
}: ClientsClientProps) {
|
}: ClientsClientProps) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const normalizedQuery = query.trim().toLowerCase();
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
|
|
||||||
const [draggedClientId, setDraggedClientId] = useState<string | null>(null);
|
const [draggedClientId, setDraggedClientId] = useState<string | null>(null);
|
||||||
const [localClients, setLocalClients] = useState(clients);
|
const [pipelineOverrides, setPipelineOverrides] = useState<
|
||||||
|
Partial<Record<string, ClientPipelineStage>>
|
||||||
useEffect(() => {
|
>({});
|
||||||
setLocalClients(clients);
|
const localClients = clients.map((client) => ({
|
||||||
}, [clients]);
|
...client,
|
||||||
|
pipeline_stage: pipelineOverrides[client.id] ?? client.pipeline_stage,
|
||||||
|
}));
|
||||||
|
|
||||||
function handleDragStart(event: React.DragEvent<HTMLDivElement>, clientId: string) {
|
function handleDragStart(event: React.DragEvent<HTMLDivElement>, clientId: string) {
|
||||||
setDraggedClientId(clientId);
|
setDraggedClientId(clientId);
|
||||||
@@ -119,7 +106,7 @@ export function ClientsClient({
|
|||||||
event.dataTransfer.setData("text/plain", clientId);
|
event.dataTransfer.setData("text/plain", clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDrop(newStage: string) {
|
async function handleDrop(newStage: ClientPipelineStage) {
|
||||||
if (!draggedClientId) return;
|
if (!draggedClientId) return;
|
||||||
|
|
||||||
const clientId = draggedClientId;
|
const clientId = draggedClientId;
|
||||||
@@ -128,15 +115,17 @@ export function ClientsClient({
|
|||||||
const client = localClients.find(c => c.id === clientId);
|
const client = localClients.find(c => c.id === clientId);
|
||||||
if (!client || client.pipeline_stage === newStage) return;
|
if (!client || client.pipeline_stage === newStage) return;
|
||||||
|
|
||||||
setLocalClients(prev =>
|
const previousStage = client.pipeline_stage;
|
||||||
prev.map(c => c.id === clientId ? { ...c, pipeline_stage: newStage as any } : c)
|
setPipelineOverrides((current) => ({ ...current, [clientId]: newStage }));
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateClientPipelineStage(clientId, newStage as any);
|
await updateClientPipelineStage(clientId, newStage);
|
||||||
toast.success("Müşteri aşaması güncellendi.");
|
toast.success("Müşteri aşaması güncellendi.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLocalClients(clients);
|
setPipelineOverrides((current) => ({
|
||||||
|
...current,
|
||||||
|
[clientId]: previousStage,
|
||||||
|
}));
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ export default async function ClientsPage() {
|
|||||||
clients={clients}
|
clients={clients}
|
||||||
totalRevenue={clients.reduce((sum, client) => sum + client.revenueTotal, 0)}
|
totalRevenue={clients.reduce((sum, client) => sum + client.revenueTotal, 0)}
|
||||||
activeCount={clients.filter((client) => client.status === "active").length}
|
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">
|
<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>
|
<p className="font-medium text-foreground mb-2 text-sm">{label}</p>
|
||||||
<div className="space-y-1.5">
|
<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 key={index} className="flex items-center justify-between gap-6 text-xs">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||||
<span className="text-muted-foreground">{entry.name === 'income' ? 'Gelir' : 'Gider'}</span>
|
<span className="text-muted-foreground">{entry.name === 'income' ? 'Gelir' : 'Gider'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-foreground">
|
<span className="font-semibold text-foreground">
|
||||||
{formatCurrency(entry.value)}
|
{formatCurrency(Number(entry.value ?? 0))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -644,8 +644,10 @@ function AIFinanceDialog() {
|
|||||||
throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
|
throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
|
||||||
}
|
}
|
||||||
setResult(data.text);
|
setResult(data.text);
|
||||||
} catch (err: any) {
|
} catch (error) {
|
||||||
setResult("Hata: " + err.message);
|
setResult(
|
||||||
|
`Hata: ${error instanceof Error ? error.message : "Bilinmeyen bir hata oluştu."}`,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type ProjectDetailTaskItem,
|
type ProjectDetailTaskItem,
|
||||||
type ProjectFinanceItem,
|
type ProjectFinanceItem,
|
||||||
type ProjectPlanningSectionItem,
|
type ProjectPlanningSectionItem,
|
||||||
|
type ProjectRevisionItem,
|
||||||
} from "@/app/(dashboard)/projects/[id]/project-detail-client";
|
} from "@/app/(dashboard)/projects/[id]/project-detail-client";
|
||||||
import { DomainError } from "@/server/domain/errors";
|
import { DomainError } from "@/server/domain/errors";
|
||||||
import { requireFreelancerBackend } from "@/server/web/freelancer";
|
import { requireFreelancerBackend } from "@/server/web/freelancer";
|
||||||
@@ -18,7 +19,7 @@ export default async function ProjectDetailPage({ params }: { params: Promise<{
|
|||||||
sections: ProjectPlanningSectionItem[];
|
sections: ProjectPlanningSectionItem[];
|
||||||
tasks: ProjectDetailTaskItem[];
|
tasks: ProjectDetailTaskItem[];
|
||||||
financeTransactions: ProjectFinanceItem[];
|
financeTransactions: ProjectFinanceItem[];
|
||||||
revisions: Array<Record<string, unknown>>;
|
revisions: ProjectRevisionItem[];
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const row = service.getProject(actor, id);
|
const row = service.getProject(actor, id);
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Wallet,
|
Wallet,
|
||||||
} from "lucide-react";
|
} 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 = {
|
export type ProjectDetail = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -105,12 +106,20 @@ export type ProjectFinanceItem = {
|
|||||||
category: string | null;
|
category: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProjectRevisionItem = {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
status: "pending" | "in_progress" | "completed" | "rejected";
|
||||||
|
created_at: string;
|
||||||
|
requested_by: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ProjectDetailClientProps = {
|
type ProjectDetailClientProps = {
|
||||||
project: ProjectDetail;
|
project: ProjectDetail;
|
||||||
sections: ProjectPlanningSectionItem[];
|
sections: ProjectPlanningSectionItem[];
|
||||||
tasks: ProjectDetailTaskItem[];
|
tasks: ProjectDetailTaskItem[];
|
||||||
financeTransactions: ProjectFinanceItem[];
|
financeTransactions: ProjectFinanceItem[];
|
||||||
revisions: any[];
|
revisions: ProjectRevisionItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeLabels = {
|
const typeLabels = {
|
||||||
@@ -239,11 +248,14 @@ export function ProjectDetailClient({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{project.coverImageUrl ? (
|
{project.coverImageUrl ? (
|
||||||
<div className="aspect-[16/7] overflow-hidden rounded-t-sm border-b border-border bg-muted">
|
<div className="relative aspect-[16/7] overflow-hidden rounded-t-sm border-b border-border bg-muted">
|
||||||
<img
|
<Image
|
||||||
src={project.coverImageUrl}
|
src={project.coverImageUrl}
|
||||||
alt={project.cover_image_alt || project.name}
|
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>
|
</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);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
async function handleStatusChange(id: string, status: string) {
|
async function handleStatusChange(
|
||||||
|
id: string,
|
||||||
|
status: ProjectRevisionItem["status"],
|
||||||
|
) {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
const { updateRevisionStatus } = await import("@/app/(dashboard)/projects/actions");
|
const { updateRevisionStatus } = await import("@/app/(dashboard)/projects/actions");
|
||||||
await updateRevisionStatus(id, projectId, status);
|
await updateRevisionStatus(id, projectId, status);
|
||||||
} catch (err: any) {
|
} catch (error) {
|
||||||
console.error(err);
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Revizyon durumu güncellenemedi.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
@@ -370,7 +395,12 @@ function RevisionsPanel({ projectId, revisions }: { projectId: string; revisions
|
|||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={rev.status}
|
defaultValue={rev.status}
|
||||||
onValueChange={(val) => handleStatusChange(rev.id, val)}
|
onValueChange={(value) =>
|
||||||
|
handleStatusChange(
|
||||||
|
rev.id,
|
||||||
|
value as ProjectRevisionItem["status"],
|
||||||
|
)
|
||||||
|
}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40 h-8 text-xs">
|
<SelectTrigger className="w-40 h-8 text-xs">
|
||||||
@@ -591,26 +621,31 @@ function TaskPanel({
|
|||||||
tasks: ProjectDetailTaskItem[];
|
tasks: ProjectDetailTaskItem[];
|
||||||
}) {
|
}) {
|
||||||
const [view, setView] = useState<"list" | "kanban">("list");
|
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 [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
|
const localTasks = tasks.map((task) => ({
|
||||||
useEffect(() => {
|
...task,
|
||||||
setLocalTasks(tasks);
|
status: statusOverrides[task.id] ?? task.status,
|
||||||
}, [tasks]);
|
}));
|
||||||
|
|
||||||
function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) {
|
function handleTaskStatusChange(taskId: string, status: ProjectDetailTaskItem["status"]) {
|
||||||
const previousTasks = localTasks;
|
const previousStatus = localTasks.find((task) => task.id === taskId)?.status;
|
||||||
|
|
||||||
setPendingTask(taskId, true);
|
setPendingTask(taskId, true);
|
||||||
setLocalTasks((currentTasks) =>
|
setStatusOverrides((current) => ({ ...current, [taskId]: status }));
|
||||||
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
|
|
||||||
);
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
void updateTaskStatusRecord(taskId, status, projectId)
|
void updateTaskStatusRecord(taskId, status, projectId)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setLocalTasks(previousTasks);
|
setStatusOverrides((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
if (previousStatus) next[taskId] = previousStatus;
|
||||||
|
else delete next[taskId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
@@ -924,7 +959,7 @@ function ProjectSettingsDialog({ project }: { project: ProjectDetail }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{progressType === "auto" && (
|
{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">
|
<div className="grid gap-2">
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ import {
|
|||||||
Brain,
|
Brain,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import Image from "next/image";
|
||||||
import { useEffect, useState, type ChangeEvent } from "react";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState, useTransition, type ChangeEvent } from "react";
|
||||||
import { StatCard } from "@/components/system/stat-card";
|
import { StatCard } from "@/components/system/stat-card";
|
||||||
|
|
||||||
export type ProjectClientOption = {
|
export type ProjectClientOption = {
|
||||||
@@ -215,17 +216,13 @@ function ProjectCard({
|
|||||||
clients: ProjectClientOption[];
|
clients: ProjectClientOption[];
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const [isNavigating, startNavigation] = useTransition();
|
||||||
const [isNavigating, setIsNavigating] = useState(false);
|
|
||||||
const detailHref = `/projects/${project.id}`;
|
const detailHref = `/projects/${project.id}`;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsNavigating(false);
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
function goToProjectDetail() {
|
function goToProjectDetail() {
|
||||||
setIsNavigating(true);
|
startNavigation(() => {
|
||||||
router.push(detailHref);
|
router.push(detailHref);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefetchProjectDetail() {
|
function prefetchProjectDetail() {
|
||||||
@@ -287,11 +284,14 @@ function ProjectCard({
|
|||||||
function ProjectCover({ project }: { project: ProjectListItem }) {
|
function ProjectCover({ project }: { project: ProjectListItem }) {
|
||||||
if (project.coverImageUrl) {
|
if (project.coverImageUrl) {
|
||||||
return (
|
return (
|
||||||
<div className="aspect-video overflow-hidden rounded-sm border border-border bg-muted">
|
<div className="relative aspect-video overflow-hidden rounded-sm border border-border bg-muted">
|
||||||
<img
|
<Image
|
||||||
src={project.coverImageUrl}
|
src={project.coverImageUrl}
|
||||||
alt={project.cover_image_alt || project.name}
|
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>
|
</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"
|
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 ? (
|
{previewUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
alt={project?.cover_image_alt || project?.name || "Proje kapak görseli önizlemesi"}
|
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">
|
<div className="flex flex-col items-center gap-3 text-muted-foreground transition-colors group-hover:text-primary">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState, useTransition, type DragEvent } from "react";
|
import { useState, useTransition, type DragEvent } from "react";
|
||||||
|
|
||||||
export type TaskRelationOption = {
|
export type TaskRelationOption = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -82,29 +82,37 @@ type TasksClientProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TasksClient({ tasks, clients, projects }: 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 [query, setQuery] = useState("");
|
||||||
const [projectFilter, setProjectFilter] = useState("__all");
|
const [projectFilter, setProjectFilter] = useState("__all");
|
||||||
const [view, setView] = useState<"list" | "kanban">("list");
|
const [view, setView] = useState<"list" | "kanban">("list");
|
||||||
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
|
const [pendingTaskIds, setPendingTaskIds] = useState<Set<string>>(new Set());
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
|
const localTasks = tasks
|
||||||
useEffect(() => {
|
.filter((task) => !deletedTaskIds.has(task.id))
|
||||||
setLocalTasks(tasks);
|
.map((task) => ({
|
||||||
}, [tasks]);
|
...task,
|
||||||
|
status: statusOverrides[task.id] ?? task.status,
|
||||||
|
}));
|
||||||
|
|
||||||
function handleTaskStatusChange(taskId: string, status: TaskListItem["status"]) {
|
function handleTaskStatusChange(taskId: string, status: TaskListItem["status"]) {
|
||||||
const previousTasks = localTasks;
|
const previousStatus = localTasks.find((task) => task.id === taskId)?.status;
|
||||||
|
|
||||||
setPendingTask(taskId, true);
|
setPendingTask(taskId, true);
|
||||||
setLocalTasks((currentTasks) =>
|
setStatusOverrides((current) => ({ ...current, [taskId]: status }));
|
||||||
currentTasks.map((task) => (task.id === taskId ? { ...task, status } : task)),
|
|
||||||
);
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
void updateTaskStatusRecord(taskId, status)
|
void updateTaskStatusRecord(taskId, status)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setLocalTasks(previousTasks);
|
setStatusOverrides((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
if (previousStatus) next[taskId] = previousStatus;
|
||||||
|
else delete next[taskId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
@@ -118,7 +126,6 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskDelete(taskId: string) {
|
function handleTaskDelete(taskId: string) {
|
||||||
const previousTasks = localTasks;
|
|
||||||
const task = localTasks.find((item) => item.id === taskId);
|
const task = localTasks.find((item) => item.id === taskId);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set("id", taskId);
|
formData.set("id", taskId);
|
||||||
@@ -128,12 +135,16 @@ export function TasksClient({ tasks, clients, projects }: TasksClientProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPendingTask(taskId, true);
|
setPendingTask(taskId, true);
|
||||||
setLocalTasks((currentTasks) => currentTasks.filter((item) => item.id !== taskId));
|
setDeletedTaskIds((current) => new Set(current).add(taskId));
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
void deleteTaskRecord(formData)
|
void deleteTaskRecord(formData)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setLocalTasks(previousTasks);
|
setDeletedTaskIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(taskId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "Görev silinemedi.",
|
error instanceof Error ? error.message : "Görev silinemedi.",
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user