From 651fb16feb8a7749cdc9026376ebd00ef5cf2b06 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 11 Jul 2026 11:06:43 +0330 Subject: [PATCH] CREATE AND DELETE column --- src/langs/fa.json | 6 + .../taskmanager/components/AddNewColumn.tsx | 148 +++++++++++++++++- src/pages/taskmanager/components/Column.tsx | 53 +++++-- .../taskmanager/components/ColumnMenu.tsx | 38 +++++ .../task-phase/hooks/useTaskPhaseData.ts | 27 ++++ .../task-phase/service/TaskPhaseService.ts | 12 ++ .../task-phase/types/TaskPhaseTypes.ts | 14 ++ src/pages/taskmanager/types.ts | 2 + src/pages/taskmanager/workspace.tsx | 30 +++- 9 files changed, 313 insertions(+), 17 deletions(-) create mode 100644 src/pages/taskmanager/components/ColumnMenu.tsx create mode 100644 src/pages/taskmanager/task-phase/hooks/useTaskPhaseData.ts create mode 100644 src/pages/taskmanager/task-phase/service/TaskPhaseService.ts create mode 100644 src/pages/taskmanager/task-phase/types/TaskPhaseTypes.ts diff --git a/src/langs/fa.json b/src/langs/fa.json index 7d39e6f..30e1db7 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -1000,6 +1000,12 @@ "select_date": "انتخاب تاریخ", "projects": "پروژه‌ها", "delete_project": "پاک کردن", + "add_new_column": "اضافه کردن ستون جدید", + "add_column": "اضافه کردن", + "column_name": "نام ستون", + "delete_column": "حذف ستون", + "delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟", + "column_deleted": "ستون با موفقیت حذف شد", "task_detail": { "labels": "برچسب ها", "user_management": "مدیریت کاربران", diff --git a/src/pages/taskmanager/components/AddNewColumn.tsx b/src/pages/taskmanager/components/AddNewColumn.tsx index e29ce44..54d49e8 100644 --- a/src/pages/taskmanager/components/AddNewColumn.tsx +++ b/src/pages/taskmanager/components/AddNewColumn.tsx @@ -1,12 +1,146 @@ -import { AddCircle } from "iconsax-react"; -import { type FC } from "react"; +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 ColorsImage from "../../../assets/images/colors.png"; +import { ErrorType } from "../../../helpers/types"; +import { clx } from "../../../helpers/utils"; +import { useCreateTaskPhase } from "../task-phase/hooks/useTaskPhaseData"; +import type { TaskPhaseItemType } from "../task-phase/types/TaskPhaseTypes"; +import { LABEL_COLOR_OPTIONS } from "./task-detail/labels/constants"; + +const DEFAULT_COLUMN_COLOR = "#4F46E5"; + +type Props = { + projectId: string; + nextOrder: number; + onColumnCreated: (phase: TaskPhaseItemType) => void; +}; + +const AddNewColumn: FC = ({ projectId, nextOrder, onColumnCreated }) => { + const { t } = useTranslation("global"); + const colorInputRef = useRef(null); + const createTaskPhase = useCreateTaskPhase(); + + const [isAdding, setIsAdding] = useState(false); + const [name, setName] = useState(""); + const [selectedColor, setSelectedColor] = useState(DEFAULT_COLUMN_COLOR); + + const resetForm = () => { + setName(""); + setSelectedColor(DEFAULT_COLUMN_COLOR); + setIsAdding(false); + }; + + const handleSubmit = () => { + const trimmedName = name.trim(); + if (!trimmedName || !projectId) return; + + createTaskPhase.mutate( + { + name: trimmedName, + order: nextOrder, + color: selectedColor, + projectId, + }, + { + onSuccess: (data) => { + const phase = (data?.data ?? data) as TaskPhaseItemType; + onColumnCreated(phase); + toast.success(t("success")); + resetForm(); + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error.message[0]); + }, + }, + ); + }; + + if (!isAdding) { + return ( + + ); + } -const AddNewColumn: FC = () => { return ( - +
+ setName(e.target.value)} + placeholder={t("taskmanager.column_name")} + className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none" + autoFocus + /> + +
+
{t("taskmanager.choose_color")}
+
+ + {LABEL_COLOR_OPTIONS.map((color) => ( +
+
+ +
+ + +
+
); }; diff --git a/src/pages/taskmanager/components/Column.tsx b/src/pages/taskmanager/components/Column.tsx index 3396b34..1e39afb 100644 --- a/src/pages/taskmanager/components/Column.tsx +++ b/src/pages/taskmanager/components/Column.tsx @@ -1,6 +1,12 @@ -import { Add, AddCircle, CloseCircle, More } from "iconsax-react"; +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 = { @@ -15,6 +21,7 @@ type Props = { onColumnDragEnd: () => void; onColumnDrop: (overColumnId: string) => void; onTaskClick?: (task: TaskType) => void; + onColumnDeleted?: (columnId: string) => void; }; const Column: FC = ({ @@ -29,8 +36,13 @@ const Column: FC = ({ 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(null); @@ -116,6 +128,19 @@ const Column: FC = ({ onColumnDrop(column.id); }; + const handleDeleteColumn = () => { + deleteTaskPhase.mutate(column.id, { + onSuccess: () => { + onColumnDeleted?.(column.id); + setIsDeleteConfirmOpen(false); + toast.success(t("taskmanager.column_deleted")); + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error.message[0]); + }, + }); + }; + return (
= ({ } }} > -
-
{column.title}
- +
+
+ {column.title} +
+ setIsDeleteConfirmOpen(true)} />
+ setIsDeleteConfirmOpen(false)} + onConfrim={handleDeleteColumn} + isLoading={deleteTaskPhase.isPending} + label={t("taskmanager.delete_column_confirm")} + /> +
{tasks.map((task, index) => (
void; +}; + +const ColumnMenu: FC = ({ onDelete }) => { + const { t } = useTranslation("global"); + + return ( + + e.stopPropagation()} + > + + + + + + + + ); +}; + +export default ColumnMenu; diff --git a/src/pages/taskmanager/task-phase/hooks/useTaskPhaseData.ts b/src/pages/taskmanager/task-phase/hooks/useTaskPhaseData.ts new file mode 100644 index 0000000..6c52fa3 --- /dev/null +++ b/src/pages/taskmanager/task-phase/hooks/useTaskPhaseData.ts @@ -0,0 +1,27 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import * as api from "../service/TaskPhaseService"; +import { CreateTaskPhaseType } from "../types/TaskPhaseTypes"; + +export const useCreateTaskPhase = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (variables: CreateTaskPhaseType) => api.createTaskPhase(variables), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: ["task-phases", variables.projectId], + }); + }, + }); +}; + +export const useDeleteTaskPhase = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => api.deleteTaskPhase(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["task-phases"] }); + }, + }); +}; diff --git a/src/pages/taskmanager/task-phase/service/TaskPhaseService.ts b/src/pages/taskmanager/task-phase/service/TaskPhaseService.ts new file mode 100644 index 0000000..5e3683b --- /dev/null +++ b/src/pages/taskmanager/task-phase/service/TaskPhaseService.ts @@ -0,0 +1,12 @@ +import axios from "../../../../config/axios"; +import { CreateTaskPhaseType } from "../types/TaskPhaseTypes"; + +export const createTaskPhase = async (params: CreateTaskPhaseType) => { + const { data } = await axios.post(`/task-manager/task-phases`, params); + return data; +}; + +export const deleteTaskPhase = async (id: string) => { + const { data } = await axios.delete(`/task-manager/task-phases/${id}`); + return data; +}; diff --git a/src/pages/taskmanager/task-phase/types/TaskPhaseTypes.ts b/src/pages/taskmanager/task-phase/types/TaskPhaseTypes.ts new file mode 100644 index 0000000..316841b --- /dev/null +++ b/src/pages/taskmanager/task-phase/types/TaskPhaseTypes.ts @@ -0,0 +1,14 @@ +export type CreateTaskPhaseType = { + name: string; + order: number; + color: string; + projectId: string; +}; + +export type TaskPhaseItemType = { + id: string; + name: string; + order: number; + color: string; + projectId: string; +}; diff --git a/src/pages/taskmanager/types.ts b/src/pages/taskmanager/types.ts index 477d3c8..c528d95 100644 --- a/src/pages/taskmanager/types.ts +++ b/src/pages/taskmanager/types.ts @@ -1,6 +1,8 @@ export type Column = { id: string; title: string; + color?: string; + order?: number; }; export type Task = { diff --git a/src/pages/taskmanager/workspace.tsx b/src/pages/taskmanager/workspace.tsx index 9c5e905..db37917 100644 --- a/src/pages/taskmanager/workspace.tsx +++ b/src/pages/taskmanager/workspace.tsx @@ -1,14 +1,17 @@ import { useMemo, useState, type FC } from "react"; +import { useParams } from "react-router-dom"; 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 type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes"; import type { Task, WorkspaceData } from "./types"; import { reorderColumns } from "./utils/reorderColumns"; import { reorderTasks } from "./utils/reorderTasks"; const Workspace: FC = () => { + const { slug: projectId = "" } = useParams<{ slug: string }>(); const [columns, setColumns] = useState( (tasksData as WorkspaceData).columns, ); @@ -54,6 +57,26 @@ const Workspace: FC = () => { setDraggedColumnId(null); }; + const handleColumnCreated = (phase: TaskPhaseItemType) => { + setColumns((prev) => [ + ...prev, + { + id: phase.id, + title: phase.name, + color: phase.color, + order: phase.order, + }, + ]); + }; + + const handleColumnDeleted = (columnId: string) => { + setColumns((prev) => prev.filter((column) => column.id !== columnId)); + setTasks((prev) => prev.filter((task) => task.columnId !== columnId)); + if (selectedTask?.columnId === columnId) { + setSelectedTask(null); + } + }; + const selectedTaskColumn = selectedTask ? columns.find((column) => column.id === selectedTask.columnId) : null; @@ -77,10 +100,15 @@ const Workspace: FC = () => { onColumnDragEnd={handleColumnDragEnd} onColumnDrop={handleColumnDrop} onTaskClick={setSelectedTask} + onColumnDeleted={handleColumnDeleted} /> ))} - +