Files
danak-admin/src/pages/taskmanager/components/Column.tsx
T
2026-07-11 11:45:18 +03:30

255 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Add, AddCircle, CloseCircle } from "iconsax-react";
import { useRef, useState, type FC } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";
import ModalConfrim from "../../../components/ModalConfrim";
import { ErrorType } from "../../../helpers/types";
import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
import type { Column as ColumnType, Task as TaskType } from "../types";
import ColumnMenu from "./ColumnMenu";
import Task from "./Task";
type Props = {
column: ColumnType;
projectId: string;
tasks: TaskType[];
draggedTaskId: string | null;
draggedColumnId: string | null;
onDragStart: (taskId: string) => void;
onDragEnd: () => void;
onDrop: (columnId: string, index: number) => void;
onColumnDragStart: (columnId: string) => void;
onColumnDragEnd: () => void;
onColumnDrop: (overColumnId: string) => void;
onTaskClick?: (task: TaskType) => void;
onColumnDeleted?: (columnId: string) => void;
};
const Column: FC<Props> = ({
column,
projectId,
tasks,
draggedTaskId,
draggedColumnId,
onDragStart,
onDragEnd,
onDrop,
onColumnDragStart,
onColumnDragEnd,
onColumnDrop,
onTaskClick,
onColumnDeleted,
}) => {
const { t } = useTranslation("global");
const deleteTaskPhase = useDeleteTaskPhase();
const [isAdding, setIsAdding] = useState(false);
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
const columnRef = useRef<HTMLDivElement>(null);
const dragImageRef = useRef<HTMLElement | null>(null);
const isDraggingColumn = draggedColumnId === column.id;
const handleColumnDragStart = (e: React.DragEvent) => {
const columnEl = columnRef.current;
if (columnEl) {
const clone = columnEl.cloneNode(true) as HTMLElement;
clone.style.position = "fixed";
clone.style.top = "-10000px";
clone.style.left = "-10000px";
clone.style.width = `${columnEl.offsetWidth}px`;
clone.style.opacity = "0.95";
clone.style.boxShadow = "0 8px 24px rgba(0, 0, 0, 0.15)";
clone.style.pointerEvents = "none";
clone.style.zIndex = "9999";
document.body.appendChild(clone);
dragImageRef.current = clone;
const rect = columnEl.getBoundingClientRect();
e.dataTransfer.setDragImage(
clone,
e.clientX - rect.left,
e.clientY - rect.top,
);
e.dataTransfer.effectAllowed = "move";
}
onColumnDragStart(column.id);
};
const handleColumnDragEnd = () => {
if (dragImageRef.current) {
document.body.removeChild(dragImageRef.current);
dragImageRef.current = null;
}
onColumnDragEnd();
};
const handleDragOver = (e: React.DragEvent) => {
if (draggedColumnId) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
if (draggedColumnId) return;
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent, index: number) => {
if (draggedColumnId) return;
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
onDrop(column.id, index);
};
const handleColumnDragOver = (e: React.DragEvent) => {
if (!draggedColumnId || draggedColumnId === column.id) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setIsColumnDragOver(true);
};
const handleColumnDragLeave = (e: React.DragEvent) => {
if (!draggedColumnId) return;
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsColumnDragOver(false);
}
};
const handleColumnDrop = (e: React.DragEvent) => {
if (!draggedColumnId || draggedColumnId === column.id) return;
e.preventDefault();
e.stopPropagation();
setIsColumnDragOver(false);
onColumnDrop(column.id);
};
const handleDeleteColumn = () => {
deleteTaskPhase.mutate(
{ id: column.id, projectId },
{
onSuccess: () => {
onColumnDeleted?.(column.id);
setIsDeleteConfirmOpen(false);
toast.success(t("taskmanager.column_deleted"));
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0]);
},
},
);
};
return (
<div
ref={columnRef}
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col max-h-[calc(100dvh-140px)] sm:max-h-[calc(100dvh-152px)] xl:max-h-[calc(100dvh-220px)] transition-colors ${
isDragOver ? "ring-2 ring-primary/40" : ""
} ${isColumnDragOver ? "ring-2 ring-black/30" : ""} ${
isDraggingColumn ? "opacity-50" : ""
}`}
onDragOver={(e) => {
handleColumnDragOver(e);
handleDragOver(e);
}}
onDragLeave={(e) => {
handleColumnDragLeave(e);
handleDragLeave(e);
}}
onDrop={(e) => {
handleColumnDrop(e);
if (!draggedColumnId) {
handleDrop(e, tasks.length);
}
}}
>
<div className="flex justify-between items-center shrink-0 gap-2">
<div
draggable
onDragStart={handleColumnDragStart}
onDragEnd={handleColumnDragEnd}
className="flex-1 min-w-0 font-bold text-sm truncate cursor-grab active:cursor-grabbing"
>
{column.title}
</div>
<ColumnMenu onDelete={() => setIsDeleteConfirmOpen(true)} />
</div>
<ModalConfrim
isOpen={isDeleteConfirmOpen}
close={() => setIsDeleteConfirmOpen(false)}
onConfrim={handleDeleteColumn}
isLoading={deleteTaskPhase.isPending}
label={t("taskmanager.delete_column_confirm")}
/>
<div className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4">
{tasks.map((task, index) => (
<div
key={task.id}
onDragOver={(e) => {
if (draggedColumnId) return;
e.preventDefault();
e.stopPropagation();
}}
onDrop={(e) => handleDrop(e, index)}
>
<Task
task={task}
isDragging={draggedTaskId === task.id}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onClick={onTaskClick}
/>
</div>
))}
</div>
{isAdding ? (
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
<input
type="text"
placeholder="عنوان تسک"
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
autoFocus
/>
<div className="flex items-center gap-2">
<button
type="button"
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer"
>
<span>اضافه کردن</span>
<Add size={18} color="white" />
</button>
<button
type="button"
onClick={() => setIsAdding(false)}
className="cursor-pointer"
aria-label="انصراف"
>
<CloseCircle size={22} color="#888888" variant="Bold" />
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setIsAdding(true)}
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
>
<AddCircle size={18} color="#888888" />
<span className="text-description text-[13px] mt-0.5">اضافه کردن تسک</span>
</button>
)}
</div>
);
};
export default Column;