task list and create
This commit is contained in:
@@ -1006,6 +1006,9 @@
|
|||||||
"delete_column": "حذف ستون",
|
"delete_column": "حذف ستون",
|
||||||
"delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟",
|
"delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟",
|
||||||
"column_deleted": "ستون با موفقیت حذف شد",
|
"column_deleted": "ستون با موفقیت حذف شد",
|
||||||
|
"add_new_task": "اضافه کردن تسک",
|
||||||
|
"add_task": "اضافه کردن",
|
||||||
|
"task_title": "عنوان تسک",
|
||||||
"task_detail": {
|
"task_detail": {
|
||||||
"labels": "برچسب ها",
|
"labels": "برچسب ها",
|
||||||
"user_management": "مدیریت کاربران",
|
"user_management": "مدیریت کاربران",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import ModalConfrim from "../../../components/ModalConfrim";
|
import ModalConfrim from "../../../components/ModalConfrim";
|
||||||
import { ErrorType } from "../../../helpers/types";
|
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 { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||||||
import type { Column as ColumnType, Task as TaskType } from "../types";
|
import type { Column as ColumnType, Task as TaskType } from "../types";
|
||||||
import ColumnMenu from "./ColumnMenu";
|
import ColumnMenu from "./ColumnMenu";
|
||||||
@@ -23,6 +25,7 @@ type Props = {
|
|||||||
onColumnDrop: (overColumnId: string) => void;
|
onColumnDrop: (overColumnId: string) => void;
|
||||||
onTaskClick?: (task: TaskType) => void;
|
onTaskClick?: (task: TaskType) => void;
|
||||||
onColumnDeleted?: (columnId: string) => void;
|
onColumnDeleted?: (columnId: string) => void;
|
||||||
|
onTaskCreated?: (task: TaskItemType, taskPhaseId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Column: FC<Props> = ({
|
const Column: FC<Props> = ({
|
||||||
@@ -39,11 +42,14 @@ const Column: FC<Props> = ({
|
|||||||
onColumnDrop,
|
onColumnDrop,
|
||||||
onTaskClick,
|
onTaskClick,
|
||||||
onColumnDeleted,
|
onColumnDeleted,
|
||||||
|
onTaskCreated,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const deleteTaskPhase = useDeleteTaskPhase();
|
const deleteTaskPhase = useDeleteTaskPhase();
|
||||||
|
const createTask = useCreateTask();
|
||||||
|
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
||||||
@@ -130,6 +136,36 @@ const Column: FC<Props> = ({
|
|||||||
onColumnDrop(column.id);
|
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 = () => {
|
const handleDeleteColumn = () => {
|
||||||
deleteTaskPhase.mutate(
|
deleteTaskPhase.mutate(
|
||||||
{ id: column.id, projectId },
|
{ 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">
|
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="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" />
|
<Add size={18} color="white" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsAdding(false)}
|
onClick={resetTaskForm}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
aria-label="انصراف"
|
aria-label={t("cancel")}
|
||||||
>
|
>
|
||||||
<CloseCircle size={22} color="#888888" variant="Bold" />
|
<CloseCircle size={22} color="#888888" variant="Bold" />
|
||||||
</button>
|
</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"
|
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
<AddCircle size={18} color="#888888" />
|
<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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IResponse } from "../../../../types/response.types";
|
import { IResponse } from "../../../../types/response.types";
|
||||||
|
import { TaskItemType } from "../../task/types/TaskTypes";
|
||||||
|
|
||||||
export type ProjectItem = {
|
export type ProjectItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,7 +33,7 @@ export type ProjectDetailType = CreateProjectType & {
|
|||||||
|
|
||||||
export type UpdateProjectType = CreateProjectType;
|
export type UpdateProjectType = CreateProjectType;
|
||||||
|
|
||||||
export type ProjectTaskType = unknown;
|
export type ProjectTaskType = TaskItemType;
|
||||||
|
|
||||||
export type ProjectTaskPhaseType = {
|
export type ProjectTaskPhaseType = {
|
||||||
id: string;
|
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,
|
||||||
|
});
|
||||||
@@ -5,8 +5,9 @@ import AddNewColumn from "./components/AddNewColumn";
|
|||||||
import Column from "./components/Column";
|
import Column from "./components/Column";
|
||||||
import HeaderWorkspace from "./components/HeaderWorkspace";
|
import HeaderWorkspace from "./components/HeaderWorkspace";
|
||||||
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
||||||
import tasksData from "./data/tasks.json";
|
|
||||||
import { useGetProject } from "./project/hooks/useProjectData";
|
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 { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
||||||
import type { Column as ColumnType, Task } from "./types";
|
import type { Column as ColumnType, Task } from "./types";
|
||||||
import { reorderColumns } from "./utils/reorderColumns";
|
import { reorderColumns } from "./utils/reorderColumns";
|
||||||
@@ -16,6 +17,10 @@ const Workspace: FC = () => {
|
|||||||
const { slug: projectId = "" } = useParams<{ slug: string }>();
|
const { slug: projectId = "" } = useParams<{ slug: string }>();
|
||||||
const { data: project, isPending } = useGetProject(projectId);
|
const { data: project, isPending } = useGetProject(projectId);
|
||||||
const [columns, setColumns] = useState<ColumnType[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!project?.data?.taskPhases) return;
|
if (!project?.data?.taskPhases) return;
|
||||||
@@ -31,10 +36,18 @@ const Workspace: FC = () => {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}, [project?.data?.taskPhases]);
|
}, [project?.data?.taskPhases]);
|
||||||
const [tasks, setTasks] = useState<Task[]>(tasksData.tasks as Task[]);
|
|
||||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
useEffect(() => {
|
||||||
const [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
|
if (!project?.data?.taskPhases) return;
|
||||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
||||||
|
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(() => {
|
const tasksByColumn = useMemo(() => {
|
||||||
return columns.reduce<Record<string, Task[]>>((acc, column) => {
|
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;
|
const selectedTaskColumn = selectedTask ? columns.find((column) => column.id === selectedTask.columnId) : null;
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
@@ -118,6 +138,7 @@ const Workspace: FC = () => {
|
|||||||
onColumnDrop={handleColumnDrop}
|
onColumnDrop={handleColumnDrop}
|
||||||
onTaskClick={setSelectedTask}
|
onTaskClick={setSelectedTask}
|
||||||
onColumnDeleted={handleColumnDeleted}
|
onColumnDeleted={handleColumnDeleted}
|
||||||
|
onTaskCreated={handleTaskCreated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user