fix: clean dashboard client type issues
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user