Files
danak-admin/src/pages/taskmanager/workspace.tsx
T
hamid zarghami d3fd21177b
deploy to danak / build_and_deploy (push) Has been cancelled
change toast
2026-07-20 10:55:33 +03:30

306 lines
12 KiB
TypeScript

import { DragDropContext, type DragStart, type DropResult } from "@hello-pangea/dnd";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState, type FC } from "react";
import { useParams } from "react-router-dom";
import { toast } from '../../components/Toast';
import PageLoading from "../../components/PageLoading";
import { ErrorType } from "../../helpers/types";
import AddNewColumn from "./components/AddNewColumn";
import Column from "./components/Column";
import HeaderWorkspace from "./components/HeaderWorkspace";
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
import { useGetProject, useGetProjectDetail } from "./project/hooks/useProjectData";
import ProjectSettingsPanel from "./project/components/ProjectSettingsPanel";
import type { ProjectDetailType } from "./project/types/ProjectTypes";
import { getProjectBackgroundStyle } from "./project/utils/getProjectBackgroundStyle";
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
import { useChangeTaskPhase, useChangeTaskPriority } from "./task/hooks/useTaskData";
import type { TaskItemType } from "./task/types/TaskTypes";
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
import type { Column as ColumnType, Task } from "./types";
import { getPriorityDestinationTaskId } from "./utils/getPriorityDestinationTaskId";
import { reorderColumns } from "./utils/reorderColumns";
import { reorderTaskItems } from "./utils/reorderTaskItems";
import { getTaskPhaseItems, insertTaskPhaseItem, updateTaskPhaseItems, type TasksByTaskPhaseInfiniteData } from "./utils/taskPhaseQueryCache";
const Workspace: FC = () => {
const queryClient = useQueryClient();
const { slug: projectId = "" } = useParams<{ slug: string }>();
const { data: project, isPending } = useGetProject(projectId);
const { data: projectDetailData } = useGetProjectDetail(projectId);
const projectDetail: ProjectDetailType | undefined =
projectDetailData?.data?.project ?? projectDetailData?.data;
const { mutate: changeTaskPhase } = useChangeTaskPhase();
const { mutate: changeTaskPriority } = useChangeTaskPriority();
const { mutate: updateTaskPhase } = useUpdateTaskPhase();
const [columns, setColumns] = useState<ColumnType[]>([]);
const [draggedTask, setDraggedTask] = useState<Task | null>(null);
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
useEffect(() => {
if (!project?.data?.taskPhases) return;
setColumns(
[...project.data.taskPhases]
.sort((a, b) => a.order - b.order)
.map((phase) => ({
id: phase.id,
title: phase.name,
color: phase.color,
order: phase.order,
})),
);
}, [project?.data?.taskPhases]);
const handleTaskDragStart = (start: DragStart) => {
if (start.type !== "TASK") return;
const sourceColumnId = start.source.droppableId;
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId]);
const items = getTaskPhaseItems(previousData);
const item = items.find((task) => task.id === start.draggableId);
if (!item) return;
setDraggedTask(mapTaskItemToTask(item as TaskItemType, sourceColumnId, start.source.index));
};
const handleDrop = (columnId: string, index: number, activeTask: Task) => {
const sourceColumnId = activeTask.columnId;
const columnChanged = sourceColumnId !== columnId;
if (!columnChanged) {
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
const currentItems = getTaskPhaseItems(previousData);
const destTaskId = getPriorityDestinationTaskId(currentItems, activeTask.id, index);
if (!destTaskId) return;
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)),
);
changeTaskPriority(
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
{
onError: (error: ErrorType) => {
if (previousData) {
queryClient.setQueryData(["tasks-by-task-phase", columnId], previousData);
}
toast(error.response?.data?.error.message[0], 'error');
},
},
);
return;
}
const previousSource = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId]);
const previousDest = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId], (current) =>
updateTaskPhaseItems(current, (items) => items.filter((task) => task.id !== activeTask.id)),
);
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
insertTaskPhaseItem(
current,
{
id: activeTask.id,
title: activeTask.title,
taskPhaseId: columnId,
order: index,
priority: index,
tag: activeTask.tag,
dateRange: activeTask.dateRange,
color: activeTask.color,
attachments: activeTask.attachments,
checklistDone: activeTask.checklistDone,
checklistTotal: activeTask.checklistTotal,
},
index,
),
);
const destTaskId = getPriorityDestinationTaskId(getTaskPhaseItems(previousDest), activeTask.id, index, false);
if (selectedTask?.id === activeTask.id) {
setSelectedTask({ ...activeTask, columnId });
}
changeTaskPhase(
{ id: activeTask.id, params: { taskPhaseId: columnId }, projectId },
{
onSuccess: () => {
if (destTaskId) {
changeTaskPriority(
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
{
onError: (error: ErrorType) => {
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", sourceColumnId] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", columnId] });
toast(error.response?.data?.error.message[0], 'error');
},
},
);
return;
}
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", sourceColumnId] });
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", columnId] });
},
onError: (error: ErrorType) => {
if (previousSource) {
queryClient.setQueryData(["tasks-by-task-phase", sourceColumnId], previousSource);
}
if (previousDest) {
queryClient.setQueryData(["tasks-by-task-phase", columnId], previousDest);
}
if (selectedTask?.id === activeTask.id) {
setSelectedTask(activeTask);
}
toast(error.response?.data?.error.message[0], 'error');
},
},
);
};
const handleTaskDragEnd = (result: DropResult) => {
const activeTask = draggedTask;
setDraggedTask(null);
if (!activeTask || !result.destination || result.type !== "TASK") return;
const { source, destination } = result;
if (source.droppableId === destination.droppableId && source.index === destination.index) return;
handleDrop(destination.droppableId, destination.index, activeTask);
};
const handleTaskPhaseChanged = (taskId: string, taskPhaseId: string) => {
const sourceColumnId = selectedTask?.columnId;
if (sourceColumnId) {
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", sourceColumnId] });
}
queryClient.invalidateQueries({ queryKey: ["tasks-by-task-phase", taskPhaseId] });
setSelectedTask((prev) => (prev?.id === taskId ? { ...prev, columnId: taskPhaseId } : prev));
};
const handleColumnDragStart = (columnId: string) => {
setDraggedColumnId(columnId);
};
const handleColumnDragEnd = () => {
setDraggedColumnId(null);
};
const handleColumnDrop = (overColumnId: string) => {
if (!draggedColumnId || draggedColumnId === overColumnId) return;
const previousColumns = columns;
const nextColumns = reorderColumns(columns, draggedColumnId, overColumnId);
const movedColumn = nextColumns.find((column) => column.id === draggedColumnId);
setColumns(nextColumns);
setDraggedColumnId(null);
if (!movedColumn?.order) return;
updateTaskPhase(
{ id: draggedColumnId, params: { order: movedColumn.order }, projectId },
{
onError: (error: ErrorType) => {
setColumns(previousColumns);
toast(error.response?.data?.error.message[0], 'error');
},
},
);
};
const nextColumnOrder = columns.length ? Math.max(...columns.map((column) => column.order ?? 0)) + 1 : 1;
const handleColumnCreated = (phase: TaskPhaseItemType) => {
setColumns((prev) => [...prev, { id: phase.id, title: phase.name, color: phase.color, order: phase.order }].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)));
};
const handleColumnDeleted = (columnId: string) => {
setColumns((prev) => prev.filter((column) => column.id !== columnId));
queryClient.removeQueries({ queryKey: ["tasks-by-task-phase", columnId] });
if (selectedTask?.columnId === columnId) {
setSelectedTask(null);
}
};
const handleColumnTitleUpdated = (columnId: string, title: string) => {
setColumns((prev) => prev.map((column) => (column.id === columnId ? { ...column, title } : column)));
};
if (isPending) {
return <PageLoading />;
}
return (
<div
className="h-full min-h-0 rounded-none xl:rounded-[32px] overflow-hidden flex flex-col"
style={getProjectBackgroundStyle({
backgroundColor: projectDetail?.backgroundColor,
backgroundPicture: projectDetail?.backgroundPicture,
})}
>
<HeaderWorkspace
projectName={project?.data?.name ?? ""}
onSettingsClick={() => setIsSettingsOpen(true)}
/>
<div className="flex-1 min-h-0 flex flex-col px-3 sm:px-4 xl:px-8 py-4 xl:py-6">
<DragDropContext onDragStart={handleTaskDragStart} onDragEnd={handleTaskDragEnd}>
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-hidden overscroll-x-contain">
<div className="flex gap-3 sm:gap-4 xl:gap-7 h-full min-h-0 items-stretch w-max min-w-full">
{columns.map((column) => (
<Column
key={column.id}
column={column}
projectId={projectId}
draggedColumnId={draggedColumnId}
onColumnDragStart={handleColumnDragStart}
onColumnDragEnd={handleColumnDragEnd}
onColumnDrop={handleColumnDrop}
onTaskClick={setSelectedTask}
onColumnDeleted={handleColumnDeleted}
onColumnTitleUpdated={handleColumnTitleUpdated}
/>
))}
<AddNewColumn
projectId={projectId}
nextOrder={nextColumnOrder}
onColumnCreated={handleColumnCreated}
/>
</div>
</div>
</DragDropContext>
</div>
<TaskDetailModal
open={selectedTask !== null}
task={selectedTask}
columns={columns}
projectId={projectId}
onClose={() => setSelectedTask(null)}
onTaskPhaseChanged={handleTaskPhaseChanged}
/>
<ProjectSettingsPanel
open={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
projectId={projectId}
project={projectDetail}
/>
</div>
);
};
export default Workspace;