This commit is contained in:
hamid zarghami
2026-07-11 12:22:15 +03:30
parent c4150ab8ac
commit ee8da0b88c
7 changed files with 104 additions and 22 deletions
@@ -33,8 +33,14 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
const updateTask = useUpdateTask(); const updateTask = useUpdateTask();
const deleteTask = useDeleteTask(); const deleteTask = useDeleteTask();
const taskDetail: TaskDetailType | undefined = const rawTaskDetail = getTaskDetail.data?.data?.task ?? getTaskDetail.data?.data;
getTaskDetail.data?.data?.task ?? getTaskDetail.data?.data; const taskDetail: TaskDetailType | undefined = rawTaskDetail
? {
...rawTaskDetail,
id: rawTaskDetail.id ?? task?.id ?? "",
labels: rawTaskDetail.labels ?? rawTaskDetail.remarks,
}
: undefined;
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
@@ -128,7 +134,12 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
/> />
<div className="mt-4"> <div className="mt-4">
<TaskDetailToolbar activeTab={activeTab} onTabChange={handleTabChange} taskDetail={taskDetail} /> <TaskDetailToolbar
activeTab={activeTab}
onTabChange={handleTabChange}
taskDetail={taskDetail}
taskId={task.id}
/>
</div> </div>
<TaskDetailDescription <TaskDetailDescription
@@ -1,4 +1,6 @@
import { type FC, useEffect, useState } from "react"; import { type FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "react-toastify";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import { DEFAULT_DATE_RANGE } from "./date/constants"; import { DEFAULT_DATE_RANGE } from "./date/constants";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover"; import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
@@ -9,7 +11,6 @@ import persian_fa from "react-date-object/locales/persian_fa";
import { DEFAULT_CHECKLISTS } from "./checklist/constants"; import { DEFAULT_CHECKLISTS } from "./checklist/constants";
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover"; import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
import type { TaskChecklist } from "./checklist/types"; import type { TaskChecklist } from "./checklist/types";
import { DEFAULT_LABELS } from "./labels/constants";
import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover"; import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover";
import type { LabelsView, TaskLabel } from "./labels/types"; import type { LabelsView, TaskLabel } from "./labels/types";
import TaskDetailActionBar from "./TaskDetailActionBar"; import TaskDetailActionBar from "./TaskDetailActionBar";
@@ -18,11 +19,14 @@ import type { TaskDetailTab } from "./types";
import { DEFAULT_USERS } from "./users/constants"; import { DEFAULT_USERS } from "./users/constants";
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover"; import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
import type { TaskDetailType } from "../../task/types/TaskTypes"; import type { TaskDetailType } from "../../task/types/TaskTypes";
import { useCreateTaskRemark } from "../../task/hooks/useTaskData";
import { ErrorType } from "../../../../helpers/types";
type Props = { type Props = {
activeTab: TaskDetailTab | null; activeTab: TaskDetailTab | null;
onTabChange: (tab: TaskDetailTab) => void; onTabChange: (tab: TaskDetailTab) => void;
taskDetail?: TaskDetailType; taskDetail?: TaskDetailType;
taskId: string;
}; };
const parseDate = (value?: string): DateObject | null => { const parseDate = (value?: string): DateObject | null => {
@@ -31,10 +35,12 @@ const parseDate = (value?: string): DateObject | null => {
return date.isValid ? date : null; return date.isValid ? date : null;
}; };
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail }) => { const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
const { t } = useTranslation("global");
const createTaskRemark = useCreateTaskRemark();
const [labelsView, setLabelsView] = useState<LabelsView>("list"); const [labelsView, setLabelsView] = useState<LabelsView>("list");
const [labels, setLabels] = useState<TaskLabel[]>(DEFAULT_LABELS); const [labels, setLabels] = useState<TaskLabel[]>([]);
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set(["1"])); const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set(["1"])); const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set(["1"]));
const [checklists, setChecklists] = useState<TaskChecklist[]>(DEFAULT_CHECKLISTS); const [checklists, setChecklists] = useState<TaskChecklist[]>(DEFAULT_CHECKLISTS);
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate); const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
@@ -43,9 +49,14 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail }) =>
useEffect(() => { useEffect(() => {
if (!taskDetail) return; if (!taskDetail) return;
if (taskDetail.labels?.length) { const taskLabels = taskDetail.labels ?? taskDetail.remarks ?? [];
setLabels(taskDetail.labels);
setSelectedLabelIds(new Set(taskDetail.labels.map((label) => label.id))); if (taskLabels.length) {
setLabels(taskLabels);
setSelectedLabelIds(new Set(taskLabels.map((label) => label.id)));
} else {
setLabels([]);
setSelectedLabelIds(new Set());
} }
if (taskDetail.users?.length) { if (taskDetail.users?.length) {
@@ -108,14 +119,34 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail }) =>
}; };
const handleCreate = (title: string, color: string) => { const handleCreate = (title: string, color: string) => {
const newLabel: TaskLabel = { const effectiveTaskId = taskId || taskDetail?.id;
id: crypto.randomUUID(), if (!effectiveTaskId) return;
title,
color, createTaskRemark.mutate(
}; { title, color, taskId: effectiveTaskId },
setLabels((prev) => [...prev, newLabel]); {
setSelectedLabelIds((prev) => new Set([...prev, newLabel.id])); onSuccess: (response) => {
setLabelsView("list"); const remark =
response.data?.remark ??
(response as { remark?: TaskLabel }).remark ??
(response.data as TaskLabel | undefined);
if (remark?.id) {
const newLabel: TaskLabel = {
id: remark.id,
title: remark.title,
color: remark.color,
};
setLabels((prev) => [...prev, newLabel]);
setSelectedLabelIds((prev) => new Set([...prev, newLabel.id]));
}
setLabelsView("list");
toast.success(t("success"));
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0]);
},
},
);
}; };
const handleChecklistClose = () => { const handleChecklistClose = () => {
@@ -163,6 +194,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail }) =>
onViewChange={setLabelsView} onViewChange={setLabelsView}
onToggle={handleLabelToggle} onToggle={handleLabelToggle}
onCreate={handleCreate} onCreate={handleCreate}
isSubmitting={createTaskRemark.isPending}
/> />
) : null ) : null
} }
@@ -10,6 +10,7 @@ type Props = {
onViewChange: (view: LabelsView) => void; onViewChange: (view: LabelsView) => void;
onToggle: (id: string) => void; onToggle: (id: string) => void;
onCreate: (title: string, color: string) => void; onCreate: (title: string, color: string) => void;
isSubmitting?: boolean;
}; };
const TaskDetailLabelsPopover: FC<Props> = ({ const TaskDetailLabelsPopover: FC<Props> = ({
@@ -19,6 +20,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
onViewChange, onViewChange,
onToggle, onToggle,
onCreate, onCreate,
isSubmitting = false,
}) => { }) => {
return ( return (
<> <>
@@ -34,6 +36,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
<TaskDetailNewLabelForm <TaskDetailNewLabelForm
onBack={() => onViewChange("list")} onBack={() => onViewChange("list")}
onSubmit={onCreate} onSubmit={onCreate}
isSubmitting={isSubmitting}
/> />
)} )}
</> </>
@@ -10,9 +10,10 @@ import { LABEL_COLOR_OPTIONS } from "./constants";
type Props = { type Props = {
onBack: () => void; onBack: () => void;
onSubmit: (title: string, color: string) => void; onSubmit: (title: string, color: string) => void;
isSubmitting?: boolean;
}; };
const TaskDetailNewLabelForm: FC<Props> = ({ onBack, onSubmit }) => { const TaskDetailNewLabelForm: FC<Props> = ({ onBack, onSubmit, isSubmitting = false }) => {
const { t } = useTranslation("global"); const { t } = useTranslation("global");
const colorInputRef = useRef<HTMLInputElement>(null); const colorInputRef = useRef<HTMLInputElement>(null);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@@ -48,7 +49,8 @@ const TaskDetailNewLabelForm: FC<Props> = ({ onBack, onSubmit }) => {
<Button <Button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={!title.trim()} disabled={!title.trim() || isSubmitting}
isLoading={isSubmitting}
className="flex items-center justify-center gap-2 bg-white border border-[#D0D0D0] text-[#292D32] rounded-[6px] h-7 text-xs disabled:opacity-50" className="flex items-center justify-center gap-2 bg-white border border-[#D0D0D0] text-[#292D32] rounded-[6px] h-7 text-xs disabled:opacity-50"
> >
<AddCircle size={14} color="#292D32" /> <AddCircle size={14} color="#292D32" />
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/TaskService"; import * as api from "../service/TaskService";
import { CreateTaskType, UpdateTaskType } from "../types/TaskTypes"; import { CreateTaskRemarkType, CreateTaskType, UpdateTaskType } from "../types/TaskTypes";
export const useGetTaskDetail = (id: string, enabled = true) => { export const useGetTaskDetail = (id: string, enabled = true) => {
return useQuery({ return useQuery({
@@ -45,3 +45,15 @@ export const useDeleteTask = () => {
}, },
}); });
}; };
export const useCreateTaskRemark = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: CreateTaskRemarkType) => api.createTaskRemark(variables),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
queryClient.invalidateQueries({ queryKey: ["project"] });
},
});
};
@@ -1,5 +1,11 @@
import axios from "../../../../config/axios"; import axios from "../../../../config/axios";
import { CreateTaskType, GetTaskDetailResponse, UpdateTaskType } from "../types/TaskTypes"; import {
CreateTaskRemarkResponse,
CreateTaskRemarkType,
CreateTaskType,
GetTaskDetailResponse,
UpdateTaskType,
} from "../types/TaskTypes";
export const createTask = async (params: CreateTaskType) => { export const createTask = async (params: CreateTaskType) => {
const { data } = await axios.post(`/task-manager/tasks`, params); const { data } = await axios.post(`/task-manager/tasks`, params);
@@ -20,3 +26,8 @@ export const deleteTask = async (id: string) => {
const { data } = await axios.delete(`/task-manager/tasks/${id}`); const { data } = await axios.delete(`/task-manager/tasks/${id}`);
return data; return data;
}; };
export const createTaskRemark = async (params: CreateTaskRemarkType): Promise<CreateTaskRemarkResponse> => {
const { data } = await axios.post(`/task-manager/remarks`, params);
return data;
};
@@ -25,6 +25,16 @@ export type TaskLabelType = {
color: string; color: string;
}; };
export type CreateTaskRemarkType = {
title: string;
color: string;
taskId: string;
};
export interface CreateTaskRemarkResponse extends IResponse<{ remark: TaskLabelType }> {
statusCode: number;
}
export type TaskUserType = { export type TaskUserType = {
id: string; id: string;
firstName: string; firstName: string;
@@ -61,6 +71,7 @@ export type TaskDetailType = {
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
labels?: TaskLabelType[]; labels?: TaskLabelType[];
remarks?: TaskLabelType[];
users?: TaskUserType[]; users?: TaskUserType[];
checklists?: TaskChecklistType[]; checklists?: TaskChecklistType[];
attachments?: TaskAttachmentType[]; attachments?: TaskAttachmentType[];