task list and create

This commit is contained in:
hamid zarghami
2026-07-11 11:54:23 +03:30
parent 2bc7a37082
commit 833ac2446f
8 changed files with 133 additions and 12 deletions
+3
View File
@@ -1006,6 +1006,9 @@
"delete_column": "حذف ستون",
"delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟",
"column_deleted": "ستون با موفقیت حذف شد",
"add_new_task": "اضافه کردن تسک",
"add_task": "اضافه کردن",
"task_title": "عنوان تسک",
"task_detail": {
"labels": "برچسب ها",
"user_management": "مدیریت کاربران",
+49 -6
View File
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";
import ModalConfrim from "../../../components/ModalConfrim";
import { ErrorType } from "../../../helpers/types";
import { useCreateTask } from "../task/hooks/useTaskData";
import type { TaskItemType } from "../task/types/TaskTypes";
import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
import type { Column as ColumnType, Task as TaskType } from "../types";
import ColumnMenu from "./ColumnMenu";
@@ -23,6 +25,7 @@ type Props = {
onColumnDrop: (overColumnId: string) => void;
onTaskClick?: (task: TaskType) => void;
onColumnDeleted?: (columnId: string) => void;
onTaskCreated?: (task: TaskItemType, taskPhaseId: string) => void;
};
const Column: FC<Props> = ({
@@ -39,11 +42,14 @@ const Column: FC<Props> = ({
onColumnDrop,
onTaskClick,
onColumnDeleted,
onTaskCreated,
}) => {
const { t } = useTranslation("global");
const deleteTaskPhase = useDeleteTaskPhase();
const createTask = useCreateTask();
const [isAdding, setIsAdding] = useState(false);
const [title, setTitle] = useState("");
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
@@ -130,6 +136,36 @@ const Column: FC<Props> = ({
onColumnDrop(column.id);
};
const resetTaskForm = () => {
setTitle("");
setIsAdding(false);
};
const handleCreateTask = () => {
const trimmedTitle = title.trim();
if (!trimmedTitle || !projectId) return;
createTask.mutate(
{
title: trimmedTitle,
taskPhaseId: column.id,
order: tasks.length,
projectId,
},
{
onSuccess: (data) => {
const task = (data?.data ?? data) as TaskItemType;
onTaskCreated?.(task, column.id);
toast.success(t("success"));
resetTaskForm();
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0]);
},
},
);
};
const handleDeleteColumn = () => {
deleteTaskPhase.mutate(
{ id: column.id, projectId },
@@ -215,23 +251,30 @@ const Column: FC<Props> = ({
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
<input
type="text"
placeholder="عنوان تسک"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleCreateTask();
}}
placeholder={t("taskmanager.task_title")}
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"
onClick={handleCreateTask}
disabled={!title.trim() || createTask.isPending}
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer disabled:opacity-50"
>
<span>اضافه کردن</span>
<span>{t("taskmanager.add_task")}</span>
<Add size={18} color="white" />
</button>
<button
type="button"
onClick={() => setIsAdding(false)}
onClick={resetTaskForm}
className="cursor-pointer"
aria-label="انصراف"
aria-label={t("cancel")}
>
<CloseCircle size={22} color="#888888" variant="Bold" />
</button>
@@ -244,7 +287,7 @@ const Column: FC<Props> = ({
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>
<span className="text-description text-[13px] mt-0.5">{t("taskmanager.add_new_task")}</span>
</button>
)}
</div>
@@ -1,4 +1,5 @@
import { IResponse } from "../../../../types/response.types";
import { TaskItemType } from "../../task/types/TaskTypes";
export type ProjectItem = {
id: string;
@@ -32,7 +33,7 @@ export type ProjectDetailType = CreateProjectType & {
export type UpdateProjectType = CreateProjectType;
export type ProjectTaskType = unknown;
export type ProjectTaskType = TaskItemType;
export type ProjectTaskPhaseType = {
id: string;
@@ -0,0 +1,14 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/TaskService";
import { CreateTaskType } from "../types/TaskTypes";
export const useCreateTask = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: CreateTaskType) => api.createTask(variables),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["project", variables.projectId] });
},
});
};
@@ -0,0 +1,7 @@
import axios from "../../../../config/axios";
import { CreateTaskType } from "../types/TaskTypes";
export const createTask = async (params: CreateTaskType) => {
const { data } = await axios.post(`/task-manager/tasks`, params);
return data;
};
@@ -0,0 +1,18 @@
export type CreateTaskType = {
title: string;
taskPhaseId: string;
order: number;
projectId: string;
};
export type TaskItemType = {
id: string;
title: string;
taskPhaseId: string;
order: number;
tag?: string;
dateRange?: string;
attachments?: number;
checklistDone?: number;
checklistTotal?: number;
};
@@ -0,0 +1,14 @@
import type { Task } from "../../types";
import type { TaskItemType } from "../types/TaskTypes";
export const mapTaskItemToTask = (task: TaskItemType, taskPhaseId: string, fallbackOrder: number): Task => ({
id: task.id,
columnId: task.taskPhaseId ?? taskPhaseId,
order: task.order ?? fallbackOrder,
title: task.title,
tag: task.tag ?? "",
dateRange: task.dateRange ?? "",
attachments: task.attachments ?? 0,
checklistDone: task.checklistDone ?? 0,
checklistTotal: task.checklistTotal ?? 0,
});
+26 -5
View File
@@ -5,8 +5,9 @@ import AddNewColumn from "./components/AddNewColumn";
import Column from "./components/Column";
import HeaderWorkspace from "./components/HeaderWorkspace";
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
import tasksData from "./data/tasks.json";
import { useGetProject } from "./project/hooks/useProjectData";
import { mapTaskItemToTask } from "./task/utils/mapTaskItemToTask";
import type { TaskItemType } from "./task/types/TaskTypes";
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
import type { Column as ColumnType, Task } from "./types";
import { reorderColumns } from "./utils/reorderColumns";
@@ -16,6 +17,10 @@ const Workspace: FC = () => {
const { slug: projectId = "" } = useParams<{ slug: string }>();
const { data: project, isPending } = useGetProject(projectId);
const [columns, setColumns] = useState<ColumnType[]>([]);
const [tasks, setTasks] = useState<Task[]>([]);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
useEffect(() => {
if (!project?.data?.taskPhases) return;
@@ -31,10 +36,18 @@ const Workspace: FC = () => {
})),
);
}, [project?.data?.taskPhases]);
const [tasks, setTasks] = useState<Task[]>(tasksData.tasks as Task[]);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
useEffect(() => {
if (!project?.data?.taskPhases) return;
const apiTasks = project.data.taskPhases.flatMap((phase) =>
(phase.tasks ?? []).map((task, index) =>
mapTaskItemToTask(task as TaskItemType, phase.id, index),
),
);
setTasks(apiTasks);
}, [project?.data?.taskPhases]);
const tasksByColumn = useMemo(() => {
return columns.reduce<Record<string, Task[]>>((acc, column) => {
@@ -91,6 +104,13 @@ const Workspace: FC = () => {
}
};
const handleTaskCreated = (task: TaskItemType, taskPhaseId: string) => {
setTasks((prev) => [
...prev,
mapTaskItemToTask(task, taskPhaseId, prev.filter((item) => item.columnId === taskPhaseId).length),
]);
};
const selectedTaskColumn = selectedTask ? columns.find((column) => column.id === selectedTask.columnId) : null;
if (isPending) {
@@ -118,6 +138,7 @@ const Workspace: FC = () => {
onColumnDrop={handleColumnDrop}
onTaskClick={setSelectedTask}
onColumnDeleted={handleColumnDeleted}
onTaskCreated={handleTaskCreated}
/>
))}